daemon.dart 38.5 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 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:async/async.dart';
8
import 'package:meta/meta.dart';
9
import 'package:uuid/uuid.dart';
10

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

30
const String protocolVersion = '0.6.0';
31

Devon Carew's avatar
Devon Carew committed
32 33 34 35 36 37 38
/// 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 {
39
  DaemonCommand({ this.hidden = false });
40

41
  @override
Devon Carew's avatar
Devon Carew committed
42
  final String name = 'daemon';
43 44

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

47
  @override
48
  final bool hidden;
49

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

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

67
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
68 69

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

    // Start listening.
84
    _commandSubscription = commandStream.listen(
85
      _handleRequest,
86
      onDone: () {
87
        if (!_onExitCompleter.isCompleted) {
88
          _onExitCompleter.complete(0);
89
        }
90
      },
Devon Carew's avatar
Devon Carew committed
91 92 93
    );
  }

94 95 96
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
97
  EmulatorDomain emulatorDomain;
98
  DevToolsDomain devToolsDomain;
99
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
100 101
  int _outgoingRequestId = 1;
  final Map<String, Completer<dynamic>> _outgoingRequestCompleters = <String, Completer<dynamic>>{};
102

Devon Carew's avatar
Devon Carew committed
103
  final DispatchCommand sendCommand;
104
  final NotifyingLogger notifyingLogger;
105
  final bool logToStdout;
106

107
  final Completer<int> _onExitCompleter = Completer<int>();
108 109
  final Map<String, Domain> _domainMap = <String, Domain>{};

Devon Carew's avatar
Devon Carew committed
110
  void _registerDomain(Domain domain) {
111
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
112 113 114 115
  }

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

Ian Hickson's avatar
Ian Hickson committed
116
  void _handleRequest(Map<String, dynamic> request) {
117 118 119
    // {id, method, params}

    // [id] is an opaque type to us.
120
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
121 122

    if (id == null) {
123
      globals.stdio.stderrWrite('no id for request: $request\n');
Devon Carew's avatar
Devon Carew committed
124 125 126 127
      return;
    }

    try {
128
      final String method = request['method'] as String;
129 130 131 132
      if (method != null) {
        if (!method.contains('.')) {
          throw 'method not understood: $method';
        }
Devon Carew's avatar
Devon Carew committed
133

134 135 136 137 138 139 140 141 142 143 144 145 146 147
        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
148

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

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
  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
180
  void _send(Map<String, dynamic> map) => sendCommand(map);
Devon Carew's avatar
Devon Carew committed
181

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

abstract class Domain {
199 200
  Domain(this.daemon, this.name);

201

Devon Carew's avatar
Devon Carew committed
202 203
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
204
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
205 206 207 208 209

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

210
  @override
Devon Carew's avatar
Devon Carew committed
211 212
  String toString() => name;

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

234
  void sendEvent(String name, [ dynamic args ]) {
235
    final Map<String, dynamic> map = <String, dynamic>{'event': name};
236
    if (args != null) {
237
      map['params'] = _toJsonable(args);
238
    }
239 240 241
    _send(map);
  }

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

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

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

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

277
  Future<void> dispose() async { }
Devon Carew's avatar
Devon Carew committed
278 279 280
}

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

289 290 291 292 293 294 295 296
    sendEvent(
      'daemon.connected',
      <String, dynamic>{
        'version': protocolVersion,
        'pid': pid,
      },
    );

297
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
298 299 300 301 302 303
      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') {
304
          globals.stdio.stderrWrite('${message.message}\n');
305
          if (message.stackTrace != null) {
306 307 308
            globals.stdio.stderrWrite(
              '${message.stackTrace.toString().trimRight()}\n',
            );
309
          }
310
        }
311
      } else {
312 313 314 315
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
316
            'stackTrace': message.stackTrace.toString(),
317 318 319 320
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
321
            'message': message.message,
322 323
          });
        }
324 325
      }
    });
Devon Carew's avatar
Devon Carew committed
326 327
  }

328
  StreamSubscription<LogMessage> _subscription;
329

330
  Future<String> version(Map<String, dynamic> args) {
331
    return Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
332 333
  }

334 335 336 337 338 339 340 341 342 343
  /// 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 {
344
      globals.printError('Invalid response to exposeUrl - params should include a String url field');
345 346 347 348
      return url;
    }
  }

349
  Future<void> shutdown(Map<String, dynamic> args) {
350
    Timer.run(daemon.shutdown);
351
    return Future<void>.value();
Devon Carew's avatar
Devon Carew committed
352
  }
353

354
  @override
355 356
  Future<void> dispose() async {
    await _subscription?.cancel();
357
  }
358 359 360 361 362 363 364 365 366 367 368 369

  /// 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 {
      // TODO(jonahwilliams): replace this with a project metadata check once
      // that has been implemented.
370
      final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
      if (flutterProject.linux.existsSync()) {
        result.add('linux');
      }
      if (flutterProject.macos.existsSync()) {
        result.add('macos');
      }
      if (flutterProject.windows.existsSync()) {
        result.add('windows');
      }
      if (flutterProject.ios.existsSync()) {
        result.add('ios');
      }
      if (flutterProject.android.existsSync()) {
        result.add('android');
      }
      if (flutterProject.web.existsSync()) {
        result.add('web');
      }
      if (flutterProject.fuchsia.existsSync()) {
        result.add('fuchsia');
      }
      return <String, Object>{
        'platforms': result,
      };
395
    } on Exception catch (err, stackTrace) {
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
      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
411 412
}

413
typedef _RunOrAttach = Future<void> Function({
414
  Completer<DebugConnectionInfo> connectionInfoCompleter,
415
  Completer<void> appStartedCompleter,
416 417
});

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

429
  static final Uuid _uuidGenerator = Uuid();
430

431
  static String _getNewAppId() => _uuidGenerator.v4();
432

433
  final List<AppInstance> _apps = <AppInstance>[];
434

435 436
  final DebounceOperationQueue<OperationResult, OperationType> operationQueue = DebounceOperationQueue<OperationResult, OperationType>();

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

460
    // We change the current working directory for the duration of the `start` command.
461 462
    final Directory cwd = globals.fs.currentDirectory;
    globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
463
    final FlutterProject flutterProject = FlutterProject.current();
464

465
    final FlutterDevice flutterDevice = await FlutterDevice.create(
466
      device,
467
      target: target,
468
      buildInfo: options.buildInfo,
469
      platform: globals.platform,
470
    );
471

472 473
    ResidentRunner runner;

474
    if (await device.targetPlatform == TargetPlatform.web_javascript) {
475
      runner = webRunnerFactory.createWebRunner(
476
        flutterDevice,
477 478
        flutterProject: flutterProject,
        target: target,
479 480
        debuggingOptions: options,
        ipv6: ipv6,
481
        stayResident: true,
482
        urlTunneller: options.webEnableExposeUrl ? daemon.daemonDomain.exposeUrl : null,
483
        machine: machine,
484 485
      );
    } else if (enableHotReload) {
486
      runner = HotRunner(
487
        <FlutterDevice>[flutterDevice],
488 489
        target: target,
        debuggingOptions: options,
490 491
        applicationBinary: applicationBinary,
        projectRootPath: projectRootPath,
492
        dillOutputPath: dillOutputPath,
493
        ipv6: ipv6,
494
        hostIsIde: true,
495
        machine: machine,
496 497
      );
    } else {
498
      runner = ColdRunner(
499
        <FlutterDevice>[flutterDevice],
500 501
        target: target,
        debuggingOptions: options,
502
        applicationBinary: applicationBinary,
503
        ipv6: ipv6,
504
        machine: machine,
505 506
      );
    }
507

508
    return launch(
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
      runner,
      ({
        Completer<DebugConnectionInfo> connectionInfoCompleter,
        Completer<void> appStartedCompleter,
      }) {
        return runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
      },
      device,
      projectDirectory,
      enableHotReload,
      cwd,
524
      LaunchMode.run,
525
      asLogger<AppRunLogger>(globals.logger),
526
    );
527
  }
528

529
  Future<AppInstance> launch(
530 531 532 533 534 535 536
    ResidentRunner runner,
    _RunOrAttach runOrAttach,
    Device device,
    String projectDirectory,
    bool enableHotReload,
    Directory cwd,
    LaunchMode launchMode,
537
    AppRunLogger logger,
538
  ) async {
539
    final AppInstance app = AppInstance(_getNewAppId(),
540
        runner: runner, logToStdout: daemon.logToStdout, logger: logger);
541
    _apps.add(app);
542 543 544 545 546 547

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

548
    _sendAppEvent(app, 'start', <String, dynamic>{
549
      'deviceId': device.id,
550
      'directory': projectDirectory,
551
      'supportsRestart': isRestartSupported(enableHotReload, device),
552
      'launchMode': launchMode.toString(),
553 554
    });

555
    Completer<DebugConnectionInfo> connectionInfoCompleter;
556

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

582
    await app._runInZone<void>(this, () async {
583
      try {
584
        await runOrAttach(
585 586 587
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
        );
588
        _sendAppEvent(app, 'stop');
589
      } on Exception catch (error, trace) {
590 591 592 593
        _sendAppEvent(app, 'stop', <String, dynamic>{
          'error': _toJsonable(error),
          'trace': '$trace',
        });
594
      } finally {
595 596 597
        // If the full directory is used instead of the path then this causes
        // a TypeError with the ErrorHandlingFileSystem.
        globals.fs.currentDirectory = cwd.path;
598
        _apps.remove(app);
599
      }
600
    });
601
    return app;
Devon Carew's avatar
Devon Carew committed
602 603
  }

604
  bool isRestartSupported(bool enableHotReload, Device device) =>
605
      enableHotReload && device.supportsHotRestart;
606

607
  final int _hotReloadDebounceDurationMs = 50;
608

Devon Carew's avatar
Devon Carew committed
609
  Future<OperationResult> restart(Map<String, dynamic> args) async {
610 611 612
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
613
    final String restartReason = _getStringArg(args, 'reason');
614 615 616
    final bool debounce = _getBoolArg(args, 'debounce') ?? false;
    // This is an undocumented parameter used for integration tests.
    final int debounceDurationOverrideMs = _getIntArg(args, 'debounceDurationOverrideMs');
617

618
    final AppInstance app = _getApp(appId);
619
    if (app == null) {
620
      throw "app '$appId' not found";
621
    }
622

623 624 625 626 627 628 629 630 631 632 633 634
    return _queueAndDebounceReloadAction(
      app,
      fullRestart ? OperationType.restart: OperationType.reload,
      debounce,
      debounceDurationOverrideMs,
      () {
        return app.restart(
            fullRestart: fullRestart,
            pause: pauseAfterRestart,
            reason: restartReason);
      },
    );
635
  }
636

637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
  /// 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),
    );
660 661
  }

662 663 664 665 666 667 668 669 670 671
  /// 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 {
672 673
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
674
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
675

676
    final AppInstance app = _getApp(appId);
677
    if (app == null) {
678
      throw "app '$appId' not found";
679
    }
680

681 682
    final Map<String, dynamic> result = await app.runner
        .invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params);
683
    if (result == null) {
684
      throw 'method not available: $methodName';
685
    }
686

687
    if (result.containsKey('error')) {
688
      throw result['error'];
689
    }
690 691

    return result;
692 693
  }

694
  Future<bool> stop(Map<String, dynamic> args) async {
695
    final String appId = _getStringArg(args, 'appId', required: true);
696

697
    final AppInstance app = _getApp(appId);
698
    if (app == null) {
699
      throw "app '$appId' not found";
700
    }
701

702 703 704
    return app.stop().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
705
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
706 707 708 709 710
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
711 712
  }

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

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

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

732 733 734 735
  AppInstance _getApp(String id) {
    return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
  }

736
  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
737 738 739 740
    sendEvent('app.$name', <String, dynamic>{
      'appId': app.id,
      ...?args,
    });
Devon Carew's avatar
Devon Carew committed
741 742 743
  }
}

744
typedef _DeviceEventHandler = void Function(Device device);
745

746 747
/// This domain lets callers list and monitor connected devices.
///
748 749
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
750 751 752
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
753 754
    registerHandler('enable', enable);
    registerHandler('disable', disable);
755 756
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
757

758 759
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
760
    globals.deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
761
  }
762

763
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
764
    if (!discoverer.supportsPlatform) {
765
      return;
766
    }
767

768
    if (discoverer is PollingDeviceDiscovery) {
769
      _discoverers.add(discoverer);
770 771 772
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
773 774
  }

775
  Future<void> _serializeDeviceEvents = Future<void>.value();
776

777 778
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
779
      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
780 781 782
        try {
          final Map<String, Object> response = await _deviceToMap(device);
          sendEvent(eventName, response);
783
        } on Exception catch (err) {
784
          globals.printError('$err');
785
        }
786 787 788 789
      });
    };
  }

790
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
791

792 793
  /// Return a list of the current devices, with each device represented as a map
  /// of properties (id, name, platform, ...).
794
  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
795
    return <Map<String, dynamic>>[
796 797
      for (final PollingDeviceDiscovery discoverer in _discoverers)
        for (final Device device in await discoverer.devices)
798 799
          await _deviceToMap(device),
    ];
800 801
  }

802
  /// Enable device events.
803
  Future<void> enable(Map<String, dynamic> args) async {
804
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
805
      discoverer.startPolling();
806
    }
807 808
  }

809
  /// Disable device events.
810
  Future<void> disable(Map<String, dynamic> args) async {
811
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
812
      discoverer.stopPolling();
813
    }
814 815
  }

816 817
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
818 819
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
820
    int hostPort = _getIntArg(args, 'hostPort');
821

822
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
823
    if (device == null) {
824
      throw "device '$deviceId' not found";
825
    }
826 827 828

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

829
    return <String, dynamic>{'hostPort': hostPort};
830 831 832
  }

  /// Removes a forwarded port.
833
  Future<void> unforward(Map<String, dynamic> args) async {
834 835 836
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
837

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

843
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
844 845
  }

846
  @override
847
  Future<void> dispose() {
848
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
849
      discoverer.dispose();
850
    }
851
    return Future<void>.value();
852 853 854
  }

  /// Return the device matching the deviceId field in the args.
855
  Future<Device> _getDevice(String deviceId) async {
856
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
857
      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
858
      if (device != null) {
859
        return device;
860
      }
861 862
    }
    return null;
863 864 865
  }
}

866 867 868 869 870 871 872
class DevToolsDomain extends Domain {
  DevToolsDomain(Daemon daemon) : super(daemon, 'devtools') {
    registerHandler('serve', serve);
  }

  DevtoolsLauncher _devtoolsLauncher;

873
  Future<Map<String, dynamic>> serve([ Map<String, dynamic> args ]) async {
874
    _devtoolsLauncher ??= DevtoolsLauncher.instance;
875
    final DevToolsServerAddress server = await _devtoolsLauncher.serve();
876

877
    return<String, dynamic>{
878 879
      'host': server?.host,
      'port': server?.port,
880
    };
881 882 883 884 885 886 887 888
  }

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

889
Stream<Map<String, dynamic>> get stdinCommandStream => globals.stdio.stdin
890 891
  .transform<String>(utf8.decoder)
  .transform<String>(const LineSplitter())
892
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
893
  .map<Map<String, dynamic>>((String line) {
894
    line = line.substring(1, line.length - 1);
895
    return castStringKeyedMap(json.decode(line));
896 897 898
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
899
  globals.stdio.stdoutWrite(
900 901 902 903 904
    '[${jsonEncodeObject(command)}]\n',
    fallback: (String message, dynamic error, StackTrace stack) {
      throwToolExit('Failed to write daemon command response to stdout: $error');
    },
  );
905 906
}

907
String jsonEncodeObject(dynamic object) {
908
  return json.encode(object, toEncodable: _toEncodable);
909 910 911
}

dynamic _toEncodable(dynamic object) {
912
  if (object is OperationResult) {
913
    return _operationResultToMap(object);
914
  }
915 916 917
  return object;
}

918
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
919
  return <String, dynamic>{
920
    'id': device.id,
921
    'name': device.name,
922
    'platform': getNameForTargetPlatform(await device.targetPlatform),
923
    'emulator': await device.isLocalEmulator,
924 925 926
    'category': device.category?.toString(),
    'platformType': device.platformType?.toString(),
    'ephemeral': device.ephemeral,
927
    'emulatorId': await device.emulatorId,
928 929 930
  };
}

931 932 933 934
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
935 936
    'category': emulator.category?.toString(),
    'platformType': emulator.platformType?.toString(),
937 938 939
  };
}

Devon Carew's avatar
Devon Carew committed
940
Map<String, dynamic> _operationResultToMap(OperationResult result) {
941
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
942
    'code': result.code,
943
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
944 945 946
  };
}

Devon Carew's avatar
Devon Carew committed
947
dynamic _toJsonable(dynamic obj) {
948
  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
949
    return obj;
950 951
  }
  if (obj is OperationResult) {
Devon Carew's avatar
Devon Carew committed
952
    return obj;
953 954
  }
  if (obj is ToolExit) {
955
    return obj.message;
956
  }
Hixie's avatar
Hixie committed
957
  return '$obj';
Devon Carew's avatar
Devon Carew committed
958
}
959

960 961
class NotifyingLogger extends DelegatingLogger {
  NotifyingLogger({ @required this.verbose, @required Logger parent }) : super(parent) {
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
    _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();
    }
  }
977 978 979

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

980
  @override
981
  void printError(
982 983 984 985 986 987 988 989
    String message, {
    StackTrace stackTrace,
    bool emphasis = false,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
990
    _sendMessage(LogMessage('error', message, stackTrace));
991 992
  }

993
  @override
994
  void printStatus(
995 996 997 998 999 1000 1001 1002
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1003
    _sendMessage(LogMessage('status', message));
1004 1005
  }

1006
  @override
1007
  void printTrace(String message) {
1008 1009 1010
    if (!verbose) {
      return;
    }
1011
    super.printError(message);
1012
  }
1013 1014

  @override
1015 1016
  Status startProgress(
    String message, {
1017
    @required Duration timeout,
1018
    String progressId,
1019
    bool multilineOutput = false,
1020
    int progressIndicatorPadding = kDefaultStatusPadding,
1021
  }) {
1022
    assert(timeout != null);
1023
    printStatus(message);
1024 1025 1026
    return SilentStatus(
      stopwatch: Stopwatch(),
    );
1027
  }
1028

1029 1030 1031 1032 1033 1034 1035
  void _sendMessage(LogMessage logMessage) {
    if (_messageController.hasListener) {
      return _messageController.add(logMessage);
    }
    messageBuffer.add(logMessage);
  }

1036 1037 1038
  void dispose() {
    _messageController.close();
  }
1039 1040

  @override
1041
  void sendEvent(String name, [Map<String, dynamic> args]) { }
1042 1043 1044 1045 1046 1047

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

  @override
  bool get hasTerminal => false;
1048 1049 1050 1051

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

1054 1055
/// A running application, started by this daemon.
class AppInstance {
1056 1057
  AppInstance(this.id, { this.runner, this.logToStdout = false, @required AppRunLogger logger })
    : _logger = logger;
1058 1059

  final String id;
1060
  final ResidentRunner runner;
1061
  final bool logToStdout;
1062
  final AppRunLogger _logger;
1063

1064 1065
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
1066
  }
1067

1068
  Future<void> stop() => runner.exit();
1069
  Future<void> detach() => runner.detach();
1070 1071 1072 1073 1074

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

1075 1076
  Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> method()) async {
    return method();
1077 1078 1079
  }
}

1080 1081 1082 1083 1084
/// 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);
1085
    registerHandler('create', create);
1086 1087
  }

1088 1089 1090 1091 1092 1093 1094
  EmulatorManager emulators = EmulatorManager(
    fileSystem: globals.fs,
    logger: globals.logger,
    androidSdk: globals.androidSdk,
    processManager: globals.processManager,
    androidWorkflow: androidWorkflow,
  );
1095

1096
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
1097
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
1098
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
1099 1100
  }

1101
  Future<void> launch(Map<String, dynamic> args) async {
1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
    final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
    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 {
      await matches.first.launch();
    }
  }
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122

  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,
    };
  }
1123 1124
}

1125
/// A [Logger] which sends log messages to a listening daemon client.
1126 1127 1128 1129
///
/// 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
1130 1131 1132
//
// 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.
1133 1134
class AppRunLogger extends DelegatingLogger {
  AppRunLogger({ @required Logger parent }) : super(parent);
1135 1136

  AppDomain domain;
1137
  AppInstance app;
1138
  int _nextProgressId = 0;
1139

Devon Carew's avatar
Devon Carew committed
1140 1141
  Status _status;

1142
  @override
1143 1144
  Status startProgress(
    String message, {
1145
    @required Duration timeout,
1146
    String progressId,
1147 1148
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
1149
  }) {
1150
    final int id = _nextProgressId++;
1151

1152 1153 1154 1155 1156
    _sendProgressEvent(
      eventId: id.toString(),
      eventType: progressId,
      message: message,
    );
1157

1158 1159 1160
    _status = SilentStatus(
      onFinish: () {
        _status = null;
1161 1162 1163 1164 1165
        _sendProgressEvent(
          eventId: id.toString(),
          eventType: progressId,
          finished: true,
        );
1166
      }, stopwatch: Stopwatch())..start();
Devon Carew's avatar
Devon Carew committed
1167
    return _status;
1168 1169 1170 1171 1172
  }

  void close() {
    domain = null;
  }
1173

1174 1175 1176 1177 1178 1179
  void _sendProgressEvent({
    @required String eventId,
    @required String eventType,
    bool finished = false,
    String message,
  }) {
1180
    if (domain == null) {
1181 1182 1183 1184 1185
      // If we're sending progress events before an app has started, send the
      // progress messages as plain status messages.
      if (message != null) {
        printStatus(message);
      }
1186
    } else {
1187 1188 1189 1190 1191 1192 1193
      final Map<String, dynamic> event = <String, dynamic>{
        'id': eventId,
        'progressId': eventType,
        if (message != null) 'message': message,
        if (finished != null) 'finished': finished,
      };

1194
      domain._sendAppEvent(app, 'progress', event);
1195
    }
1196
  }
1197 1198

  @override
1199 1200 1201 1202 1203 1204
  void sendEvent(String name, [Map<String, dynamic> args]) {
    if (domain == null) {
      printStatus('event sent after app closed: $name');
    } else {
      domain.sendEvent(name, args);
    }
1205
  }
1206 1207 1208 1209 1210 1211

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

  @override
  bool get hasTerminal => false;
1212 1213 1214 1215

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

1218
class LogMessage {
1219 1220
  LogMessage(this.level, this.message, [this.stackTrace]);

1221 1222 1223 1224
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240

/// 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;
}
1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 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

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