daemon.dart 36.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 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:meta/meta.dart';
8
import 'package:uuid/uuid.dart';
9

10
import '../base/common.dart';
11
import '../base/context.dart';
12
import '../base/file_system.dart';
13
import '../base/io.dart';
14
import '../base/logger.dart';
15
import '../base/terminal.dart';
16
import '../base/utils.dart';
17
import '../build_info.dart';
18
import '../cache.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.5.3';
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
    isRunningFromDaemon = true;
54

55
    final NotifyingLogger notifyingLogger = NotifyingLogger();
Devon Carew's avatar
Devon Carew committed
56

57 58
    Cache.releaseLockEarly();

59
    await context.run<void>(
60
      body: () async {
61
        final Daemon daemon = Daemon(
62 63 64
          stdinCommandStream, stdoutCommandResponse,
          notifyingLogger: notifyingLogger,
        );
65 66

        final int code = await daemon.onExit;
67
        if (code != 0) {
68
          throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
69
        }
70 71 72 73 74
      },
      overrides: <Type, Generator>{
        Logger: () => notifyingLogger,
      },
    );
75
    return FlutterCommandResult.success();
76
  }
Devon Carew's avatar
Devon Carew committed
77 78
}

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

81
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
82 83

class Daemon {
84 85 86
  Daemon(
    Stream<Map<String, dynamic>> commandStream,
    this.sendCommand, {
87
    this.notifyingLogger,
88
    this.logToStdout = false,
89
  }) {
Devon Carew's avatar
Devon Carew committed
90
    // Set up domains.
91 92 93 94
    _registerDomain(daemonDomain = DaemonDomain(this));
    _registerDomain(appDomain = AppDomain(this));
    _registerDomain(deviceDomain = DeviceDomain(this));
    _registerDomain(emulatorDomain = EmulatorDomain(this));
Devon Carew's avatar
Devon Carew committed
95 96

    // Start listening.
97
    _commandSubscription = commandStream.listen(
98
      _handleRequest,
99
      onDone: () {
100
        if (!_onExitCompleter.isCompleted) {
101
          _onExitCompleter.complete(0);
102
        }
103
      },
Devon Carew's avatar
Devon Carew committed
104 105 106
    );
  }

107 108 109
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
110
  EmulatorDomain emulatorDomain;
111
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
112 113
  int _outgoingRequestId = 1;
  final Map<String, Completer<dynamic>> _outgoingRequestCompleters = <String, Completer<dynamic>>{};
114

Devon Carew's avatar
Devon Carew committed
115
  final DispatchCommand sendCommand;
116
  final NotifyingLogger notifyingLogger;
117
  final bool logToStdout;
118

119
  final Completer<int> _onExitCompleter = Completer<int>();
120 121
  final Map<String, Domain> _domainMap = <String, Domain>{};

Devon Carew's avatar
Devon Carew committed
122
  void _registerDomain(Domain domain) {
123
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
124 125 126 127
  }

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

Ian Hickson's avatar
Ian Hickson committed
128
  void _handleRequest(Map<String, dynamic> request) {
129 130 131
    // {id, method, params}

    // [id] is an opaque type to us.
132
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
133 134

    if (id == null) {
135
      globals.stdio.stderrWrite('no id for request: $request\n');
Devon Carew's avatar
Devon Carew committed
136 137 138 139
      return;
    }

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

146 147 148 149 150 151 152 153 154 155 156 157 158 159
        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
160

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

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

194
  void shutdown({ dynamic error }) {
195
    _commandSubscription?.cancel();
196
    for (final Domain domain in _domainMap.values) {
197
      domain.dispose();
198
    }
199
    if (!_onExitCompleter.isCompleted) {
200
      if (error == null) {
201
        _onExitCompleter.complete(0);
202
      } else {
203
        _onExitCompleter.completeError(error);
204
      }
205
    }
Devon Carew's avatar
Devon Carew committed
206 207 208 209
  }
}

abstract class Domain {
210 211
  Domain(this.daemon, this.name);

212

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

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

221
  @override
Devon Carew's avatar
Devon Carew committed
222 223
  String toString() => name;

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

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

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

255
  String _getStringArg(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! String) {
261
      throw '$name is not a String';
262
    }
263
    return val as String;
264 265
  }

266
  bool _getBoolArg(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! bool) {
272
      throw '$name is not a bool';
273
    }
274
    return val as bool;
275 276
  }

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

288
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
289 290 291
}

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

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

308
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
309 310 311 312 313 314
      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') {
315
          globals.stdio.stderrWrite('${message.message}\n');
316
          if (message.stackTrace != null) {
317 318 319
            globals.stdio.stderrWrite(
              '${message.stackTrace.toString().trimRight()}\n',
            );
320
          }
321
        }
322
      } else {
323 324 325 326
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
327
            'stackTrace': message.stackTrace.toString(),
328 329 330 331
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
332
            'message': message.message,
333 334
          });
        }
335 336
      }
    });
Devon Carew's avatar
Devon Carew committed
337 338
  }

339
  StreamSubscription<LogMessage> _subscription;
340

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

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

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

365
  @override
366 367 368
  void dispose() {
    _subscription?.cancel();
  }
369 370 371 372 373 374 375 376 377 378 379 380

  /// 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.
381
      final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
      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,
      };
406
    } on Exception catch (err, stackTrace) {
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
      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
422 423
}

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

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

441
  static final Uuid _uuidGenerator = Uuid();
442

443
  static String _getNewAppId() => _uuidGenerator.v4();
444

445
  final List<AppInstance> _apps = <AppInstance>[];
446

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
  }) async {
462
    if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
463
      throw Exception('${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.');
464
    }
465
    // We change the current working directory for the duration of the `start` command.
466 467
    final Directory cwd = globals.fs.currentDirectory;
    globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
468
    final FlutterProject flutterProject = FlutterProject.current();
469

470
    final FlutterDevice flutterDevice = await FlutterDevice.create(
471
      device,
472
      flutterProject: flutterProject,
473
      viewFilter: isolateFilter,
474
      target: target,
475
      buildInfo: options.buildInfo,
476
    );
477

478 479
    ResidentRunner runner;

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

512
    return launch(
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
      runner,
      ({
        Completer<DebugConnectionInfo> connectionInfoCompleter,
        Completer<void> appStartedCompleter,
      }) {
        return runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
      },
      device,
      projectDirectory,
      enableHotReload,
      cwd,
528
      LaunchMode.run,
529
    );
530
  }
531

532
  Future<AppInstance> launch(
533 534 535 536 537 538 539 540
    ResidentRunner runner,
    _RunOrAttach runOrAttach,
    Device device,
    String projectDirectory,
    bool enableHotReload,
    Directory cwd,
    LaunchMode launchMode,
  ) async {
541
    final AppInstance app = AppInstance(_getNewAppId(),
542
        runner: runner, logToStdout: daemon.logToStdout);
543 544
    _apps.add(app);
    _sendAppEvent(app, 'start', <String, dynamic>{
545
      'deviceId': device.id,
546
      'directory': projectDirectory,
547
      'supportsRestart': isRestartSupported(enableHotReload, device),
548
      'launchMode': launchMode.toString(),
549 550
    });

551
    Completer<DebugConnectionInfo> connectionInfoCompleter;
552

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

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

600
  bool isRestartSupported(bool enableHotReload, Device device) =>
601
      enableHotReload && device.supportsHotRestart;
602

603 604
  Future<OperationResult> _inProgressHotReload;

Devon Carew's avatar
Devon Carew committed
605
  Future<OperationResult> restart(Map<String, dynamic> args) async {
606 607 608
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
609
    final String restartReason = _getStringArg(args, 'reason');
610

611
    final AppInstance app = _getApp(appId);
612
    if (app == null) {
613
      throw "app '$appId' not found";
614
    }
615

616
    if (_inProgressHotReload != null) {
617
      throw 'hot restart already in progress';
618
    }
619

620
    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
621
      return app.restart(fullRestart: fullRestart, pause: pauseAfterRestart, reason: restartReason);
622
    });
623 624 625
    return _inProgressHotReload.whenComplete(() {
      _inProgressHotReload = null;
    });
626
  }
627

628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
  Future<OperationResult> reloadMethod(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);
    final String classId = _getStringArg(args, 'class', required: true);
    final String libraryId =  _getStringArg(args, 'library', required: true);

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

    if (_inProgressHotReload != null) {
      throw 'hot restart already in progress';
    }

    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
      return app.reloadMethod(classId: classId, libraryId: libraryId);
    });
    return _inProgressHotReload.whenComplete(() {
      _inProgressHotReload = null;
    });
  }

650 651 652 653 654 655 656 657 658 659
  /// 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 {
660 661
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
662
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
663

664
    final AppInstance app = _getApp(appId);
665
    if (app == null) {
666
      throw "app '$appId' not found";
667
    }
668

669 670
    final Map<String, dynamic> result = await app.runner
        .invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params);
671
    if (result == null) {
672
      throw 'method not available: $methodName';
673
    }
674

675
    if (result.containsKey('error')) {
676
      throw result['error'];
677
    }
678 679

    return result;
680 681
  }

682
  Future<bool> stop(Map<String, dynamic> args) async {
683
    final String appId = _getStringArg(args, 'appId', required: true);
684

685
    final AppInstance app = _getApp(appId);
686
    if (app == null) {
687
      throw "app '$appId' not found";
688
    }
689

690 691 692
    return app.stop().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
693
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
694 695 696 697 698
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
699 700
  }

701 702 703 704
  Future<bool> detach(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);

    final AppInstance app = _getApp(appId);
705
    if (app == null) {
706
      throw "app '$appId' not found";
707
    }
708

709 710 711
    return app.detach().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
712
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
713 714 715 716 717
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
718 719
  }

720 721 722 723
  AppInstance _getApp(String id) {
    return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
  }

724
  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
725 726 727 728
    sendEvent('app.$name', <String, dynamic>{
      'appId': app.id,
      ...?args,
    });
Devon Carew's avatar
Devon Carew committed
729 730 731
  }
}

732
typedef _DeviceEventHandler = void Function(Device device);
733

734 735
/// This domain lets callers list and monitor connected devices.
///
736 737
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
738 739 740
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
741 742
    registerHandler('enable', enable);
    registerHandler('disable', disable);
743 744
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
745

746 747 748
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
    deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
749
  }
750

751
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
752
    if (!discoverer.supportsPlatform) {
753
      return;
754
    }
755

756
    if (discoverer is PollingDeviceDiscovery) {
757
      _discoverers.add(discoverer);
758 759 760
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
761 762
  }

763
  Future<void> _serializeDeviceEvents = Future<void>.value();
764

765 766
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
767
      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
768 769 770
        try {
          final Map<String, Object> response = await _deviceToMap(device);
          sendEvent(eventName, response);
771
        } on Exception catch (err) {
772
          globals.printError('$err');
773
        }
774 775 776 777
      });
    };
  }

778
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
779

780 781
  /// Return a list of the current devices, with each device represented as a map
  /// of properties (id, name, platform, ...).
782
  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
783
    return <Map<String, dynamic>>[
784 785
      for (final PollingDeviceDiscovery discoverer in _discoverers)
        for (final Device device in await discoverer.devices)
786 787
          await _deviceToMap(device),
    ];
788 789
  }

790
  /// Enable device events.
791
  Future<void> enable(Map<String, dynamic> args) {
792
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
793
      discoverer.startPolling();
794
    }
795
    return Future<void>.value();
796 797
  }

798
  /// Disable device events.
799
  Future<void> disable(Map<String, dynamic> args) {
800
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
801
      discoverer.stopPolling();
802
    }
803
    return Future<void>.value();
804 805
  }

806 807
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
808 809
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
810
    int hostPort = _getIntArg(args, 'hostPort');
811

812
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
813
    if (device == null) {
814
      throw "device '$deviceId' not found";
815
    }
816 817 818

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

819
    return <String, dynamic>{'hostPort': hostPort};
820 821 822
  }

  /// Removes a forwarded port.
823
  Future<void> unforward(Map<String, dynamic> args) async {
824 825 826
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
827

828
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
829
    if (device == null) {
830
      throw "device '$deviceId' not found";
831
    }
832

833
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
834 835
  }

836
  @override
837
  void dispose() {
838
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
839
      discoverer.dispose();
840
    }
841 842 843
  }

  /// Return the device matching the deviceId field in the args.
844
  Future<Device> _getDevice(String deviceId) async {
845
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
846
      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
847
      if (device != null) {
848
        return device;
849
      }
850 851
    }
    return null;
852 853 854
  }
}

855
Stream<Map<String, dynamic>> get stdinCommandStream => globals.stdio.stdin
856 857
  .transform<String>(utf8.decoder)
  .transform<String>(const LineSplitter())
858
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
859
  .map<Map<String, dynamic>>((String line) {
860
    line = line.substring(1, line.length - 1);
861
    return castStringKeyedMap(json.decode(line));
862 863 864
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
865
  globals.stdio.stdoutWrite(
866 867 868 869 870
    '[${jsonEncodeObject(command)}]\n',
    fallback: (String message, dynamic error, StackTrace stack) {
      throwToolExit('Failed to write daemon command response to stdout: $error');
    },
  );
871 872
}

873
String jsonEncodeObject(dynamic object) {
874
  return json.encode(object, toEncodable: _toEncodable);
875 876 877
}

dynamic _toEncodable(dynamic object) {
878
  if (object is OperationResult) {
879
    return _operationResultToMap(object);
880
  }
881 882 883
  return object;
}

884
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
885
  return <String, dynamic>{
886
    'id': device.id,
887
    'name': device.name,
888
    'platform': getNameForTargetPlatform(await device.targetPlatform),
889
    'emulator': await device.isLocalEmulator,
890 891 892
    'category': device.category?.toString(),
    'platformType': device.platformType?.toString(),
    'ephemeral': device.ephemeral,
893
    'emulatorId': await device.emulatorId,
894 895 896
  };
}

897 898 899 900
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
901 902
    'category': emulator.category?.toString(),
    'platformType': emulator.platformType?.toString(),
903 904 905
  };
}

Devon Carew's avatar
Devon Carew committed
906
Map<String, dynamic> _operationResultToMap(OperationResult result) {
907
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
908
    'code': result.code,
909
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
910 911 912
  };
}

Devon Carew's avatar
Devon Carew committed
913
dynamic _toJsonable(dynamic obj) {
914
  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
915
    return obj;
916 917
  }
  if (obj is OperationResult) {
Devon Carew's avatar
Devon Carew committed
918
    return obj;
919 920
  }
  if (obj is ToolExit) {
921
    return obj.message;
922
  }
Hixie's avatar
Hixie committed
923
  return '$obj';
Devon Carew's avatar
Devon Carew committed
924
}
925

926
class NotifyingLogger extends Logger {
927
  final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast();
928 929 930

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

931
  @override
932
  void printError(
933 934 935 936 937 938 939 940
    String message, {
    StackTrace stackTrace,
    bool emphasis = false,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
941
    _messageController.add(LogMessage('error', message, stackTrace));
942 943
  }

944
  @override
945
  void printStatus(
946 947 948 949 950 951 952 953
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
954
    _messageController.add(LogMessage('status', message));
955 956
  }

957
  @override
958
  void printTrace(String message) {
959
    // This is a lot of traffic to send over the wire.
960
  }
961 962

  @override
963 964
  Status startProgress(
    String message, {
965
    @required Duration timeout,
966
    String progressId,
967
    bool multilineOutput = false,
968
    int progressIndicatorPadding = kDefaultStatusPadding,
969
  }) {
970
    assert(timeout != null);
971
    printStatus(message);
972 973 974 975 976
    return SilentStatus(
      timeout: timeout,
      timeoutConfiguration: timeoutConfiguration,
      stopwatch: Stopwatch(),
    );
977
  }
978 979 980 981

  void dispose() {
    _messageController.close();
  }
982 983

  @override
984
  void sendEvent(String name, [Map<String, dynamic> args]) { }
985 986 987 988 989 990

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

  @override
  bool get hasTerminal => false;
991 992 993 994

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

997 998
/// A running application, started by this daemon.
class AppInstance {
999
  AppInstance(this.id, { this.runner, this.logToStdout = false });
1000 1001

  final String id;
1002
  final ResidentRunner runner;
1003
  final bool logToStdout;
1004 1005 1006

  _AppRunLogger _logger;

1007 1008
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
1009
  }
1010

1011 1012 1013 1014
  Future<OperationResult> reloadMethod({ String classId, String libraryId }) {
    return runner.reloadMethod(classId: classId, libraryId: libraryId);
  }

1015
  Future<void> stop() => runner.exit();
1016
  Future<void> detach() => runner.detach();
1017 1018 1019 1020 1021

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

1022
  Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> method()) {
1023
    _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? globals.logger : null);
1024

1025
    return context.run<T>(
1026 1027 1028 1029 1030
      body: method,
      overrides: <Type, Generator>{
        Logger: () => _logger,
      },
    );
1031 1032 1033
  }
}

1034 1035 1036 1037 1038
/// 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);
1039
    registerHandler('create', create);
1040 1041
  }

1042 1043
  EmulatorManager emulators = EmulatorManager();

1044
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
1045
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
1046
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
1047 1048
  }

1049
  Future<void> launch(Map<String, dynamic> args) async {
1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060
    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();
    }
  }
1061 1062 1063 1064 1065 1066 1067 1068 1069 1070

  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,
    };
  }
1071 1072
}

1073
/// A [Logger] which sends log messages to a listening daemon client.
1074 1075 1076 1077
///
/// 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
1078 1079 1080
//
// 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.
1081
class _AppRunLogger extends Logger {
1082
  _AppRunLogger(this.domain, this.app, { this.parent });
1083 1084 1085

  AppDomain domain;
  final AppInstance app;
1086
  final Logger parent;
1087
  int _nextProgressId = 0;
1088 1089

  @override
1090
  void printError(
1091 1092 1093 1094 1095 1096 1097 1098
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1099
    if (parent != null) {
1100 1101 1102 1103 1104 1105
      parent.printError(
        message,
        stackTrace: stackTrace,
        emphasis: emphasis,
        indent: indent,
        hangingIndent: hangingIndent,
1106
        wrap: wrap,
1107
      );
1108
    } else {
1109 1110 1111 1112
      if (stackTrace != null) {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'stackTrace': stackTrace.toString(),
1113
          'error': true,
1114 1115 1116 1117
        });
      } else {
        _sendLogEvent(<String, dynamic>{
          'log': message,
1118
          'error': true,
1119 1120
        });
      }
1121 1122 1123 1124
    }
  }

  @override
1125
  void printStatus(
1126 1127 1128 1129 1130 1131 1132 1133
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1134
    if (parent != null) {
1135 1136 1137 1138 1139 1140
      parent.printStatus(
        message,
        emphasis: emphasis,
        color: color,
        newline: newline,
        indent: indent,
1141
        hangingIndent: hangingIndent,
1142
        wrap: wrap,
1143
      );
1144
    } else {
1145
      _sendLogEvent(<String, dynamic>{'log': message});
1146
    }
1147 1148 1149
  }

  @override
1150 1151 1152 1153
  void printTrace(String message) {
    if (parent != null) {
      parent.printTrace(message);
    } else {
1154
      _sendLogEvent(<String, dynamic>{'log': message, 'trace': true});
1155 1156
    }
  }
1157

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

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

1171 1172
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
1173
      'progressId': progressId,
1174
      'message': message,
1175 1176
    });

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

  void close() {
    domain = null;
  }
1194 1195

  void _sendLogEvent(Map<String, dynamic> event) {
1196
    if (domain == null) {
1197
      printStatus('event sent after app closed: $event');
1198
    } else {
1199
      domain._sendAppEvent(app, 'log', event);
1200
    }
1201
  }
1202 1203

  void _sendProgressEvent(Map<String, dynamic> event) {
1204
    if (domain == null) {
1205
      printStatus('event sent after app closed: $event');
1206
    } else {
1207
      domain._sendAppEvent(app, 'progress', event);
1208
    }
1209
  }
1210 1211

  @override
1212 1213 1214 1215 1216 1217
  void sendEvent(String name, [Map<String, dynamic> args]) {
    if (domain == null) {
      printStatus('event sent after app closed: $name');
    } else {
      domain.sendEvent(name, args);
    }
1218
  }
1219 1220 1221 1222 1223 1224

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

  @override
  bool get hasTerminal => false;
1225 1226 1227 1228

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

1231
class LogMessage {
1232 1233
  LogMessage(this.level, this.message, [this.stackTrace]);

1234 1235 1236 1237
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253

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