1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../base/common.dart'; 10import '../base/context.dart'; 11import '../base/file_system.dart'; 12import '../base/io.dart'; 13import '../base/logger.dart'; 14import '../base/terminal.dart'; 15import '../base/utils.dart'; 16import '../build_info.dart'; 17import '../cache.dart'; 18import '../convert.dart'; 19import '../device.dart'; 20import '../emulator.dart'; 21import '../globals.dart'; 22import '../project.dart'; 23import '../resident_runner.dart'; 24import '../run_cold.dart'; 25import '../run_hot.dart'; 26import '../runner/flutter_command.dart'; 27import '../web/web_runner.dart'; 28 29const String protocolVersion = '0.5.3'; 30 31/// A server process command. This command will start up a long-lived server. 32/// It reads JSON-RPC based commands from stdin, executes them, and returns 33/// JSON-RPC based responses and events to stdout. 34/// 35/// It can be shutdown with a `daemon.shutdown` command (or by killing the 36/// process). 37class DaemonCommand extends FlutterCommand { 38 DaemonCommand({ this.hidden = false }); 39 40 @override 41 final String name = 'daemon'; 42 43 @override 44 final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.'; 45 46 @override 47 final bool hidden; 48 49 @override 50 Future<FlutterCommandResult> runCommand() async { 51 printStatus('Starting device daemon...'); 52 isRunningFromDaemon = true; 53 54 final NotifyingLogger notifyingLogger = NotifyingLogger(); 55 56 Cache.releaseLockEarly(); 57 58 await context.run<void>( 59 body: () async { 60 final Daemon daemon = Daemon( 61 stdinCommandStream, stdoutCommandResponse, 62 daemonCommand: this, notifyingLogger: notifyingLogger); 63 64 final int code = await daemon.onExit; 65 if (code != 0) 66 throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code); 67 }, 68 overrides: <Type, Generator>{ 69 Logger: () => notifyingLogger, 70 }, 71 ); 72 return null; 73 } 74} 75 76typedef DispatchCommand = void Function(Map<String, dynamic> command); 77 78typedef CommandHandler = Future<dynamic> Function(Map<String, dynamic> args); 79 80class Daemon { 81 Daemon( 82 Stream<Map<String, dynamic>> commandStream, 83 this.sendCommand, { 84 this.daemonCommand, 85 this.notifyingLogger, 86 this.logToStdout = false, 87 }) { 88 // Set up domains. 89 _registerDomain(daemonDomain = DaemonDomain(this)); 90 _registerDomain(appDomain = AppDomain(this)); 91 _registerDomain(deviceDomain = DeviceDomain(this)); 92 _registerDomain(emulatorDomain = EmulatorDomain(this)); 93 94 // Start listening. 95 _commandSubscription = commandStream.listen( 96 _handleRequest, 97 onDone: () { 98 if (!_onExitCompleter.isCompleted) 99 _onExitCompleter.complete(0); 100 }, 101 ); 102 } 103 104 DaemonDomain daemonDomain; 105 AppDomain appDomain; 106 DeviceDomain deviceDomain; 107 EmulatorDomain emulatorDomain; 108 StreamSubscription<Map<String, dynamic>> _commandSubscription; 109 110 final DispatchCommand sendCommand; 111 final DaemonCommand daemonCommand; 112 final NotifyingLogger notifyingLogger; 113 final bool logToStdout; 114 115 final Completer<int> _onExitCompleter = Completer<int>(); 116 final Map<String, Domain> _domainMap = <String, Domain>{}; 117 118 void _registerDomain(Domain domain) { 119 _domainMap[domain.name] = domain; 120 } 121 122 Future<int> get onExit => _onExitCompleter.future; 123 124 void _handleRequest(Map<String, dynamic> request) { 125 // {id, method, params} 126 127 // [id] is an opaque type to us. 128 final dynamic id = request['id']; 129 130 if (id == null) { 131 stderr.writeln('no id for request: $request'); 132 return; 133 } 134 135 try { 136 final String method = request['method']; 137 if (!method.contains('.')) 138 throw 'method not understood: $method'; 139 140 final String prefix = method.substring(0, method.indexOf('.')); 141 final String name = method.substring(method.indexOf('.') + 1); 142 if (_domainMap[prefix] == null) 143 throw 'no domain for method: $method'; 144 145 _domainMap[prefix].handleCommand(name, id, request['params'] ?? const <String, dynamic>{}); 146 } catch (error, trace) { 147 _send(<String, dynamic>{ 148 'id': id, 149 'error': _toJsonable(error), 150 'trace': '$trace', 151 }); 152 } 153 } 154 155 void _send(Map<String, dynamic> map) => sendCommand(map); 156 157 void shutdown({ dynamic error }) { 158 _commandSubscription?.cancel(); 159 for (Domain domain in _domainMap.values) 160 domain.dispose(); 161 if (!_onExitCompleter.isCompleted) { 162 if (error == null) 163 _onExitCompleter.complete(0); 164 else 165 _onExitCompleter.completeError(error); 166 } 167 } 168} 169 170abstract class Domain { 171 Domain(this.daemon, this.name); 172 173 final Daemon daemon; 174 final String name; 175 final Map<String, CommandHandler> _handlers = <String, CommandHandler>{}; 176 177 void registerHandler(String name, CommandHandler handler) { 178 _handlers[name] = handler; 179 } 180 181 FlutterCommand get command => daemon.daemonCommand; 182 183 @override 184 String toString() => name; 185 186 void handleCommand(String command, dynamic id, Map<String, dynamic> args) { 187 Future<dynamic>.sync(() { 188 if (_handlers.containsKey(command)) 189 return _handlers[command](args); 190 throw 'command not understood: $name.$command'; 191 }).then<dynamic>((dynamic result) { 192 if (result == null) { 193 _send(<String, dynamic>{'id': id}); 194 } else { 195 _send(<String, dynamic>{'id': id, 'result': _toJsonable(result)}); 196 } 197 }).catchError((dynamic error, dynamic trace) { 198 _send(<String, dynamic>{ 199 'id': id, 200 'error': _toJsonable(error), 201 'trace': '$trace', 202 }); 203 }); 204 } 205 206 void sendEvent(String name, [ dynamic args ]) { 207 final Map<String, dynamic> map = <String, dynamic>{'event': name}; 208 if (args != null) 209 map['params'] = _toJsonable(args); 210 _send(map); 211 } 212 213 void _send(Map<String, dynamic> map) => daemon._send(map); 214 215 String _getStringArg(Map<String, dynamic> args, String name, { bool required = false }) { 216 if (required && !args.containsKey(name)) 217 throw '$name is required'; 218 final dynamic val = args[name]; 219 if (val != null && val is! String) 220 throw '$name is not a String'; 221 return val; 222 } 223 224 bool _getBoolArg(Map<String, dynamic> args, String name, { bool required = false }) { 225 if (required && !args.containsKey(name)) 226 throw '$name is required'; 227 final dynamic val = args[name]; 228 if (val != null && val is! bool) 229 throw '$name is not a bool'; 230 return val; 231 } 232 233 int _getIntArg(Map<String, dynamic> args, String name, { bool required = false }) { 234 if (required && !args.containsKey(name)) 235 throw '$name is required'; 236 final dynamic val = args[name]; 237 if (val != null && val is! int) 238 throw '$name is not an int'; 239 return val; 240 } 241 242 void dispose() { } 243} 244 245/// This domain responds to methods like [version] and [shutdown]. 246/// 247/// This domain fires the `daemon.logMessage` event. 248class DaemonDomain extends Domain { 249 DaemonDomain(Daemon daemon) : super(daemon, 'daemon') { 250 registerHandler('version', version); 251 registerHandler('shutdown', shutdown); 252 registerHandler('getSupportedPlatforms', getSupportedPlatforms); 253 254 sendEvent( 255 'daemon.connected', 256 <String, dynamic>{ 257 'version': protocolVersion, 258 'pid': pid, 259 }, 260 ); 261 262 _subscription = daemon.notifyingLogger.onMessage.listen((LogMessage message) { 263 if (daemon.logToStdout) { 264 if (message.level == 'status') { 265 // We use `print()` here instead of `stdout.writeln()` in order to 266 // capture the print output for testing. 267 print(message.message); 268 } else if (message.level == 'error') { 269 stderr.writeln(message.message); 270 if (message.stackTrace != null) 271 stderr.writeln(message.stackTrace.toString().trimRight()); 272 } 273 } else { 274 if (message.stackTrace != null) { 275 sendEvent('daemon.logMessage', <String, dynamic>{ 276 'level': message.level, 277 'message': message.message, 278 'stackTrace': message.stackTrace.toString(), 279 }); 280 } else { 281 sendEvent('daemon.logMessage', <String, dynamic>{ 282 'level': message.level, 283 'message': message.message, 284 }); 285 } 286 } 287 }); 288 } 289 290 StreamSubscription<LogMessage> _subscription; 291 292 Future<String> version(Map<String, dynamic> args) { 293 return Future<String>.value(protocolVersion); 294 } 295 296 Future<void> shutdown(Map<String, dynamic> args) { 297 Timer.run(daemon.shutdown); 298 return Future<void>.value(); 299 } 300 301 @override 302 void dispose() { 303 _subscription?.cancel(); 304 } 305 306 /// Enumerates the platforms supported by the provided project. 307 /// 308 /// This does not filter based on the current workflow restrictions, such 309 /// as whether command line tools are installed or whether the host platform 310 /// is correct. 311 Future<Map<String, Object>> getSupportedPlatforms(Map<String, dynamic> args) async { 312 final String projectRoot = _getStringArg(args, 'projectRoot', required: true); 313 final List<String> result = <String>[]; 314 try { 315 // TODO(jonahwilliams): replace this with a project metadata check once 316 // that has been implemented. 317 final FlutterProject flutterProject = FlutterProject.fromDirectory(fs.directory(projectRoot)); 318 if (flutterProject.linux.existsSync()) { 319 result.add('linux'); 320 } 321 if (flutterProject.macos.existsSync()) { 322 result.add('macos'); 323 } 324 if (flutterProject.windows.existsSync()) { 325 result.add('windows'); 326 } 327 if (flutterProject.ios.existsSync()) { 328 result.add('ios'); 329 } 330 if (flutterProject.android.existsSync()) { 331 result.add('android'); 332 } 333 if (flutterProject.web.existsSync()) { 334 result.add('web'); 335 } 336 if (flutterProject.fuchsia.existsSync()) { 337 result.add('fuchsia'); 338 } 339 return <String, Object>{ 340 'platforms': result, 341 }; 342 } catch (err, stackTrace) { 343 sendEvent('log', <String, dynamic>{ 344 'log': 'Failed to parse project metadata', 345 'stackTrace': stackTrace.toString(), 346 'error': true, 347 }); 348 // On any sort of failure, fall back to Android and iOS for backwards 349 // comparability. 350 return <String, Object>{ 351 'platforms': <String>[ 352 'android', 353 'ios', 354 ], 355 }; 356 } 357 } 358} 359 360typedef _RunOrAttach = Future<void> Function({ 361 Completer<DebugConnectionInfo> connectionInfoCompleter, 362 Completer<void> appStartedCompleter, 363}); 364 365/// This domain responds to methods like [start] and [stop]. 366/// 367/// It fires events for application start, stop, and stdout and stderr. 368class AppDomain extends Domain { 369 AppDomain(Daemon daemon) : super(daemon, 'app') { 370 registerHandler('restart', restart); 371 registerHandler('callServiceExtension', callServiceExtension); 372 registerHandler('stop', stop); 373 registerHandler('detach', detach); 374 } 375 376 static final Uuid _uuidGenerator = Uuid(); 377 378 static String _getNewAppId() => _uuidGenerator.generateV4(); 379 380 final List<AppInstance> _apps = <AppInstance>[]; 381 382 Future<AppInstance> startApp( 383 Device device, 384 String projectDirectory, 385 String target, 386 String route, 387 DebuggingOptions options, 388 bool enableHotReload, { 389 File applicationBinary, 390 @required bool trackWidgetCreation, 391 String projectRootPath, 392 String packagesFilePath, 393 String dillOutputPath, 394 bool ipv6 = false, 395 String isolateFilter, 396 }) async { 397 if (await device.isLocalEmulator && !options.buildInfo.supportsEmulator) { 398 throw '${toTitleCase(options.buildInfo.friendlyModeName)} mode is not supported for emulators.'; 399 } 400 // We change the current working directory for the duration of the `start` command. 401 final Directory cwd = fs.currentDirectory; 402 fs.currentDirectory = fs.directory(projectDirectory); 403 final FlutterProject flutterProject = FlutterProject.current(); 404 405 final FlutterDevice flutterDevice = await FlutterDevice.create( 406 device, 407 flutterProject: flutterProject, 408 trackWidgetCreation: trackWidgetCreation, 409 viewFilter: isolateFilter, 410 target: target, 411 buildMode: options.buildInfo.mode, 412 ); 413 414 ResidentRunner runner; 415 416 if (await device.targetPlatform == TargetPlatform.web_javascript) { 417 runner = webRunnerFactory.createWebRunner( 418 device, 419 flutterProject: flutterProject, 420 target: target, 421 debuggingOptions: options, 422 ipv6: ipv6, 423 ); 424 } else if (enableHotReload) { 425 runner = HotRunner( 426 <FlutterDevice>[flutterDevice], 427 target: target, 428 debuggingOptions: options, 429 usesTerminalUi: false, 430 applicationBinary: applicationBinary, 431 projectRootPath: projectRootPath, 432 packagesFilePath: packagesFilePath, 433 dillOutputPath: dillOutputPath, 434 ipv6: ipv6, 435 hostIsIde: true, 436 ); 437 } else { 438 runner = ColdRunner( 439 <FlutterDevice>[flutterDevice], 440 target: target, 441 debuggingOptions: options, 442 applicationBinary: applicationBinary, 443 usesTerminalUi: false, 444 ipv6: ipv6, 445 ); 446 } 447 448 return launch( 449 runner, 450 ({ 451 Completer<DebugConnectionInfo> connectionInfoCompleter, 452 Completer<void> appStartedCompleter, 453 }) { 454 return runner.run( 455 connectionInfoCompleter: connectionInfoCompleter, 456 appStartedCompleter: appStartedCompleter, 457 route: route, 458 ); 459 }, 460 device, 461 projectDirectory, 462 enableHotReload, 463 cwd, 464 LaunchMode.run, 465 ); 466 } 467 468 Future<AppInstance> launch( 469 ResidentRunner runner, 470 _RunOrAttach runOrAttach, 471 Device device, 472 String projectDirectory, 473 bool enableHotReload, 474 Directory cwd, 475 LaunchMode launchMode, 476 ) async { 477 final AppInstance app = AppInstance(_getNewAppId(), 478 runner: runner, logToStdout: daemon.logToStdout); 479 _apps.add(app); 480 _sendAppEvent(app, 'start', <String, dynamic>{ 481 'deviceId': device.id, 482 'directory': projectDirectory, 483 'supportsRestart': isRestartSupported(enableHotReload, device), 484 'launchMode': launchMode.toString(), 485 }); 486 487 Completer<DebugConnectionInfo> connectionInfoCompleter; 488 489 if (runner.debuggingOptions.debuggingEnabled) { 490 connectionInfoCompleter = Completer<DebugConnectionInfo>(); 491 // We don't want to wait for this future to complete and callbacks won't fail. 492 // As it just writes to stdout. 493 unawaited(connectionInfoCompleter.future.then<void>( 494 (DebugConnectionInfo info) { 495 final Map<String, dynamic> params = <String, dynamic>{ 496 // The web vmservice proxy does not have an http address. 497 'port': info.httpUri?.port ?? info.wsUri.port, 498 'wsUri': info.wsUri.toString(), 499 }; 500 if (info.baseUri != null) 501 params['baseUri'] = info.baseUri; 502 _sendAppEvent(app, 'debugPort', params); 503 }, 504 )); 505 } 506 final Completer<void> appStartedCompleter = Completer<void>(); 507 // We don't want to wait for this future to complete, and callbacks won't fail, 508 // as it just writes to stdout. 509 unawaited(appStartedCompleter.future.then<void>((void value) { 510 _sendAppEvent(app, 'started'); 511 })); 512 513 await app._runInZone<void>(this, () async { 514 try { 515 await runOrAttach( 516 connectionInfoCompleter: connectionInfoCompleter, 517 appStartedCompleter: appStartedCompleter, 518 ); 519 _sendAppEvent(app, 'stop'); 520 } catch (error, trace) { 521 _sendAppEvent(app, 'stop', <String, dynamic>{ 522 'error': _toJsonable(error), 523 'trace': '$trace', 524 }); 525 } finally { 526 fs.currentDirectory = cwd; 527 _apps.remove(app); 528 } 529 }); 530 return app; 531 } 532 533 bool isRestartSupported(bool enableHotReload, Device device) => 534 enableHotReload && device.supportsHotRestart; 535 536 Future<OperationResult> _inProgressHotReload; 537 538 Future<OperationResult> restart(Map<String, dynamic> args) async { 539 final String appId = _getStringArg(args, 'appId', required: true); 540 final bool fullRestart = _getBoolArg(args, 'fullRestart') ?? false; 541 final bool pauseAfterRestart = _getBoolArg(args, 'pause') ?? false; 542 final String restartReason = _getStringArg(args, 'reason'); 543 544 final AppInstance app = _getApp(appId); 545 if (app == null) 546 throw "app '$appId' not found"; 547 548 if (_inProgressHotReload != null) 549 throw 'hot restart already in progress'; 550 551 _inProgressHotReload = app._runInZone<OperationResult>(this, () { 552 return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason); 553 }); 554 return _inProgressHotReload.whenComplete(() { 555 _inProgressHotReload = null; 556 }); 557 } 558 559 /// Returns an error, or the service extension result (a map with two fixed 560 /// keys, `type` and `method`). The result may have one or more additional keys, 561 /// depending on the specific service extension end-point. For example: 562 /// 563 /// { 564 /// "value":"android", 565 /// "type":"_extensionType", 566 /// "method":"ext.flutter.platformOverride" 567 /// } 568 Future<Map<String, dynamic>> callServiceExtension(Map<String, dynamic> args) async { 569 final String appId = _getStringArg(args, 'appId', required: true); 570 final String methodName = _getStringArg(args, 'methodName'); 571 final Map<String, dynamic> params = args['params'] == null ? <String, dynamic>{} : castStringKeyedMap(args['params']); 572 573 final AppInstance app = _getApp(appId); 574 if (app == null) 575 throw "app '$appId' not found"; 576 577 final Map<String, dynamic> result = await app.runner 578 .invokeFlutterExtensionRpcRawOnFirstIsolate(methodName, params: params); 579 if (result == null) 580 throw 'method not available: $methodName'; 581 582 if (result.containsKey('error')) 583 throw result['error']; 584 585 return result; 586 } 587 588 Future<bool> stop(Map<String, dynamic> args) async { 589 final String appId = _getStringArg(args, 'appId', required: true); 590 591 final AppInstance app = _getApp(appId); 592 if (app == null) 593 throw "app '$appId' not found"; 594 595 return app.stop().then<bool>( 596 (void value) => true, 597 onError: (dynamic error, StackTrace stack) { 598 _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true}); 599 app.closeLogger(); 600 _apps.remove(app); 601 return false; 602 }, 603 ); 604 } 605 606 Future<bool> detach(Map<String, dynamic> args) async { 607 final String appId = _getStringArg(args, 'appId', required: true); 608 609 final AppInstance app = _getApp(appId); 610 if (app == null) 611 throw "app '$appId' not found"; 612 613 return app.detach().then<bool>( 614 (void value) => true, 615 onError: (dynamic error, StackTrace stack) { 616 _sendAppEvent(app, 'log', <String, dynamic>{'log': '$error', 'error': true}); 617 app.closeLogger(); 618 _apps.remove(app); 619 return false; 620 }, 621 ); 622 } 623 624 AppInstance _getApp(String id) { 625 return _apps.firstWhere((AppInstance app) => app.id == id, orElse: () => null); 626 } 627 628 void _sendAppEvent(AppInstance app, String name, [ Map<String, dynamic> args ]) { 629 sendEvent('app.$name', <String, dynamic>{ 630 'appId': app.id, 631 ...?args, 632 }); 633 } 634} 635 636typedef _DeviceEventHandler = void Function(Device device); 637 638/// This domain lets callers list and monitor connected devices. 639/// 640/// It exports a `getDevices()` call, as well as firing `device.added` and 641/// `device.removed` events. 642class DeviceDomain extends Domain { 643 DeviceDomain(Daemon daemon) : super(daemon, 'device') { 644 registerHandler('getDevices', getDevices); 645 registerHandler('enable', enable); 646 registerHandler('disable', disable); 647 registerHandler('forward', forward); 648 registerHandler('unforward', unforward); 649 650 // Use the device manager discovery so that client provided device types 651 // are usable via the daemon protocol. 652 deviceManager.deviceDiscoverers.forEach(addDeviceDiscoverer); 653 } 654 655 void addDeviceDiscoverer(DeviceDiscovery discoverer) { 656 if (!discoverer.supportsPlatform) 657 return; 658 659 _discoverers.add(discoverer); 660 if (discoverer is PollingDeviceDiscovery) { 661 discoverer.onAdded.listen(_onDeviceEvent('device.added')); 662 discoverer.onRemoved.listen(_onDeviceEvent('device.removed')); 663 } 664 } 665 666 Future<void> _serializeDeviceEvents = Future<void>.value(); 667 668 _DeviceEventHandler _onDeviceEvent(String eventName) { 669 return (Device device) { 670 _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async { 671 try { 672 final Map<String, Object> response = await _deviceToMap(device); 673 sendEvent(eventName, response); 674 } catch (err) { 675 printError('$err'); 676 } 677 }); 678 }; 679 } 680 681 final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[]; 682 683 /// Return a list of the current devices, with each device represented as a map 684 /// of properties (id, name, platform, ...). 685 Future<List<Map<String, dynamic>>> getDevices([ Map<String, dynamic> args ]) async { 686 final List<Map<String, dynamic>> devicesInfo = <Map<String, dynamic>>[]; 687 688 for (PollingDeviceDiscovery discoverer in _discoverers) { 689 for (Device device in await discoverer.devices) { 690 devicesInfo.add(await _deviceToMap(device)); 691 } 692 } 693 694 return devicesInfo; 695 } 696 697 /// Enable device events. 698 Future<void> enable(Map<String, dynamic> args) { 699 for (PollingDeviceDiscovery discoverer in _discoverers) 700 discoverer.startPolling(); 701 return Future<void>.value(); 702 } 703 704 /// Disable device events. 705 Future<void> disable(Map<String, dynamic> args) { 706 for (PollingDeviceDiscovery discoverer in _discoverers) 707 discoverer.stopPolling(); 708 return Future<void>.value(); 709 } 710 711 /// Forward a host port to a device port. 712 Future<Map<String, dynamic>> forward(Map<String, dynamic> args) async { 713 final String deviceId = _getStringArg(args, 'deviceId', required: true); 714 final int devicePort = _getIntArg(args, 'devicePort', required: true); 715 int hostPort = _getIntArg(args, 'hostPort'); 716 717 final Device device = await daemon.deviceDomain._getDevice(deviceId); 718 if (device == null) 719 throw "device '$deviceId' not found"; 720 721 hostPort = await device.portForwarder.forward(devicePort, hostPort: hostPort); 722 723 return <String, dynamic>{'hostPort': hostPort}; 724 } 725 726 /// Removes a forwarded port. 727 Future<void> unforward(Map<String, dynamic> args) async { 728 final String deviceId = _getStringArg(args, 'deviceId', required: true); 729 final int devicePort = _getIntArg(args, 'devicePort', required: true); 730 final int hostPort = _getIntArg(args, 'hostPort', required: true); 731 732 final Device device = await daemon.deviceDomain._getDevice(deviceId); 733 if (device == null) 734 throw "device '$deviceId' not found"; 735 736 return device.portForwarder.unforward(ForwardedPort(hostPort, devicePort)); 737 } 738 739 @override 740 void dispose() { 741 for (PollingDeviceDiscovery discoverer in _discoverers) 742 discoverer.dispose(); 743 } 744 745 /// Return the device matching the deviceId field in the args. 746 Future<Device> _getDevice(String deviceId) async { 747 for (PollingDeviceDiscovery discoverer in _discoverers) { 748 final Device device = (await discoverer.devices).firstWhere((Device device) => device.id == deviceId, orElse: () => null); 749 if (device != null) 750 return device; 751 } 752 return null; 753 } 754} 755 756Stream<Map<String, dynamic>> get stdinCommandStream => stdin 757 .transform<String>(utf8.decoder) 758 .transform<String>(const LineSplitter()) 759 .where((String line) => line.startsWith('[{') && line.endsWith('}]')) 760 .map<Map<String, dynamic>>((String line) { 761 line = line.substring(1, line.length - 1); 762 return json.decode(line); 763 }); 764 765void stdoutCommandResponse(Map<String, dynamic> command) { 766 stdout.writeln('[${jsonEncodeObject(command)}]'); 767} 768 769String jsonEncodeObject(dynamic object) { 770 return json.encode(object, toEncodable: _toEncodable); 771} 772 773dynamic _toEncodable(dynamic object) { 774 if (object is OperationResult) 775 return _operationResultToMap(object); 776 return object; 777} 778 779Future<Map<String, dynamic>> _deviceToMap(Device device) async { 780 return <String, dynamic>{ 781 'id': device.id, 782 'name': device.name, 783 'platform': getNameForTargetPlatform(await device.targetPlatform), 784 'emulator': await device.isLocalEmulator, 785 'category': device.category?.toString(), 786 'platformType': device.platformType?.toString(), 787 'ephemeral': device.ephemeral, 788 'emulatorId': await device.emulatorId, 789 }; 790} 791 792Map<String, dynamic> _emulatorToMap(Emulator emulator) { 793 return <String, dynamic>{ 794 'id': emulator.id, 795 'name': emulator.name, 796 'category': emulator.category?.toString(), 797 'platformType': emulator.platformType?.toString(), 798 }; 799} 800 801Map<String, dynamic> _operationResultToMap(OperationResult result) { 802 return <String, dynamic>{ 803 'code': result.code, 804 'message': result.message, 805 }; 806} 807 808dynamic _toJsonable(dynamic obj) { 809 if (obj is String || obj is int || obj is bool || obj is Map<dynamic, dynamic> || obj is List<dynamic> || obj == null) 810 return obj; 811 if (obj is OperationResult) 812 return obj; 813 if (obj is ToolExit) 814 return obj.message; 815 return '$obj'; 816} 817 818class NotifyingLogger extends Logger { 819 final StreamController<LogMessage> _messageController = StreamController<LogMessage>.broadcast(); 820 821 Stream<LogMessage> get onMessage => _messageController.stream; 822 823 @override 824 void printError( 825 String message, { 826 StackTrace stackTrace, 827 bool emphasis = false, 828 TerminalColor color, 829 int indent, 830 int hangingIndent, 831 bool wrap, 832 }) { 833 _messageController.add(LogMessage('error', message, stackTrace)); 834 } 835 836 @override 837 void printStatus( 838 String message, { 839 bool emphasis = false, 840 TerminalColor color, 841 bool newline = true, 842 int indent, 843 int hangingIndent, 844 bool wrap, 845 }) { 846 _messageController.add(LogMessage('status', message)); 847 } 848 849 @override 850 void printTrace(String message) { 851 // This is a lot of traffic to send over the wire. 852 } 853 854 @override 855 Status startProgress( 856 String message, { 857 @required Duration timeout, 858 String progressId, 859 bool multilineOutput = false, 860 int progressIndicatorPadding = kDefaultStatusPadding, 861 }) { 862 assert(timeout != null); 863 printStatus(message); 864 return SilentStatus(timeout: timeout); 865 } 866 867 void dispose() { 868 _messageController.close(); 869 } 870} 871 872/// A running application, started by this daemon. 873class AppInstance { 874 AppInstance(this.id, { this.runner, this.logToStdout = false }); 875 876 final String id; 877 final ResidentRunner runner; 878 final bool logToStdout; 879 880 _AppRunLogger _logger; 881 882 Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { 883 return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason); 884 } 885 886 Future<void> stop() => runner.exit(); 887 Future<void> detach() => runner.detach(); 888 889 void closeLogger() { 890 _logger.close(); 891 } 892 893 Future<T> _runInZone<T>(AppDomain domain, dynamic method()) { 894 _logger ??= _AppRunLogger(domain, this, parent: logToStdout ? logger : null); 895 896 return context.run<T>( 897 body: method, 898 overrides: <Type, Generator>{ 899 Logger: () => _logger, 900 }, 901 ); 902 } 903} 904 905/// This domain responds to methods like [getEmulators] and [launch]. 906class EmulatorDomain extends Domain { 907 EmulatorDomain(Daemon daemon) : super(daemon, 'emulator') { 908 registerHandler('getEmulators', getEmulators); 909 registerHandler('launch', launch); 910 registerHandler('create', create); 911 } 912 913 EmulatorManager emulators = EmulatorManager(); 914 915 Future<List<Map<String, dynamic>>> getEmulators([ Map<String, dynamic> args ]) async { 916 final List<Emulator> list = await emulators.getAllAvailableEmulators(); 917 return list.map<Map<String, dynamic>>(_emulatorToMap).toList(); 918 } 919 920 Future<void> launch(Map<String, dynamic> args) async { 921 final String emulatorId = _getStringArg(args, 'emulatorId', required: true); 922 final List<Emulator> matches = 923 await emulators.getEmulatorsMatching(emulatorId); 924 if (matches.isEmpty) { 925 throw "emulator '$emulatorId' not found"; 926 } else if (matches.length > 1) { 927 throw "multiple emulators match '$emulatorId'"; 928 } else { 929 await matches.first.launch(); 930 } 931 } 932 933 Future<Map<String, dynamic>> create(Map<String, dynamic> args) async { 934 final String name = _getStringArg(args, 'name', required: false); 935 final CreateEmulatorResult res = await emulators.createEmulator(name: name); 936 return <String, dynamic>{ 937 'success': res.success, 938 'emulatorName': res.emulatorName, 939 'error': res.error, 940 }; 941 } 942} 943 944/// A [Logger] which sends log messages to a listening daemon client. 945/// 946/// This class can either: 947/// 1) Send stdout messages and progress events to the client IDE 948/// 1) Log messages to stdout and send progress events to the client IDE 949// 950// TODO(devoncarew): To simplify this code a bit, we could choose to specialize 951// this class into two, one for each of the above use cases. 952class _AppRunLogger extends Logger { 953 _AppRunLogger(this.domain, this.app, { this.parent }); 954 955 AppDomain domain; 956 final AppInstance app; 957 final Logger parent; 958 int _nextProgressId = 0; 959 960 @override 961 void printError( 962 String message, { 963 StackTrace stackTrace, 964 bool emphasis, 965 TerminalColor color, 966 int indent, 967 int hangingIndent, 968 bool wrap, 969 }) { 970 if (parent != null) { 971 parent.printError( 972 message, 973 stackTrace: stackTrace, 974 emphasis: emphasis, 975 indent: indent, 976 hangingIndent: hangingIndent, 977 wrap: wrap, 978 ); 979 } else { 980 if (stackTrace != null) { 981 _sendLogEvent(<String, dynamic>{ 982 'log': message, 983 'stackTrace': stackTrace.toString(), 984 'error': true, 985 }); 986 } else { 987 _sendLogEvent(<String, dynamic>{ 988 'log': message, 989 'error': true, 990 }); 991 } 992 } 993 } 994 995 @override 996 void printStatus( 997 String message, { 998 bool emphasis = false, 999 TerminalColor color, 1000 bool newline = true, 1001 int indent, 1002 int hangingIndent, 1003 bool wrap, 1004 }) { 1005 if (parent != null) { 1006 parent.printStatus( 1007 message, 1008 emphasis: emphasis, 1009 color: color, 1010 newline: newline, 1011 indent: indent, 1012 hangingIndent: hangingIndent, 1013 wrap: wrap, 1014 ); 1015 } else { 1016 _sendLogEvent(<String, dynamic>{'log': message}); 1017 } 1018 } 1019 1020 @override 1021 void printTrace(String message) { 1022 if (parent != null) { 1023 parent.printTrace(message); 1024 } else { 1025 _sendLogEvent(<String, dynamic>{'log': message, 'trace': true}); 1026 } 1027 } 1028 1029 Status _status; 1030 1031 @override 1032 Status startProgress( 1033 String message, { 1034 @required Duration timeout, 1035 String progressId, 1036 bool multilineOutput = false, 1037 int progressIndicatorPadding = kDefaultStatusPadding, 1038 }) { 1039 assert(timeout != null); 1040 final int id = _nextProgressId++; 1041 1042 _sendProgressEvent(<String, dynamic>{ 1043 'id': id.toString(), 1044 'progressId': progressId, 1045 'message': message, 1046 }); 1047 1048 _status = SilentStatus( 1049 timeout: timeout, 1050 onFinish: () { 1051 _status = null; 1052 _sendProgressEvent(<String, dynamic>{ 1053 'id': id.toString(), 1054 'progressId': progressId, 1055 'finished': true, 1056 }); 1057 })..start(); 1058 return _status; 1059 } 1060 1061 void close() { 1062 domain = null; 1063 } 1064 1065 void _sendLogEvent(Map<String, dynamic> event) { 1066 if (domain == null) 1067 printStatus('event sent after app closed: $event'); 1068 else 1069 domain._sendAppEvent(app, 'log', event); 1070 } 1071 1072 void _sendProgressEvent(Map<String, dynamic> event) { 1073 if (domain == null) 1074 printStatus('event sent after app closed: $event'); 1075 else 1076 domain._sendAppEvent(app, 'progress', event); 1077 } 1078} 1079 1080class LogMessage { 1081 LogMessage(this.level, this.message, [this.stackTrace]); 1082 1083 final String level; 1084 final String message; 1085 final StackTrace stackTrace; 1086} 1087 1088/// The method by which the flutter app was launched. 1089class LaunchMode { 1090 const LaunchMode._(this._value); 1091 1092 /// The app was launched via `flutter run`. 1093 static const LaunchMode run = LaunchMode._('run'); 1094 1095 /// The app was launched via `flutter attach`. 1096 static const LaunchMode attach = LaunchMode._('attach'); 1097 1098 final String _value; 1099 1100 @override 1101 String toString() => _value; 1102} 1103