• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import '../base/common.dart';
10import '../base/context.dart';
11import '../base/file_system.dart';
12import '../base/io.dart';
13import '../base/logger.dart';
14import '../base/terminal.dart';
15import '../base/utils.dart';
16import '../build_info.dart';
17import '../cache.dart';
18import '../convert.dart';
19import '../device.dart';
20import '../emulator.dart';
21import '../globals.dart';
22import '../project.dart';
23import '../resident_runner.dart';
24import '../run_cold.dart';
25import '../run_hot.dart';
26import '../runner/flutter_command.dart';
27import '../web/web_runner.dart';
28
29const String protocolVersion = '0.5.3';
30
31/// A server process command. This command will start up a long-lived server.
32/// It reads JSON-RPC based commands from stdin, executes them, and returns
33/// JSON-RPC based responses and events to stdout.
34///
35/// It can be shutdown with a `daemon.shutdown` command (or by killing the
36/// process).
37class DaemonCommand extends FlutterCommand {
38  DaemonCommand({ this.hidden = false });
39
40  @override
41  final String name = 'daemon';
42
43  @override
44  final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
45
46  @override
47  final bool hidden;
48
49  @override
50  Future<FlutterCommandResult> runCommand() async {
51    printStatus('Starting device daemon...');
52    isRunningFromDaemon = true;
53
54    final NotifyingLogger notifyingLogger = NotifyingLogger();
55
56    Cache.releaseLockEarly();
57
58    await context.run<void>(
59      body: () async {
60        final Daemon daemon = Daemon(
61            stdinCommandStream, stdoutCommandResponse,
62            daemonCommand: this, notifyingLogger: notifyingLogger);
63
64        final int code = await daemon.onExit;
65        if (code != 0)
66          throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
67      },
68      overrides: <Type, Generator>{
69        Logger: () => notifyingLogger,
70      },
71    );
72    return null;
73  }
74}
75
76typedef DispatchCommand = void Function(Map<String, dynamic> command);
77
78typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
79
80class Daemon {
81  Daemon(
82    Stream<Map<String, dynamic>> commandStream,
83    this.sendCommand, {
84    this.daemonCommand,
85    this.notifyingLogger,
86    this.logToStdout = false,
87  }) {
88    // Set up domains.
89    _registerDomain(daemonDomain = DaemonDomain(this));
90    _registerDomain(appDomain = AppDomain(this));
91    _registerDomain(deviceDomain = DeviceDomain(this));
92    _registerDomain(emulatorDomain = EmulatorDomain(this));
93
94    // Start listening.
95    _commandSubscription = commandStream.listen(
96      _handleRequest,
97      onDone: () {
98        if (!_onExitCompleter.isCompleted)
99          _onExitCompleter.complete(0);
100      },
101    );
102  }
103
104  DaemonDomain daemonDomain;
105  AppDomain appDomain;
106  DeviceDomain deviceDomain;
107  EmulatorDomain emulatorDomain;
108  StreamSubscription<Map<String, dynamic>> _commandSubscription;
109
110  final DispatchCommand sendCommand;
111  final DaemonCommand daemonCommand;
112  final NotifyingLogger notifyingLogger;
113  final bool logToStdout;
114
115  final Completer<int> _onExitCompleter = Completer<int>();
116  final Map<String, Domain> _domainMap = <String, Domain>{};
117
118  void _registerDomain(Domain domain) {
119    _domainMap[domain.name] = domain;
120  }
121
122  Future<int> get onExit => _onExitCompleter.future;
123
124  void _handleRequest(Map<String, dynamic> request) {
125    // {id, method, params}
126
127    // [id] is an opaque type to us.
128    final dynamic id = request['id'];
129
130    if (id == null) {
131      stderr.writeln('no id for request: $request');
132      return;
133    }
134
135    try {
136      final String method = request['method'];
137      if (!method.contains('.'))
138        throw 'method not understood: $method';
139
140      final String prefix = method.substring(0, method.indexOf('.'));
141      final String name = method.substring(method.indexOf('.') + 1);
142      if (_domainMap[prefix] == null)
143        throw 'no domain for method: $method';
144
145      _domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{});
146    } catch (error, trace) {
147      _send(<String, dynamic>{
148        'id': id,
149        'error': _toJsonable(error),
150        'trace': '$trace',
151      });
152    }
153  }
154
155  void _send(Map<String, dynamic> map) => sendCommand(map);
156
157  void shutdown({ dynamic error }) {
158    _commandSubscription?.cancel();
159    for (Domain domain in _domainMap.values)
160      domain.dispose();
161    if (!_onExitCompleter.isCompleted) {
162      if (error == null)
163        _onExitCompleter.complete(0);
164      else
165        _onExitCompleter.completeError(error);
166    }
167  }
168}
169
170abstract class Domain {
171  Domain(this.daemon, this.name);
172
173  final Daemon daemon;
174  final String name;
175  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
176
177  void registerHandler(String name, CommandHandler handler) {
178    _handlers[name] = handler;
179  }
180
181  FlutterCommand get command => daemon.daemonCommand;
182
183  @override
184  String toString() => name;
185
186  void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
187    Future<dynamic>.sync(() {
188      if (_handlers.containsKey(command))
189        return _handlers[command](args);
190      throw 'command not understood: $name.$command';
191    }).then<dynamic>((dynamic result) {
192      if (result == null) {
193        _send(<String, dynamic>{'id': id});
194      } else {
195        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
196      }
197    }).catchError((dynamic error, dynamic trace) {
198      _send(<String, dynamic>{
199        'id': id,
200        'error': _toJsonable(error),
201        'trace': '$trace',
202      });
203    });
204  }
205
206  void sendEvent(String name, [ dynamic args ]) {
207    final Map<String, dynamic> map = <String, dynamic>{'event': name};
208    if (args != null)
209      map['params'] = _toJsonable(args);
210    _send(map);
211  }
212
213  void _send(Map<String, dynamic> map) => daemon._send(map);
214
215  String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) {
216    if (required && !args.containsKey(name))
217      throw '$name is required';
218    final dynamic val = args[name];
219    if (val != null && val is! String)
220      throw '$name is not a String';
221    return val;
222  }
223
224  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) {
225    if (required && !args.containsKey(name))
226      throw '$name is required';
227    final dynamic val = args[name];
228    if (val != null && val is! bool)
229      throw '$name is not a bool';
230    return val;
231  }
232
233  int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) {
234    if (required && !args.containsKey(name))
235      throw '$name is required';
236    final dynamic val = args[name];
237    if (val != null && val is! int)
238      throw '$name is not an int';
239    return val;
240  }
241
242  void dispose() { }
243}
244
245/// This domain responds to methods like [version] and [shutdown].
246///
247/// This domain fires the `daemon.logMessage` event.
248class DaemonDomain extends Domain {
249  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
250    registerHandler('version', version);
251    registerHandler('shutdown', shutdown);
252    registerHandler('getSupportedPlatforms', getSupportedPlatforms);
253
254    sendEvent(
255      'daemon.connected',
256      <String, dynamic>{
257        'version': protocolVersion,
258        'pid': pid,
259      },
260    );
261
262    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
263      if (daemon.logToStdout) {
264        if (message.level == 'status') {
265          // We use `print()` here instead of `stdout.writeln()` in order to
266          // capture the print output for testing.
267          print(message.message);
268        } else if (message.level == 'error') {
269          stderr.writeln(message.message);
270          if (message.stackTrace != null)
271            stderr.writeln(message.stackTrace.toString().trimRight());
272        }
273      } else {
274        if (message.stackTrace != null) {
275          sendEvent('daemon.logMessage', <String, dynamic>{
276            'level': message.level,
277            'message': message.message,
278            'stackTrace': message.stackTrace.toString(),
279          });
280        } else {
281          sendEvent('daemon.logMessage', <String, dynamic>{
282            'level': message.level,
283            'message': message.message,
284          });
285        }
286      }
287    });
288  }
289
290  StreamSubscription<LogMessage> _subscription;
291
292  Future<String> version(Map<String, dynamic> args) {
293    return Future<String>.value(protocolVersion);
294  }
295
296  Future<void> shutdown(Map<String, dynamic> args) {
297    Timer.run(daemon.shutdown);
298    return Future<void>.value();
299  }
300
301  @override
302  void dispose() {
303    _subscription?.cancel();
304  }
305
306  /// Enumerates the platforms supported by the provided project.
307  ///
308  /// This does not filter based on the current workflow restrictions, such
309  /// as whether command line tools are installed or whether the host platform
310  /// is correct.
311  Future<Map<String, Object>> getSupportedPlatforms(Map<String, dynamic> args) async {
312    final String projectRoot = _getStringArg(args, 'projectRoot', required: true);
313    final List<String> result = <String>[];
314    try {
315      // TODO(jonahwilliams): replace this with a project metadata check once
316      // that has been implemented.
317      final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.directory(projectRoot));
318      if (flutterProject.linux.existsSync()) {
319        result.add('linux');
320      }
321      if (flutterProject.macos.existsSync()) {
322        result.add('macos');
323      }
324      if (flutterProject.windows.existsSync()) {
325        result.add('windows');
326      }
327      if (flutterProject.ios.existsSync()) {
328        result.add('ios');
329      }
330      if (flutterProject.android.existsSync()) {
331        result.add('android');
332      }
333      if (flutterProject.web.existsSync()) {
334        result.add('web');
335      }
336      if (flutterProject.fuchsia.existsSync()) {
337        result.add('fuchsia');
338      }
339      return <String, Object>{
340        'platforms': result,
341      };
342    } catch (err, stackTrace) {
343      sendEvent('log', <String, dynamic>{
344        'log': 'Failed to parse project metadata',
345        'stackTrace': stackTrace.toString(),
346        'error': true,
347      });
348      // On any sort of failure, fall back to Android and iOS for backwards
349      // comparability.
350      return <String, Object>{
351        'platforms': <String>[
352          'android',
353          'ios',
354        ],
355      };
356    }
357  }
358}
359
360typedef _RunOrAttach = Future<void> Function({
361  Completer<DebugConnectionInfo> connectionInfoCompleter,
362  Completer<void> appStartedCompleter,
363});
364
365/// This domain responds to methods like [start] and [stop].
366///
367/// It fires events for application start, stop, and stdout and stderr.
368class AppDomain extends Domain {
369  AppDomain(Daemon daemon) : super(daemon, 'app') {
370    registerHandler('restart', restart);
371    registerHandler('callServiceExtension', callServiceExtension);
372    registerHandler('stop', stop);
373    registerHandler('detach', detach);
374  }
375
376  static final Uuid _uuidGenerator = Uuid();
377
378  static String _getNewAppId() => _uuidGenerator.generateV4();
379
380  final List<AppInstance> _apps = <AppInstance>[];
381
382  Future<AppInstance> startApp(
383    Device device,
384    String projectDirectory,
385    String target,
386    String route,
387    DebuggingOptions options,
388    bool enableHotReload, {
389    File applicationBinary,
390    @required bool trackWidgetCreation,
391    String projectRootPath,
392    String packagesFilePath,
393    String dillOutputPath,
394    bool ipv6 = false,
395    String isolateFilter,
396  }) async {
397    if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
398      throw '${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.';
399    }
400    // We change the current working directory for the duration of the `start` command.
401    final Directory cwd = fs.currentDirectory;
402    fs.currentDirectory = fs.directory(projectDirectory);
403    final FlutterProject flutterProject = FlutterProject.current();
404
405    final FlutterDevice flutterDevice = await FlutterDevice.create(
406      device,
407      flutterProject: flutterProject,
408      trackWidgetCreation: trackWidgetCreation,
409      viewFilter: isolateFilter,
410      target: target,
411      buildMode: options.buildInfo.mode,
412    );
413
414    ResidentRunner runner;
415
416    if (await device.targetPlatform == TargetPlatform.web_javascript) {
417      runner = webRunnerFactory.createWebRunner(
418        device,
419        flutterProject: flutterProject,
420        target: target,
421        debuggingOptions: options,
422        ipv6: ipv6,
423      );
424    } else if (enableHotReload) {
425      runner = HotRunner(
426        <FlutterDevice>[flutterDevice],
427        target: target,
428        debuggingOptions: options,
429        usesTerminalUi: false,
430        applicationBinary: applicationBinary,
431        projectRootPath: projectRootPath,
432        packagesFilePath: packagesFilePath,
433        dillOutputPath: dillOutputPath,
434        ipv6: ipv6,
435        hostIsIde: true,
436      );
437    } else {
438      runner = ColdRunner(
439        <FlutterDevice>[flutterDevice],
440        target: target,
441        debuggingOptions: options,
442        applicationBinary: applicationBinary,
443        usesTerminalUi: false,
444        ipv6: ipv6,
445      );
446    }
447
448    return launch(
449      runner,
450      ({
451        Completer<DebugConnectionInfo> connectionInfoCompleter,
452        Completer<void> appStartedCompleter,
453      }) {
454        return runner.run(
455          connectionInfoCompleter: connectionInfoCompleter,
456          appStartedCompleter: appStartedCompleter,
457          route: route,
458        );
459      },
460      device,
461      projectDirectory,
462      enableHotReload,
463      cwd,
464      LaunchMode.run,
465    );
466  }
467
468  Future<AppInstance> launch(
469    ResidentRunner runner,
470    _RunOrAttach runOrAttach,
471    Device device,
472    String projectDirectory,
473    bool enableHotReload,
474    Directory cwd,
475    LaunchMode launchMode,
476  ) async {
477    final AppInstance app = AppInstance(_getNewAppId(),
478        runner: runner, logToStdout: daemon.logToStdout);
479    _apps.add(app);
480    _sendAppEvent(app, 'start', <String, dynamic>{
481      'deviceId': device.id,
482      'directory': projectDirectory,
483      'supportsRestart': isRestartSupported(enableHotReload, device),
484      'launchMode': launchMode.toString(),
485    });
486
487    Completer<DebugConnectionInfo> connectionInfoCompleter;
488
489    if (runner.debuggingOptions.debuggingEnabled) {
490      connectionInfoCompleter = Completer<DebugConnectionInfo>();
491      // We don't want to wait for this future to complete and callbacks won't fail.
492      // As it just writes to stdout.
493      unawaited(connectionInfoCompleter.future.then<void>(
494        (DebugConnectionInfo info) {
495          final Map<String, dynamic> params = <String, dynamic>{
496            // The web vmservice proxy does not have an http address.
497            'port': info.httpUri?.port ?? info.wsUri.port,
498            'wsUri': info.wsUri.toString(),
499          };
500          if (info.baseUri != null)
501            params['baseUri'] = info.baseUri;
502          _sendAppEvent(app, 'debugPort', params);
503        },
504      ));
505    }
506    final Completer<void> appStartedCompleter = Completer<void>();
507    // We don't want to wait for this future to complete, and callbacks won't fail,
508    // as it just writes to stdout.
509    unawaited(appStartedCompleter.future.then<void>((void value) {
510      _sendAppEvent(app, 'started');
511    }));
512
513    await app._runInZone<void>(this, () async {
514      try {
515        await runOrAttach(
516          connectionInfoCompleter: connectionInfoCompleter,
517          appStartedCompleter: appStartedCompleter,
518        );
519        _sendAppEvent(app, 'stop');
520      } catch (error, trace) {
521        _sendAppEvent(app, 'stop', <String, dynamic>{
522          'error': _toJsonable(error),
523          'trace': '$trace',
524        });
525      } finally {
526        fs.currentDirectory = cwd;
527        _apps.remove(app);
528      }
529    });
530    return app;
531  }
532
533  bool isRestartSupported(bool enableHotReload, Device device) =>
534      enableHotReload && device.supportsHotRestart;
535
536  Future<OperationResult> _inProgressHotReload;
537
538  Future<OperationResult> restart(Map<String, dynamic> args) async {
539    final String appId = _getStringArg(args, 'appId', required: true);
540    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
541    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
542    final String restartReason = _getStringArg(args, 'reason');
543
544    final AppInstance app = _getApp(appId);
545    if (app == null)
546      throw "app '$appId' not found";
547
548    if (_inProgressHotReload != null)
549      throw 'hot restart already in progress';
550
551    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
552      return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason);
553    });
554    return _inProgressHotReload.whenComplete(() {
555      _inProgressHotReload = null;
556    });
557  }
558
559  /// Returns an error, or the service extension result (a map with two fixed
560  /// keys, `type` and `method`). The result may have one or more additional keys,
561  /// depending on the specific service extension end-point. For example:
562  ///
563  ///     {
564  ///       "value":"android",
565  ///       "type":"_extensionType",
566  ///       "method":"ext.flutter.platformOverride"
567  ///     }
568  Future<Map<String, dynamic>> callServiceExtension(Map<String, dynamic> args) async {
569    final String appId = _getStringArg(args, 'appId', required: true);
570    final String methodName = _getStringArg(args, 'methodName');
571    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
572
573    final AppInstance app = _getApp(appId);
574    if (app == null)
575      throw "app '$appId' not found";
576
577    final Map<String, dynamic> result = await app.runner
578        .invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params);
579    if (result == null)
580      throw 'method not available: $methodName';
581
582    if (result.containsKey('error'))
583      throw result['error'];
584
585    return result;
586  }
587
588  Future<bool> stop(Map<String, dynamic> args) async {
589    final String appId = _getStringArg(args, 'appId', required: true);
590
591    final AppInstance app = _getApp(appId);
592    if (app == null)
593      throw "app '$appId' not found";
594
595    return app.stop().then<bool>(
596      (void value) => true,
597      onError: (dynamic error, StackTrace stack) {
598        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
599        app.closeLogger();
600        _apps.remove(app);
601        return false;
602      },
603    );
604  }
605
606  Future<bool> detach(Map<String, dynamic> args) async {
607    final String appId = _getStringArg(args, 'appId', required: true);
608
609    final AppInstance app = _getApp(appId);
610    if (app == null)
611      throw "app '$appId' not found";
612
613    return app.detach().then<bool>(
614      (void value) => true,
615      onError: (dynamic error, StackTrace stack) {
616        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
617        app.closeLogger();
618        _apps.remove(app);
619        return false;
620      },
621    );
622  }
623
624  AppInstance _getApp(String id) {
625    return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null);
626  }
627
628  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
629    sendEvent('app.$name', <String, dynamic>{
630      'appId': app.id,
631      ...?args,
632    });
633  }
634}
635
636typedef _DeviceEventHandler = void Function(Device device);
637
638/// This domain lets callers list and monitor connected devices.
639///
640/// It exports a `getDevices()` call, as well as firing `device.added` and
641/// `device.removed` events.
642class DeviceDomain extends Domain {
643  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
644    registerHandler('getDevices', getDevices);
645    registerHandler('enable', enable);
646    registerHandler('disable', disable);
647    registerHandler('forward', forward);
648    registerHandler('unforward', unforward);
649
650    // Use the device manager discovery so that client provided device types
651    // are usable via the daemon protocol.
652    deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
653  }
654
655  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
656    if (!discoverer.supportsPlatform)
657      return;
658
659    _discoverers.add(discoverer);
660    if (discoverer is PollingDeviceDiscovery) {
661      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
662      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
663    }
664  }
665
666  Future<void> _serializeDeviceEvents = Future<void>.value();
667
668  _DeviceEventHandler _onDeviceEvent(String eventName) {
669    return (Device device) {
670      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
671        try {
672          final Map<String, Object> response = await _deviceToMap(device);
673          sendEvent(eventName, response);
674        } catch (err) {
675          printError('$err');
676        }
677      });
678    };
679  }
680
681  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
682
683  /// Return a list of the current devices, with each device represented as a map
684  /// of properties (id, name, platform, ...).
685  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
686    final List<Map<String, dynamic>> devicesInfo = <Map<String, dynamic>>[];
687
688    for (PollingDeviceDiscovery discoverer in _discoverers) {
689      for (Device device in await discoverer.devices) {
690        devicesInfo.add(await _deviceToMap(device));
691      }
692    }
693
694    return devicesInfo;
695  }
696
697  /// Enable device events.
698  Future<void> enable(Map<String, dynamic> args) {
699    for (PollingDeviceDiscovery discoverer in _discoverers)
700      discoverer.startPolling();
701    return Future<void>.value();
702  }
703
704  /// Disable device events.
705  Future<void> disable(Map<String, dynamic> args) {
706    for (PollingDeviceDiscovery discoverer in _discoverers)
707      discoverer.stopPolling();
708    return Future<void>.value();
709  }
710
711  /// Forward a host port to a device port.
712  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
713    final String deviceId = _getStringArg(args, 'deviceId', required: true);
714    final int devicePort = _getIntArg(args, 'devicePort', required: true);
715    int hostPort = _getIntArg(args, 'hostPort');
716
717    final Device device = await daemon.deviceDomain._getDevice(deviceId);
718    if (device == null)
719      throw "device '$deviceId' not found";
720
721    hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort);
722
723    return <String, dynamic>{'hostPort': hostPort};
724  }
725
726  /// Removes a forwarded port.
727  Future<void> unforward(Map<String, dynamic> args) async {
728    final String deviceId = _getStringArg(args, 'deviceId', required: true);
729    final int devicePort = _getIntArg(args, 'devicePort', required: true);
730    final int hostPort = _getIntArg(args, 'hostPort', required: true);
731
732    final Device device = await daemon.deviceDomain._getDevice(deviceId);
733    if (device == null)
734      throw "device '$deviceId' not found";
735
736    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
737  }
738
739  @override
740  void dispose() {
741    for (PollingDeviceDiscovery discoverer in _discoverers)
742      discoverer.dispose();
743  }
744
745  /// Return the device matching the deviceId field in the args.
746  Future<Device> _getDevice(String deviceId) async {
747    for (PollingDeviceDiscovery discoverer in _discoverers) {
748      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
749      if (device != null)
750        return device;
751    }
752    return null;
753  }
754}
755
756Stream<Map<String, dynamic>> get stdinCommandStream => stdin
757  .transform<String>(utf8.decoder)
758  .transform<String>(const LineSplitter())
759  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
760  .map<Map<String, dynamic>>((String line) {
761    line = line.substring(1, line.length - 1);
762    return json.decode(line);
763  });
764
765void stdoutCommandResponse(Map<String, dynamic> command) {
766  stdout.writeln('[${jsonEncodeObject(command)}]');
767}
768
769String jsonEncodeObject(dynamic object) {
770  return json.encode(object, toEncodable: _toEncodable);
771}
772
773dynamic _toEncodable(dynamic object) {
774  if (object is OperationResult)
775    return _operationResultToMap(object);
776  return object;
777}
778
779Future<Map<String, dynamic>> _deviceToMap(Device device) async {
780  return <String, dynamic>{
781    'id': device.id,
782    'name': device.name,
783    'platform': getNameForTargetPlatform(await device.targetPlatform),
784    'emulator': await device.isLocalEmulator,
785    'category': device.category?.toString(),
786    'platformType': device.platformType?.toString(),
787    'ephemeral': device.ephemeral,
788    'emulatorId': await device.emulatorId,
789  };
790}
791
792Map<String, dynamic> _emulatorToMap(Emulator emulator) {
793  return <String, dynamic>{
794    'id': emulator.id,
795    'name': emulator.name,
796    'category': emulator.category?.toString(),
797    'platformType': emulator.platformType?.toString(),
798  };
799}
800
801Map<String, dynamic> _operationResultToMap(OperationResult result) {
802  return <String, dynamic>{
803    'code': result.code,
804    'message': result.message,
805  };
806}
807
808dynamic _toJsonable(dynamic obj) {
809  if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null)
810    return obj;
811  if (obj is OperationResult)
812    return obj;
813  if (obj is ToolExit)
814    return obj.message;
815  return '$obj';
816}
817
818class NotifyingLogger extends Logger {
819  final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast();
820
821  Stream<LogMessage> get onMessage => _messageController.stream;
822
823  @override
824  void printError(
825    String message, {
826    StackTrace stackTrace,
827    bool emphasis = false,
828    TerminalColor color,
829    int indent,
830    int hangingIndent,
831    bool wrap,
832  }) {
833    _messageController.add(LogMessage('error', message, stackTrace));
834  }
835
836  @override
837  void printStatus(
838    String message, {
839    bool emphasis = false,
840    TerminalColor color,
841    bool newline = true,
842    int indent,
843    int hangingIndent,
844    bool wrap,
845  }) {
846    _messageController.add(LogMessage('status', message));
847  }
848
849  @override
850  void printTrace(String message) {
851    // This is a lot of traffic to send over the wire.
852  }
853
854  @override
855  Status startProgress(
856    String message, {
857    @required Duration timeout,
858    String progressId,
859    bool multilineOutput = false,
860    int progressIndicatorPadding = kDefaultStatusPadding,
861  }) {
862    assert(timeout != null);
863    printStatus(message);
864    return SilentStatus(timeout: timeout);
865  }
866
867  void dispose() {
868    _messageController.close();
869  }
870}
871
872/// A running application, started by this daemon.
873class AppInstance {
874  AppInstance(this.id, { this.runner, this.logToStdout = false });
875
876  final String id;
877  final ResidentRunner runner;
878  final bool logToStdout;
879
880  _AppRunLogger _logger;
881
882  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) {
883    return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason);
884  }
885
886  Future<void> stop() => runner.exit();
887  Future<void> detach() => runner.detach();
888
889  void closeLogger() {
890    _logger.close();
891  }
892
893  Future<T> _runInZone<T>(AppDomain domain, dynamic method()) {
894    _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? logger : null);
895
896    return context.run<T>(
897      body: method,
898      overrides: <Type, Generator>{
899        Logger: () => _logger,
900      },
901    );
902  }
903}
904
905/// This domain responds to methods like [getEmulators] and [launch].
906class EmulatorDomain extends Domain {
907  EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') {
908    registerHandler('getEmulators', getEmulators);
909    registerHandler('launch', launch);
910    registerHandler('create', create);
911  }
912
913  EmulatorManager emulators = EmulatorManager();
914
915  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
916    final List<Emulator> list = await emulators.getAllAvailableEmulators();
917    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
918  }
919
920  Future<void> launch(Map<String, dynamic> args) async {
921    final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
922    final List<Emulator> matches =
923        await emulators.getEmulatorsMatching(emulatorId);
924    if (matches.isEmpty) {
925      throw "emulator '$emulatorId' not found";
926    } else if (matches.length > 1) {
927      throw "multiple emulators match '$emulatorId'";
928    } else {
929      await matches.first.launch();
930    }
931  }
932
933  Future<Map<String, dynamic>> create(Map<String, dynamic> args) async {
934    final String name = _getStringArg(args, 'name', required: false);
935    final CreateEmulatorResult res = await emulators.createEmulator(name: name);
936    return <String, dynamic>{
937      'success': res.success,
938      'emulatorName': res.emulatorName,
939      'error': res.error,
940    };
941  }
942}
943
944/// A [Logger] which sends log messages to a listening daemon client.
945///
946/// This class can either:
947///   1) Send stdout messages and progress events to the client IDE
948///   1) Log messages to stdout and send progress events to the client IDE
949//
950// TODO(devoncarew): To simplify this code a bit, we could choose to specialize
951// this class into two, one for each of the above use cases.
952class _AppRunLogger extends Logger {
953  _AppRunLogger(this.domain, this.app, { this.parent });
954
955  AppDomain domain;
956  final AppInstance app;
957  final Logger parent;
958  int _nextProgressId = 0;
959
960  @override
961  void printError(
962    String message, {
963    StackTrace stackTrace,
964    bool emphasis,
965    TerminalColor color,
966    int indent,
967    int hangingIndent,
968    bool wrap,
969  }) {
970    if (parent != null) {
971      parent.printError(
972        message,
973        stackTrace: stackTrace,
974        emphasis: emphasis,
975        indent: indent,
976        hangingIndent: hangingIndent,
977        wrap: wrap,
978      );
979    } else {
980      if (stackTrace != null) {
981        _sendLogEvent(<String, dynamic>{
982          'log': message,
983          'stackTrace': stackTrace.toString(),
984          'error': true,
985        });
986      } else {
987        _sendLogEvent(<String, dynamic>{
988          'log': message,
989          'error': true,
990        });
991      }
992    }
993  }
994
995  @override
996  void printStatus(
997    String message, {
998    bool emphasis = false,
999    TerminalColor color,
1000    bool newline = true,
1001    int indent,
1002    int hangingIndent,
1003    bool wrap,
1004  }) {
1005    if (parent != null) {
1006      parent.printStatus(
1007        message,
1008        emphasis: emphasis,
1009        color: color,
1010        newline: newline,
1011        indent: indent,
1012        hangingIndent: hangingIndent,
1013        wrap: wrap,
1014      );
1015    } else {
1016      _sendLogEvent(<String, dynamic>{'log': message});
1017    }
1018  }
1019
1020  @override
1021  void printTrace(String message) {
1022    if (parent != null) {
1023      parent.printTrace(message);
1024    } else {
1025      _sendLogEvent(<String, dynamic>{'log': message, 'trace': true});
1026    }
1027  }
1028
1029  Status _status;
1030
1031  @override
1032  Status startProgress(
1033    String message, {
1034    @required Duration timeout,
1035    String progressId,
1036    bool multilineOutput = false,
1037    int progressIndicatorPadding = kDefaultStatusPadding,
1038  }) {
1039    assert(timeout != null);
1040    final int id = _nextProgressId++;
1041
1042    _sendProgressEvent(<String, dynamic>{
1043      'id': id.toString(),
1044      'progressId': progressId,
1045      'message': message,
1046    });
1047
1048    _status = SilentStatus(
1049      timeout: timeout,
1050      onFinish: () {
1051        _status = null;
1052        _sendProgressEvent(<String, dynamic>{
1053          'id': id.toString(),
1054          'progressId': progressId,
1055          'finished': true,
1056        });
1057      })..start();
1058    return _status;
1059  }
1060
1061  void close() {
1062    domain = null;
1063  }
1064
1065  void _sendLogEvent(Map<String, dynamic> event) {
1066    if (domain == null)
1067      printStatus('event sent after app closed: $event');
1068    else
1069      domain._sendAppEvent(app, 'log', event);
1070  }
1071
1072  void _sendProgressEvent(Map<String, dynamic> event) {
1073    if (domain == null)
1074      printStatus('event sent after app closed: $event');
1075    else
1076      domain._sendAppEvent(app, 'progress', event);
1077  }
1078}
1079
1080class LogMessage {
1081  LogMessage(this.level, this.message, [this.stackTrace]);
1082
1083  final String level;
1084  final String message;
1085  final StackTrace stackTrace;
1086}
1087
1088/// The method by which the flutter app was launched.
1089class LaunchMode {
1090  const LaunchMode._(this._value);
1091
1092  /// The app was launched via `flutter run`.
1093  static const LaunchMode run = LaunchMode._('run');
1094
1095  /// The app was launched via `flutter attach`.
1096  static const LaunchMode attach = LaunchMode._('attach');
1097
1098  final String _value;
1099
1100  @override
1101  String toString() => _value;
1102}
1103