daemon.dart 36.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Devon Carew's avatar
Devon Carew committed
2 3 4 5 6
// 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' as globals;
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 '../web/web_runner.dart';
Devon Carew's avatar
Devon Carew committed
28

29
const String protocolVersion = '0.5.3';
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 39 40
  DaemonCommand({ this.hidden = false }) {
    usesDartDefines();
  }
41

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

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

48
  @override
49
  final bool hidden;
50

51
  @override
52
  Future<FlutterCommandResult> runCommand() async {
53
    globals.printStatus('Starting device daemon...');
54
    isRunningFromDaemon = true;
55

56
    final NotifyingLogger notifyingLogger = NotifyingLogger();
Devon Carew's avatar
Devon Carew committed
57

58 59
    Cache.releaseLockEarly();

60
    await context.run<void>(
61
      body: () async {
62
        final Daemon daemon = Daemon(
63 64 65 66
          stdinCommandStream, stdoutCommandResponse,
          notifyingLogger: notifyingLogger,
          dartDefines: dartDefines,
        );
67 68

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

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

83
typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args);
Devon Carew's avatar
Devon Carew committed
84 85

class Daemon {
86 87 88
  Daemon(
    Stream<Map<String, dynamic>> commandStream,
    this.sendCommand, {
89
    this.notifyingLogger,
90
    this.logToStdout = false,
91
    @required this.dartDefines,
92
  }) {
93 94 95 96 97 98 99
    if (dartDefines == null) {
      throw Exception(
        'dartDefines must not be null. This is a bug in Flutter.\n'
        'Please file an issue at https://github.com/flutter/flutter/issues/new/choose',
      );
    }

Devon Carew's avatar
Devon Carew committed
100
    // Set up domains.
101 102 103 104
    _registerDomain(daemonDomain = DaemonDomain(this));
    _registerDomain(appDomain = AppDomain(this));
    _registerDomain(deviceDomain = DeviceDomain(this));
    _registerDomain(emulatorDomain = EmulatorDomain(this));
Devon Carew's avatar
Devon Carew committed
105 106

    // Start listening.
107
    _commandSubscription = commandStream.listen(
108
      _handleRequest,
109
      onDone: () {
110
        if (!_onExitCompleter.isCompleted) {
111
          _onExitCompleter.complete(0);
112
        }
113
      },
Devon Carew's avatar
Devon Carew committed
114 115 116
    );
  }

117 118 119
  DaemonDomain daemonDomain;
  AppDomain appDomain;
  DeviceDomain deviceDomain;
120
  EmulatorDomain emulatorDomain;
121
  StreamSubscription<Map<String, dynamic>> _commandSubscription;
122 123
  int _outgoingRequestId = 1;
  final Map<String, Completer<dynamic>> _outgoingRequestCompleters = <String, Completer<dynamic>>{};
124

Devon Carew's avatar
Devon Carew committed
125
  final DispatchCommand sendCommand;
126
  final NotifyingLogger notifyingLogger;
127
  final bool logToStdout;
128
  final List<String> dartDefines;
129

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

Devon Carew's avatar
Devon Carew committed
133
  void _registerDomain(Domain domain) {
134
    _domainMap[domain.name] = domain;
Devon Carew's avatar
Devon Carew committed
135 136 137 138
  }

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

Ian Hickson's avatar
Ian Hickson committed
139
  void _handleRequest(Map<String, dynamic> request) {
140 141 142
    // {id, method, params}

    // [id] is an opaque type to us.
143
    final dynamic id = request['id'];
Devon Carew's avatar
Devon Carew committed
144 145

    if (id == null) {
146
      stderr.writeln('no id for request: $request');
Devon Carew's avatar
Devon Carew committed
147 148 149 150
      return;
    }

    try {
151
      final String method = request['method'] as String;
152 153 154 155
      if (method != null) {
        if (!method.contains('.')) {
          throw 'method not understood: $method';
        }
Devon Carew's avatar
Devon Carew committed
156

157 158 159 160 161 162 163 164 165 166 167 168 169 170
        final String prefix = method.substring(0, method.indexOf('.'));
        final String name = method.substring(method.indexOf('.') + 1);
        if (_domainMap[prefix] == null) {
          throw 'no domain for method: $method';
        }

        _domainMap[prefix].handleCommand(name, id, castStringKeyedMap(request['params']) ?? const <String, dynamic>{});
      } else {
        // If there was no 'method' field then it's a response to a daemon-to-editor request.
        final Completer<dynamic> completer = _outgoingRequestCompleters[id.toString()];
        if (completer == null) {
          throw 'unexpected response with id: $id';
        }
        _outgoingRequestCompleters.remove(id.toString());
Devon Carew's avatar
Devon Carew committed
171

172 173 174 175 176 177
        if (request['error'] != null) {
          completer.completeError(request['error']);
        } else {
          completer.complete(request['result']);
        }
      }
178 179 180 181 182 183
    } catch (error, trace) {
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
184 185 186
    }
  }

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
  Future<dynamic> sendRequest(String method, [ dynamic args ]) {
    final Map<String, dynamic> map = <String, dynamic>{'method': method};
    if (args != null) {
      map['params'] = _toJsonable(args);
    }

    final int id = _outgoingRequestId++;
    final Completer<dynamic> completer = Completer<dynamic>();

    map['id'] = id.toString();
    _outgoingRequestCompleters[id.toString()] = completer;

    _send(map);
    return completer.future;
  }

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

205
  void shutdown({ dynamic error }) {
206
    _commandSubscription?.cancel();
207
    for (final Domain domain in _domainMap.values) {
208
      domain.dispose();
209
    }
210
    if (!_onExitCompleter.isCompleted) {
211
      if (error == null) {
212
        _onExitCompleter.complete(0);
213
      } else {
214
        _onExitCompleter.completeError(error);
215
      }
216
    }
Devon Carew's avatar
Devon Carew committed
217 218 219 220
  }
}

abstract class Domain {
221 222
  Domain(this.daemon, this.name);

223

Devon Carew's avatar
Devon Carew committed
224 225
  final Daemon daemon;
  final String name;
Ian Hickson's avatar
Ian Hickson committed
226
  final Map<String, CommandHandler> _handlers = <String, CommandHandler>{};
Devon Carew's avatar
Devon Carew committed
227 228 229 230 231

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

232
  @override
Devon Carew's avatar
Devon Carew committed
233 234
  String toString() => name;

235
  void handleCommand(String command, dynamic id, Map<String, dynamic> args) {
236
    Future<dynamic>.sync(() {
237
      if (_handlers.containsKey(command)) {
238
        return _handlers[command](args);
239
      }
240
      throw 'command not understood: $name.$command';
241
    }).then<dynamic>((dynamic result) {
Devon Carew's avatar
Devon Carew committed
242
      if (result == null) {
Ian Hickson's avatar
Ian Hickson committed
243
        _send(<String, dynamic>{'id': id});
Devon Carew's avatar
Devon Carew committed
244
      } else {
Ian Hickson's avatar
Ian Hickson committed
245
        _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)});
Devon Carew's avatar
Devon Carew committed
246
      }
Ian Hickson's avatar
Ian Hickson committed
247
    }).catchError((dynamic error, dynamic trace) {
248 249 250 251 252
      _send(<String, dynamic>{
        'id': id,
        'error': _toJsonable(error),
        'trace': '$trace',
      });
Devon Carew's avatar
Devon Carew committed
253 254 255
    });
  }

256
  void sendEvent(String name, [ dynamic args ]) {
257
    final Map<String, dynamic> map = <String, dynamic>{'event': name};
258
    if (args != null) {
259
      map['params'] = _toJsonable(args);
260
    }
261 262 263
    _send(map);
  }

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

266
  String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) {
267
    if (required && !args.containsKey(name)) {
268
      throw '$name is required';
269
    }
270
    final dynamic val = args[name];
271
    if (val != null && val is! String) {
272
      throw '$name is not a String';
273
    }
274
    return val as String;
275 276
  }

277
  bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) {
278
    if (required && !args.containsKey(name)) {
279
      throw '$name is required';
280
    }
281
    final dynamic val = args[name];
282
    if (val != null && val is! bool) {
283
      throw '$name is not a bool';
284
    }
285
    return val as bool;
286 287
  }

288
  int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) {
289
    if (required && !args.containsKey(name)) {
290
      throw '$name is required';
291
    }
292
    final dynamic val = args[name];
293
    if (val != null && val is! int) {
294
      throw '$name is not an int';
295
    }
296
    return val as int;
297 298
  }

299
  void dispose() { }
Devon Carew's avatar
Devon Carew committed
300 301 302
}

/// This domain responds to methods like [version] and [shutdown].
303 304
///
/// This domain fires the `daemon.logMessage` event.
Devon Carew's avatar
Devon Carew committed
305 306 307 308
class DaemonDomain extends Domain {
  DaemonDomain(Daemon daemon) : super(daemon, 'daemon') {
    registerHandler('version', version);
    registerHandler('shutdown', shutdown);
309
    registerHandler('getSupportedPlatforms', getSupportedPlatforms);
310

311 312 313 314 315 316 317 318
    sendEvent(
      'daemon.connected',
      <String, dynamic>{
        'version': protocolVersion,
        'pid': pid,
      },
    );

319
    _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) {
320 321 322 323 324 325
      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') {
326
          stderr.writeln(message.message);
327
          if (message.stackTrace != null) {
328
            stderr.writeln(message.stackTrace.toString().trimRight());
329
          }
330
        }
331
      } else {
332 333 334 335
        if (message.stackTrace != null) {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
            'message': message.message,
336
            'stackTrace': message.stackTrace.toString(),
337 338 339 340
          });
        } else {
          sendEvent('daemon.logMessage', <String, dynamic>{
            'level': message.level,
341
            'message': message.message,
342 343
          });
        }
344 345
      }
    });
Devon Carew's avatar
Devon Carew committed
346 347
  }

348
  StreamSubscription<LogMessage> _subscription;
349

350
  Future<String> version(Map<String, dynamic> args) {
351
    return Future<String>.value(protocolVersion);
Devon Carew's avatar
Devon Carew committed
352 353
  }

354 355 356 357 358 359 360 361 362 363
  /// Sends a request back to the client asking it to expose/tunnel a URL.
  ///
  /// This method should only be called if the client opted-in with the
  /// --web-allow-expose-url switch. The client may return the same URL back if
  /// tunnelling is not required for a given URL.
  Future<String> exposeUrl(String url) async {
    final dynamic res = await daemon.sendRequest('app.exposeUrl', <String, String>{'url': url});
    if (res is Map<String, dynamic> && res['url'] is String) {
      return res['url'] as String;
    } else {
364
      globals.printError('Invalid response to exposeUrl - params should include a String url field');
365 366 367 368
      return url;
    }
  }

369
  Future<void> shutdown(Map<String, dynamic> args) {
370
    Timer.run(daemon.shutdown);
371
    return Future<void>.value();
Devon Carew's avatar
Devon Carew committed
372
  }
373

374
  @override
375 376 377
  void dispose() {
    _subscription?.cancel();
  }
378 379 380 381 382 383 384 385 386 387 388 389

  /// Enumerates the platforms supported by the provided project.
  ///
  /// This does not filter based on the current workflow restrictions, such
  /// as whether command line tools are installed or whether the host platform
  /// is correct.
  Future<Map<String, Object>> getSupportedPlatforms(Map<String, dynamic> args) async {
    final String projectRoot = _getStringArg(args, 'projectRoot', required: true);
    final List<String> result = <String>[];
    try {
      // TODO(jonahwilliams): replace this with a project metadata check once
      // that has been implemented.
390
      final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
      if (flutterProject.linux.existsSync()) {
        result.add('linux');
      }
      if (flutterProject.macos.existsSync()) {
        result.add('macos');
      }
      if (flutterProject.windows.existsSync()) {
        result.add('windows');
      }
      if (flutterProject.ios.existsSync()) {
        result.add('ios');
      }
      if (flutterProject.android.existsSync()) {
        result.add('android');
      }
      if (flutterProject.web.existsSync()) {
        result.add('web');
      }
      if (flutterProject.fuchsia.existsSync()) {
        result.add('fuchsia');
      }
      return <String, Object>{
        'platforms': result,
      };
    } catch (err, stackTrace) {
      sendEvent('log', <String, dynamic>{
        'log': 'Failed to parse project metadata',
        'stackTrace': stackTrace.toString(),
        'error': true,
      });
      // On any sort of failure, fall back to Android and iOS for backwards
      // comparability.
      return <String, Object>{
        'platforms': <String>[
          'android',
          'ios',
        ],
      };
    }
  }
Devon Carew's avatar
Devon Carew committed
431 432
}

433
typedef _RunOrAttach = Future<void> Function({
434
  Completer<DebugConnectionInfo> connectionInfoCompleter,
435
  Completer<void> appStartedCompleter,
436 437
});

438
/// This domain responds to methods like [start] and [stop].
Devon Carew's avatar
Devon Carew committed
439
///
440
/// It fires events for application start, stop, and stdout and stderr.
Devon Carew's avatar
Devon Carew committed
441 442
class AppDomain extends Domain {
  AppDomain(Daemon daemon) : super(daemon, 'app') {
443
    registerHandler('restart', restart);
444
    registerHandler('reloadMethod', reloadMethod);
445
    registerHandler('callServiceExtension', callServiceExtension);
446
    registerHandler('stop', stop);
447
    registerHandler('detach', detach);
Devon Carew's avatar
Devon Carew committed
448 449
  }

450
  static final Uuid _uuidGenerator = Uuid();
451

452
  static String _getNewAppId() => _uuidGenerator.generateV4();
453

454
  final List<AppInstance> _apps = <AppInstance>[];
455

456
  Future<AppInstance> startApp(
457 458 459 460 461 462
    Device device,
    String projectDirectory,
    String target,
    String route,
    DebuggingOptions options,
    bool enableHotReload, {
463
    File applicationBinary,
464
    @required bool trackWidgetCreation,
465 466
    String projectRootPath,
    String packagesFilePath,
467
    String dillOutputPath,
468
    bool ipv6 = false,
469
    String isolateFilter,
470
  }) async {
471
    if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) {
472
      throw '${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.';
473
    }
474
    // We change the current working directory for the duration of the `start` command.
475 476
    final Directory cwd = globals.fs.currentDirectory;
    globals.fs.currentDirectory = globals.fs.directory(projectDirectory);
477
    final FlutterProject flutterProject = FlutterProject.current();
478

479
    final FlutterDevice flutterDevice = await FlutterDevice.create(
480
      device,
481
      flutterProject: flutterProject,
482
      trackWidgetCreation: trackWidgetCreation,
483
      viewFilter: isolateFilter,
484
      target: target,
485
      buildMode: options.buildInfo.mode,
486
      dartDefines: daemon.dartDefines,
487
    );
488

489 490
    ResidentRunner runner;

491
    if (await device.targetPlatform == TargetPlatform.web_javascript) {
492
      runner = webRunnerFactory.createWebRunner(
493
        flutterDevice,
494 495
        flutterProject: flutterProject,
        target: target,
496 497
        debuggingOptions: options,
        ipv6: ipv6,
498
        stayResident: true,
499
        dartDefines: daemon.dartDefines,
500
        urlTunneller: options.webEnableExposeUrl ? daemon.daemonDomain.exposeUrl : null,
501 502
      );
    } else if (enableHotReload) {
503
      runner = HotRunner(
504
        <FlutterDevice>[flutterDevice],
505 506
        target: target,
        debuggingOptions: options,
507 508 509
        applicationBinary: applicationBinary,
        projectRootPath: projectRootPath,
        packagesFilePath: packagesFilePath,
510
        dillOutputPath: dillOutputPath,
511
        ipv6: ipv6,
512
        hostIsIde: true,
513 514
      );
    } else {
515
      runner = ColdRunner(
516
        <FlutterDevice>[flutterDevice],
517 518
        target: target,
        debuggingOptions: options,
519
        applicationBinary: applicationBinary,
520
        ipv6: ipv6,
521 522
      );
    }
523

524
    return launch(
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
      runner,
      ({
        Completer<DebugConnectionInfo> connectionInfoCompleter,
        Completer<void> appStartedCompleter,
      }) {
        return runner.run(
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
          route: route,
        );
      },
      device,
      projectDirectory,
      enableHotReload,
      cwd,
540
      LaunchMode.run,
541
    );
542
  }
543

544
  Future<AppInstance> launch(
545 546 547 548 549 550 551 552
    ResidentRunner runner,
    _RunOrAttach runOrAttach,
    Device device,
    String projectDirectory,
    bool enableHotReload,
    Directory cwd,
    LaunchMode launchMode,
  ) async {
553
    final AppInstance app = AppInstance(_getNewAppId(),
554
        runner: runner, logToStdout: daemon.logToStdout);
555 556
    _apps.add(app);
    _sendAppEvent(app, 'start', <String, dynamic>{
557
      'deviceId': device.id,
558
      'directory': projectDirectory,
559
      'supportsRestart': isRestartSupported(enableHotReload, device),
560
      'launchMode': launchMode.toString(),
561 562
    });

563
    Completer<DebugConnectionInfo> connectionInfoCompleter;
564

565
    if (runner.debuggingEnabled) {
566
      connectionInfoCompleter = Completer<DebugConnectionInfo>();
567 568
      // We don't want to wait for this future to complete and callbacks won't fail.
      // As it just writes to stdout.
569 570 571
      unawaited(connectionInfoCompleter.future.then<void>(
        (DebugConnectionInfo info) {
          final Map<String, dynamic> params = <String, dynamic>{
572 573
            // The web vmservice proxy does not have an http address.
            'port': info.httpUri?.port ?? info.wsUri.port,
574 575
            'wsUri': info.wsUri.toString(),
          };
576
          if (info.baseUri != null) {
577
            params['baseUri'] = info.baseUri;
578
          }
579 580 581
          _sendAppEvent(app, 'debugPort', params);
        },
      ));
582
    }
583
    final Completer<void> appStartedCompleter = Completer<void>();
584 585
    // We don't want to wait for this future to complete, and callbacks won't fail,
    // as it just writes to stdout.
586 587 588
    unawaited(appStartedCompleter.future.then<void>((void value) {
      _sendAppEvent(app, 'started');
    }));
589

590
    await app._runInZone<void>(this, () async {
591
      try {
592
        await runOrAttach(
593 594 595
          connectionInfoCompleter: connectionInfoCompleter,
          appStartedCompleter: appStartedCompleter,
        );
596
        _sendAppEvent(app, 'stop');
597 598 599 600 601
      } catch (error, trace) {
        _sendAppEvent(app, 'stop', <String, dynamic>{
          'error': _toJsonable(error),
          'trace': '$trace',
        });
602
      } finally {
603
        globals.fs.currentDirectory = cwd;
604
        _apps.remove(app);
605
      }
606
    });
607
    return app;
Devon Carew's avatar
Devon Carew committed
608 609
  }

610
  bool isRestartSupported(bool enableHotReload, Device device) =>
611
      enableHotReload && device.supportsHotRestart;
612

613 614
  Future<OperationResult> _inProgressHotReload;

Devon Carew's avatar
Devon Carew committed
615
  Future<OperationResult> restart(Map<String, dynamic> args) async {
616 617 618
    final String appId = _getStringArg(args, 'appId', required: true);
    final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false;
    final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false;
619
    final String restartReason = _getStringArg(args, 'reason');
620

621
    final AppInstance app = _getApp(appId);
622
    if (app == null) {
623
      throw "app '$appId' not found";
624
    }
625

626
    if (_inProgressHotReload != null) {
627
      throw 'hot restart already in progress';
628
    }
629

630
    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
631
      return app.restart(fullRestart: fullRestart, pause: pauseAfterRestart, reason: restartReason);
632
    });
633 634 635
    return _inProgressHotReload.whenComplete(() {
      _inProgressHotReload = null;
    });
636
  }
637

638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659
  Future<OperationResult> reloadMethod(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);
    final String classId = _getStringArg(args, 'class', required: true);
    final String libraryId =  _getStringArg(args, 'library', required: true);

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

    if (_inProgressHotReload != null) {
      throw 'hot restart already in progress';
    }

    _inProgressHotReload = app._runInZone<OperationResult>(this, () {
      return app.reloadMethod(classId: classId, libraryId: libraryId);
    });
    return _inProgressHotReload.whenComplete(() {
      _inProgressHotReload = null;
    });
  }

660 661 662 663 664 665 666 667 668 669
  /// 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 {
670 671
    final String appId = _getStringArg(args, 'appId', required: true);
    final String methodName = _getStringArg(args, 'methodName');
672
    final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']);
673

674
    final AppInstance app = _getApp(appId);
675
    if (app == null) {
676
      throw "app '$appId' not found";
677
    }
678

679 680
    final Map<String, dynamic> result = await app.runner
        .invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params);
681
    if (result == null) {
682
      throw 'method not available: $methodName';
683
    }
684

685
    if (result.containsKey('error')) {
686
      throw result['error'];
687
    }
688 689

    return result;
690 691
  }

692
  Future<bool> stop(Map<String, dynamic> args) async {
693
    final String appId = _getStringArg(args, 'appId', required: true);
694

695
    final AppInstance app = _getApp(appId);
696
    if (app == null) {
697
      throw "app '$appId' not found";
698
    }
699

700 701 702
    return app.stop().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
703
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
704 705 706 707 708
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
709 710
  }

711 712 713 714
  Future<bool> detach(Map<String, dynamic> args) async {
    final String appId = _getStringArg(args, 'appId', required: true);

    final AppInstance app = _getApp(appId);
715
    if (app == null) {
716
      throw "app '$appId' not found";
717
    }
718

719 720 721
    return app.detach().then<bool>(
      (void value) => true,
      onError: (dynamic error, StackTrace stack) {
722
        _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true});
723 724 725 726 727
        app.closeLogger();
        _apps.remove(app);
        return false;
      },
    );
728 729
  }

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

734
  void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) {
735 736 737 738
    sendEvent('app.$name', <String, dynamic>{
      'appId': app.id,
      ...?args,
    });
Devon Carew's avatar
Devon Carew committed
739 740 741
  }
}

742
typedef _DeviceEventHandler = void Function(Device device);
743

744 745
/// This domain lets callers list and monitor connected devices.
///
746 747
/// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed` events.
748 749 750
class DeviceDomain extends Domain {
  DeviceDomain(Daemon daemon) : super(daemon, 'device') {
    registerHandler('getDevices', getDevices);
751 752
    registerHandler('enable', enable);
    registerHandler('disable', disable);
753 754
    registerHandler('forward', forward);
    registerHandler('unforward', unforward);
755

756 757 758
    // Use the device manager discovery so that client provided device types
    // are usable via the daemon protocol.
    deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer);
759
  }
760

761
  void addDeviceDiscoverer(DeviceDiscovery discoverer) {
762
    if (!discoverer.supportsPlatform) {
763
      return;
764
    }
765

766
    if (discoverer is PollingDeviceDiscovery) {
767
      _discoverers.add(discoverer);
768 769 770
      discoverer.onAdded.listen(_onDeviceEvent('device.added'));
      discoverer.onRemoved.listen(_onDeviceEvent('device.removed'));
    }
771 772
  }

773
  Future<void> _serializeDeviceEvents = Future<void>.value();
774

775 776
  _DeviceEventHandler _onDeviceEvent(String eventName) {
    return (Device device) {
777
      _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
778 779 780 781
        try {
          final Map<String, Object> response = await _deviceToMap(device);
          sendEvent(eventName, response);
        } catch (err) {
782
          globals.printError('$err');
783
        }
784 785 786 787
      });
    };
  }

788
  final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
789

790 791
  /// Return a list of the current devices, with each device represented as a map
  /// of properties (id, name, platform, ...).
792
  Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async {
793
    return <Map<String, dynamic>>[
794 795
      for (final PollingDeviceDiscovery discoverer in _discoverers)
        for (final Device device in await discoverer.devices)
796 797
          await _deviceToMap(device),
    ];
798 799
  }

800
  /// Enable device events.
801
  Future<void> enable(Map<String, dynamic> args) {
802
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
803
      discoverer.startPolling();
804
    }
805
    return Future<void>.value();
806 807
  }

808
  /// Disable device events.
809
  Future<void> disable(Map<String, dynamic> args) {
810
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
811
      discoverer.stopPolling();
812
    }
813
    return Future<void>.value();
814 815
  }

816 817
  /// Forward a host port to a device port.
  Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async {
818 819
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
820
    int hostPort = _getIntArg(args, 'hostPort');
821

822
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
823
    if (device == null) {
824
      throw "device '$deviceId' not found";
825
    }
826 827 828

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

829
    return <String, dynamic>{'hostPort': hostPort};
830 831 832
  }

  /// Removes a forwarded port.
833
  Future<void> unforward(Map<String, dynamic> args) async {
834 835 836
    final String deviceId = _getStringArg(args, 'deviceId', required: true);
    final int devicePort = _getIntArg(args, 'devicePort', required: true);
    final int hostPort = _getIntArg(args, 'hostPort', required: true);
837

838
    final Device device = await daemon.deviceDomain._getDevice(deviceId);
839
    if (device == null) {
840
      throw "device '$deviceId' not found";
841
    }
842

843
    return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort));
844 845
  }

846
  @override
847
  void dispose() {
848
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
849
      discoverer.dispose();
850
    }
851 852 853
  }

  /// Return the device matching the deviceId field in the args.
854
  Future<Device> _getDevice(String deviceId) async {
855
    for (final PollingDeviceDiscovery discoverer in _discoverers) {
856
      final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null);
857
      if (device != null) {
858
        return device;
859
      }
860 861
    }
    return null;
862 863 864
  }
}

865
Stream<Map<String, dynamic>> get stdinCommandStream => stdin
866 867
  .transform<String>(utf8.decoder)
  .transform<String>(const LineSplitter())
868
  .where((String line) => line.startsWith('[{') && line.endsWith('}]'))
869
  .map<Map<String, dynamic>>((String line) {
870
    line = line.substring(1, line.length - 1);
871
    return castStringKeyedMap(json.decode(line));
872 873 874
  });

void stdoutCommandResponse(Map<String, dynamic> command) {
875
  stdout.writeln('[${jsonEncodeObject(command)}]');
876 877
}

878
String jsonEncodeObject(dynamic object) {
879
  return json.encode(object, toEncodable: _toEncodable);
880 881 882
}

dynamic _toEncodable(dynamic object) {
883
  if (object is OperationResult) {
884
    return _operationResultToMap(object);
885
  }
886 887 888
  return object;
}

889
Future<Map<String, dynamic>> _deviceToMap(Device device) async {
890
  return <String, dynamic>{
891
    'id': device.id,
892
    'name': device.name,
893
    'platform': getNameForTargetPlatform(await device.targetPlatform),
894
    'emulator': await device.isLocalEmulator,
895 896 897
    'category': device.category?.toString(),
    'platformType': device.platformType?.toString(),
    'ephemeral': device.ephemeral,
898
    'emulatorId': await device.emulatorId,
899 900 901
  };
}

902 903 904 905
Map<String, dynamic> _emulatorToMap(Emulator emulator) {
  return <String, dynamic>{
    'id': emulator.id,
    'name': emulator.name,
906 907
    'category': emulator.category?.toString(),
    'platformType': emulator.platformType?.toString(),
908 909 910
  };
}

Devon Carew's avatar
Devon Carew committed
911
Map<String, dynamic> _operationResultToMap(OperationResult result) {
912
  return <String, dynamic>{
Devon Carew's avatar
Devon Carew committed
913
    'code': result.code,
914
    'message': result.message,
Devon Carew's avatar
Devon Carew committed
915 916 917
  };
}

Devon Carew's avatar
Devon Carew committed
918
dynamic _toJsonable(dynamic obj) {
919
  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
920
    return obj;
921 922
  }
  if (obj is OperationResult) {
Devon Carew's avatar
Devon Carew committed
923
    return obj;
924 925
  }
  if (obj is ToolExit) {
926
    return obj.message;
927
  }
Hixie's avatar
Hixie committed
928
  return '$obj';
Devon Carew's avatar
Devon Carew committed
929
}
930

931
class NotifyingLogger extends Logger {
932
  final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast();
933 934 935

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

936
  @override
937
  void printError(
938 939 940 941 942 943 944 945
    String message, {
    StackTrace stackTrace,
    bool emphasis = false,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
946
    _messageController.add(LogMessage('error', message, stackTrace));
947 948
  }

949
  @override
950
  void printStatus(
951 952 953 954 955 956 957 958
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
959
    _messageController.add(LogMessage('status', message));
960 961
  }

962
  @override
963
  void printTrace(String message) {
964
    // This is a lot of traffic to send over the wire.
965
  }
966 967

  @override
968 969
  Status startProgress(
    String message, {
970
    @required Duration timeout,
971
    String progressId,
972
    bool multilineOutput = false,
973
    int progressIndicatorPadding = kDefaultStatusPadding,
974
  }) {
975
    assert(timeout != null);
976
    printStatus(message);
977 978 979 980 981
    return SilentStatus(
      timeout: timeout,
      timeoutConfiguration: timeoutConfiguration,
      stopwatch: Stopwatch(),
    );
982
  }
983 984 985 986

  void dispose() {
    _messageController.close();
  }
987 988

  @override
989
  void sendEvent(String name, [Map<String, dynamic> args]) { }
990 991 992 993 994 995

  @override
  bool get supportsColor => throw UnimplementedError();

  @override
  bool get hasTerminal => false;
996 997
}

998 999
/// A running application, started by this daemon.
class AppInstance {
1000
  AppInstance(this.id, { this.runner, this.logToStdout = false });
1001 1002

  final String id;
1003
  final ResidentRunner runner;
1004
  final bool logToStdout;
1005 1006 1007

  _AppRunLogger _logger;

1008 1009
  Future<OperationResult> restart({ bool fullRestart = false, bool pause = false, String reason }) {
    return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason);
1010
  }
1011

1012 1013 1014 1015
  Future<OperationResult> reloadMethod({ String classId, String libraryId }) {
    return runner.reloadMethod(classId: classId, libraryId: libraryId);
  }

1016
  Future<void> stop() => runner.exit();
1017
  Future<void> detach() => runner.detach();
1018 1019 1020 1021 1022

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

1023
  Future<T> _runInZone<T>(AppDomain domain, FutureOr<T> method()) {
1024
    _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? globals.logger : null);
1025

1026
    return context.run<T>(
1027 1028 1029 1030 1031
      body: method,
      overrides: <Type, Generator>{
        Logger: () => _logger,
      },
    );
1032 1033 1034
  }
}

1035 1036 1037 1038 1039
/// 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);
1040
    registerHandler('create', create);
1041 1042
  }

1043 1044
  EmulatorManager emulators = EmulatorManager();

1045
  Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async {
1046
    final List<Emulator> list = await emulators.getAllAvailableEmulators();
1047
    return list.map<Map<String, dynamic>>(_emulatorToMap).toList();
1048 1049
  }

1050
  Future<void> launch(Map<String, dynamic> args) async {
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061
    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();
    }
  }
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071

  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,
    };
  }
1072 1073
}

1074
/// A [Logger] which sends log messages to a listening daemon client.
1075 1076 1077 1078
///
/// 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
1079 1080 1081
//
// 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.
1082
class _AppRunLogger extends Logger {
1083
  _AppRunLogger(this.domain, this.app, { this.parent });
1084 1085 1086

  AppDomain domain;
  final AppInstance app;
1087
  final Logger parent;
1088
  int _nextProgressId = 0;
1089 1090

  @override
1091
  void printError(
1092 1093 1094 1095 1096 1097 1098 1099
    String message, {
    StackTrace stackTrace,
    bool emphasis,
    TerminalColor color,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1100
    if (parent != null) {
1101 1102 1103 1104 1105 1106
      parent.printError(
        message,
        stackTrace: stackTrace,
        emphasis: emphasis,
        indent: indent,
        hangingIndent: hangingIndent,
1107
        wrap: wrap,
1108
      );
1109
    } else {
1110 1111 1112 1113
      if (stackTrace != null) {
        _sendLogEvent(<String, dynamic>{
          'log': message,
          'stackTrace': stackTrace.toString(),
1114
          'error': true,
1115 1116 1117 1118
        });
      } else {
        _sendLogEvent(<String, dynamic>{
          'log': message,
1119
          'error': true,
1120 1121
        });
      }
1122 1123 1124 1125
    }
  }

  @override
1126
  void printStatus(
1127 1128 1129 1130 1131 1132 1133 1134
    String message, {
    bool emphasis = false,
    TerminalColor color,
    bool newline = true,
    int indent,
    int hangingIndent,
    bool wrap,
  }) {
1135
    if (parent != null) {
1136 1137 1138 1139 1140 1141
      parent.printStatus(
        message,
        emphasis: emphasis,
        color: color,
        newline: newline,
        indent: indent,
1142
        hangingIndent: hangingIndent,
1143
        wrap: wrap,
1144
      );
1145
    } else {
1146
      _sendLogEvent(<String, dynamic>{'log': message});
1147
    }
1148 1149 1150
  }

  @override
1151 1152 1153 1154
  void printTrace(String message) {
    if (parent != null) {
      parent.printTrace(message);
    } else {
1155
      _sendLogEvent(<String, dynamic>{'log': message, 'trace': true});
1156 1157
    }
  }
1158

Devon Carew's avatar
Devon Carew committed
1159 1160
  Status _status;

1161
  @override
1162 1163
  Status startProgress(
    String message, {
1164
    @required Duration timeout,
1165
    String progressId,
1166 1167
    bool multilineOutput = false,
    int progressIndicatorPadding = kDefaultStatusPadding,
1168
  }) {
1169
    assert(timeout != null);
1170
    final int id = _nextProgressId++;
1171

1172 1173
    _sendProgressEvent(<String, dynamic>{
      'id': id.toString(),
1174
      'progressId': progressId,
1175
      'message': message,
1176 1177
    });

1178 1179
    _status = SilentStatus(
      timeout: timeout,
1180
      timeoutConfiguration: timeoutConfiguration,
1181 1182 1183 1184 1185 1186
      onFinish: () {
        _status = null;
        _sendProgressEvent(<String, dynamic>{
          'id': id.toString(),
          'progressId': progressId,
          'finished': true,
1187
        });
1188
      }, stopwatch: Stopwatch())..start();
Devon Carew's avatar
Devon Carew committed
1189
    return _status;
1190 1191 1192 1193 1194
  }

  void close() {
    domain = null;
  }
1195 1196

  void _sendLogEvent(Map<String, dynamic> event) {
1197
    if (domain == null) {
1198
      printStatus('event sent after app closed: $event');
1199
    } else {
1200
      domain._sendAppEvent(app, 'log', event);
1201
    }
1202
  }
1203 1204

  void _sendProgressEvent(Map<String, dynamic> event) {
1205
    if (domain == null) {
1206
      printStatus('event sent after app closed: $event');
1207
    } else {
1208
      domain._sendAppEvent(app, 'progress', event);
1209
    }
1210
  }
1211 1212

  @override
1213 1214 1215 1216 1217 1218
  void sendEvent(String name, [Map<String, dynamic> args]) {
    if (domain == null) {
      printStatus('event sent after app closed: $name');
    } else {
      domain.sendEvent(name, args);
    }
1219
  }
1220 1221 1222 1223 1224 1225

  @override
  bool get supportsColor => throw UnimplementedError();

  @override
  bool get hasTerminal => false;
1226 1227
}

1228
class LogMessage {
1229 1230
  LogMessage(this.level, this.message, [this.stackTrace]);

1231 1232 1233 1234
  final String level;
  final String message;
  final StackTrace stackTrace;
}
1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250

/// 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;
}