daemon.dart 39.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Devon Carew's avatar
Devon Carew committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

Devon Carew's avatar
Devon Carew committed
7 8
import 'dart:async';

9
import 'package:async/async.dart';
10
import 'package:meta/meta.dart';
11
import 'package:uuid/uuid.dart';
12

13
import '../android/android_workflow.dart';
14
import '../base/common.dart';
15
import '../base/file_system.dart';
16
import '../base/io.dart';
17
import '../base/logger.dart';
18
import '../base/terminal.dart';
19
import '../base/utils.dart';
20
import '../build_info.dart';
21
import '../convert.dart';
22
import '../device.dart';
23
import '../device_port_forwarder.dart';
24
import '../emulator.dart';
25
import '../features.dart';
26
import '../globals.dart' as globals;
27
import '../project.dart';
28
import '../resident_runner.dart';
29 30
import '../run_cold.dart';
import '../run_hot.dart';
31
import '../runner/flutter_command.dart';
32
import '../vmservice.dart';
33
import '../web/web_runner.dart';
Devon Carew's avatar
Devon Carew committed
34

35
const String protocolVersion = '0.6.1';
36

Devon Carew's avatar
Devon Carew committed
37 38 39 40 41 42 43
/// A server process command. This command will start up a long-lived server.
/// It reads JSON-RPC based commands from stdin, executes them, and returns
/// JSON-RPC based responses and events to stdout.
///
/// It can be shutdown with a `daemon.shutdown` command (or by killing the
/// process).
class DaemonCommand extends FlutterCommand {
44
  DaemonCommand({ this.hidden = false });
45

46
  @override
Devon Carew's avatar
Devon Carew committed
47
  final String name = 'daemon';
48 49

  @override
50
  final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
Devon Carew's avatar
Devon Carew committed
51

52
  @override
53
  final bool hidden;
54

55
  @override
56
  Future<FlutterCommandResult> runCommand() async {
57
    globals.printStatus('Starting device daemon...');
58 59
    final Daemon daemon = Daemon(
      stdinCommandStream, stdoutCommandResponse,
60
      notifyingLogger: asLogger<NotifyingLogger>(globals.logger),
61
    );
62 63 64 65
    final int code = await daemon.onExit;
    if (code != 0) {
      throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
    }
66
    return FlutterCommandResult.success();
67
  }
Devon Carew's avatar
Devon Carew committed
68 69
}

70
typedef DispatchCommand = void Function(Map<String, dynamic> command);
Devon Carew's avatar
Devon Carew committed
71

72
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
73 74

class Daemon {
75 76 77
  Daemon(
    Stream<Map<String, dynamic>> commandStream,
    this.sendCommand, {
78
    this.notifyingLogger,
79
    this.logToStdout = false,
80
  }) {
Devon Carew's avatar
Devon Carew committed
81
    // Set up domains.
82 83 84 85
    _registerDomain(daemonDomain = DaemonDomain(this));
    _registerDomain(appDomain = AppDomain(this));
    _registerDomain(deviceDomain = DeviceDomain(this));
    _registerDomain(emulatorDomain = EmulatorDomain(this));
86
    _registerDomain(devToolsDomain = DevToolsDomain(this));
Devon Carew's avatar
Devon Carew committed
87 88

    // Start listening.
89
    _commandSubscription = commandStream.listen(
90
      _handleRequest,
91
      onDone: () {
92
        if (!_onExitCompleter.isCompleted) {
93
          _onExitCompleter.complete(0);
94
        }
95
      },
Devon Carew's avatar
Devon Carew committed
96 97 98
    );
  }

99 100 101
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
102
  EmulatorDomain emulatorDomain;
103
  DevToolsDomain devToolsDomain;
104
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
105 106
  int _outgoingRequestId = 1;
  final Map<String, Completer<dynamic>> _outgoingRequestCompleters = <String, Completer<dynamic>>{};
107

Devon Carew's avatar
Devon Carew committed
108
  final DispatchCommand sendCommand;
109
  final NotifyingLogger notifyingLogger;
110
  final bool logToStdout;
111

112
  final Completer<int> _onExitCompleter = Completer<int>();
113 114
  final Map<String, Domain> _domainMap = <String, Domain>{};

Devon Carew's avatar
Devon Carew committed
115
  void _registerDomain(Domain domain) {
116
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
117 118 119 120
  }

  Future<int> get onExit => _onExitCompleter.future;

Ian Hickson's avatar
Ian Hickson committed
121
  void _handleRequest(Map<String, dynamic> request) {
122 123 124
    // {id, method, params}

    // [id] is an opaque type to us.
125
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
126 127

    if (id == null) {
128
      globals.stdio.stderrWrite('no id for request: $request\n');
Devon Carew's avatar
Devon Carew committed
129 130 131 132
      return;
    }

    try {
133
      final String method = request['method'] as String;
134 135 136 137
      if (method != null) {
        if (!method.contains('.')) {
          throw 'method not understood: $method';
        }
Devon Carew's avatar
Devon Carew committed
138

139 140 141 142 143 144 145 146 147 148 149 150 151 152
        final String prefix = method.substring(0, method.indexOf('.'));
        final String name = method.substring(method.indexOf('.') + 1);
        if (_domainMap[prefix] == null) {
          throw 'no domain for method: $method';
        }

        _domainMap[prefix].handleCommand(name, id, castStringKeyedMap(request['params']) ?? const <String, dynamic>{});
      } else {
        // If there was no 'method' field then it's a response to a daemon-to-editor request.
        final Completer<dynamic> completer = _outgoingRequestCompleters[id.toString()];
        if (completer == null) {
          throw 'unexpected response with id: $id';
        }
        _outgoingRequestCompleters.remove(id.toString());
Devon Carew's avatar
Devon Carew committed
153

154 155 156 157 158 159
        if (request['error'] != null) {
          completer.completeError(request['error']);
        } else {
          completer.complete(request['result']);
        }
      }
160
    } on Exception catch (error, trace) {
161 162 163 164 165
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
166 167 168
    }
  }

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  Future<dynamic> sendRequest(String method, [ dynamic args ]) {
    final Map<String, dynamic> map = <String, dynamic>{'method': method};
    if (args != null) {
      map['params'] = _toJsonable(args);
    }

    final int id = _outgoingRequestId++;
    final Completer<dynamic> completer = Completer<dynamic>();

    map['id'] = id.toString();
    _outgoingRequestCompleters[id.toString()] = completer;

    _send(map);
    return completer.future;
  }

Ian Hickson's avatar
Ian Hickson committed
185
  void _send(Map<String, dynamic> map) => sendCommand(map);
Devon Carew's avatar
Devon Carew committed
186

187
  Future<void> shutdown({ dynamic error }) async {
188
    await devToolsDomain?.dispose();
189
    await _commandSubscription?.cancel();
190
    for (final Domain domain in _domainMap.values) {
191
      await domain.dispose();
192
    }
193
    if (!_onExitCompleter.isCompleted) {
194
      if (error == null) {
195
        _onExitCompleter.complete(0);
196
      } else {
197
        _onExitCompleter.completeError(error);
198
      }
199
    }
Devon Carew's avatar
Devon Carew committed
200 201 202 203
  }
}

abstract class Domain {
204 205
  Domain(this.daemon, this.name);

206

Devon Carew's avatar
Devon Carew committed
207 208
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
209
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
210 211 212 213 214

  void registerHandler(String name, CommandHandler handler) {
    _handlers[name] = handler;
  }

215
  @override
Devon Carew's avatar
Devon Carew committed
216 217
  String toString() => name;

218
  void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
219
    Future<dynamic>.sync(() {
220
      if (_handlers.containsKey(command)) {
221
        return _handlers[command](args);
222
      }
223
      throw 'command not understood: $name.$command';
224
    }).then<dynamic>((dynamic result) {
Devon Carew's avatar
Devon Carew committed
225
      if (result == null) {
Ian Hickson's avatar
Ian Hickson committed
226
        _send(<String, dynamic>{'id': id});
Devon Carew's avatar
Devon Carew committed
227
      } else {
Ian Hickson's avatar
Ian Hickson committed
228
        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
Devon Carew's avatar
Devon Carew committed
229
      }
Ian Hickson's avatar
Ian Hickson committed
230
    }).catchError((dynamic error, dynamic trace) {
231 232 233 234 235
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
236 237 238
    });
  }

239
  void sendEvent(String name, [ dynamic args ]) {
240
    final Map<String, dynamic> map = <String, dynamic>{'event': name};
241
    if (args != null) {
242
      map['params'] = _toJsonable(args);
243
    }
244 245 246
    _send(map);
  }

Ian Hickson's avatar
Ian Hickson committed
247
  void _send(Map<String, dynamic> map) => daemon._send(map);
248

249
  String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) {
250
    if (required && !args.containsKey(name)) {
251
      throw '$name is required';
252
    }
253
    final dynamic val = args[name];
254
    if (val != null && val is! String) {
255
      throw '$name is not a String';
256
    }
257
    return val as String;
258 259
  }

260
  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) {
261
    if (required && !args.containsKey(name)) {
262
      throw '$name is required';
263
    }
264
    final dynamic val = args[name];
265
    if (val != null && val is! bool) {
266
      throw '$name is not a bool';
267
    }
268
    return val as bool;
269 270
  }

271
  int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) {
272
    if (required && !args.containsKey(name)) {
273
      throw '$name is required';
274
    }
275
    final dynamic val = args[name];
276
    if (val != null && val is! int) {
277
      throw '$name is not an int';
278
    }
279
    return val as int;
280 281
  }

282
  Future<void> dispose() async { }
Devon Carew's avatar
Devon Carew committed
283 284 285
}

/// This domain responds to methods like [version] and [shutdown].
286 287
///
/// This domain fires the `daemon.logMessage` event.
Devon Carew's avatar
Devon Carew committed
288 289 290 291
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
292
    registerHandler('getSupportedPlatforms', getSupportedPlatforms);
293

294 295 296 297 298 299 300 301
    sendEvent(
      'daemon.connected',
      <String, dynamic>{
        'version': protocolVersion,
        'pid': pid,
      },
    );

302
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
303 304 305 306 307 308
      if (daemon.logToStdout) {
        if (message.level == 'status') {
          // We use `print()` here instead of `stdout.writeln()` in order to
          // capture the print output for testing.
          print(message.message);
        } else if (message.level == 'error') {
309
          globals.stdio.stderrWrite('${message.message}\n');
310
          if (message.stackTrace != null) {
311 312 313
            globals.stdio.stderrWrite(
              '${message.stackTrace.toString().trimRight()}\n',
            );
314
          }
315
        }
316
      } else {
317 318 319 320
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
321
            'stackTrace': message.stackTrace.toString(),
322 323 324 325
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
326
            'message': message.message,
327 328
          });
        }
329 330
      }
    });
Devon Carew's avatar
Devon Carew committed
331 332
  }

333
  StreamSubscription<LogMessage> _subscription;
334

335
  Future<String> version(Map<String, dynamic> args) {
336
    return Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
337 338
  }

339 340 341 342 343 344 345 346 347 348
  /// Sends a request back to the client asking it to expose/tunnel a URL.
  ///
  /// This method should only be called if the client opted-in with the
  /// --web-allow-expose-url switch. The client may return the same URL back if
  /// tunnelling is not required for a given URL.
  Future<String> exposeUrl(String url) async {
    final dynamic res = await daemon.sendRequest('app.exposeUrl', <String, String>{'url': url});
    if (res is Map<String, dynamic> && res['url'] is String) {
      return res['url'] as String;
    } else {
349
      globals.printError('Invalid response to exposeUrl - params should include a String url field');
350 351 352 353
      return url;
    }
  }

354
  Future<void> shutdown(Map<String, dynamic> args) {
355
    Timer.run(daemon.shutdown);
356
    return Future<void>.value();
Devon Carew's avatar
Devon Carew committed
357
  }
358

359
  @override
360 361
  Future<void> dispose() async {
    await _subscription?.cancel();
362
  }
363 364 365 366 367 368 369 370 371 372

  /// Enumerates the platforms supported by the provided project.
  ///
  /// This does not filter based on the current workflow restrictions, such
  /// as whether command line tools are installed or whether the host platform
  /// is correct.
  Future<Map<String, Object>> getSupportedPlatforms(Map<String, dynamic> args) async {
    final String projectRoot = _getStringArg(args, 'projectRoot', required: true);
    final List<String> result = <String>[];
    try {
373
      final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
374
      if (featureFlags.isLinuxEnabled && flutterProject.linux.existsSync()) {
375 376
        result.add('linux');
      }
377
      if (featureFlags.isMacOSEnabled && flutterProject.macos.existsSync()) {
378 379
        result.add('macos');
      }
380
      if (featureFlags.isWindowsEnabled && flutterProject.windows.existsSync()) {
381 382
        result.add('windows');
      }
383
      if (featureFlags.isIOSEnabled && flutterProject.ios.existsSync()) {
384 385
        result.add('ios');
      }
386
      if (featureFlags.isAndroidEnabled && flutterProject.android.existsSync()) {
387 388
        result.add('android');
      }
389
      if (featureFlags.isWebEnabled && flutterProject.web.existsSync()) {
390 391
        result.add('web');
      }
392
      if (featureFlags.isFuchsiaEnabled && flutterProject.fuchsia.existsSync()) {
393 394
        result.add('fuchsia');
      }
395 396 397
      if (featureFlags.areCustomDevicesEnabled) {
        result.add('custom');
      }
398 399 400
      return <String, Object>{
        'platforms': result,
      };
401
    } on Exception catch (err, stackTrace) {
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
      sendEvent('log', <String, dynamic>{
        'log': 'Failed to parse project metadata',
        'stackTrace': stackTrace.toString(),
        'error': true,
      });
      // On any sort of failure, fall back to Android and iOS for backwards
      // comparability.
      return <String, Object>{
        'platforms': <String>[
          'android',
          'ios',
        ],
      };
    }
  }
Devon Carew's avatar
Devon Carew committed
417 418
}

419
typedef RunOrAttach = Future<void> Function({
420
  Completer<DebugConnectionInfo> connectionInfoCompleter,
421
  Completer<void> appStartedCompleter,
422 423
});

424
/// This domain responds to methods like [start] and [stop].
Devon Carew's avatar
Devon Carew committed
425
///
426
/// It fires events for application start, stop, and stdout and stderr.
Devon Carew's avatar
Devon Carew committed
427 428
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
429
    registerHandler('restart', restart);
430
    registerHandler('callServiceExtension', callServiceExtension);
431
    registerHandler('stop', stop);
432
    registerHandler('detach', detach);
Devon Carew's avatar
Devon Carew committed
433 434
  }

435
  static const Uuid _uuidGenerator = Uuid();
436

437
  static String _getNewAppId() => _uuidGenerator.v4();
438

439
  final List<AppInstance> _apps = <AppInstance>[];
440

441 442
  final DebounceOperationQueue<OperationResult, OperationType> operationQueue = DebounceOperationQueue<OperationResult, OperationType>();

443
  Future<AppInstance> startApp(
444 445 446 447 448 449
    Device device,
    String projectDirectory,
    String target,
    String route,
    DebuggingOptions options,
    bool enableHotReload, {
450
    File applicationBinary,
451
    @required bool trackWidgetCreation,
452 453
    String projectRootPath,
    String packagesFilePath,
454
    String dillOutputPath,
455
    bool ipv6 = false,
456
    String isolateFilter,
457
    bool machine = true,
458
  }) async {
459 460 461 462 463
    if (!await device.supportsRuntimeMode(options.buildInfo.mode)) {
      throw Exception(
        '${toTitleCase(options.buildInfo.friendlyModeName)} '
        'mode is not supported for ${device.name}.',
      );
464
    }
465

466
    // We change the current working directory for the duration of the `start` command.
467 468
    final Directory cwd = globals.fs.currentDirectory;
    globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
469
    final FlutterProject flutterProject = FlutterProject.current();
470

471
    final FlutterDevice flutterDevice = await FlutterDevice.create(
472
      device,
473
      target: target,
474
      buildInfo: options.buildInfo,
475
      platform: globals.platform,
476
    );
477

478 479
    ResidentRunner runner;

480
    if (await device.targetPlatform == TargetPlatform.web_javascript) {
481
      runner = webRunnerFactory.createWebRunner(
482
        flutterDevice,
483 484
        flutterProject: flutterProject,
        target: target,
485 486
        debuggingOptions: options,
        ipv6: ipv6,
487
        stayResident: true,
488
        urlTunneller: options.webEnableExposeUrl ? daemon.daemonDomain.exposeUrl : null,
489
        machine: machine,
490 491 492 493
        usage: globals.flutterUsage,
        systemClock: globals.systemClock,
        logger: globals.logger,
        fileSystem: globals.fs,
494 495
      );
    } else if (enableHotReload) {
496
      runner = HotRunner(
497
        <FlutterDevice>[flutterDevice],
498 499
        target: target,
        debuggingOptions: options,
500 501
        applicationBinary: applicationBinary,
        projectRootPath: projectRootPath,
502
        dillOutputPath: dillOutputPath,
503
        ipv6: ipv6,
504
        hostIsIde: true,
505
        machine: machine,
506 507
      );
    } else {
508
      runner = ColdRunner(
509
        <FlutterDevice>[flutterDevice],
510 511
        target: target,
        debuggingOptions: options,
512
        applicationBinary: applicationBinary,
513
        ipv6: ipv6,
514
        machine: machine,
515 516
      );
    }
517

518
    return launch(
519 520 521 522 523 524 525 526
      runner,
      ({
        Completer<DebugConnectionInfo> connectionInfoCompleter,
        Completer<void> appStartedCompleter,
      }) {
        return runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
527
          enableDevTools: true,
528 529 530 531 532 533 534
          route: route,
        );
      },
      device,
      projectDirectory,
      enableHotReload,
      cwd,
535
      LaunchMode.run,
536
      asLogger<AppRunLogger>(globals.logger),
537
    );
538
  }
539

540
  Future<AppInstance> launch(
541
    ResidentRunner runner,
542
    RunOrAttach runOrAttach,
543 544 545 546 547
    Device device,
    String projectDirectory,
    bool enableHotReload,
    Directory cwd,
    LaunchMode launchMode,
548
    AppRunLogger logger,
549
  ) async {
550
    final AppInstance app = AppInstance(_getNewAppId(),
551
        runner: runner, logToStdout: daemon.logToStdout, logger: logger);
552
    _apps.add(app);
553 554 555 556 557 558

    // Set the domain and app for the given AppRunLogger. This allows the logger
    // to log messages containing the app ID to the host.
    logger.domain = this;
    logger.app = app;

559
    _sendAppEvent(app, 'start', <String, dynamic>{
560
      'deviceId': device.id,
561
      'directory': projectDirectory,
562
      'supportsRestart': isRestartSupported(enableHotReload, device),
563
      'launchMode': launchMode.toString(),
564 565
    });

566
    Completer<DebugConnectionInfo> connectionInfoCompleter;
567

568
    if (runner.debuggingEnabled) {
569
      connectionInfoCompleter = Completer<DebugConnectionInfo>();
570 571
      // We don't want to wait for this future to complete and callbacks won't fail.
      // As it just writes to stdout.
572 573 574
      unawaited(connectionInfoCompleter.future.then<void>(
        (DebugConnectionInfo info) {
          final Map<String, dynamic> params = <String, dynamic>{
575 576
            // The web vmservice proxy does not have an http address.
            'port': info.httpUri?.port ?? info.wsUri.port,
577 578
            'wsUri': info.wsUri.toString(),
          };
579
          if (info.baseUri != null) {
580
            params['baseUri'] = info.baseUri;
581
          }
582 583 584
          _sendAppEvent(app, 'debugPort', params);
        },
      ));
585
    }
586
    final Completer<void> appStartedCompleter = Completer<void>();
587 588
    // We don't want to wait for this future to complete, and callbacks won't fail,
    // as it just writes to stdout.
589 590 591
    unawaited(appStartedCompleter.future.then<void>((void value) {
      _sendAppEvent(app, 'started');
    }));
592

593
    await app._runInZone<void>(this, () async {
594
      try {
595
        await runOrAttach(
596 597 598
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
        );
599
        _sendAppEvent(app, 'stop');
600
      } on Exception catch (error, trace) {
601 602 603 604
        _sendAppEvent(app, 'stop', <String, dynamic>{
          'error': _toJsonable(error),
          'trace': '$trace',
        });
605
      } finally {
606 607 608
        // If the full directory is used instead of the path then this causes
        // a TypeError with the ErrorHandlingFileSystem.
        globals.fs.currentDirectory = cwd.path;
609
        _apps.remove(app);
610
      }
611
    });
612
    return app;
Devon Carew's avatar
Devon Carew committed
613 614
  }

615
  bool isRestartSupported(bool enableHotReload, Device device) =>
616
      enableHotReload && device.supportsHotRestart;
617

618
  final int _hotReloadDebounceDurationMs = 50;
619

Devon Carew's avatar
Devon Carew committed
620
  Future<OperationResult> restart(Map<String, dynamic> args) async {
621 622 623
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
624
    final String restartReason = _getStringArg(args, 'reason');
625 626 627
    final bool debounce = _getBoolArg(args, 'debounce') ?? false;
    // This is an undocumented parameter used for integration tests.
    final int debounceDurationOverrideMs = _getIntArg(args, 'debounceDurationOverrideMs');
628

629
    final AppInstance app = _getApp(appId);
630
    if (app == null) {
631
      throw "app '$appId' not found";
632
    }
633

634 635 636 637 638 639 640 641 642 643 644 645
    return _queueAndDebounceReloadAction(
      app,
      fullRestart ? OperationType.restart: OperationType.reload,
      debounce,
      debounceDurationOverrideMs,
      () {
        return app.restart(
            fullRestart: fullRestart,
            pause: pauseAfterRestart,
            reason: restartReason);
      },
    );
646
  }
647

648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  /// Debounce and queue reload actions.
  ///
  /// Only one reload action will run at a time. Actions requested in quick
  /// succession (within [_hotReloadDebounceDuration]) will be merged together
  /// and all return the same result. If an action is requested after an identical
  /// action has already started, it will be queued and run again once the first
  /// action completes.
  Future<OperationResult> _queueAndDebounceReloadAction(
    AppInstance app,
    OperationType operationType,
    bool debounce,
    int debounceDurationOverrideMs,
    Future<OperationResult> Function() action,
  ) {
    final Duration debounceDuration = debounce
        ? Duration(milliseconds: debounceDurationOverrideMs ?? _hotReloadDebounceDurationMs)
        : Duration.zero;

    return operationQueue.queueAndDebounce(
      operationType,
      debounceDuration,
      () => app._runInZone<OperationResult>(this, action),
    );
671 672
  }

673 674 675 676 677 678 679 680 681 682
  /// Returns an error, or the service extension result (a map with two fixed
  /// keys, `type` and `method`). The result may have one or more additional keys,
  /// depending on the specific service extension end-point. For example:
  ///
  ///     {
  ///       "value":"android",
  ///       "type":"_extensionType",
  ///       "method":"ext.flutter.platformOverride"
  ///     }
  Future<Map<String, dynamic>> callServiceExtension(Map<String, dynamic> args) async {
683 684
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
685
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
686

687
    final AppInstance app = _getApp(appId);
688
    if (app == null) {
689
      throw "app '$appId' not found";
690
    }
691 692 693 694 695 696 697 698 699 700
    final FlutterDevice device = app.runner.flutterDevices.first;
    final List<FlutterView> views = await device.vmService.getFlutterViews();
    final Map<String, dynamic> result = await device
      .vmService
      .invokeFlutterExtensionRpcRaw(
        methodName,
        args: params,
        isolateId: views
          .first.uiIsolate.id
      );
701
    if (result == null) {
702
      throw 'method not available: $methodName';
703
    }
704

705
    if (result.containsKey('error')) {
706
      throw result['error'];
707
    }
708 709

    return result;
710 711
  }

712
  Future<bool> stop(Map<String, dynamic> args) async {
713
    final String appId = _getStringArg(args, 'appId', required: true);
714

715
    final AppInstance app = _getApp(appId);
716
    if (app == null) {
717
      throw "app '$appId' not found";
718
    }
719

720 721 722
    return app.stop().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
723
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
724 725 726 727 728
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
729 730
  }

731 732 733 734
  Future<bool> detach(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);

    final AppInstance app = _getApp(appId);
735
    if (app == null) {
736
      throw "app '$appId' not found";
737
    }
738

739 740 741
    return app.detach().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
742
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
743 744 745 746 747
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
748 749
  }

750 751 752 753
  AppInstance _getApp(String id) {
    return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
  }

754
  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
755 756 757 758
    sendEvent('app.$name', <String, dynamic>{
      'appId': app.id,
      ...?args,
    });
Devon Carew's avatar
Devon Carew committed
759 760 761
  }
}

762
typedef _DeviceEventHandler = void Function(Device device);
763

764 765
/// This domain lets callers list and monitor connected devices.
///
766 767
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
768 769 770
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
771 772
    registerHandler('enable', enable);
    registerHandler('disable', disable);
773 774
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
775

776 777
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
778
    globals.deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
779
  }
780

781
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
782
    if (!discoverer.supportsPlatform) {
783
      return;
784
    }
785

786
    if (discoverer is PollingDeviceDiscovery) {
787
      _discoverers.add(discoverer);
788 789 790
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
791 792
  }

793
  Future<void> _serializeDeviceEvents = Future<void>.value();
794

795 796
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
797
      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
798 799 800
        try {
          final Map<String, Object> response = await _deviceToMap(device);
          sendEvent(eventName, response);
801
        } on Exception catch (err) {
802
          globals.printError('$err');
803
        }
804 805 806 807
      });
    };
  }

808
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
809

810 811
  /// Return a list of the current devices, with each device represented as a map
  /// of properties (id, name, platform, ...).
812
  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
813
    return <Map<String, dynamic>>[
814 815
      for (final PollingDeviceDiscovery discoverer in _discoverers)
        for (final Device device in await discoverer.devices)
816 817
          await _deviceToMap(device),
    ];
818 819
  }

820
  /// Enable device events.
821
  Future<void> enable(Map<String, dynamic> args) async {
822
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
823
      discoverer.startPolling();
824
    }
825 826
  }

827
  /// Disable device events.
828
  Future<void> disable(Map<String, dynamic> args) async {
829
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
830
      discoverer.stopPolling();
831
    }
832 833
  }

834 835
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
836 837
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
838
    int hostPort = _getIntArg(args, 'hostPort');
839

840
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
841
    if (device == null) {
842
      throw "device '$deviceId' not found";
843
    }
844 845 846

    hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort);

847
    return <String, dynamic>{'hostPort': hostPort};
848 849 850
  }

  /// Removes a forwarded port.
851
  Future<void> unforward(Map<String, dynamic> args) async {
852 853 854
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
855

856
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
857
    if (device == null) {
858
      throw "device '$deviceId' not found";
859
    }
860

861
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
862 863
  }

864
  @override
865
  Future<void> dispose() {
866
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
867
      discoverer.dispose();
868
    }
869
    return Future<void>.value();
870 871 872
  }

  /// Return the device matching the deviceId field in the args.
873
  Future<Device> _getDevice(String deviceId) async {
874
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
875
      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
876
      if (device != null) {
877
        return device;
878
      }
879 880
    }
    return null;
881 882 883
  }
}

884 885 886 887 888 889 890
class DevToolsDomain extends Domain {
  DevToolsDomain(Daemon daemon) : super(daemon, 'devtools') {
    registerHandler('serve', serve);
  }

  DevtoolsLauncher _devtoolsLauncher;

891
  Future<Map<String, dynamic>> serve([ Map<String, dynamic> args ]) async {
892
    _devtoolsLauncher ??= DevtoolsLauncher.instance;
893
    final DevToolsServerAddress server = await _devtoolsLauncher.serve();
894
    return<String, dynamic>{
895 896
      'host': server?.host,
      'port': server?.port,
897
    };
898 899 900 901 902 903 904 905
  }

  @override
  Future<void> dispose() async {
    await _devtoolsLauncher?.close();
  }
}

906
Stream<Map<String, dynamic>> get stdinCommandStream => globals.stdio.stdin
907 908
  .transform<String>(utf8.decoder)
  .transform<String>(const LineSplitter())
909
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
910
  .map<Map<String, dynamic>>((String line) {
911
    line = line.substring(1, line.length - 1);
912
    return castStringKeyedMap(json.decode(line));
913 914 915
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
916
  globals.stdio.stdoutWrite(
917 918 919 920 921
    '[${jsonEncodeObject(command)}]\n',
    fallback: (String message, dynamic error, StackTrace stack) {
      throwToolExit('Failed to write daemon command response to stdout: $error');
    },
  );
922 923
}

924
String jsonEncodeObject(dynamic object) {
925
  return json.encode(object, toEncodable: _toEncodable);
926 927 928
}

dynamic _toEncodable(dynamic object) {
929
  if (object is OperationResult) {
930
    return _operationResultToMap(object);
931
  }
932 933 934
  return object;
}

935
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
936
  return <String, dynamic>{
937
    'id': device.id,
938
    'name': device.name,
939
    'platform': getNameForTargetPlatform(await device.targetPlatform),
940
    'emulator': await device.isLocalEmulator,
941 942 943
    'category': device.category?.toString(),
    'platformType': device.platformType?.toString(),
    'ephemeral': device.ephemeral,
944
    'emulatorId': await device.emulatorId,
945 946 947
  };
}

948 949 950 951
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
952
    'category': emulator.category?.toString(),
953
    'platformType': emulator.platformType?.toString(),
954 955 956
  };
}

Devon Carew's avatar
Devon Carew committed
957
Map<String, dynamic> _operationResultToMap(OperationResult result) {
958
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
959
    'code': result.code,
960
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
961 962 963
  };
}

Devon Carew's avatar
Devon Carew committed
964
dynamic _toJsonable(dynamic obj) {
965
  if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null) {
Devon Carew's avatar
Devon Carew committed
966
    return obj;
967 968
  }
  if (obj is OperationResult) {
Devon Carew's avatar
Devon Carew committed
969
    return obj;
970 971
  }
  if (obj is ToolExit) {
972
    return obj.message;
973
  }
Hixie's avatar
Hixie committed
974
  return '$obj';
Devon Carew's avatar
Devon Carew committed
975
}
976

977 978
class NotifyingLogger extends DelegatingLogger {
  NotifyingLogger({ @required this.verbose, @required Logger parent }) : super(parent) {
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
    _messageController = StreamController<LogMessage>.broadcast(
      onListen: _onListen,
    );
  }

  final bool verbose;
  final List<LogMessage> messageBuffer = <LogMessage>[];
  StreamController<LogMessage> _messageController;

  void _onListen() {
    if (messageBuffer.isNotEmpty) {
      messageBuffer.forEach(_messageController.add);
      messageBuffer.clear();
    }
  }
994 995 996

  Stream<LogMessage> get onMessage => _messageController.stream;

997
  @override
998
  void printError(
999 1000 1001 1002 1003 1004 1005 1006
    String message, {
    StackTrace stackTrace,
    bool emphasis = false,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1007
    _sendMessage(LogMessage('error', message, stackTrace));
1008 1009
  }

1010
  @override
1011
  void printStatus(
1012 1013 1014 1015 1016 1017 1018 1019
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1020
    _sendMessage(LogMessage('status', message));
1021 1022
  }

1023
  @override
1024
  void printTrace(String message) {
1025 1026 1027
    if (!verbose) {
      return;
    }
1028
    super.printError(message);
1029
  }
1030 1031

  @override
1032 1033
  Status startProgress(
    String message, {
1034
    @required Duration timeout,
1035
    String progressId,
1036
    bool multilineOutput = false,
1037
    bool includeTiming = true,
1038
    int progressIndicatorPadding = kDefaultStatusPadding,
1039
  }) {
1040
    assert(timeout != null);
1041
    printStatus(message);
1042 1043 1044
    return SilentStatus(
      stopwatch: Stopwatch(),
    );
1045
  }
1046

1047 1048 1049 1050 1051 1052 1053
  void _sendMessage(LogMessage logMessage) {
    if (_messageController.hasListener) {
      return _messageController.add(logMessage);
    }
    messageBuffer.add(logMessage);
  }

1054 1055 1056
  void dispose() {
    _messageController.close();
  }
1057 1058

  @override
1059
  void sendEvent(String name, [Map<String, dynamic> args]) { }
1060 1061 1062 1063 1064 1065

  @override
  bool get supportsColor => throw UnimplementedError();

  @override
  bool get hasTerminal => false;
1066 1067 1068 1069

  // This method is only relevant for terminals.
  @override
  void clear() { }
1070 1071
}

1072 1073
/// A running application, started by this daemon.
class AppInstance {
1074 1075
  AppInstance(this.id, { this.runner, this.logToStdout = false, @required AppRunLogger logger })
    : _logger = logger;
1076 1077

  final String id;
1078
  final ResidentRunner runner;
1079
  final bool logToStdout;
1080
  final AppRunLogger _logger;
1081

1082 1083
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
1084
  }
1085

1086
  Future<void> stop() => runner.exit();
1087
  Future<void> detach() => runner.detach();
1088 1089 1090 1091 1092

  void closeLogger() {
    _logger.close();
  }

1093
  Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> Function() method) async {
1094
    return method();
1095 1096 1097
  }
}

1098 1099 1100 1101 1102
/// This domain responds to methods like [getEmulators] and [launch].
class EmulatorDomain extends Domain {
  EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') {
    registerHandler('getEmulators', getEmulators);
    registerHandler('launch', launch);
1103
    registerHandler('create', create);
1104 1105
  }

1106 1107 1108 1109 1110 1111 1112
  EmulatorManager emulators = EmulatorManager(
    fileSystem: globals.fs,
    logger: globals.logger,
    androidSdk: globals.androidSdk,
    processManager: globals.processManager,
    androidWorkflow: androidWorkflow,
  );
1113

1114
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
1115
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
1116
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
1117 1118
  }

1119
  Future<void> launch(Map<String, dynamic> args) async {
1120
    final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
1121
    final bool coldBoot = _getBoolArg(args, 'coldBoot') ?? false;
1122 1123 1124 1125 1126 1127 1128
    final List<Emulator> matches =
        await emulators.getEmulatorsMatching(emulatorId);
    if (matches.isEmpty) {
      throw "emulator '$emulatorId' not found";
    } else if (matches.length > 1) {
      throw "multiple emulators match '$emulatorId'";
    } else {
1129
      await matches.first.launch(coldBoot: coldBoot);
1130 1131
    }
  }
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141

  Future<Map<String, dynamic>> create(Map<String, dynamic> args) async {
    final String name = _getStringArg(args, 'name', required: false);
    final CreateEmulatorResult res = await emulators.createEmulator(name: name);
    return <String, dynamic>{
      'success': res.success,
      'emulatorName': res.emulatorName,
      'error': res.error,
    };
  }
1142 1143
}

1144
/// A [Logger] which sends log messages to a listening daemon client.
1145 1146 1147 1148
///
/// This class can either:
///   1) Send stdout messages and progress events to the client IDE
///   1) Log messages to stdout and send progress events to the client IDE
1149 1150 1151
//
// TODO(devoncarew): To simplify this code a bit, we could choose to specialize
// this class into two, one for each of the above use cases.
1152 1153
class AppRunLogger extends DelegatingLogger {
  AppRunLogger({ @required Logger parent }) : super(parent);
1154 1155

  AppDomain domain;
1156
  AppInstance app;
1157
  int _nextProgressId = 0;
1158

Devon Carew's avatar
Devon Carew committed
1159 1160
  Status _status;

1161
  @override
1162 1163
  Status startProgress(
    String message, {
1164
    @required Duration timeout,
1165
    String progressId,
1166
    bool multilineOutput = false,
1167
    bool includeTiming = true,
1168
    int progressIndicatorPadding = kDefaultStatusPadding,
1169
  }) {
1170
    final int id = _nextProgressId++;
1171

1172 1173 1174 1175 1176
    _sendProgressEvent(
      eventId: id.toString(),
      eventType: progressId,
      message: message,
    );
1177

1178 1179 1180
    _status = SilentStatus(
      onFinish: () {
        _status = null;
1181 1182 1183 1184 1185
        _sendProgressEvent(
          eventId: id.toString(),
          eventType: progressId,
          finished: true,
        );
1186
      }, stopwatch: Stopwatch())..start();
Devon Carew's avatar
Devon Carew committed
1187
    return _status;
1188 1189 1190 1191 1192
  }

  void close() {
    domain = null;
  }
1193

1194 1195 1196 1197 1198 1199
  void _sendProgressEvent({
    @required String eventId,
    @required String eventType,
    bool finished = false,
    String message,
  }) {
1200
    if (domain == null) {
1201 1202 1203 1204 1205
      // If we're sending progress events before an app has started, send the
      // progress messages as plain status messages.
      if (message != null) {
        printStatus(message);
      }
1206
    } else {
1207 1208 1209 1210 1211 1212 1213
      final Map<String, dynamic> event = <String, dynamic>{
        'id': eventId,
        'progressId': eventType,
        if (message != null) 'message': message,
        if (finished != null) 'finished': finished,
      };

1214
      domain._sendAppEvent(app, 'progress', event);
1215
    }
1216
  }
1217 1218

  @override
1219 1220 1221 1222 1223 1224
  void sendEvent(String name, [Map<String, dynamic> args]) {
    if (domain == null) {
      printStatus('event sent after app closed: $name');
    } else {
      domain.sendEvent(name, args);
    }
1225
  }
1226 1227 1228 1229 1230 1231

  @override
  bool get supportsColor => throw UnimplementedError();

  @override
  bool get hasTerminal => false;
1232 1233 1234 1235

  // This method is only relevant for terminals.
  @override
  void clear() { }
1236 1237
}

1238
class LogMessage {
1239 1240
  LogMessage(this.level, this.message, [this.stackTrace]);

1241 1242 1243 1244
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260

/// The method by which the flutter app was launched.
class LaunchMode {
  const LaunchMode._(this._value);

  /// The app was launched via `flutter run`.
  static const LaunchMode run = LaunchMode._('run');

  /// The app was launched via `flutter attach`.
  static const LaunchMode attach = LaunchMode._('attach');

  final String _value;

  @override
  String toString() => _value;
}
1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313

enum OperationType {
  reload,
  restart
}

/// A queue that debounces operations for a period and merges operations of the same type.
/// Only one action (or any type) will run at a time. Actions of the same type requested
/// in quick succession will be merged together and all return the same result. If an action
/// is requested after an identical action has already started, it will be queued
/// and run again once the first action completes.
class DebounceOperationQueue<T, K> {
  final Map<K, RestartableTimer> _debounceTimers = <K, RestartableTimer>{};
  final Map<K, Future<T>> _operationQueue = <K, Future<T>>{};
  Future<void> _inProgressAction;

  Future<T> queueAndDebounce(
    K operationType,
    Duration debounceDuration,
    Future<T> Function() action,
  ) {
    // If there is already an operation of this type waiting to run, reset its
    // debounce timer and return its future.
    if (_operationQueue[operationType] != null) {
      _debounceTimers[operationType]?.reset();
      return _operationQueue[operationType];
    }

    // Otherwise, put one in the queue with a timer.
    final Completer<T> completer = Completer<T>();
    _operationQueue[operationType] = completer.future;
    _debounceTimers[operationType] = RestartableTimer(
      debounceDuration,
      () async {
        // Remove us from the queue so we can't be reset now we've started.
        unawaited(_operationQueue.remove(operationType));
        _debounceTimers.remove(operationType);

        // No operations should be allowed to run concurrently even if they're
        // different types.
        while (_inProgressAction != null) {
          await _inProgressAction;
        }

        _inProgressAction = action()
            .then(completer.complete, onError: completer.completeError)
            .whenComplete(() => _inProgressAction = null);
      },
    );

    return completer.future;
  }
}