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

import 'dart:async';
import 'dart:convert';

8
import '../android/android_device.dart';
9
import '../base/common.dart';
10
import '../base/context.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart';
13
import '../base/logger.dart';
14
import '../base/utils.dart';
15
import '../build_info.dart';
16
import '../cache.dart';
17
import '../device.dart';
18
import '../globals.dart';
19 20
import '../ios/devices.dart';
import '../ios/simulators.dart';
21
import '../resident_runner.dart';
22 23
import '../run_cold.dart';
import '../run_hot.dart';
24
import '../runner/flutter_command.dart';
25
import '../vmservice.dart';
Devon Carew's avatar
Devon Carew committed
26

27
const String protocolVersion = '0.2.0';
28

Devon Carew's avatar
Devon Carew committed
29 30 31 32 33 34 35
/// 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 {
36
  DaemonCommand({ this.hidden: false });
37

38
  @override
Devon Carew's avatar
Devon Carew committed
39
  final String name = 'daemon';
40 41

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

44
  @override
45
  final bool hidden;
46

47
  @override
48
  Future<Null> runCommand() {
49 50
    printStatus('Starting device daemon...');

51 52
    AppContext appContext = new AppContext();
    NotifyingLogger notifyingLogger = new NotifyingLogger();
53
    appContext.setVariable(Logger, notifyingLogger);
Devon Carew's avatar
Devon Carew committed
54

55 56
    Cache.releaseLockEarly();

57
    return appContext.runInZone(() async {
58 59 60
      Daemon daemon = new Daemon(
          stdinCommandStream, stdoutCommandResponse,
          daemonCommand: this, notifyingLogger: notifyingLogger);
Devon Carew's avatar
Devon Carew committed
61

62
      int code = await daemon.onExit;
63 64
      if (code != 0)
        throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
65
    }, onError: _handleError);
Devon Carew's avatar
Devon Carew committed
66
  }
67

68 69 70
  dynamic _handleError(dynamic error, StackTrace stackTrace) {
    printError('Error from flutter daemon: $error', stackTrace);
    return null;
71
  }
Devon Carew's avatar
Devon Carew committed
72 73
}

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

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

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

    // Start listening.
    commandStream.listen(
Ian Hickson's avatar
Ian Hickson committed
91
      (Map<String, dynamic> request) => _handleRequest(request),
92 93 94 95
      onDone: () {
        if (!_onExitCompleter.isCompleted)
            _onExitCompleter.complete(0);
      }
Devon Carew's avatar
Devon Carew committed
96 97 98
    );
  }

99 100 101 102
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;

Devon Carew's avatar
Devon Carew committed
103
  final DispatchCommand sendCommand;
104
  final DaemonCommand daemonCommand;
105
  final NotifyingLogger notifyingLogger;
106
  final bool logToStdout;
107 108 109 110

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

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

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

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

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

    if (id == null) {
124
      stderr.writeln('no id for request: $request');
Devon Carew's avatar
Devon Carew committed
125 126 127 128
      return;
    }

    try {
129 130 131
      String method = request['method'];
      if (method.indexOf('.') == -1)
        throw 'method not understood: $method';
Devon Carew's avatar
Devon Carew committed
132

133 134 135 136
      String prefix = method.substring(0, method.indexOf('.'));
      String name = method.substring(method.indexOf('.') + 1);
      if (_domainMap[prefix] == null)
        throw 'no domain for method: $method';
Devon Carew's avatar
Devon Carew committed
137

138
      _domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{});
139
    } catch (error) {
140
      _send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
Devon Carew's avatar
Devon Carew committed
141 142 143
    }
  }

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

  void shutdown() {
147
    _domainMap.values.forEach((Domain domain) => domain.dispose());
Devon Carew's avatar
Devon Carew committed
148 149 150 151 152 153
    if (!_onExitCompleter.isCompleted)
      _onExitCompleter.complete(0);
  }
}

abstract class Domain {
154 155
  Domain(this.daemon, this.name);

Devon Carew's avatar
Devon Carew committed
156 157
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
158
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
159 160 161 162 163

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

164 165
  FlutterCommand get command => daemon.daemonCommand;

166
  @override
Devon Carew's avatar
Devon Carew committed
167 168
  String toString() => name;

169
  void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
Ian Hickson's avatar
Ian Hickson committed
170
    new Future<dynamic>.sync(() {
171 172 173
      if (_handlers.containsKey(command))
        return _handlers[command](args);
      throw 'command not understood: $name.$command';
174
    }).then<Null>((dynamic result) {
Devon Carew's avatar
Devon Carew committed
175
      if (result == null) {
Ian Hickson's avatar
Ian Hickson committed
176
        _send(<String, dynamic>{'id': id});
Devon Carew's avatar
Devon Carew committed
177
      } else {
Ian Hickson's avatar
Ian Hickson committed
178
        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
Devon Carew's avatar
Devon Carew committed
179
      }
Ian Hickson's avatar
Ian Hickson committed
180 181
    }).catchError((dynamic error, dynamic trace) {
      _send(<String, dynamic>{'id': id, 'error': _toJsonable(error)});
Devon Carew's avatar
Devon Carew committed
182 183 184
    });
  }

185
  void sendEvent(String name, [dynamic args]) {
186
    Map<String, dynamic> map = <String, dynamic>{ 'event': name };
187 188 189 190 191
    if (args != null)
      map['params'] = _toJsonable(args);
    _send(map);
  }

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

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
  String _getStringArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
      throw "$name is required";
    dynamic val = args[name];
    if (val != null && val is! String)
      throw "$name is not a String";
    return val;
  }

  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
      throw "$name is required";
    dynamic val = args[name];
    if (val != null && val is! bool)
      throw "$name is not a bool";
    return val;
  }

  int _getIntArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
      throw "$name is required";
    dynamic val = args[name];
    if (val != null && val is! int)
      throw "$name is not an int";
    return val;
  }

221
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
222 223 224
}

/// This domain responds to methods like [version] and [shutdown].
225 226
///
/// This domain fires the `daemon.logMessage` event.
Devon Carew's avatar
Devon Carew committed
227 228 229 230
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
231

232
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
233 234 235 236 237 238
      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') {
239
          stderr.writeln(message.message);
240
          if (message.stackTrace != null)
241
            stderr.writeln(message.stackTrace.toString().trimRight());
242
        }
243
      } else {
244 245 246 247 248 249 250 251 252 253 254 255
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
            'stackTrace': message.stackTrace.toString()
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message
          });
        }
256 257
      }
    });
Devon Carew's avatar
Devon Carew committed
258 259
  }

260
  StreamSubscription<LogMessage> _subscription;
261

262
  Future<String> version(Map<String, dynamic> args) {
Ian Hickson's avatar
Ian Hickson committed
263
    return new Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
264 265
  }

266
  Future<Null> shutdown(Map<String, dynamic> args) {
Devon Carew's avatar
Devon Carew committed
267
    Timer.run(() => daemon.shutdown());
Ian Hickson's avatar
Ian Hickson committed
268
    return new Future<Null>.value();
Devon Carew's avatar
Devon Carew committed
269
  }
270

271
  @override
272 273 274
  void dispose() {
    _subscription?.cancel();
  }
Devon Carew's avatar
Devon Carew committed
275 276
}

277
/// This domain responds to methods like [start] and [stop].
Devon Carew's avatar
Devon Carew committed
278
///
279
/// It fires events for application start, stop, and stdout and stderr.
Devon Carew's avatar
Devon Carew committed
280 281 282
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
    registerHandler('start', start);
283
    registerHandler('restart', restart);
284
    registerHandler('callServiceExtension', callServiceExtension);
285
    registerHandler('stop', stop);
286
    registerHandler('discover', discover);
Devon Carew's avatar
Devon Carew committed
287 288
  }

289
  static Uuid _uuidGenerator = new Uuid();
290

291
  static String _getNewAppId() => _uuidGenerator.generateV4();
292 293 294 295 296 297

  List<AppInstance> _apps = <AppInstance>[];

  Future<Map<String, dynamic>> start(Map<String, dynamic> args) async {
    String deviceId = _getStringArg(args, 'deviceId', required: true);
    String projectDirectory = _getStringArg(args, 'projectDirectory', required: true);
298
    bool startPaused = _getBoolArg(args, 'startPaused') ?? false;
Devon Carew's avatar
Devon Carew committed
299
    String route = _getStringArg(args, 'route');
300 301
    String mode = _getStringArg(args, 'mode');
    String target = _getStringArg(args, 'target');
302
    bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault;
303

304
    Device device = daemon.deviceDomain._getOrLocateDevice(deviceId);
305 306
    if (device == null)
      throw "device '$deviceId' not found";
307

308
    if (!fs.isDirectorySync(projectDirectory))
309
      throw "'$projectDirectory' does not exist";
310

311
    BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
312 313 314 315 316 317 318 319 320 321 322 323 324 325

    AppInstance app = startApp(
        device, projectDirectory, target, route,
        buildMode, startPaused, enableHotReload);

    return <String, dynamic>{
      'appId': app.id,
      'deviceId': device.id,
      'directory': projectDirectory,
      'supportsRestart': isRestartSupported(enableHotReload, device)
    };
  }

  AppInstance startApp(
326 327 328 329 330 331 332
    Device device, String projectDirectory, String target, String route,
    BuildMode buildMode, bool startPaused, bool enableHotReload, {
    String applicationBinary,
    String projectRootPath,
    String packagesFilePath,
    String projectAssets,
  }) {
333 334 335 336 337 338 339 340 341 342 343 344 345 346
    DebuggingOptions options;

    switch (buildMode) {
      case BuildMode.debug:
      case BuildMode.profile:
        options = new DebuggingOptions.enabled(buildMode, startPaused: startPaused);
        break;
      case BuildMode.release:
        options = new DebuggingOptions.disabled(buildMode);
        break;
      default:
        throw 'unhandle build mode: $buildMode';
    }

347 348 349
    if (device.isLocalEmulator && !isEmulatorBuildMode(buildMode))
      throw '${toTitleCase(getModeName(buildMode))} mode is not supported for emulators.';

350
    // We change the current working directory for the duration of the `start` command.
351 352
    Directory cwd = fs.currentDirectory;
    fs.currentDirectory = fs.directory(projectDirectory);
353

354 355
    ResidentRunner runner;

356
    if (enableHotReload) {
357 358 359 360
      runner = new HotRunner(
        device,
        target: target,
        debuggingOptions: options,
361 362 363 364 365
        usesTerminalUI: false,
        applicationBinary: applicationBinary,
        projectRootPath: projectRootPath,
        packagesFilePath: packagesFilePath,
        projectAssets: projectAssets,
366 367
      );
    } else {
368
      runner = new ColdRunner(
369 370 371
        device,
        target: target,
        debuggingOptions: options,
372 373
        usesTerminalUI: false,
        applicationBinary: applicationBinary,
374 375
      );
    }
376

377
    AppInstance app = new AppInstance(_getNewAppId(), runner: runner, logToStdout: daemon.logToStdout);
378 379
    _apps.add(app);
    _sendAppEvent(app, 'start', <String, dynamic>{
380
      'deviceId': device.id,
381
      'directory': projectDirectory,
382
      'supportsRestart': isRestartSupported(enableHotReload, device)
383 384
    });

385
    Completer<DebugConnectionInfo> connectionInfoCompleter;
386 387

    if (options.debuggingEnabled) {
388
      connectionInfoCompleter = new Completer<DebugConnectionInfo>();
389
      connectionInfoCompleter.future.then<Null>((DebugConnectionInfo info) {
390
        Map<String, dynamic> params = <String, dynamic>{
391 392
          'port': info.httpUri.port,
          'wsUri': info.wsUri.toString(),
393
        };
394 395 396
        if (info.baseUri != null)
          params['baseUri'] = info.baseUri;
        _sendAppEvent(app, 'debugPort', params);
397
      });
398
    }
399
    Completer<Null> appStartedCompleter = new Completer<Null>();
400
    appStartedCompleter.future.then<Null>((Null value) {
401 402
      _sendAppEvent(app, 'started');
    });
403

404 405 406 407 408 409 410
    app._runInZone(this, () async {
      try {
        await runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
411
        _sendAppEvent(app, 'stop');
412 413 414
      } catch (error) {
        _sendAppEvent(app, 'stop', <String, dynamic>{'error': error.toString()});
      } finally {
415
        fs.currentDirectory = cwd;
416
        _apps.remove(app);
417
      }
418 419
    });

420
    return app;
Devon Carew's avatar
Devon Carew committed
421 422
  }

423
  bool isRestartSupported(bool enableHotReload, Device device) =>
424
      enableHotReload && device.supportsHotMode;
425

Devon Carew's avatar
Devon Carew committed
426
  Future<OperationResult> restart(Map<String, dynamic> args) async {
427
    String appId = _getStringArg(args, 'appId', required: true);
428
    bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
Devon Carew's avatar
Devon Carew committed
429
    bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
430

431 432 433
    AppInstance app = _getApp(appId);
    if (app == null)
      throw "app '$appId' not found";
434

435
    return app._runInZone(this, () {
Devon Carew's avatar
Devon Carew committed
436
      return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
437 438
    });
  }
439

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
  Future<OperationResult> callServiceExtension(Map<String, dynamic> args) async {
    String appId = _getStringArg(args, 'appId', required: true);
    String methodName = _getStringArg(args, 'methodName');
    Map<String, String> params = args['params'] ?? <String, String>{};

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

    Isolate isolate = app.runner.currentView.uiIsolate;
    Map<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
    if (result == null)
      return new OperationResult(1, 'method not available: $methodName');

    if (result.containsKey('error'))
      return new OperationResult(1, result['error']);
    else
      return OperationResult.ok;
  }

460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
  Future<bool> stop(Map<String, dynamic> args) async {
    String appId = _getStringArg(args, 'appId', required: true);

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

    return app.stop().timeout(new Duration(seconds: 5)).then((_) {
      return true;
    }).catchError((dynamic error) {
      _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
      app.closeLogger();
      _apps.remove(app);
      return false;
    });
475 476
  }

477
  Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async {
478 479 480 481 482 483
    String deviceId = _getStringArg(args, 'deviceId', required: true);

    Device device = daemon.deviceDomain._getDevice(deviceId);
    if (device == null)
      throw "device '$deviceId' not found";

484
    List<DiscoveredApp> apps = await device.discoverApps();
485 486 487
    return apps.map((DiscoveredApp app) {
      return <String, dynamic>{
        'id': app.id,
488 489
        'observatoryDevicePort': app.observatoryPort,
        'diagnosticDevicePort': app.diagnosticPort,
490 491 492 493 494 495 496 497 498 499 500 501 502
      };
    }).toList();
  }

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

  void _sendAppEvent(AppInstance app, String name, [Map<String, dynamic> args]) {
    Map<String, dynamic> eventArgs = <String, dynamic> { 'appId': app.id };
    if (args != null)
      eventArgs.addAll(args);
    sendEvent('app.$name', eventArgs);
Devon Carew's avatar
Devon Carew committed
503 504 505
  }
}

506 507
/// This domain lets callers list and monitor connected devices.
///
508 509
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
510 511 512
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
513 514
    registerHandler('enable', enable);
    registerHandler('disable', disable);
515 516
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
517

518 519 520
    PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);
521

522 523 524 525 526 527 528 529 530 531
    deviceDiscovery = new IOSDevices();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);

    deviceDiscovery = new IOSSimulators();
    if (deviceDiscovery.supportsPlatform)
      _discoverers.add(deviceDiscovery);

    for (PollingDeviceDiscovery discoverer in _discoverers) {
      discoverer.onAdded.listen((Device device) {
532 533
        sendEvent('device.added', _deviceToMap(device));
      });
534
      discoverer.onRemoved.listen((Device device) {
535 536 537
        sendEvent('device.removed', _deviceToMap(device));
      });
    }
538 539
  }

540
  List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
541

542
  Future<List<Device>> getDevices([Map<String, dynamic> args]) {
543 544 545
    List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
      return discoverer.devices;
    }).toList();
Ian Hickson's avatar
Ian Hickson committed
546
    return new Future<List<Device>>.value(devices);
547 548
  }

549
  /// Enable device events.
550 551
  Future<Null> enable(Map<String, dynamic> args) {
    for (PollingDeviceDiscovery discoverer in _discoverers)
552
      discoverer.startPolling();
Ian Hickson's avatar
Ian Hickson committed
553
    return new Future<Null>.value();
554 555
  }

556
  /// Disable device events.
557 558
  Future<Null> disable(Map<String, dynamic> args) {
    for (PollingDeviceDiscovery discoverer in _discoverers)
559
      discoverer.stopPolling();
Ian Hickson's avatar
Ian Hickson committed
560
    return new Future<Null>.value();
561 562
  }

563 564
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
565 566 567
    String deviceId = _getStringArg(args, 'deviceId', required: true);
    int devicePort = _getIntArg(args, 'devicePort', required: true);
    int hostPort = _getIntArg(args, 'hostPort');
568

569 570 571
    Device device = daemon.deviceDomain._getDevice(deviceId);
    if (device == null)
      throw "device '$deviceId' not found";
572 573 574

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

575
    return <String, dynamic>{ 'hostPort': hostPort };
576 577 578 579
  }

  /// Removes a forwarded port.
  Future<Null> unforward(Map<String, dynamic> args) async {
580 581 582
    String deviceId = _getStringArg(args, 'deviceId', required: true);
    int devicePort = _getIntArg(args, 'devicePort', required: true);
    int hostPort = _getIntArg(args, 'hostPort', required: true);
583

584 585 586
    Device device = daemon.deviceDomain._getDevice(deviceId);
    if (device == null)
      throw "device '$deviceId' not found";
587

588
    return device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort));
589 590
  }

591
  @override
592
  void dispose() {
593
    for (PollingDeviceDiscovery discoverer in _discoverers)
594
      discoverer.dispose();
595 596 597 598 599 600 601 602
  }

  /// Return the device matching the deviceId field in the args.
  Device _getDevice(String deviceId) {
    List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
      return discoverer.devices;
    }).toList();
    return devices.firstWhere((Device device) => device.id == deviceId, orElse: () => null);
603
  }
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622

  /// Return a known matching device, or scan for devices if no known match is found.
  Device _getOrLocateDevice(String deviceId) {
    // Look for an already known device.
    Device device = _getDevice(deviceId);
    if (device != null)
      return device;

    // Scan the different device providers for a match.
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      List<Device> devices = discoverer.pollingGetDevices();
      for (Device device in devices)
        if (device.id == deviceId)
          return device;
    }

    // No match found.
    return null;
  }
623 624
}

625
Stream<Map<String, dynamic>> get stdinCommandStream => stdin
626 627 628 629 630 631 632 633 634
  .transform(UTF8.decoder)
  .transform(const LineSplitter())
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
  .map((String line) {
    line = line.substring(1, line.length - 1);
    return JSON.decode(line);
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
635
  stdout.writeln('[${JSON.encode(command, toEncodable: _jsonEncodeObject)}]');
636 637 638 639 640 641 642 643 644 645
}

dynamic _jsonEncodeObject(dynamic object) {
  if (object is Device)
    return _deviceToMap(object);
  if (object is OperationResult)
    return _operationResultToMap(object);
  return object;
}

646 647
Map<String, dynamic> _deviceToMap(Device device) {
  return <String, dynamic>{
648
    'id': device.id,
649
    'name': device.name,
650 651
    'platform': getNameForTargetPlatform(device.platform),
    'emulator': device.isLocalEmulator
652 653 654
  };
}

Devon Carew's avatar
Devon Carew committed
655 656 657 658 659 660 661
Map<String, dynamic> _operationResultToMap(OperationResult result) {
  return <String, dynamic>{
    'code': result.code,
    'message': result.message
  };
}

Devon Carew's avatar
Devon Carew committed
662
dynamic _toJsonable(dynamic obj) {
Ian Hickson's avatar
Ian Hickson committed
663
  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
664
    return obj;
665 666
  if (obj is Device)
    return obj;
Devon Carew's avatar
Devon Carew committed
667 668
  if (obj is OperationResult)
    return obj;
Hixie's avatar
Hixie committed
669
  return '$obj';
Devon Carew's avatar
Devon Carew committed
670
}
671

672
class NotifyingLogger extends Logger {
673 674 675 676
  StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast();

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

677
  @override
678 679 680 681
  void printError(String message, [StackTrace stackTrace]) {
    _messageController.add(new LogMessage('error', message, stackTrace));
  }

682
  @override
683
  void printStatus(String message, { bool emphasis: false, bool newline: true, String ansiAlternative }) {
684 685 686
    _messageController.add(new LogMessage('status', message));
  }

687
  @override
688
  void printTrace(String message) {
689
    // This is a lot of traffic to send over the wire.
690
  }
691 692

  @override
693
  Status startProgress(String message, { String progressId }) {
694 695 696
    printStatus(message);
    return new Status();
  }
697 698 699 700

  void dispose() {
    _messageController.close();
  }
701 702
}

703 704
/// A running application, started by this daemon.
class AppInstance {
705
  AppInstance(this.id, { this.runner, this.logToStdout = false });
706 707

  final String id;
708
  final ResidentRunner runner;
709
  final bool logToStdout;
710 711 712

  _AppRunLogger _logger;

Devon Carew's avatar
Devon Carew committed
713 714
  Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
    return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
715
  }
716 717 718 719 720 721 722 723 724

  Future<Null> stop() => runner.stop();

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

  dynamic _runInZone(AppDomain domain, dynamic method()) {
    if (_logger == null)
725
      _logger = new _AppRunLogger(domain, this, logToStdout: logToStdout);
726 727

    AppContext appContext = new AppContext();
728
    appContext.setVariable(Logger, _logger);
729 730 731 732 733 734
    return appContext.runInZone(method);
  }
}

/// A [Logger] which sends log messages to a listening daemon client.
class _AppRunLogger extends Logger {
735
  _AppRunLogger(this.domain, this.app, { this.logToStdout: false });
736 737 738

  AppDomain domain;
  final AppInstance app;
739
  final bool logToStdout;
740
  int _nextProgressId = 0;
741 742 743

  @override
  void printError(String message, [StackTrace stackTrace]) {
744
    if (logToStdout) {
745
      stderr.writeln(message);
746
      if (stackTrace != null)
747
        stderr.writeln(stackTrace.toString().trimRight());
748
    } else {
749 750 751 752 753 754 755 756 757 758 759 760
      if (stackTrace != null) {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'stackTrace': stackTrace.toString(),
          'error': true
        });
      } else {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'error': true
        });
      }
761 762 763 764
    }
  }

  @override
765
  void printStatus(String message, { bool emphasis: false, bool newline: true, String ansiAlternative }) {
766 767 768 769 770
    if (logToStdout) {
      print(message);
    } else {
      _sendLogEvent(<String, dynamic>{ 'log': message });
    }
771 772 773 774 775
  }

  @override
  void printTrace(String message) { }

Devon Carew's avatar
Devon Carew committed
776 777
  Status _status;

778
  @override
779
  Status startProgress(String message, { String progressId }) {
Devon Carew's avatar
Devon Carew committed
780 781 782 783
    // Ignore nested progresses; return a no-op status object.
    if (_status != null)
      return new Status();

784 785
    int id = _nextProgressId++;

786 787
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
788
      'progressId': progressId,
789
      'message': message,
790 791
    });

792
    _status = new _AppLoggerStatus(this, id, progressId);
Devon Carew's avatar
Devon Carew committed
793
    return _status;
794 795 796 797 798
  }

  void close() {
    domain = null;
  }
799 800 801 802 803 804 805

  void _sendLogEvent(Map<String, dynamic> event) {
    if (domain == null)
      printStatus('event sent after app closed: $event');
    else
      domain._sendAppEvent(app, 'log', event);
  }
806 807 808 809 810 811 812

  void _sendProgressEvent(Map<String, dynamic> event) {
    if (domain == null)
      printStatus('event sent after app closed: $event');
    else
      domain._sendAppEvent(app, 'progress', event);
  }
813 814 815
}

class _AppLoggerStatus implements Status {
816
  _AppLoggerStatus(this.logger, this.id, this.progressId);
817 818 819

  final _AppRunLogger logger;
  final int id;
820
  final String progressId;
821 822

  @override
Devon Carew's avatar
Devon Carew committed
823 824
  void stop({ bool showElapsedTime: true }) {
    logger._status = null;
825 826 827 828 829
    _sendFinished();
  }

  @override
  void cancel() {
Devon Carew's avatar
Devon Carew committed
830
    logger._status = null;
831 832 833 834
    _sendFinished();
  }

  void _sendFinished() {
835
    logger._sendProgressEvent(<String, dynamic>{
836
      'id': id.toString(),
837
      'progressId': progressId,
838 839 840
      'finished': true
    });
  }
841 842
}

843 844 845 846 847 848 849
class LogMessage {
  final String level;
  final String message;
  final StackTrace stackTrace;

  LogMessage(this.level, this.message, [this.stackTrace]);
}