daemon.dart 39.4 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_null_migrated.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 53 54
  @override
  final String category = FlutterCommandCategory.tools;

55
  @override
56
  final bool hidden;
57

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

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

75
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
76 77

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

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

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

Devon Carew's avatar
Devon Carew committed
111
  final DispatchCommand sendCommand;
112
  final NotifyingLogger notifyingLogger;
113
  final bool logToStdout;
114

115
  final Completer<int> _onExitCompleter = Completer<int>();
116 117
  final Map<String, Domain> _domainMap = <String, Domain>{};

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

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

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

    // [id] is an opaque type to us.
128
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
129 130

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

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

142 143 144 145 146 147 148 149 150 151 152 153 154 155
        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
156

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

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
  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
188
  void _send(Map<String, dynamic> map) => sendCommand(map);
Devon Carew's avatar
Devon Carew committed
189

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

abstract class Domain {
207 208
  Domain(this.daemon, this.name);

209

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

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

218
  @override
Devon Carew's avatar
Devon Carew committed
219 220
  String toString() => name;

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

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

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

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

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

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

285
  Future<void> dispose() async { }
Devon Carew's avatar
Devon Carew committed
286 287 288
}

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

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

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

337
  StreamSubscription<LogMessage> _subscription;
338

339
  Future<String> version(Map<String, dynamic> args) {
340
    return Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
341 342
  }

343 344 345 346 347 348 349 350 351 352
  /// 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 {
353
      globals.printError('Invalid response to exposeUrl - params should include a String url field');
354 355 356 357
      return url;
    }
  }

358
  Future<void> shutdown(Map<String, dynamic> args) {
359
    Timer.run(daemon.shutdown);
360
    return Future<void>.value();
Devon Carew's avatar
Devon Carew committed
361
  }
362

363
  @override
364 365
  Future<void> dispose() async {
    await _subscription?.cancel();
366
  }
367 368 369 370 371 372 373 374 375 376

  /// 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 {
377
      final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
378
      if (featureFlags.isLinuxEnabled && flutterProject.linux.existsSync()) {
379 380
        result.add('linux');
      }
381
      if (featureFlags.isMacOSEnabled && flutterProject.macos.existsSync()) {
382 383
        result.add('macos');
      }
384
      if (featureFlags.isWindowsEnabled && flutterProject.windows.existsSync()) {
385 386
        result.add('windows');
      }
387
      if (featureFlags.isIOSEnabled && flutterProject.ios.existsSync()) {
388 389
        result.add('ios');
      }
390
      if (featureFlags.isAndroidEnabled && flutterProject.android.existsSync()) {
391 392
        result.add('android');
      }
393
      if (featureFlags.isWebEnabled && flutterProject.web.existsSync()) {
394 395
        result.add('web');
      }
396
      if (featureFlags.isFuchsiaEnabled && flutterProject.fuchsia.existsSync()) {
397 398
        result.add('fuchsia');
      }
399 400 401
      if (featureFlags.areCustomDevicesEnabled) {
        result.add('custom');
      }
402 403 404
      return <String, Object>{
        'platforms': result,
      };
405
    } on Exception catch (err, stackTrace) {
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
      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
421 422
}

423
typedef RunOrAttach = Future<void> Function({
424
  Completer<DebugConnectionInfo> connectionInfoCompleter,
425
  Completer<void> appStartedCompleter,
426 427
});

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

439
  static const Uuid _uuidGenerator = Uuid();
440

441
  static String _getNewAppId() => _uuidGenerator.v4();
442

443
  final List<AppInstance> _apps = <AppInstance>[];
444

445 446
  final DebounceOperationQueue<OperationResult, OperationType> operationQueue = DebounceOperationQueue<OperationResult, OperationType>();

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

470
    // We change the current working directory for the duration of the `start` command.
471 472
    final Directory cwd = globals.fs.currentDirectory;
    globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
473
    final FlutterProject flutterProject = FlutterProject.current();
474

475
    final FlutterDevice flutterDevice = await FlutterDevice.create(
476
      device,
477
      target: target,
478
      buildInfo: options.buildInfo,
479
      platform: globals.platform,
480
    );
481

482 483
    ResidentRunner runner;

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

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

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

    // 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;

563
    _sendAppEvent(app, 'start', <String, dynamic>{
564
      'deviceId': device.id,
565
      'directory': projectDirectory,
566
      'supportsRestart': isRestartSupported(enableHotReload, device),
567
      'launchMode': launchMode.toString(),
568 569
    });

570
    Completer<DebugConnectionInfo> connectionInfoCompleter;
571

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

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

619
  bool isRestartSupported(bool enableHotReload, Device device) =>
620
      enableHotReload && device.supportsHotRestart;
621

622
  final int _hotReloadDebounceDurationMs = 50;
623

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

633
    final AppInstance app = _getApp(appId);
634
    if (app == null) {
635
      throw "app '$appId' not found";
636
    }
637

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

652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
  /// 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),
    );
675 676
  }

677 678 679 680 681 682 683 684 685 686
  /// 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 {
687 688
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
689
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
690

691
    final AppInstance app = _getApp(appId);
692
    if (app == null) {
693
      throw "app '$appId' not found";
694
    }
695 696 697 698 699 700 701 702 703 704
    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
      );
705
    if (result == null) {
706
      throw 'method not available: $methodName';
707
    }
708

709
    if (result.containsKey('error')) {
710
      throw result['error'];
711
    }
712 713

    return result;
714 715
  }

716
  Future<bool> stop(Map<String, dynamic> args) async {
717
    final String appId = _getStringArg(args, 'appId', required: true);
718

719
    final AppInstance app = _getApp(appId);
720
    if (app == null) {
721
      throw "app '$appId' not found";
722
    }
723

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

735 736 737 738
  Future<bool> detach(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);

    final AppInstance app = _getApp(appId);
739
    if (app == null) {
740
      throw "app '$appId' not found";
741
    }
742

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

754 755 756 757
  AppInstance _getApp(String id) {
    return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
  }

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

766
typedef _DeviceEventHandler = void Function(Device device);
767

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

780 781
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
782
    globals.deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
783
  }
784

785
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
786
    if (!discoverer.supportsPlatform) {
787
      return;
788
    }
789

790
    if (discoverer is PollingDeviceDiscovery) {
791
      _discoverers.add(discoverer);
792 793 794
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
795 796
  }

797
  Future<void> _serializeDeviceEvents = Future<void>.value();
798

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

812
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
813

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

824
  /// Enable device events.
825
  Future<void> enable(Map<String, dynamic> args) async {
826
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
827
      discoverer.startPolling();
828
    }
829 830
  }

831
  /// Disable device events.
832
  Future<void> disable(Map<String, dynamic> args) async {
833
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
834
      discoverer.stopPolling();
835
    }
836 837
  }

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

844
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
845
    if (device == null) {
846
      throw "device '$deviceId' not found";
847
    }
848 849 850

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

851
    return <String, dynamic>{'hostPort': hostPort};
852 853 854
  }

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

860
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
861
    if (device == null) {
862
      throw "device '$deviceId' not found";
863
    }
864

865
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
866 867
  }

868
  @override
869
  Future<void> dispose() {
870
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
871
      discoverer.dispose();
872
    }
873
    return Future<void>.value();
874 875 876
  }

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

888 889 890 891 892 893 894
class DevToolsDomain extends Domain {
  DevToolsDomain(Daemon daemon) : super(daemon, 'devtools') {
    registerHandler('serve', serve);
  }

  DevtoolsLauncher _devtoolsLauncher;

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

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

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

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

928
String jsonEncodeObject(dynamic object) {
929
  return json.encode(object, toEncodable: _toEncodable);
930 931 932
}

dynamic _toEncodable(dynamic object) {
933
  if (object is OperationResult) {
934
    return _operationResultToMap(object);
935
  }
936 937 938
  return object;
}

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

952 953 954 955
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
956
    'category': emulator.category?.toString(),
957
    'platformType': emulator.platformType?.toString(),
958 959 960
  };
}

Devon Carew's avatar
Devon Carew committed
961
Map<String, dynamic> _operationResultToMap(OperationResult result) {
962
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
963
    'code': result.code,
964
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
965 966 967
  };
}

Devon Carew's avatar
Devon Carew committed
968
dynamic _toJsonable(dynamic obj) {
969
  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
970
    return obj;
971 972
  }
  if (obj is OperationResult) {
Devon Carew's avatar
Devon Carew committed
973
    return obj;
974 975
  }
  if (obj is ToolExit) {
976
    return obj.message;
977
  }
Hixie's avatar
Hixie committed
978
  return '$obj';
Devon Carew's avatar
Devon Carew committed
979
}
980

981 982
class NotifyingLogger extends DelegatingLogger {
  NotifyingLogger({ @required this.verbose, @required Logger parent }) : super(parent) {
983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
    _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();
    }
  }
998 999 1000

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

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

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

1027
  @override
1028
  void printTrace(String message) {
1029 1030 1031
    if (!verbose) {
      return;
    }
1032
    super.printError(message);
1033
  }
1034 1035

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

1051 1052 1053 1054 1055 1056 1057
  void _sendMessage(LogMessage logMessage) {
    if (_messageController.hasListener) {
      return _messageController.add(logMessage);
    }
    messageBuffer.add(logMessage);
  }

1058 1059 1060
  void dispose() {
    _messageController.close();
  }
1061 1062

  @override
1063
  void sendEvent(String name, [Map<String, dynamic> args]) { }
1064 1065 1066 1067 1068 1069

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

  @override
  bool get hasTerminal => false;
1070 1071 1072 1073

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

1076 1077
/// A running application, started by this daemon.
class AppInstance {
1078 1079
  AppInstance(this.id, { this.runner, this.logToStdout = false, @required AppRunLogger logger })
    : _logger = logger;
1080 1081

  final String id;
1082
  final ResidentRunner runner;
1083
  final bool logToStdout;
1084
  final AppRunLogger _logger;
1085

1086 1087
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
1088
  }
1089

1090
  Future<void> stop() => runner.exit();
1091
  Future<void> detach() => runner.detach();
1092 1093 1094 1095 1096

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

1097
  Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> Function() method) async {
1098
    return method();
1099 1100 1101
  }
}

1102 1103 1104 1105 1106
/// 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);
1107
    registerHandler('create', create);
1108 1109
  }

1110 1111 1112 1113 1114 1115 1116
  EmulatorManager emulators = EmulatorManager(
    fileSystem: globals.fs,
    logger: globals.logger,
    androidSdk: globals.androidSdk,
    processManager: globals.processManager,
    androidWorkflow: androidWorkflow,
  );
1117

1118
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
1119
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
1120
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
1121 1122
  }

1123
  Future<void> launch(Map<String, dynamic> args) async {
1124
    final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
1125
    final bool coldBoot = _getBoolArg(args, 'coldBoot') ?? false;
1126 1127 1128 1129 1130 1131 1132
    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 {
1133
      await matches.first.launch(coldBoot: coldBoot);
1134 1135
    }
  }
1136 1137

  Future<Map<String, dynamic>> create(Map<String, dynamic> args) async {
1138
    final String name = _getStringArg(args, 'name');
1139 1140 1141 1142 1143 1144 1145
    final CreateEmulatorResult res = await emulators.createEmulator(name: name);
    return <String, dynamic>{
      'success': res.success,
      'emulatorName': res.emulatorName,
      'error': res.error,
    };
  }
1146 1147
}

1148
/// A [Logger] which sends log messages to a listening daemon client.
1149 1150 1151 1152
///
/// 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
1153 1154 1155
//
// 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.
1156 1157
class AppRunLogger extends DelegatingLogger {
  AppRunLogger({ @required Logger parent }) : super(parent);
1158 1159

  AppDomain domain;
1160
  AppInstance app;
1161
  int _nextProgressId = 0;
1162

Devon Carew's avatar
Devon Carew committed
1163 1164
  Status _status;

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

1176 1177 1178 1179 1180
    _sendProgressEvent(
      eventId: id.toString(),
      eventType: progressId,
      message: message,
    );
1181

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

  void close() {
    domain = null;
  }
1197

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

1218
      domain._sendAppEvent(app, 'progress', event);
1219
    }
1220
  }
1221 1222

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

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

  @override
  bool get hasTerminal => false;
1236 1237 1238 1239

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

1242
class LogMessage {
1243 1244
  LogMessage(this.level, this.message, [this.stackTrace]);

1245 1246 1247 1248
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264

/// 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;
}
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 1314 1315 1316 1317

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;
  }
}