daemon.dart 27.3 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
    _domainMap.values.forEach((Domain domain) => domain.dispose());
147 148 149 150 151 152
    if (!_onExitCompleter.isCompleted) {
      if (error == null)
        _onExitCompleter.complete(0);
      else
        _onExitCompleter.completeError(error);
    }
Devon Carew's avatar
Devon Carew committed
153 154 155 156
  }
}

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

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

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

167 168
  FlutterCommand get command => daemon.daemonCommand;

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

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

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

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

197 198 199
  String _getStringArg(Map<String, dynamic> args, String name, { bool required: false }) {
    if (required && !args.containsKey(name))
      throw "$name is required";
200
    final dynamic val = args[name];
201 202 203 204 205 206 207 208
    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";
209
    final dynamic val = args[name];
210 211 212 213 214 215 216 217
    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";
218
    final dynamic val = args[name];
219 220 221 222 223
    if (val != null && val is! int)
      throw "$name is not an int";
    return val;
  }

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

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

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

263
  StreamSubscription<LogMessage> _subscription;
264

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

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

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

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

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

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

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

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

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

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

315
    final BuildMode buildMode = getBuildModeForName(mode) ?? BuildMode.debug;
316 317 318 319 320 321 322 323 324 325
    DebuggingOptions options;
    if (buildMode == BuildMode.release) {
      options = new DebuggingOptions.disabled(buildMode);
    } else {
      options = new DebuggingOptions.enabled(
        buildMode,
        startPaused: startPaused,
        useTestFonts: useTestFonts,
      );
    }
326

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

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

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

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

359 360
    final FlutterDevice flutterDevice = new FlutterDevice(device);

361 362
    ResidentRunner runner;

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

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

392
    Completer<DebugConnectionInfo> connectionInfoCompleter;
393 394

    if (options.debuggingEnabled) {
395
      connectionInfoCompleter = new Completer<DebugConnectionInfo>();
396
      connectionInfoCompleter.future.then<Null>((DebugConnectionInfo info) {
397
        final Map<String, dynamic> params = <String, dynamic>{
398 399
          'port': info.httpUri.port,
          'wsUri': info.wsUri.toString(),
400
        };
401 402 403
        if (info.baseUri != null)
          params['baseUri'] = info.baseUri;
        _sendAppEvent(app, 'debugPort', params);
404
      });
405
    }
406
    final Completer<Null> appStartedCompleter = new Completer<Null>();
407
    appStartedCompleter.future.then<Null>((Null value) {
408 409
      _sendAppEvent(app, 'started');
    });
410

411
    await app._runInZone(this, () async {
412 413 414 415 416 417
      try {
        await runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
418
        _sendAppEvent(app, 'stop');
419
      } catch (error) {
420
        _sendAppEvent(app, 'stop', <String, dynamic>{'error': _toJsonable(error)});
421
      } finally {
422
        fs.currentDirectory = cwd;
423
        _apps.remove(app);
424
      }
425 426
    });

427
    return app;
Devon Carew's avatar
Devon Carew committed
428 429
  }

430
  bool isRestartSupported(bool enableHotReload, Device device) =>
431
      enableHotReload && device.supportsHotMode;
432

Devon Carew's avatar
Devon Carew committed
433
  Future<OperationResult> restart(Map<String, dynamic> args) async {
434 435 436
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
437

438
    final AppInstance app = _getApp(appId);
439 440
    if (app == null)
      throw "app '$appId' not found";
441

442
    return app._runInZone(this, () {
Devon Carew's avatar
Devon Carew committed
443
      return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
444 445
    });
  }
446

447 448 449 450 451 452 453 454 455 456
  /// 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 {
457 458 459
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
    final Map<String, String> params = args['params'] ?? <String, String>{};
460

461
    final AppInstance app = _getApp(appId);
462 463 464
    if (app == null)
      throw "app '$appId' not found";

465
    final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate;
466
    final Map<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
467
    if (result == null)
468
      throw 'method not available: $methodName';
469 470

    if (result.containsKey('error'))
471 472 473
      throw result['error'];

    return result;
474 475
  }

476
  Future<bool> stop(Map<String, dynamic> args) async {
477
    final String appId = _getStringArg(args, 'appId', required: true);
478

479
    final AppInstance app = _getApp(appId);
480 481 482
    if (app == null)
      throw "app '$appId' not found";

483
    return app.stop().timeout(const Duration(seconds: 5)).then<bool>((_) {
484 485 486 487 488 489 490
      return true;
    }).catchError((dynamic error) {
      _sendAppEvent(app, 'log', <String, dynamic>{ 'log': '$error', 'error': true });
      app.closeLogger();
      _apps.remove(app);
      return false;
    });
491 492
  }

493
  Future<List<Map<String, dynamic>>> discover(Map<String, dynamic> args) async {
494
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
495

496
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
497 498 499
    if (device == null)
      throw "device '$deviceId' not found";

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

522 523
typedef void _DeviceEventHandler(Device device);

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

536 537 538 539
    addDeviceDiscoverer(new AndroidDevices());
    addDeviceDiscoverer(new IOSDevices());
    addDeviceDiscoverer(new IOSSimulators());
  }
540

541 542 543
  void addDeviceDiscoverer(PollingDeviceDiscovery discoverer) {
    if (!discoverer.supportsPlatform)
      return;
544

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

560
    _discoverers.add(discoverer);
561

562 563
    discoverer.onAdded.listen(_onDeviceEvent('device.added'));
    discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
564 565
  }

566 567
  Future<Null> _serializeDeviceEvents = new Future<Null>.value();

568 569
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
570
      _serializeDeviceEvents = _serializeDeviceEvents.then((_) async {
571 572 573 574 575
        sendEvent(eventName, await _deviceToMap(device));
      });
    };
  }

576
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
577

578 579 580 581 582 583
  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;
584 585
  }

586
  /// Enable device events.
587 588
  Future<Null> enable(Map<String, dynamic> args) {
    for (PollingDeviceDiscovery discoverer in _discoverers)
589
      discoverer.startPolling();
Ian Hickson's avatar
Ian Hickson committed
590
    return new Future<Null>.value();
591 592
  }

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

600 601
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
602 603
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
604
    int hostPort = _getIntArg(args, 'hostPort');
605

606
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
607 608
    if (device == null)
      throw "device '$deviceId' not found";
609 610 611

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

612
    return <String, dynamic>{ 'hostPort': hostPort };
613 614 615 616
  }

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

621
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
622 623
    if (device == null)
      throw "device '$deviceId' not found";
624

625
    return device.portForwarder.unforward(new ForwardedPort(hostPort, devicePort));
626 627
  }

628
  @override
629
  void dispose() {
630
    for (PollingDeviceDiscovery discoverer in _discoverers)
631
      discoverer.dispose();
632 633 634
  }

  /// Return the device matching the deviceId field in the args.
635 636 637 638 639 640 641
  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;
642
  }
643 644

  /// Return a known matching device, or scan for devices if no known match is found.
645
  Future<Device> _getOrLocateDevice(String deviceId) async {
646
    // Look for an already known device.
647
    final Device device = await _getDevice(deviceId);
648 649 650 651 652
    if (device != null)
      return device;

    // Scan the different device providers for a match.
    for (PollingDeviceDiscovery discoverer in _discoverers) {
653
      final List<Device> devices = await discoverer.pollingGetDevices();
654 655 656 657 658 659 660 661
      for (Device device in devices)
        if (device.id == deviceId)
          return device;
    }

    // No match found.
    return null;
  }
662 663
}

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

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

684
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
685
  return <String, dynamic>{
686
    'id': device.id,
687
    'name': device.name,
688
    'platform': getNameForTargetPlatform(await device.targetPlatform),
689
    'emulator': await device.isLocalEmulator,
690 691 692
  };
}

Devon Carew's avatar
Devon Carew committed
693 694 695 696 697 698 699
Map<String, dynamic> _operationResultToMap(OperationResult result) {
  return <String, dynamic>{
    'code': result.code,
    'message': result.message
  };
}

Devon Carew's avatar
Devon Carew committed
700
dynamic _toJsonable(dynamic obj) {
Ian Hickson's avatar
Ian Hickson committed
701
  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
702
    return obj;
Devon Carew's avatar
Devon Carew committed
703 704
  if (obj is OperationResult)
    return obj;
705 706
  if (obj is ToolExit)
    return obj.message;
Hixie's avatar
Hixie committed
707
  return '$obj';
Devon Carew's avatar
Devon Carew committed
708
}
709

710
class NotifyingLogger extends Logger {
711
  final StreamController<LogMessage> _messageController = new StreamController<LogMessage>.broadcast();
712 713 714

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

715
  @override
716
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
717 718 719
    _messageController.add(new LogMessage('error', message, stackTrace));
  }

720
  @override
721 722 723 724
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  ) {
725 726 727
    _messageController.add(new LogMessage('status', message));
  }

728
  @override
729
  void printTrace(String message) {
730
    // This is a lot of traffic to send over the wire.
731
  }
732 733

  @override
734
  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
735 736 737
    printStatus(message);
    return new Status();
  }
738 739 740 741

  void dispose() {
    _messageController.close();
  }
742 743
}

744 745
/// A running application, started by this daemon.
class AppInstance {
746
  AppInstance(this.id, { this.runner, this.logToStdout = false });
747 748

  final String id;
749
  final ResidentRunner runner;
750
  final bool logToStdout;
751 752 753

  _AppRunLogger _logger;

Devon Carew's avatar
Devon Carew committed
754 755
  Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
    return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart);
756
  }
757 758 759 760 761 762 763 764

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

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

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

767
    final AppContext appContext = new AppContext();
768
    appContext.setVariable(Logger, _logger);
769 770 771 772 773
    return appContext.runInZone(method);
  }
}

/// A [Logger] which sends log messages to a listening daemon client.
774 775 776 777 778 779 780
///
/// 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.
781
class _AppRunLogger extends Logger {
782
  _AppRunLogger(this.domain, this.app, { this.parent });
783 784 785

  AppDomain domain;
  final AppInstance app;
786
  final Logger parent;
787
  int _nextProgressId = 0;
788 789

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

  @override
810
  void printStatus(
811 812 813 814 815 816
    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);
817 818 819
    } else {
      _sendLogEvent(<String, dynamic>{ 'log': message });
    }
820 821 822
  }

  @override
823 824 825 826 827 828 829
  void printTrace(String message) {
    if (parent != null) {
      parent.printTrace(message);
    } else {
      _sendLogEvent(<String, dynamic>{ 'log': message, 'trace': true });
    }
  }
830

Devon Carew's avatar
Devon Carew committed
831 832
  Status _status;

833
  @override
834
  Status startProgress(String message, { String progressId, bool expectSlowOperation: false }) {
Devon Carew's avatar
Devon Carew committed
835 836 837 838
    // Ignore nested progresses; return a no-op status object.
    if (_status != null)
      return new Status();

839
    final int id = _nextProgressId++;
840

841 842
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
843
      'progressId': progressId,
844
      'message': message,
845 846
    });

847
    _status = new _AppLoggerStatus(this, id, progressId);
Devon Carew's avatar
Devon Carew committed
848
    return _status;
849 850 851 852 853
  }

  void close() {
    domain = null;
  }
854 855 856 857 858 859 860

  void _sendLogEvent(Map<String, dynamic> event) {
    if (domain == null)
      printStatus('event sent after app closed: $event');
    else
      domain._sendAppEvent(app, 'log', event);
  }
861 862 863 864 865 866 867

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

class _AppLoggerStatus implements Status {
871
  _AppLoggerStatus(this.logger, this.id, this.progressId);
872 873 874

  final _AppRunLogger logger;
  final int id;
875
  final String progressId;
876 877

  @override
878
  void stop() {
Devon Carew's avatar
Devon Carew committed
879
    logger._status = null;
880 881 882 883 884
    _sendFinished();
  }

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

  void _sendFinished() {
890
    logger._sendProgressEvent(<String, dynamic>{
891
      'id': id.toString(),
892
      'progressId': progressId,
893 894 895
      'finished': true
    });
  }
896 897
}

898 899 900 901 902 903 904
class LogMessage {
  final String level;
  final String message;
  final StackTrace stackTrace;

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