daemon.dart 27.6 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
    final AppContext appContext = new AppContext();
    final 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
      final Daemon daemon = new Daemon(
59 60
          stdinCommandStream, stdoutCommandResponse,
          daemonCommand: this, notifyingLogger: notifyingLogger);
Devon Carew's avatar
Devon Carew committed
61

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

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

71
typedef Future<dynamic> CommandHandler(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
72 73

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

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

96 97 98
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
99
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
100

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

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

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

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

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

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

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

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

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

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

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

144 145
  void shutdown({dynamic error}) {
    _commandSubscription?.cancel();
146 147
    for (Domain domain in _domainMap.values)
      domain.dispose();
148 149 150 151 152 153
    if (!_onExitCompleter.isCompleted) {
      if (error == null)
        _onExitCompleter.complete(0);
      else
        _onExitCompleter.completeError(error);
    }
Devon Carew's avatar
Devon Carew committed
154 155 156 157
  }
}

abstract class Domain {
158 159
  Domain(this.daemon, this.name);

Devon Carew's avatar
Devon Carew committed
160 161
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
162
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
163 164 165 166 167

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

168 169
  FlutterCommand get command => daemon.daemonCommand;

170
  @override
Devon Carew's avatar
Devon Carew committed
171 172
  String toString() => name;

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

189
  void sendEvent(String name, [dynamic args]) {
190
    final Map<String, dynamic> map = <String, dynamic>{ 'event': name };
191 192 193 194 195
    if (args != null)
      map['params'] = _toJsonable(args);
    _send(map);
  }

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

198 199
  String _getStringArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
200
      throw '$name is required';
201
    final dynamic val = args[name];
202
    if (val != null && val is! String)
203
      throw '$name is not a String';
204 205 206 207 208
    return val;
  }

  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
209
      throw '$name is required';
210
    final dynamic val = args[name];
211
    if (val != null && val is! bool)
212
      throw '$name is not a bool';
213 214 215 216 217
    return val;
  }

  int _getIntArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
218
      throw '$name is required';
219
    final dynamic val = args[name];
220
    if (val != null && val is! int)
221
      throw '$name is not an int';
222 223 224
    return val;
  }

225
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
226 227 228
}

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

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

264
  StreamSubscription<LogMessage> _subscription;
265

266
  Future<String> version(Map<String, dynamic> args) {
Ian Hickson's avatar
Ian Hickson committed
267
    return new Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
268 269
  }

270
  Future<Null> shutdown(Map<String, dynamic> args) {
271
    Timer.run(daemon.shutdown);
Ian Hickson's avatar
Ian Hickson committed
272
    return new Future<Null>.value();
Devon Carew's avatar
Devon Carew committed
273
  }
274

275
  @override
276 277 278
  void dispose() {
    _subscription?.cancel();
  }
Devon Carew's avatar
Devon Carew committed
279 280
}

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

293
  static final Uuid _uuidGenerator = new Uuid();
294

295
  static String _getNewAppId() => _uuidGenerator.generateV4();
296

297
  final List<AppInstance> _apps = <AppInstance>[];
298 299

  Future<Map<String, dynamic>> start(Map<String, dynamic> args) async {
300 301 302
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final String projectDirectory = _getStringArg(args, 'projectDirectory', required: true);
    final bool startPaused = _getBoolArg(args, 'startPaused') ?? false;
303
    final bool useTestFonts = _getBoolArg(args, 'useTestFonts') ?? false;
304 305
    final String route = _getStringArg(args, 'route');
    final String mode = _getStringArg(args, 'mode');
306
    final String flavor = _getStringArg(args, 'flavor');
307 308 309
    final String target = _getStringArg(args, 'target');
    final bool enableHotReload = _getBoolArg(args, 'hot') ?? kHotReloadDefault;

310
    final Device device = await daemon.deviceDomain._getOrLocateDevice(deviceId);
311 312
    if (device == null)
      throw "device '$deviceId' not found";
313

314
    if (!fs.isDirectorySync(projectDirectory))
315
      throw "'$projectDirectory' does not exist";
316

317
    final BuildInfo buildInfo = new BuildInfo(getBuildModeForName(mode) ?? BuildMode.debug, flavor);
318
    DebuggingOptions options;
319 320
    if (buildInfo.isRelease) {
      options = new DebuggingOptions.disabled(buildInfo);
321 322
    } else {
      options = new DebuggingOptions.enabled(
323
        buildInfo,
324 325 326 327
        startPaused: startPaused,
        useTestFonts: useTestFonts,
      );
    }
328

329
    final AppInstance app = await startApp(
330 331 332 333 334 335 336
      device,
      projectDirectory,
      target,
      route,
      options,
      enableHotReload,
    );
337 338 339 340 341 342 343 344 345

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

346
  Future<AppInstance> startApp(
347
    Device device, String projectDirectory, String target, String route,
348
    DebuggingOptions options, bool enableHotReload, {
349 350 351 352
    String applicationBinary,
    String projectRootPath,
    String packagesFilePath,
    String projectAssets,
353
  }) async {
354 355
    if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator)
      throw '${toTitleCase(options.buildInfo.modeName)} mode is not supported for emulators.';
356

357
    // We change the current working directory for the duration of the `start` command.
358
    final Directory cwd = fs.currentDirectory;
359
    fs.currentDirectory = fs.directory(projectDirectory);
360

361 362
    final FlutterDevice flutterDevice = new FlutterDevice(device);

363 364
    ResidentRunner runner;

365
    if (enableHotReload) {
366
      runner = new HotRunner(
367
        <FlutterDevice>[flutterDevice],
368 369
        target: target,
        debuggingOptions: options,
370 371 372 373 374
        usesTerminalUI: false,
        applicationBinary: applicationBinary,
        projectRootPath: projectRootPath,
        packagesFilePath: packagesFilePath,
        projectAssets: projectAssets,
375
        hostIsIde: true,
376 377
      );
    } else {
378
      runner = new ColdRunner(
379
        <FlutterDevice>[flutterDevice],
380 381
        target: target,
        debuggingOptions: options,
382 383
        usesTerminalUI: false,
        applicationBinary: applicationBinary,
384 385
      );
    }
386

387
    final AppInstance app = new AppInstance(_getNewAppId(), runner: runner, logToStdout: daemon.logToStdout);
388 389
    _apps.add(app);
    _sendAppEvent(app, 'start', <String, dynamic>{
390
      'deviceId': device.id,
391
      'directory': projectDirectory,
392
      'supportsRestart': isRestartSupported(enableHotReload, device),
393 394
    });

395
    Completer<DebugConnectionInfo> connectionInfoCompleter;
396 397

    if (options.debuggingEnabled) {
398
      connectionInfoCompleter = new Completer<DebugConnectionInfo>();
399 400 401
      // We don't want to wait for this future to complete and callbacks won't fail.
      // As it just writes to stdout.
      connectionInfoCompleter.future.then<Null>((DebugConnectionInfo info) { // ignore: unawaited_futures
402
        final Map<String, dynamic> params = <String, dynamic>{
403 404
          'port': info.httpUri.port,
          'wsUri': info.wsUri.toString(),
405
        };
406 407 408
        if (info.baseUri != null)
          params['baseUri'] = info.baseUri;
        _sendAppEvent(app, 'debugPort', params);
409
      });
410
    }
411
    final Completer<Null> appStartedCompleter = new Completer<Null>();
412 413 414
    // We don't want to wait for this future to complete and callbacks won't fail.
    // As it just writes to stdout.
    appStartedCompleter.future.then<Null>((Null value) { // ignore: unawaited_futures
415 416
      _sendAppEvent(app, 'started');
    });
417

418
    await app._runInZone(this, () async {
419 420 421 422 423 424
      try {
        await runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
425
        _sendAppEvent(app, 'stop');
426
      } catch (error) {
427
        _sendAppEvent(app, 'stop', <String, dynamic>{'error': _toJsonable(error)});
428
      } finally {
429
        fs.currentDirectory = cwd;
430
        _apps.remove(app);
431
      }
432 433
    });

434
    return app;
Devon Carew's avatar
Devon Carew committed
435 436
  }

437
  bool isRestartSupported(bool enableHotReload, Device device) =>
438
      enableHotReload && device.supportsHotMode;
439

Devon Carew's avatar
Devon Carew committed
440
  Future<OperationResult> restart(Map<String, dynamic> args) async {
441 442 443
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
444

445
    final AppInstance app = _getApp(appId);
446 447
    if (app == null)
      throw "app '$appId' not found";
448

449
    return app._runInZone(this, () {
Devon Carew's avatar
Devon Carew committed
450
      return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
451 452
    });
  }
453

454 455 456 457 458 459 460 461 462 463
  /// 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 {
464 465 466
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
    final Map<String, String> params = args['params'] ?? <String, String>{};
467

468
    final AppInstance app = _getApp(appId);
469 470 471
    if (app == null)
      throw "app '$appId' not found";

472
    final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate;
473
    final Map<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
474
    if (result == null)
475
      throw 'method not available: $methodName';
476 477

    if (result.containsKey('error'))
478 479 480
      throw result['error'];

    return result;
481 482
  }

483
  Future<bool> stop(Map<String, dynamic> args) async {
484
    final String appId = _getStringArg(args, 'appId', required: true);
485

486
    final AppInstance app = _getApp(appId);
487 488 489
    if (app == null)
      throw "app '$appId' not found";

490
    return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) {
491 492 493 494 495 496 497
      return true;
    }).catchError((dynamic error) {
      _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
      app.closeLogger();
      _apps.remove(app);
      return false;
    });
498 499
  }

500
  Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async {
501
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
502

503
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
504 505 506
    if (device == null)
      throw "device '$deviceId' not found";

507
    final List<DiscoveredApp> apps = await device.discoverApps();
508 509 510
    return apps.map((DiscoveredApp app) {
      return <String, dynamic>{
        'id': app.id,
511
        'observatoryDevicePort': app.observatoryPort,
512 513 514 515 516 517 518 519 520
      };
    }).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]) {
521
    final Map<String, dynamic> eventArgs = <String, dynamic> { 'appId': app.id };
522 523 524
    if (args != null)
      eventArgs.addAll(args);
    sendEvent('app.$name', eventArgs);
Devon Carew's avatar
Devon Carew committed
525 526 527
  }
}

528 529
typedef void _DeviceEventHandler(Device device);

530 531
/// This domain lets callers list and monitor connected devices.
///
532 533
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
534 535 536
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
537 538
    registerHandler('enable', enable);
    registerHandler('disable', disable);
539 540
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
541

542 543 544 545
    addDeviceDiscoverer(new AndroidDevices());
    addDeviceDiscoverer(new IOSDevices());
    addDeviceDiscoverer(new IOSSimulators());
  }
546

547 548 549
  void addDeviceDiscoverer(PollingDeviceDiscovery discoverer) {
    if (!discoverer.supportsPlatform)
      return;
550

551
    if (!discoverer.canListAnything) {
552 553
      // This event will affect the client UI. Coordinate changes here
      // with the Flutter IntelliJ team.
554 555 556
      sendEvent(
        'daemon.showMessage',
        <String, String>{
557
          'level': 'warning',
558 559 560 561 562 563 564 565
          'title': 'Unable to list devices',
          'message':
              'Unable to discover ${discoverer.name}. Please run '
              '"flutter doctor" to diagnose potential issues',
        },
      );
    }

566
    _discoverers.add(discoverer);
567

568 569
    discoverer.onAdded.listen(_onDeviceEvent('device.added'));
    discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
570 571
  }

572 573
  Future<Null> _serializeDeviceEvents = new Future<Null>.value();

574 575
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
576
      _serializeDeviceEvents = _serializeDeviceEvents.then((_) async {
577 578 579 580 581
        sendEvent(eventName, await _deviceToMap(device));
      });
    };
  }

582
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
583

584 585 586 587 588 589
  Future<List<Device>> getDevices([Map<String, dynamic> args]) async {
    final List<Device> devices = <Device>[];
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      devices.addAll(await discoverer.devices);
    }
    return devices;
590 591
  }

592
  /// Enable device events.
593 594
  Future<Null> enable(Map<String, dynamic> args) {
    for (PollingDeviceDiscovery discoverer in _discoverers)
595
      discoverer.startPolling();
Ian Hickson's avatar
Ian Hickson committed
596
    return new Future<Null>.value();
597 598
  }

599
  /// Disable device events.
600 601
  Future<Null> disable(Map<String, dynamic> args) {
    for (PollingDeviceDiscovery discoverer in _discoverers)
602
      discoverer.stopPolling();
Ian Hickson's avatar
Ian Hickson committed
603
    return new Future<Null>.value();
604 605
  }

606 607
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
608 609
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
610
    int hostPort = _getIntArg(args, 'hostPort');
611

612
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
613 614
    if (device == null)
      throw "device '$deviceId' not found";
615 616 617

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

618
    return <String, dynamic>{ 'hostPort': hostPort };
619 620 621 622
  }

  /// Removes a forwarded port.
  Future<Null> unforward(Map<String, dynamic> args) async {
623 624 625
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
626

627
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
628 629
    if (device == null)
      throw "device '$deviceId' not found";
630

631
    return device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort));
632 633
  }

634
  @override
635
  void dispose() {
636
    for (PollingDeviceDiscovery discoverer in _discoverers)
637
      discoverer.dispose();
638 639 640
  }

  /// Return the device matching the deviceId field in the args.
641 642 643 644 645 646 647
  Future<Device> _getDevice(String deviceId) async {
    for (PollingDeviceDiscovery discoverer in _discoverers) {
      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
      if (device != null)
        return device;
    }
    return null;
648
  }
649 650

  /// Return a known matching device, or scan for devices if no known match is found.
651
  Future<Device> _getOrLocateDevice(String deviceId) async {
652
    // Look for an already known device.
653
    final Device device = await _getDevice(deviceId);
654 655 656 657 658
    if (device != null)
      return device;

    // Scan the different device providers for a match.
    for (PollingDeviceDiscovery discoverer in _discoverers) {
659
      final List<Device> devices = await discoverer.pollingGetDevices();
660 661 662 663 664 665 666 667
      for (Device device in devices)
        if (device.id == deviceId)
          return device;
    }

    // No match found.
    return null;
  }
668 669
}

670
Stream<Map<String, dynamic>> get stdinCommandStream => stdin
671 672 673 674 675 676 677 678 679
  .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) {
680 681
  final String encoded = JSON.encode(command, toEncodable: _jsonEncodeObject);
  stdout.writeln('[$encoded]');
682 683 684 685 686 687 688 689
}

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

690
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
691
  return <String, dynamic>{
692
    'id': device.id,
693
    'name': device.name,
694
    'platform': getNameForTargetPlatform(await device.targetPlatform),
695
    'emulator': await device.isLocalEmulator,
696 697 698
  };
}

Devon Carew's avatar
Devon Carew committed
699 700 701 702 703 704 705
Map<String, dynamic> _operationResultToMap(OperationResult result) {
  return <String, dynamic>{
    'code': result.code,
    'message': result.message
  };
}

Devon Carew's avatar
Devon Carew committed
706
dynamic _toJsonable(dynamic obj) {
Ian Hickson's avatar
Ian Hickson committed
707
  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
708
    return obj;
Devon Carew's avatar
Devon Carew committed
709 710
  if (obj is OperationResult)
    return obj;
711 712
  if (obj is ToolExit)
    return obj.message;
Hixie's avatar
Hixie committed
713
  return '$obj';
Devon Carew's avatar
Devon Carew committed
714
}
715

716
class NotifyingLogger extends Logger {
717
  final StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast();
718 719 720

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

721
  @override
722
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
723 724 725
    _messageController.add(new LogMessage('error', message, stackTrace));
  }

726
  @override
727 728 729 730
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  ) {
731 732 733
    _messageController.add(new LogMessage('status', message));
  }

734
  @override
735
  void printTrace(String message) {
736
    // This is a lot of traffic to send over the wire.
737
  }
738 739

  @override
740
  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
741 742 743
    printStatus(message);
    return new Status();
  }
744 745 746 747

  void dispose() {
    _messageController.close();
  }
748 749
}

750 751
/// A running application, started by this daemon.
class AppInstance {
752
  AppInstance(this.id, { this.runner, this.logToStdout = false });
753 754

  final String id;
755
  final ResidentRunner runner;
756
  final bool logToStdout;
757 758 759

  _AppRunLogger _logger;

Devon Carew's avatar
Devon Carew committed
760 761
  Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
    return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
762
  }
763 764 765 766 767 768 769 770

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

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

  dynamic _runInZone(AppDomain domain, dynamic method()) {
771
    _logger ??= new _AppRunLogger(domain, this, parent: logToStdout ? logger : null);
772

773
    final AppContext appContext = new AppContext();
774
    appContext.setVariable(Logger, _logger);
775 776 777 778 779
    return appContext.runInZone(method);
  }
}

/// A [Logger] which sends log messages to a listening daemon client.
780 781 782 783 784 785 786
///
/// 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
///
/// 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.
787
class _AppRunLogger extends Logger {
788
  _AppRunLogger(this.domain, this.app, { this.parent });
789 790 791

  AppDomain domain;
  final AppInstance app;
792
  final Logger parent;
793
  int _nextProgressId = 0;
794 795

  @override
796
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
797 798
    if (parent != null) {
      parent.printError(message, stackTrace: stackTrace, emphasis: emphasis);
799
    } else {
800 801 802 803 804 805 806 807 808 809 810 811
      if (stackTrace != null) {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'stackTrace': stackTrace.toString(),
          'error': true
        });
      } else {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'error': true
        });
      }
812 813 814 815
    }
  }

  @override
816
  void printStatus(
817 818 819 820 821 822
    String message, {
    bool emphasis: false, bool newline: true, String ansiAlternative, int indent
  }) {
    if (parent != null) {
      parent.printStatus(message, emphasis: emphasis, newline: newline,
          ansiAlternative: ansiAlternative, indent: indent);
823 824 825
    } else {
      _sendLogEvent(<String, dynamic>{ 'log': message });
    }
826 827 828
  }

  @override
829 830 831 832 833 834 835
  void printTrace(String message) {
    if (parent != null) {
      parent.printTrace(message);
    } else {
      _sendLogEvent(<String, dynamic>{ 'log': message, 'trace': true });
    }
  }
836

Devon Carew's avatar
Devon Carew committed
837 838
  Status _status;

839
  @override
840
  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
Devon Carew's avatar
Devon Carew committed
841 842 843 844
    // Ignore nested progresses; return a no-op status object.
    if (_status != null)
      return new Status();

845
    final int id = _nextProgressId++;
846

847 848
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
849
      'progressId': progressId,
850
      'message': message,
851 852
    });

853
    _status = new _AppLoggerStatus(this, id, progressId);
Devon Carew's avatar
Devon Carew committed
854
    return _status;
855 856 857 858 859
  }

  void close() {
    domain = null;
  }
860 861 862 863 864 865 866

  void _sendLogEvent(Map<String, dynamic> event) {
    if (domain == null)
      printStatus('event sent after app closed: $event');
    else
      domain._sendAppEvent(app, 'log', event);
  }
867 868 869 870 871 872 873

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

class _AppLoggerStatus implements Status {
877
  _AppLoggerStatus(this.logger, this.id, this.progressId);
878 879 880

  final _AppRunLogger logger;
  final int id;
881
  final String progressId;
882 883

  @override
884
  void stop() {
Devon Carew's avatar
Devon Carew committed
885
    logger._status = null;
886 887 888 889 890
    _sendFinished();
  }

  @override
  void cancel() {
Devon Carew's avatar
Devon Carew committed
891
    logger._status = null;
892 893 894 895
    _sendFinished();
  }

  void _sendFinished() {
896
    logger._sendProgressEvent(<String, dynamic>{
897
      'id': id.toString(),
898
      'progressId': progressId,
899 900 901
      'finished': true
    });
  }
902 903
}

904 905 906 907 908 909 910
class LogMessage {
  final String level;
  final String message;
  final StackTrace stackTrace;

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