daemon.dart 29.6 KB
Newer Older
Devon Carew's avatar
Devon Carew committed
1 2 3 4 5 6
// 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';

7 8
import 'package:meta/meta.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/terminal.dart';
15
import '../base/utils.dart';
16
import '../build_info.dart';
17
import '../cache.dart';
18
import '../convert.dart';
19
import '../device.dart';
20
import '../emulator.dart';
21
import '../globals.dart';
22
import '../project.dart';
23
import '../resident_runner.dart';
24 25
import '../run_cold.dart';
import '../run_hot.dart';
26
import '../runner/flutter_command.dart';
27
import '../vmservice.dart';
Devon Carew's avatar
Devon Carew committed
28

29
const String protocolVersion = '0.4.2';
30

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

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

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

46
  @override
47
  final bool hidden;
48

49
  @override
50
  Future<FlutterCommandResult> runCommand() async {
51 52
    printStatus('Starting device daemon...');

53
    final NotifyingLogger notifyingLogger = NotifyingLogger();
Devon Carew's avatar
Devon Carew committed
54

55 56
    Cache.releaseLockEarly();

57
    await context.run<void>(
58
      body: () async {
59
        final Daemon daemon = Daemon(
60 61 62 63 64 65 66 67 68 69 70
            stdinCommandStream, stdoutCommandResponse,
            daemonCommand: this, notifyingLogger: notifyingLogger);

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

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

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

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

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

103 104 105
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
106
  EmulatorDomain emulatorDomain;
107
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
108

Devon Carew's avatar
Devon Carew committed
109
  final DispatchCommand sendCommand;
110
  final DaemonCommand daemonCommand;
111
  final NotifyingLogger notifyingLogger;
112
  final bool logToStdout;
113

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

Devon Carew's avatar
Devon Carew committed
117
  void _registerDomain(Domain domain) {
118
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
119 120 121 122
  }

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

Ian Hickson's avatar
Ian Hickson committed
123
  void _handleRequest(Map<String, dynamic> request) {
124 125 126
    // {id, method, params}

    // [id] is an opaque type to us.
127
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
128 129

    if (id == null) {
130
      stderr.writeln('no id for request: $request');
Devon Carew's avatar
Devon Carew committed
131 132 133 134
      return;
    }

    try {
135
      final String method = request['method'];
136
      if (!method.contains('.'))
137
        throw 'method not understood: $method';
Devon Carew's avatar
Devon Carew committed
138

139 140
      final String prefix = method.substring(0, method.indexOf('.'));
      final String name = method.substring(method.indexOf('.') + 1);
141 142
      if (_domainMap[prefix] == null)
        throw 'no domain for method: $method';
Devon Carew's avatar
Devon Carew committed
143

144
      _domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{});
145 146 147 148 149 150
    } catch (error, trace) {
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
151 152 153
    }
  }

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

156
  void shutdown({ dynamic error }) {
157
    _commandSubscription?.cancel();
158 159
    for (Domain domain in _domainMap.values)
      domain.dispose();
160 161 162 163 164 165
    if (!_onExitCompleter.isCompleted) {
      if (error == null)
        _onExitCompleter.complete(0);
      else
        _onExitCompleter.completeError(error);
    }
Devon Carew's avatar
Devon Carew committed
166 167 168 169
  }
}

abstract class Domain {
170 171
  Domain(this.daemon, this.name);

Devon Carew's avatar
Devon Carew committed
172 173
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
174
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
175 176 177 178 179

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

180 181
  FlutterCommand get command => daemon.daemonCommand;

182
  @override
Devon Carew's avatar
Devon Carew committed
183 184
  String toString() => name;

185
  void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
186
    Future<dynamic>.sync(() {
187 188 189
      if (_handlers.containsKey(command))
        return _handlers[command](args);
      throw 'command not understood: $name.$command';
190
    }).then<dynamic>((dynamic result) {
Devon Carew's avatar
Devon Carew committed
191
      if (result == null) {
Ian Hickson's avatar
Ian Hickson committed
192
        _send(<String, dynamic>{'id': id});
Devon Carew's avatar
Devon Carew committed
193
      } else {
Ian Hickson's avatar
Ian Hickson committed
194
        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
Devon Carew's avatar
Devon Carew committed
195
      }
Ian Hickson's avatar
Ian Hickson committed
196
    }).catchError((dynamic error, dynamic trace) {
197 198 199 200 201
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
202 203 204
    });
  }

205
  void sendEvent(String name, [ dynamic args ]) {
206
    final Map<String, dynamic> map = <String, dynamic>{'event': name};
207 208 209 210 211
    if (args != null)
      map['params'] = _toJsonable(args);
    _send(map);
  }

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

214
  String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) {
215
    if (required && !args.containsKey(name))
216
      throw '$name is required';
217
    final dynamic val = args[name];
218
    if (val != null && val is! String)
219
      throw '$name is not a String';
220 221 222
    return val;
  }

223
  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) {
224
    if (required && !args.containsKey(name))
225
      throw '$name is required';
226
    final dynamic val = args[name];
227
    if (val != null && val is! bool)
228
      throw '$name is not a bool';
229 230 231
    return val;
  }

232
  int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) {
233
    if (required && !args.containsKey(name))
234
      throw '$name is required';
235
    final dynamic val = args[name];
236
    if (val != null && val is! int)
237
      throw '$name is not an int';
238 239 240
    return val;
  }

241
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
242 243 244
}

/// This domain responds to methods like [version] and [shutdown].
245 246
///
/// This domain fires the `daemon.logMessage` event.
Devon Carew's avatar
Devon Carew committed
247 248 249 250
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
251

252 253 254 255 256 257 258 259
    sendEvent(
      'daemon.connected',
      <String, dynamic>{
        'version': protocolVersion,
        'pid': pid,
      },
    );

260
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
261 262 263 264 265 266
      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') {
267
          stderr.writeln(message.message);
268
          if (message.stackTrace != null)
269
            stderr.writeln(message.stackTrace.toString().trimRight());
270
        }
271
      } else {
272 273 274 275
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
276
            'stackTrace': message.stackTrace.toString(),
277 278 279 280
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
281
            'message': message.message,
282 283
          });
        }
284 285
      }
    });
Devon Carew's avatar
Devon Carew committed
286 287
  }

288
  StreamSubscription<LogMessage> _subscription;
289

290
  Future<String> version(Map<String, dynamic> args) {
291
    return Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
292 293
  }

294
  Future<void> shutdown(Map<String, dynamic> args) {
295
    Timer.run(daemon.shutdown);
296
    return Future<void>.value();
Devon Carew's avatar
Devon Carew committed
297
  }
298

299
  @override
300 301 302
  void dispose() {
    _subscription?.cancel();
  }
Devon Carew's avatar
Devon Carew committed
303 304
}

305
typedef _RunOrAttach = Future<void> Function({
306
  Completer<DebugConnectionInfo> connectionInfoCompleter,
307
  Completer<void> appStartedCompleter,
308 309
});

310
/// This domain responds to methods like [start] and [stop].
Devon Carew's avatar
Devon Carew committed
311
///
312
/// It fires events for application start, stop, and stdout and stderr.
Devon Carew's avatar
Devon Carew committed
313 314
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
315
    registerHandler('restart', restart);
316
    registerHandler('callServiceExtension', callServiceExtension);
317
    registerHandler('stop', stop);
318
    registerHandler('detach', detach);
Devon Carew's avatar
Devon Carew committed
319 320
  }

321
  static final Uuid _uuidGenerator = Uuid();
322

323
  static String _getNewAppId() => _uuidGenerator.generateV4();
324

325
  final List<AppInstance> _apps = <AppInstance>[];
326

327
  Future<AppInstance> startApp(
328 329 330 331 332 333
    Device device,
    String projectDirectory,
    String target,
    String route,
    DebuggingOptions options,
    bool enableHotReload, {
334
    File applicationBinary,
335
    @required bool trackWidgetCreation,
336 337
    String projectRootPath,
    String packagesFilePath,
338
    String dillOutputPath,
339
    bool ipv6 = false,
340
    String isolateFilter,
341
  }) async {
342
    if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
343
      throw '${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.';
344
    }
345
    // We change the current working directory for the duration of the `start` command.
346
    final Directory cwd = fs.currentDirectory;
347
    fs.currentDirectory = fs.directory(projectDirectory);
348
    final FlutterProject flutterProject = FlutterProject.current();
349

350
    final FlutterDevice flutterDevice = await FlutterDevice.create(
351
      device,
352
      flutterProject: flutterProject,
353
      trackWidgetCreation: trackWidgetCreation,
354
      dillOutputPath: dillOutputPath,
355
      viewFilter: isolateFilter,
356
      target: target,
357
      buildMode: options.buildInfo.mode,
358
    );
359

360 361
    ResidentRunner runner;

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

386
    return launch(
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
      runner,
      ({
        Completer<DebugConnectionInfo> connectionInfoCompleter,
        Completer<void> appStartedCompleter,
      }) {
        return runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
      },
      device,
      projectDirectory,
      enableHotReload,
      cwd,
402
      LaunchMode.run,
403
    );
404
  }
405

406
  Future<AppInstance> launch(
407 408 409 410 411 412 413 414
    ResidentRunner runner,
    _RunOrAttach runOrAttach,
    Device device,
    String projectDirectory,
    bool enableHotReload,
    Directory cwd,
    LaunchMode launchMode,
  ) async {
415
    final AppInstance app = AppInstance(_getNewAppId(),
416
        runner: runner, logToStdout: daemon.logToStdout);
417 418
    _apps.add(app);
    _sendAppEvent(app, 'start', <String, dynamic>{
419
      'deviceId': device.id,
420
      'directory': projectDirectory,
421
      'supportsRestart': isRestartSupported(enableHotReload, device),
422
      'launchMode': launchMode.toString(),
423 424
    });

425
    Completer<DebugConnectionInfo> connectionInfoCompleter;
426

427
    if (runner.debuggingOptions.debuggingEnabled) {
428
      connectionInfoCompleter = Completer<DebugConnectionInfo>();
429 430
      // We don't want to wait for this future to complete and callbacks won't fail.
      // As it just writes to stdout.
431 432 433 434 435 436 437 438 439 440 441
      unawaited(connectionInfoCompleter.future.then<void>(
        (DebugConnectionInfo info) {
          final Map<String, dynamic> params = <String, dynamic>{
            'port': info.httpUri.port,
            'wsUri': info.wsUri.toString(),
          };
          if (info.baseUri != null)
            params['baseUri'] = info.baseUri;
          _sendAppEvent(app, 'debugPort', params);
        },
      ));
442
    }
443
    final Completer<void> appStartedCompleter = Completer<void>();
444 445
    // We don't want to wait for this future to complete, and callbacks won't fail,
    // as it just writes to stdout.
446 447 448
    unawaited(appStartedCompleter.future.then<void>((void value) {
      _sendAppEvent(app, 'started');
    }));
449

450
    await app._runInZone<void>(this, () async {
451
      try {
452
        await runOrAttach(
453 454 455
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
        );
456
        _sendAppEvent(app, 'stop');
457 458 459 460 461
      } catch (error, trace) {
        _sendAppEvent(app, 'stop', <String, dynamic>{
          'error': _toJsonable(error),
          'trace': '$trace',
        });
462
      } finally {
463
        fs.currentDirectory = cwd;
464
        _apps.remove(app);
465
      }
466
    });
467
    return app;
Devon Carew's avatar
Devon Carew committed
468 469
  }

470
  bool isRestartSupported(bool enableHotReload, Device device) =>
471
      enableHotReload && device.supportsHotRestart;
472

473 474
  Future<OperationResult> _inProgressHotReload;

Devon Carew's avatar
Devon Carew committed
475
  Future<OperationResult> restart(Map<String, dynamic> args) async {
476 477 478
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
479
    final String restartReason = _getStringArg(args, 'reason');
480

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

485 486 487
    if (_inProgressHotReload != null)
      throw 'hot restart already in progress';

488
    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
489
      return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason);
490
    });
491 492 493
    return _inProgressHotReload.whenComplete(() {
      _inProgressHotReload = null;
    });
494
  }
495

496 497 498 499 500 501 502 503 504 505
  /// 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 {
506 507
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
508
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
509

510
    final AppInstance app = _getApp(appId);
511 512 513
    if (app == null)
      throw "app '$appId' not found";

514
    final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate;
515
    final Map<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
516
    if (result == null)
517
      throw 'method not available: $methodName';
518 519

    if (result.containsKey('error'))
520 521 522
      throw result['error'];

    return result;
523 524
  }

525
  Future<bool> stop(Map<String, dynamic> args) async {
526
    final String appId = _getStringArg(args, 'appId', required: true);
527

528
    final AppInstance app = _getApp(appId);
529 530 531
    if (app == null)
      throw "app '$appId' not found";

532 533 534
    return app.stop().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
535
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
536 537 538 539 540
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
541 542
  }

543 544 545 546 547 548 549
  Future<bool> detach(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);

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

550 551 552
    return app.detach().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
553
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
554 555 556 557 558
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
559 560
  }

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

565
  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
566
    final Map<String, dynamic> eventArgs = <String, dynamic>{'appId': app.id};
567 568 569
    if (args != null)
      eventArgs.addAll(args);
    sendEvent('app.$name', eventArgs);
Devon Carew's avatar
Devon Carew committed
570 571 572
  }
}

573
typedef _DeviceEventHandler = void Function(Device device);
574

575 576
/// This domain lets callers list and monitor connected devices.
///
577 578
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
579 580 581
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
582 583
    registerHandler('enable', enable);
    registerHandler('disable', disable);
584 585
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
586

587 588 589
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
    deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
590
  }
591

592
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
593 594
    if (!discoverer.supportsPlatform)
      return;
595

596
    _discoverers.add(discoverer);
597 598 599 600
    if (discoverer is PollingDeviceDiscovery) {
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
601 602
  }

603
  Future<void> _serializeDeviceEvents = Future<void>.value();
604

605 606
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
607
      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
608 609 610 611 612
        sendEvent(eventName, await _deviceToMap(device));
      });
    };
  }

613
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
614

615 616
  /// Return a list of the current devices, with each device represented as a map
  /// of properties (id, name, platform, ...).
617
  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
618 619
    final List<Map<String, dynamic>> devicesInfo = <Map<String, dynamic>>[];

620
    for (PollingDeviceDiscovery discoverer in _discoverers) {
621 622 623
      for (Device device in await discoverer.devices) {
        devicesInfo.add(await _deviceToMap(device));
      }
624
    }
625 626

    return devicesInfo;
627 628
  }

629
  /// Enable device events.
630
  Future<void> enable(Map<String, dynamic> args) {
631
    for (PollingDeviceDiscovery discoverer in _discoverers)
632
      discoverer.startPolling();
633
    return Future<void>.value();
634 635
  }

636
  /// Disable device events.
637
  Future<void> disable(Map<String, dynamic> args) {
638
    for (PollingDeviceDiscovery discoverer in _discoverers)
639
      discoverer.stopPolling();
640
    return Future<void>.value();
641 642
  }

643 644
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
645 646
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
647
    int hostPort = _getIntArg(args, 'hostPort');
648

649
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
650 651
    if (device == null)
      throw "device '$deviceId' not found";
652 653 654

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

655
    return <String, dynamic>{'hostPort': hostPort};
656 657 658
  }

  /// Removes a forwarded port.
659
  Future<void> unforward(Map<String, dynamic> args) async {
660 661 662
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
663

664
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
665 666
    if (device == null)
      throw "device '$deviceId' not found";
667

668
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
669 670
  }

671
  @override
672
  void dispose() {
673
    for (PollingDeviceDiscovery discoverer in _discoverers)
674
      discoverer.dispose();
675 676 677
  }

  /// Return the device matching the deviceId field in the args.
678 679 680 681 682 683 684
  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;
685 686 687
  }
}

688
Stream<Map<String, dynamic>> get stdinCommandStream => stdin
689 690
  .transform<String>(utf8.decoder)
  .transform<String>(const LineSplitter())
691
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
692
  .map<Map<String, dynamic>>((String line) {
693
    line = line.substring(1, line.length - 1);
694
    return json.decode(line);
695 696 697
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
698
  stdout.writeln('[${jsonEncodeObject(command)}]');
699 700
}

701
String jsonEncodeObject(dynamic object) {
702
  return json.encode(object, toEncodable: _toEncodable);
703 704 705
}

dynamic _toEncodable(dynamic object) {
706 707 708 709 710
  if (object is OperationResult)
    return _operationResultToMap(object);
  return object;
}

711
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
712
  return <String, dynamic>{
713
    'id': device.id,
714
    'name': device.name,
715
    'platform': getNameForTargetPlatform(await device.targetPlatform),
716
    'emulator': await device.isLocalEmulator,
717 718 719
  };
}

720 721 722 723 724 725 726
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
  };
}

Devon Carew's avatar
Devon Carew committed
727
Map<String, dynamic> _operationResultToMap(OperationResult result) {
728
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
729
    'code': result.code,
730
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
731 732 733
  };
}

Devon Carew's avatar
Devon Carew committed
734
dynamic _toJsonable(dynamic obj) {
Ian Hickson's avatar
Ian Hickson committed
735
  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
736
    return obj;
Devon Carew's avatar
Devon Carew committed
737 738
  if (obj is OperationResult)
    return obj;
739 740
  if (obj is ToolExit)
    return obj.message;
Hixie's avatar
Hixie committed
741
  return '$obj';
Devon Carew's avatar
Devon Carew committed
742
}
743

744
class NotifyingLogger extends Logger {
745
  final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast();
746 747 748

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

749
  @override
750
  void printError(
751 752 753 754 755 756 757 758
    String message, {
    StackTrace stackTrace,
    bool emphasis = false,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
759
    _messageController.add(LogMessage('error', message, stackTrace));
760 761
  }

762
  @override
763
  void printStatus(
764 765 766 767 768 769 770 771
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
772
    _messageController.add(LogMessage('status', message));
773 774
  }

775
  @override
776
  void printTrace(String message) {
777
    // This is a lot of traffic to send over the wire.
778
  }
779 780

  @override
781 782
  Status startProgress(
    String message, {
783
    @required Duration timeout,
784
    String progressId,
785
    bool multilineOutput = false,
786
    int progressIndicatorPadding = kDefaultStatusPadding,
787
  }) {
788
    assert(timeout != null);
789
    printStatus(message);
790
    return SilentStatus(timeout: timeout);
791
  }
792 793 794 795

  void dispose() {
    _messageController.close();
  }
796 797
}

798 799
/// A running application, started by this daemon.
class AppInstance {
800
  AppInstance(this.id, { this.runner, this.logToStdout = false });
801 802

  final String id;
803
  final ResidentRunner runner;
804
  final bool logToStdout;
805 806 807

  _AppRunLogger _logger;

808 809
  Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason);
810
  }
811

812 813
  Future<void> stop() => runner.stop();
  Future<void> detach() => runner.detach();
814 815 816 817 818

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

819
  Future<T> _runInZone<T>(AppDomain domain, dynamic method()) {
820
    _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? logger : null);
821

822
    return context.run<T>(
823 824 825 826 827
      body: method,
      overrides: <Type, Generator>{
        Logger: () => _logger,
      },
    );
828 829 830
  }
}

831 832 833 834 835
/// This domain responds to methods like [getEmulators] and [launch].
class EmulatorDomain extends Domain {
  EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') {
    registerHandler('getEmulators', getEmulators);
    registerHandler('launch', launch);
836
    registerHandler('create', create);
837 838
  }

839 840
  EmulatorManager emulators = EmulatorManager();

841
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
842
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
843
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
844 845
  }

846
  Future<void> launch(Map<String, dynamic> args) async {
847 848 849 850 851 852 853 854 855 856 857
    final String emulatorId = _getStringArg(args, 'emulatorId', required: true);
    final List<Emulator> matches =
        await emulators.getEmulatorsMatching(emulatorId);
    if (matches.isEmpty) {
      throw "emulator '$emulatorId' not found";
    } else if (matches.length > 1) {
      throw "multiple emulators match '$emulatorId'";
    } else {
      await matches.first.launch();
    }
  }
858 859 860 861 862 863 864 865 866 867

  Future<Map<String, dynamic>> create(Map<String, dynamic> args) async {
    final String name = _getStringArg(args, 'name', required: false);
    final CreateEmulatorResult res = await emulators.createEmulator(name: name);
    return <String, dynamic>{
      'success': res.success,
      'emulatorName': res.emulatorName,
      'error': res.error,
    };
  }
868 869
}

870
/// A [Logger] which sends log messages to a listening daemon client.
871 872 873 874
///
/// 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
875 876 877
//
// 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.
878
class _AppRunLogger extends Logger {
879
  _AppRunLogger(this.domain, this.app, { this.parent });
880 881 882

  AppDomain domain;
  final AppInstance app;
883
  final Logger parent;
884
  int _nextProgressId = 0;
885 886

  @override
887
  void printError(
888 889 890 891 892 893 894 895
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
896
    if (parent != null) {
897 898 899 900 901 902
      parent.printError(
        message,
        stackTrace: stackTrace,
        emphasis: emphasis,
        indent: indent,
        hangingIndent: hangingIndent,
903
        wrap: wrap,
904
      );
905
    } else {
906 907 908 909
      if (stackTrace != null) {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'stackTrace': stackTrace.toString(),
910
          'error': true,
911 912 913 914
        });
      } else {
        _sendLogEvent(<String, dynamic>{
          'log': message,
915
          'error': true,
916 917
        });
      }
918 919 920 921
    }
  }

  @override
922
  void printStatus(
923 924 925 926 927 928 929 930
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
931
    if (parent != null) {
932 933 934 935 936 937
      parent.printStatus(
        message,
        emphasis: emphasis,
        color: color,
        newline: newline,
        indent: indent,
938
        hangingIndent: hangingIndent,
939
        wrap: wrap,
940
      );
941
    } else {
942
      _sendLogEvent(<String, dynamic>{'log': message});
943
    }
944 945 946
  }

  @override
947 948 949 950
  void printTrace(String message) {
    if (parent != null) {
      parent.printTrace(message);
    } else {
951
      _sendLogEvent(<String, dynamic>{'log': message, 'trace': true});
952 953
    }
  }
954

Devon Carew's avatar
Devon Carew committed
955 956
  Status _status;

957
  @override
958 959
  Status startProgress(
    String message, {
960
    @required Duration timeout,
961
    String progressId,
962 963
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
964
  }) {
965
    assert(timeout != null);
966
    final int id = _nextProgressId++;
967

968 969
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
970
      'progressId': progressId,
971
      'message': message,
972 973
    });

974 975 976 977 978 979 980 981
    _status = SilentStatus(
      timeout: timeout,
      onFinish: () {
        _status = null;
        _sendProgressEvent(<String, dynamic>{
          'id': id.toString(),
          'progressId': progressId,
          'finished': true,
982 983
        });
      })..start();
Devon Carew's avatar
Devon Carew committed
984
    return _status;
985 986 987 988 989
  }

  void close() {
    domain = null;
  }
990 991 992 993 994 995 996

  void _sendLogEvent(Map<String, dynamic> event) {
    if (domain == null)
      printStatus('event sent after app closed: $event');
    else
      domain._sendAppEvent(app, 'log', event);
  }
997 998 999 1000 1001 1002 1003

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

1006
class LogMessage {
1007 1008
  LogMessage(this.level, this.message, [this.stackTrace]);

1009 1010 1011 1012
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028

/// The method by which the flutter app was launched.
class LaunchMode {
  const LaunchMode._(this._value);

  /// The app was launched via `flutter run`.
  static const LaunchMode run = LaunchMode._('run');

  /// The app was launched via `flutter attach`.
  static const LaunchMode attach = LaunchMode._('attach');

  final String _value;

  @override
  String toString() => _value;
}