From 0770c3c14fafc2e37632e087b6225f3955294b78 Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Wed, 26 Apr 2017 21:49:38 -0700 Subject: [PATCH] [flutter_tools] Adds some support for '-d all' (#9585) --- .../lib/src/commands/daemon.dart | 8 +- .../lib/src/commands/fuchsia_reload.dart | 16 +- .../flutter_tools/lib/src/commands/run.dart | 45 +- packages/flutter_tools/lib/src/device.dart | 19 +- .../lib/src/resident_runner.dart | 465 ++++++++++++++---- packages/flutter_tools/lib/src/run_cold.dart | 118 ++--- packages/flutter_tools/lib/src/run_hot.dart | 275 ++++++----- .../lib/src/runner/flutter_command.dart | 34 +- packages/flutter_tools/test/src/context.dart | 18 +- 9 files changed, 652 insertions(+), 346 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 59eba865c9..ae9aa0961c 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -353,11 +353,13 @@ class AppDomain extends Domain { final Directory cwd = fs.currentDirectory; fs.currentDirectory = fs.directory(projectDirectory); + final FlutterDevice flutterDevice = new FlutterDevice(device); + ResidentRunner runner; if (enableHotReload) { runner = new HotRunner( - device, + [flutterDevice], target: target, debuggingOptions: options, usesTerminalUI: false, @@ -368,7 +370,7 @@ class AppDomain extends Domain { ); } else { runner = new ColdRunner( - device, + [flutterDevice], target: target, debuggingOptions: options, usesTerminalUI: false, @@ -448,7 +450,7 @@ class AppDomain extends Domain { if (app == null) throw "app '$appId' not found"; - final Isolate isolate = app.runner.currentView.uiIsolate; + final Isolate isolate = app.runner.flutterDevices.first.views.first.uiIsolate; final Map result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params); if (result == null) return new OperationResult(1, 'method not available: $methodName'); diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart index 2d6ac0656c..4af62daf71 100644 --- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart +++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart @@ -16,6 +16,7 @@ import '../device.dart'; import '../flx.dart' as flx; import '../fuchsia/fuchsia_device.dart'; import '../globals.dart'; +import '../resident_runner.dart'; import '../run_hot.dart'; import '../runner/flutter_command.dart'; import '../vmservice.dart'; @@ -112,18 +113,21 @@ class FuchsiaReloadCommand extends FlutterCommand { final List fullAddresses = targetPorts.map( (int p) => '$_address:$p' ).toList(); + final List observatoryUris = fullAddresses.map( + (String a) => Uri.parse('http://$a') + ).toList(); final FuchsiaDevice device = new FuchsiaDevice(fullAddresses[0]); + final FlutterDevice flutterDevice = new FlutterDevice(device); + flutterDevice.observatoryUris = observatoryUris; final HotRunner hotRunner = new HotRunner( - device, + [flutterDevice], debuggingOptions: new DebuggingOptions.enabled(getBuildMode()), target: _target, projectRootPath: _fuchsiaProjectPath, packagesFilePath: _dotPackagesPath ); - final List observatoryUris = - fullAddresses.map((String a) => Uri.parse('http://$a')).toList(); printStatus('Connecting to $_binaryName'); - await hotRunner.attach(observatoryUris, isolateFilter: isolateName); + await hotRunner.attach(viewFilter: isolateName); } // A cache of VMService connections. @@ -151,12 +155,12 @@ class FuchsiaReloadCommand extends FlutterCommand { } // Find ports where there is a view isolate with the given name - Future> _filterPorts(List ports, String isolateFilter) async { + Future> _filterPorts(List ports, String viewFilter) async { final List result = []; for (FlutterView v in await _getViews(ports)) { final Uri addr = v.owner.vmService.httpAddress; printTrace('At $addr, found view: ${v.uiIsolate.name}'); - if (v.uiIsolate.name.indexOf(isolateFilter) == 0) + if (v.uiIsolate.name.indexOf(viewFilter) == 0) result.add(addr.port); } return result; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index aad16e6aa4..ab90f879a1 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -150,17 +150,20 @@ class RunCommand extends RunCommandBase { }; } - Device device; + List devices; @override Future get usagePath async { final String command = shouldUseHotMode() ? 'hotrun' : name; - if (device == null) + if (devices == null) return command; // Return 'run/ios'. - return '$command/${getNameForTargetPlatform(await device.targetPlatform)}'; + if (devices.length > 1) + return '$command/all'; + else + return '$command/${getNameForTargetPlatform(await devices[0].targetPlatform)}'; } @override @@ -199,9 +202,11 @@ class RunCommand extends RunCommandBase { @override Future verifyThenRunCommand() async { commandValidator(); - device = await findTargetDevice(); - if (device == null) + devices = await findAllTargetDevices(); + if (devices == null) throwToolExit(null); + if (deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication) + throwToolExit('Using -d all with --use-application-binary is not supported'); return super.verifyThenRunCommand(); } @@ -221,7 +226,6 @@ class RunCommand extends RunCommandBase { @override Future runCommand() async { - Cache.releaseLockEarly(); // Enable hot mode by default if `--no-hot` was not passed and we are in @@ -229,17 +233,20 @@ class RunCommand extends RunCommandBase { final bool hotMode = shouldUseHotMode(); if (argResults['machine']) { + if (devices.length > 1) + throwToolExit('--machine does not support -d all.'); final Daemon daemon = new Daemon(stdinCommandStream, stdoutCommandResponse, notifyingLogger: new NotifyingLogger(), logToStdout: true); AppInstance app; try { app = await daemon.appDomain.startApp( - device, fs.currentDirectory.path, targetFile, route, + devices.first, fs.currentDirectory.path, targetFile, route, _createDebuggingOptions(), hotMode, applicationBinary: argResults['use-application-binary'], projectRootPath: argResults['project-root'], packagesFilePath: argResults['packages'], - projectAssets: argResults['project-assets']); + projectAssets: argResults['project-assets'] + ); } catch (error) { throwToolExit(error.toString()); } @@ -249,12 +256,16 @@ class RunCommand extends RunCommandBase { return null; } - if (await device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) - throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.'); + for (Device device in devices) { + if (await device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) + throwToolExit('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.'); + } if (hotMode) { - if (!device.supportsHotMode) - throwToolExit('Hot mode is not supported by this device. Run with --no-hot.'); + for (Device device in devices) { + if (!device.supportsHotMode) + throwToolExit('Hot mode is not supported by ${device.name}. Run with --no-hot.'); + } } final String pidFile = argResults['pid-file']; @@ -262,11 +273,15 @@ class RunCommand extends RunCommandBase { // Write our pid to the file. fs.file(pidFile).writeAsStringSync(pid.toString()); } - ResidentRunner runner; + final List flutterDevices = devices.map((Device device) { + return new FlutterDevice(device); + }).toList(); + + ResidentRunner runner; if (hotMode) { runner = new HotRunner( - device, + flutterDevices, target: targetFile, debuggingOptions: _createDebuggingOptions(), benchmarkMode: argResults['benchmark'], @@ -279,7 +294,7 @@ class RunCommand extends RunCommandBase { ); } else { runner = new ColdRunner( - device, + flutterDevices, target: targetFile, debuggingOptions: _createDebuggingOptions(), traceStartup: traceStartup, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index b4e0fbcf84..7ba2299e10 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -32,11 +32,26 @@ class DeviceManager { final List _deviceDiscoverers = []; - /// A user-specified device ID. - String specifiedDeviceId; + String _specifiedDeviceId; + /// A user-specified device ID. + String get specifiedDeviceId { + if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') + return null; + return _specifiedDeviceId; + } + + set specifiedDeviceId(String id) { + _specifiedDeviceId = id; + } + + /// True when the user has specified a single specific device. bool get hasSpecifiedDeviceId => specifiedDeviceId != null; + /// True when the user has specified all devices by setting + /// specifiedDeviceId = 'all'. + bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all'; + Stream getDevicesById(String deviceId) async* { final Stream devices = getAllConnectedDevices(); deviceId = deviceId.toLowerCase(); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index ae85d7ebae..e4f07d15d7 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -18,13 +18,320 @@ import 'build_info.dart'; import 'dart/dependencies.dart'; import 'dart/package_map.dart'; import 'dependency_checker.dart'; +import 'devfs.dart'; import 'device.dart'; import 'globals.dart'; +import 'run_cold.dart'; +import 'run_hot.dart'; import 'vmservice.dart'; +class FlutterDevice { + final Device device; + List observatoryUris; + List vmServices; + DevFS devFS; + ApplicationPackage package; + + String _viewFilter; + StreamSubscription _loggingSubscription; + + FlutterDevice(this.device); + + String get viewFilter => _viewFilter; + set viewFilter(String filter) { + _viewFilter = filter; + _viewsCache = null; + } + + void connect() { + if (vmServices != null) + return; + vmServices = new List(observatoryUris.length); + for (int i = 0; i < observatoryUris.length; i++) { + vmServices[i] = VMService.connect(observatoryUris[i]); + printTrace('Connected to service protocol: ${observatoryUris[i]}'); + } + } + + Future refreshViews() async { + if ((vmServices == null) || vmServices.isEmpty) + return; + for (VMService service in vmServices) + await service.vm.refreshViews(); + _viewsCache = null; + } + + List _viewsCache; + List get views { + if (_viewsCache == null) { + if ((vmServices == null) || vmServices.isEmpty) + return null; + final List result = []; + if (_viewFilter == null) { + for (VMService service in vmServices) { + if (!service.isClosed) + result.addAll(service.vm.views.toList()); + } + } else { + for (VMService service in vmServices) { + if (!service.isClosed) + result.addAll(service.vm.allViewsWithName(_viewFilter)); + } + } + _viewsCache = result; + } + return _viewsCache; + } + + Future getVMs() async { + for (VMService service in vmServices) + await service.getVM(); + } + + Future waitForViews() async { + // Refresh the view list, and wait a bit for the list to populate. + for (VMService service in vmServices) + await service.waitForViews(); + } + + Future stopApps() async { + final List flutterViews = views; + if (flutterViews == null || flutterViews.isEmpty) + return; + for (FlutterView view in flutterViews) { + if (view != null && view.uiIsolate != null) + view.uiIsolate.flutterExit(); + } + await new Future.delayed(const Duration(milliseconds: 100)); + } + + Future setupDevFS(String fsName, + Directory rootDirectory, { + String packagesFilePath + }) { + // One devFS per device. Shared by all running instances. + devFS = new DevFS( + vmServices[0], + fsName, + rootDirectory, + packagesFilePath: packagesFilePath + ); + return devFS.create(); + } + + List>> reloadSources( + String entryPath, { + bool pause: false + }) { + final Uri deviceEntryUri = devFS.baseUri.resolveUri(fs.path.toUri(entryPath)); + final Uri devicePackagesUri = devFS.baseUri.resolve('.packages'); + final List>> reports = >>[]; + for (FlutterView view in views) { + final Future> report = view.uiIsolate.reloadSources( + pause: pause, + rootLibUri: deviceEntryUri, + packagesUri: devicePackagesUri + ); + reports.add(report); + } + return reports; + } + + Future debugDumpApp() async { + for (FlutterView view in views) + await view.uiIsolate.flutterDebugDumpApp(); + } + + Future debugDumpRenderTree() async { + for (FlutterView view in views) + await view.uiIsolate.flutterDebugDumpRenderTree(); + } + + Future toggleDebugPaintSizeEnabled() async { + for (FlutterView view in views) + await view.uiIsolate.flutterToggleDebugPaintSizeEnabled(); + } + + Future togglePlatform({String from}) async { + String to; + switch (from) { + case 'iOS': + to = 'android'; + break; + case 'android': + default: + to = 'iOS'; + break; + } + for (FlutterView view in views) { + await view.uiIsolate.flutterPlatformOverride(to); + } + return to; + } + + void startEchoingDeviceLog() { + if (_loggingSubscription != null) + return; + _loggingSubscription = device.getLogReader(app: package).logLines.listen((String line) { + if (!line.contains('Observatory listening on http') && + !line.contains('Diagnostic server listening on http')) + printStatus(line); + }); + } + + Future stopEchoingDeviceLog() async { + if (_loggingSubscription == null) + return; + await _loggingSubscription.cancel(); + _loggingSubscription = null; + } + + void initLogReader() { + device.getLogReader(app: package).appPid = vmServices.first.vm.pid; + } + + Future runHot({ + HotRunner hotRunner, + String route, + bool shouldBuild, + }) async { + final bool prebuiltMode = hotRunner.applicationBinary != null; + final String modeName = getModeName(hotRunner.debuggingOptions.buildMode); + printStatus('Launching ${getDisplayPath(hotRunner.mainPath)} on ${device.name} in $modeName mode...'); + + final TargetPlatform targetPlatform = await device.targetPlatform; + package = getApplicationPackageForPlatform( + targetPlatform, + applicationBinary: hotRunner.applicationBinary + ); + + if (package == null) { + String message = 'No application found for $targetPlatform.'; + final String hint = getMissingPackageHintForPlatform(targetPlatform); + if (hint != null) + message += '\n$hint'; + printError(message); + return 1; + } + + final Map platformArgs = {}; + + startEchoingDeviceLog(); + + // Start the application. + final bool hasDirtyDependencies = hotRunner.hasDirtyDependencies(this); + final Future futureResult = device.startApp( + package, + hotRunner.debuggingOptions.buildMode, + mainPath: hotRunner.mainPath, + debuggingOptions: hotRunner.debuggingOptions, + platformArgs: platformArgs, + route: route, + prebuiltApplication: prebuiltMode, + kernelPath: hotRunner.kernelFilePath, + applicationNeedsRebuild: shouldBuild || hasDirtyDependencies + ); + + final LaunchResult result = await futureResult; + + if (!result.started) { + printError('Error launching application on ${device.name}.'); + await stopEchoingDeviceLog(); + return 2; + } + observatoryUris = [result.observatoryUri]; + return 0; + } + + + Future runCold({ + ColdRunner coldRunner, + String route, + bool shouldBuild: true, + }) async { + final TargetPlatform targetPlatform = await device.targetPlatform; + package = getApplicationPackageForPlatform( + targetPlatform, + applicationBinary: coldRunner.applicationBinary + ); + + final String modeName = getModeName(coldRunner.debuggingOptions.buildMode); + final bool prebuiltMode = coldRunner.applicationBinary != null; + if (coldRunner.mainPath == null) { + assert(prebuiltMode); + printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...'); + } else { + printStatus('Launching ${getDisplayPath(coldRunner.mainPath)} on ${device.name} in $modeName mode...'); + } + + if (package == null) { + String message = 'No application found for $targetPlatform.'; + final String hint = getMissingPackageHintForPlatform(targetPlatform); + if (hint != null) + message += '\n$hint'; + printError(message); + return 1; + } + + Map platformArgs; + if (coldRunner.traceStartup != null) + platformArgs = { 'trace-startup': coldRunner.traceStartup }; + + startEchoingDeviceLog(); + + final bool hasDirtyDependencies = coldRunner.hasDirtyDependencies(this); + final LaunchResult result = await device.startApp( + package, + coldRunner.debuggingOptions.buildMode, + mainPath: coldRunner.mainPath, + debuggingOptions: coldRunner.debuggingOptions, + platformArgs: platformArgs, + route: route, + prebuiltApplication: prebuiltMode, + applicationNeedsRebuild: shouldBuild || hasDirtyDependencies + ); + + if (!result.started) { + printError('Error running application on ${device.name}.'); + await stopEchoingDeviceLog(); + return 2; + } + if (result.hasObservatory) + observatoryUris = [result.observatoryUri]; + return 0; + } + + Future updateDevFS({ + DevFSProgressReporter progressReporter, + AssetBundle bundle, + bool bundleDirty: false, + Set fileFilter + }) async { + final Status devFSStatus = logger.startProgress( + 'Syncing files to device ${device.name}...', + expectSlowOperation: true + ); + int bytes = 0; + try { + bytes = await devFS.update( + progressReporter: progressReporter, + bundle: bundle, + bundleDirty: bundleDirty, + fileFilter: fileFilter + ); + } on DevFSException { + devFSStatus.cancel(); + return false; + } + devFSStatus.stop(); + printTrace('Synced ${getSizeAsMB(bytes)}.'); + return true; + } +} + // Shared code between different resident application runners. abstract class ResidentRunner { - ResidentRunner(this.device, { + ResidentRunner(this.flutterDevices, { this.target, this.debuggingOptions, this.usesTerminalUI: true, @@ -43,7 +350,7 @@ abstract class ResidentRunner { _assetBundle = new AssetBundle(); } - final Device device; + final List flutterDevices; final String target; final DebuggingOptions debuggingOptions; final bool usesTerminalUI; @@ -58,16 +365,12 @@ abstract class ResidentRunner { String get mainPath => _mainPath; AssetBundle _assetBundle; AssetBundle get assetBundle => _assetBundle; - ApplicationPackage package; bool get isRunningDebug => debuggingOptions.buildMode == BuildMode.debug; bool get isRunningProfile => debuggingOptions.buildMode == BuildMode.profile; bool get isRunningRelease => debuggingOptions.buildMode == BuildMode.release; bool get supportsServiceProtocol => isRunningDebug || isRunningProfile; - List vmServices; - StreamSubscription _loggingSubscription; - /// Start the app and keep the process running during its lifetime. Future run({ Completer connectionInfoCompleter, @@ -78,25 +381,6 @@ abstract class ResidentRunner { bool get supportsRestart => false; - String isolateFilter; - - List _currentViewsCache; - List get currentViews { - if (_currentViewsCache == null) { - if ((vmServices == null) || vmServices.isEmpty) - return null; - if (isolateFilter == null) - return vmServices[0].vm.views.toList(); - final List result = []; - for (VMService service in vmServices) - result.addAll(service.vm.allViewsWithName(isolateFilter)); - _currentViewsCache = result; - } - return _currentViewsCache; - } - - FlutterView get currentView => (currentViews != null) ? currentViews[0] : null; - Future restart({ bool fullRestart: false, bool pauseAfterRestart: false }) { throw 'unsupported'; } @@ -115,47 +399,49 @@ abstract class ResidentRunner { } Future refreshViews() async { - if ((vmServices == null) || vmServices.isEmpty) - return; - for (VMService service in vmServices) - await service.vm.refreshViews(); - _currentViewsCache = null; + for (FlutterDevice device in flutterDevices) + await device.refreshViews(); } Future _debugDumpApp() async { await refreshViews(); - await currentView.uiIsolate.flutterDebugDumpApp(); + for (FlutterDevice device in flutterDevices) + await device.debugDumpApp(); } Future _debugDumpRenderTree() async { await refreshViews(); - await currentView.uiIsolate.flutterDebugDumpRenderTree(); + for (FlutterDevice device in flutterDevices) + await device.debugDumpRenderTree(); } Future _debugToggleDebugPaintSizeEnabled() async { await refreshViews(); - await currentView.uiIsolate.flutterToggleDebugPaintSizeEnabled(); + for (FlutterDevice device in flutterDevices) + await device.toggleDebugPaintSizeEnabled(); } - Future _screenshot() async { - final Status status = logger.startProgress('Taking screenshot...'); + Future _screenshot(FlutterDevice device) async { + final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...'); final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png'); try { if (supportsServiceProtocol && isRunningDebug) { - await refreshViews(); + await device.refreshViews(); try { - await currentView.uiIsolate.flutterDebugAllowBanner(false); + for (FlutterView view in device.views) + await view.uiIsolate.flutterDebugAllowBanner(false); } catch (error) { status.stop(); printError(error); } } try { - await device.takeScreenshot(outputFile); + await device.device.takeScreenshot(outputFile); } finally { if (supportsServiceProtocol && isRunningDebug) { try { - await currentView.uiIsolate.flutterDebugAllowBanner(true); + for (FlutterView view in device.views) + await view.uiIsolate.flutterDebugAllowBanner(true); } catch (error) { status.stop(); printError(error); @@ -171,15 +457,13 @@ abstract class ResidentRunner { } } - Future _debugRotatePlatform() async { + Future _debugTogglePlatform() async { await refreshViews(); - switch (await currentView.uiIsolate.flutterPlatformOverride()) { - case 'iOS': - return await currentView.uiIsolate.flutterPlatformOverride('android'); - case 'android': - default: - return await currentView.uiIsolate.flutterPlatformOverride('iOS'); - } + final String from = await flutterDevices[0].views[0].uiIsolate.flutterPlatformOverride(); + String to; + for (FlutterDevice device in flutterDevices) + to = await device.togglePlatform(from: from); + printStatus('Switched operating system to $to'); } void registerSignalHandlers() { @@ -215,53 +499,38 @@ abstract class ResidentRunner { } } - Future startEchoingDeviceLog(ApplicationPackage app) async { - if (_loggingSubscription != null) - return; - _loggingSubscription = device.getLogReader(app: app).logLines.listen((String line) { - if (!line.contains('Observatory listening on http') && - !line.contains('Diagnostic server listening on http')) - printStatus(line); - }); - } - Future stopEchoingDeviceLog() async { - if (_loggingSubscription != null) { - await _loggingSubscription.cancel(); - } - _loggingSubscription = null; + for (FlutterDevice device in flutterDevices) + device.stopEchoingDeviceLog(); } - Future connectToServiceProtocol( - List uris, { - String isolateFilter - }) async { + Future connectToServiceProtocol({String viewFilter}) async { if (!debuggingOptions.debuggingEnabled) { return new Future.error('Error the service protocol is not enabled.'); } - final List services = new List(uris.length); - for (int i = 0; i < uris.length; i++) { - services[i] = VMService.connect(uris[i]); - printTrace('Connected to service protocol: ${uris[i]}'); + + bool viewFound = false; + for (FlutterDevice device in flutterDevices) { + device.viewFilter = viewFilter; + device.connect(); + await device.getVMs(); + await device.waitForViews(); + if (device.views == null) + printStatus('No Flutter views available on ${device.device.name}'); + else + viewFound = true; } - vmServices = services; - for (VMService service in services) - await service.getVM(); - - this.isolateFilter = isolateFilter; - - // Refresh the view list, and wait a bit for the list to populate. - for (VMService service in services) - await service.waitForViews(); - if (currentView == null) + if (!viewFound) throwToolExit('No Flutter view is available'); // Listen for service protocol connection to close. - for (VMService service in services) { - service.done.then( - _serviceProtocolDone, - onError: _serviceProtocolError - ).whenComplete(_serviceDisconnected); + for (FlutterDevice device in flutterDevices) { + for (VMService service in device.vmServices) { + service.done.then( + _serviceProtocolDone, + onError: _serviceProtocolError + ).whenComplete(_serviceDisconnected); + } } } @@ -301,14 +570,14 @@ abstract class ResidentRunner { return true; } } else if (lower == 's') { - if (device.supportsScreenshot) { - await _screenshot(); - return true; + for (FlutterDevice device in flutterDevices) { + if (device.device.supportsScreenshot) + await _screenshot(device); } + return true; } else if (lower == 'o') { if (supportsServiceProtocol && isRunningDebug) { - final String platform = await _debugRotatePlatform(); - print('Switched operating system to: $platform'); + await _debugTogglePlatform(); return true; } } else if (lower == 'q') { @@ -381,12 +650,12 @@ abstract class ResidentRunner { return exitCode; } - bool hasDirtyDependencies() { + bool hasDirtyDependencies(FlutterDevice device) { final DartDependencySetBuilder dartDependencySetBuilder = new DartDependencySetBuilder(mainPath, packagesFilePath); final DependencyChecker dependencyChecker = new DependencyChecker(dartDependencySetBuilder, assetBundle); - final String path = package.packagePath; + final String path = device.package.packagePath; if (path == null) { return true; } @@ -404,16 +673,8 @@ abstract class ResidentRunner { Future preStop() async { } Future stopApp() async { - if (vmServices != null && - vmServices.isNotEmpty && - !vmServices[0].isClosed) { - // TODO(zra): iterate over all the views. - if ((currentView != null) && (currentView.uiIsolate != null)) { - // TODO(johnmccutchan): Wait for the exit command to complete. - currentView.uiIsolate.flutterExit(); - await new Future.delayed(const Duration(milliseconds: 100)); - } - } + for (FlutterDevice device in flutterDevices) + await device.stopApps(); appFinished(); } @@ -429,7 +690,7 @@ abstract class ResidentRunner { printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".'); } } - if (device.supportsScreenshot) + if (flutterDevices.any((FlutterDevice d) => d.device.supportsScreenshot)) printStatus('To save a screenshot to flutter.png, press "s".'); } diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index 0a7b671b33..e8377993b1 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart @@ -6,10 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'application_package.dart'; import 'base/file_system.dart'; -import 'base/utils.dart'; -import 'build_info.dart'; import 'commands/trace.dart'; import 'device.dart'; import 'globals.dart'; @@ -17,25 +14,22 @@ import 'resident_runner.dart'; class ColdRunner extends ResidentRunner { ColdRunner( - Device device, { + List devices, { String target, DebuggingOptions debuggingOptions, bool usesTerminalUI: true, this.traceStartup: false, this.applicationBinary, bool stayResident: true, - }) : super(device, + }) : super(devices, target: target, debuggingOptions: debuggingOptions, usesTerminalUI: usesTerminalUI, stayResident: stayResident); - LaunchResult _result; final bool traceStartup; final String applicationBinary; - bool get prebuiltMode => applicationBinary != null; - @override Future run({ Completer connectionInfoCompleter, @@ -43,6 +37,7 @@ class ColdRunner extends ResidentRunner { String route, bool shouldBuild: true }) async { + final bool prebuiltMode = applicationBinary != null; if (!prebuiltMode) { if (!fs.isFileSync(mainPath)) { String message = 'Tried to run $mainPath, but that file does not exist.'; @@ -53,79 +48,49 @@ class ColdRunner extends ResidentRunner { } } - final String modeName = getModeName(debuggingOptions.buildMode); - if (mainPath == null) { - assert(prebuiltMode); - printStatus('Launching ${package.displayName} on ${device.name} in $modeName mode...'); - } else { - printStatus('Launching ${getDisplayPath(mainPath)} on ${device.name} in $modeName mode...'); + for (FlutterDevice device in flutterDevices) { + final int result = await device.runCold( + coldRunner: this, + route: route, + shouldBuild: shouldBuild, + ); + if (result != 0) + return result; } - final TargetPlatform targetPlatform = await device.targetPlatform; - package = getApplicationPackageForPlatform(targetPlatform, applicationBinary: applicationBinary); - - if (package == null) { - String message = 'No application found for $targetPlatform.'; - final String hint = getMissingPackageHintForPlatform(targetPlatform); - if (hint != null) - message += '\n$hint'; - printError(message); - return 1; - } - - final Stopwatch startTime = new Stopwatch()..start(); - - Map platformArgs; - if (traceStartup != null) - platformArgs = { 'trace-startup': traceStartup }; - - await startEchoingDeviceLog(package); - - _result = await device.startApp( - package, - debuggingOptions.buildMode, - mainPath: mainPath, - debuggingOptions: debuggingOptions, - platformArgs: platformArgs, - route: route, - prebuiltApplication: prebuiltMode, - applicationNeedsRebuild: shouldBuild || hasDirtyDependencies() - ); - - if (!_result.started) { - printError('Error running application on ${device.name}.'); - await stopEchoingDeviceLog(); - return 2; - } - - startTime.stop(); - // Connect to observatory. if (debuggingOptions.debuggingEnabled) - await connectToServiceProtocol([_result.observatoryUri]); + await connectToServiceProtocol(); - if (_result.hasObservatory) { + if (flutterDevices.first.observatoryUris != null) { + // For now, only support one debugger connection. connectionInfoCompleter?.complete(new DebugConnectionInfo( - httpUri: _result.observatoryUri, - wsUri: vmServices[0].wsAddress, + httpUri: flutterDevices.first.observatoryUris.first, + wsUri: flutterDevices.first.vmServices.first.wsAddress, )); } printTrace('Application running.'); - if (vmServices != null && vmServices.isNotEmpty) { - device.getLogReader(app: package).appPid = vmServices[0].vm.pid; - await refreshViews(); - printTrace('Connected to $currentView.'); + for (FlutterDevice device in flutterDevices) { + if (device.vmServices == null) + continue; + device.initLogReader(); + await device.refreshViews(); + printTrace('Connected to ${device.device.name}'); } - if (vmServices != null && vmServices.isNotEmpty && traceStartup) { - printStatus('Downloading startup trace info...'); - try { - await downloadStartupTrace(vmServices[0]); - } catch(error) { - printError(error); - return 2; + if (traceStartup) { + // Only trace startup for the first device. + final FlutterDevice device = flutterDevices.first; + if (device.vmServices != null && device.vmServices.isNotEmpty) { + printStatus('Downloading startup trace info for ${device.device.name}'); + try { + await downloadStartupTrace(device.vmServices.first); + } catch (error) { + printError(error); + return 2; + } } appFinished(); } else if (stayResident) { @@ -158,8 +123,13 @@ class ColdRunner extends ResidentRunner { @override void printHelp({ @required bool details }) { bool haveDetails = false; - if (_result.hasObservatory) - printStatus('The Observatory debugger and profiler is available at: ${_result.observatoryUri}'); + for (FlutterDevice device in flutterDevices) { + final String dname = device.device.name; + if (device.observatoryUris != null) { + for (Uri uri in device.observatoryUris) + printStatus('An Observatory debugger and profiler on $dname is available at $uri'); + } + } if (supportsServiceProtocol) { haveDetails = true; if (details) @@ -174,8 +144,10 @@ class ColdRunner extends ResidentRunner { @override Future preStop() async { - // If we're running in release mode, stop the app using the device logic. - if (vmServices == null || vmServices.isEmpty) - await device.stopApp(package); + for (FlutterDevice device in flutterDevices) { + // If we're running in release mode, stop the app using the device logic. + if (device.vmServices == null || device.vmServices.isEmpty) + await device.device.stopApp(device.package); + } } } diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 3b05d16faa..0d9eae6986 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'application_package.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; @@ -33,7 +32,7 @@ const bool kHotReloadDefault = true; class HotRunner extends ResidentRunner { HotRunner( - Device device, { + List devices, { String target, DebuggingOptions debuggingOptions, bool usesTerminalUI: true, @@ -44,7 +43,7 @@ class HotRunner extends ResidentRunner { String packagesFilePath, String projectAssets, bool stayResident: true, - }) : super(device, + }) : super(devices, target: target, debuggingOptions: debuggingOptions, usesTerminalUI: usesTerminalUI, @@ -54,9 +53,7 @@ class HotRunner extends ResidentRunner { stayResident: stayResident); final String applicationBinary; - bool get prebuiltMode => applicationBinary != null; Set _dartDependencies; - Uri _observatoryUri; final bool benchmarkMode; final Map benchmarkData = {}; @@ -87,30 +84,30 @@ class HotRunner extends ResidentRunner { return true; } - Future attach(List observatoryUris, { + Future attach({ Completer connectionInfoCompleter, Completer appStartedCompleter, - String isolateFilter, + String viewFilter, }) async { - _observatoryUri = observatoryUris[0]; try { - await connectToServiceProtocol( - observatoryUris, isolateFilter: isolateFilter); + await connectToServiceProtocol(viewFilter: viewFilter); } catch (error) { printError('Error connecting to the service protocol: $error'); return 2; } - device.getLogReader(app: package).appPid = vmServices[0].vm.pid; + for (FlutterDevice device in flutterDevices) + device.initLogReader(); try { - final Uri baseUri = await _initDevFS(); + final List baseUris = await _initDevFS(); if (connectionInfoCompleter != null) { + // Only handle one debugger connection. connectionInfoCompleter.complete( new DebugConnectionInfo( - httpUri: _observatoryUri, - wsUri: vmServices[0].wsAddress, - baseUri: baseUri.toString() + httpUri: flutterDevices.first.observatoryUris.first, + wsUri: flutterDevices.first.vmServices.first.wsAddress, + baseUri: baseUris.first.toString() ) ); } @@ -124,8 +121,10 @@ class HotRunner extends ResidentRunner { } await refreshViews(); - for (FlutterView view in currentViews) - printTrace('Connected to $view.'); + for (FlutterDevice device in flutterDevices) { + for (FlutterView view in device.views) + printTrace('Connected to $view.'); + } if (stayResident) { setupTerminal(); @@ -174,55 +173,27 @@ class HotRunner extends ResidentRunner { return 1; } - final String modeName = getModeName(debuggingOptions.buildMode); - printStatus('Launching ${getDisplayPath(mainPath)} on ${device.name} in $modeName mode...'); - - final TargetPlatform targetPlatform = await device.targetPlatform; - package = getApplicationPackageForPlatform(targetPlatform, applicationBinary: applicationBinary); - - if (package == null) { - String message = 'No application found for $targetPlatform.'; - final String hint = getMissingPackageHintForPlatform(targetPlatform); - if (hint != null) - message += '\n$hint'; - printError(message); - return 1; - } - // Determine the Dart dependencies eagerly. if (!_refreshDartDependencies()) { // Some kind of source level error or missing file in the Dart code. return 1; } - final Map platformArgs = {}; - - await startEchoingDeviceLog(package); - - // Start the application. - final Future futureResult = device.startApp( - package, - debuggingOptions.buildMode, - mainPath: mainPath, - debuggingOptions: debuggingOptions, - platformArgs: platformArgs, - route: route, - prebuiltApplication: prebuiltMode, - kernelPath: kernelFilePath, - applicationNeedsRebuild: shouldBuild || hasDirtyDependencies() - ); - - final LaunchResult result = await futureResult; - - if (!result.started) { - printError('Error launching application on ${device.name}.'); - await stopEchoingDeviceLog(); - return 2; + for (FlutterDevice device in flutterDevices) { + final int result = await device.runHot( + hotRunner: this, + route: route, + shouldBuild: shouldBuild, + ); + if (result != 0) { + return result; + } } - return attach([result.observatoryUri], - connectionInfoCompleter: connectionInfoCompleter, - appStartedCompleter: appStartedCompleter); + return attach( + connectionInfoCompleter: connectionInfoCompleter, + appStartedCompleter: appStartedCompleter + ); } @override @@ -238,15 +209,18 @@ class HotRunner extends ResidentRunner { } } - DevFS _devFS; - - Future _initDevFS() { + Future> _initDevFS() async { final String fsName = fs.path.basename(projectRootPath); - _devFS = new DevFS(vmServices[0], - fsName, - fs.directory(projectRootPath), - packagesFilePath: packagesFilePath); - return _devFS.create(); + final List devFSUris = []; + for (FlutterDevice device in flutterDevices) { + final Uri uri = await device.setupDevFS( + fsName, + fs.directory(projectRootPath), + packagesFilePath: packagesFilePath + ); + devFSUris.add(uri); + } + return devFSUris; } Future _updateDevFS({ DevFSProgressReporter progressReporter }) async { @@ -261,67 +235,72 @@ class HotRunner extends ResidentRunner { if (result != 0) return false; } - final Status devFSStatus = logger.startProgress('Syncing files to device...', - expectSlowOperation: true); - int bytes = 0; - try { - bytes = await _devFS.update(progressReporter: progressReporter, - bundle: assetBundle, - bundleDirty: rebuildBundle, - fileFilter: _dartDependencies); - } on DevFSException { - devFSStatus.cancel(); - return false; + + for (FlutterDevice device in flutterDevices) { + final bool result = await device.updateDevFS( + progressReporter: progressReporter, + bundle: assetBundle, + bundleDirty: rebuildBundle, + fileFilter: _dartDependencies, + ); + if (!result) + return false; } - devFSStatus.stop(); + if (!hotRunnerConfig.stableDartDependencies) { // Clear the set after the sync so they are recomputed next time. _dartDependencies = null; } - printTrace('Synced ${getSizeAsMB(bytes)}.'); return true; } Future _evictDirtyAssets() async { - if (_devFS.assetPathsToEvict.isEmpty) - return; - if (currentView.uiIsolate == null) - throw 'Application isolate not found'; - for (String assetPath in _devFS.assetPathsToEvict) { - await currentView.uiIsolate.flutterEvictAsset(assetPath); + for (FlutterDevice device in flutterDevices) { + if (device.devFS.assetPathsToEvict.isEmpty) + return; + if (device.views.first.uiIsolate == null) + throw 'Application isolate not found'; + for (String assetPath in device.devFS.assetPathsToEvict) + await device.views.first.uiIsolate.flutterEvictAsset(assetPath); + device.devFS.assetPathsToEvict.clear(); } - _devFS.assetPathsToEvict.clear(); } Future _cleanupDevFS() async { - if (_devFS != null) { - // Cleanup the devFS; don't wait indefinitely, and ignore any errors. - await _devFS.destroy() - .timeout(const Duration(milliseconds: 250)) - .catchError((dynamic error) { - printTrace('$error'); - }); + for (FlutterDevice device in flutterDevices) { + if (device.devFS != null) { + // Cleanup the devFS; don't wait indefinitely, and ignore any errors. + await device.devFS.destroy() + .timeout(const Duration(milliseconds: 250)) + .catchError((dynamic error) { + printTrace('$error'); + }); + } + device.devFS = null; } - _devFS = null; } - Future _launchInView(Uri entryUri, + Future _launchInView(FlutterDevice device, + Uri entryUri, Uri packagesUri, Uri assetsDirectoryUri) async { - final FlutterView view = currentView; - return view.runFromSource(entryUri, packagesUri, assetsDirectoryUri); + for (FlutterView view in device.views) + await view.runFromSource(entryUri, packagesUri, assetsDirectoryUri); } - Future _launchFromDevFS(ApplicationPackage package, - String mainScript) async { + Future _launchFromDevFS(String mainScript) async { final String entryUri = fs.path.relative(mainScript, from: projectRootPath); - final Uri deviceEntryUri = _devFS.baseUri.resolveUri(fs.path.toUri(entryUri)); - final Uri devicePackagesUri = _devFS.baseUri.resolve('.packages'); - final Uri deviceAssetsDirectoryUri = - _devFS.baseUri.resolveUri(fs.path.toUri(getAssetBuildDirectory())); - await _launchInView(deviceEntryUri, - devicePackagesUri, - deviceAssetsDirectoryUri); + for (FlutterDevice device in flutterDevices) { + final Uri deviceEntryUri = device.devFS.baseUri.resolveUri( + fs.path.toUri(entryUri)); + final Uri devicePackagesUri = device.devFS.baseUri.resolve('.packages'); + final Uri deviceAssetsDirectoryUri = device.devFS.baseUri.resolveUri( + fs.path.toUri(getAssetBuildDirectory())); + await _launchInView(device, + deviceEntryUri, + devicePackagesUri, + deviceAssetsDirectoryUri); + } } Future _restartFromSources() async { @@ -333,18 +312,22 @@ class HotRunner extends ResidentRunner { if (!updatedDevFS) return new OperationResult(1, 'DevFS Synchronization Failed'); // Check if the isolate is paused and resume it. - if (currentView?.uiIsolate != null) { - // Reload the isolate. - await currentView.uiIsolate.reload(); - final ServiceEvent pauseEvent = currentView.uiIsolate.pauseEvent; - if ((pauseEvent != null) && pauseEvent.isPauseEvent) { - // Resume the isolate so that it can be killed by the embedder. - await currentView.uiIsolate.resume(); + for (FlutterDevice device in flutterDevices) { + for (FlutterView view in device.views) { + if (view.uiIsolate != null) { + // Reload the isolate. + await view.uiIsolate.reload(); + final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; + if ((pauseEvent != null) && pauseEvent.isPauseEvent) { + // Resume the isolate so that it can be killed by the embedder. + await view.uiIsolate.resume(); + } + } } } // We are now running from source. _runningFromSnapshot = false; - await _launchFromDevFS(package, mainPath); + await _launchFromDevFS(mainPath); restartTimer.stop(); printTrace('Restart performed in ' '${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); @@ -380,7 +363,10 @@ class HotRunner extends ResidentRunner { @override Future restart({ bool fullRestart: false, bool pauseAfterRestart: false }) async { if (fullRestart) { - final Status status = logger.startProgress('Performing full restart...', progressId: 'hot.restart'); + final Status status = logger.startProgress( + 'Performing full restart...', + progressId: 'hot.restart' + ); try { final Stopwatch timer = new Stopwatch()..start(); await _restartFromSources(); @@ -395,7 +381,10 @@ class HotRunner extends ResidentRunner { } else { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; - final Status status = logger.startProgress('$progressPrefix hot reload...', progressId: 'hot.reload'); + final Status status = logger.startProgress( + '$progressPrefix hot reload...', + progressId: 'hot.reload' + ); try { final Stopwatch timer = new Stopwatch()..start(); final OperationResult result = await _reloadSources(pause: pauseAfterRestart); @@ -414,8 +403,12 @@ class HotRunner extends ResidentRunner { Future _reloadSources({ bool pause: false }) async { printTrace('Refreshing active FlutterViews before reloading.'); await refreshViews(); - if (currentView.uiIsolate == null) - throw 'Application isolate not found'; + for (FlutterDevice device in flutterDevices) { + for (FlutterView view in device.views) { + if (view.uiIsolate == null) + throw 'Application isolate not found'; + } + } // The initial launch is from a script snapshot. When we reload from source // on top of a script snapshot, the first reload will be a worst case reload // because all of the sources will end up being dirty (library paths will @@ -448,22 +441,19 @@ class HotRunner extends ResidentRunner { String reloadMessage; try { final String entryPath = fs.path.relative(mainPath, from: projectRootPath); - final Uri deviceEntryUri = _devFS.baseUri.resolveUri(fs.path.toUri(entryPath)); - final Uri devicePackagesUri = _devFS.baseUri.resolve('.packages'); if (benchmarkMode) vmReloadTimer.start(); - Map reloadReport; - for (FlutterView view in currentViews) { - final Map report = await view.uiIsolate.reloadSources( - pause: pause, - rootLibUri: deviceEntryUri, - packagesUri: devicePackagesUri + final List>> reloadReports = >>[]; + for (FlutterDevice device in flutterDevices) { + final List>> reports = device.reloadSources( + entryPath, + pause: pause ); - // Just take the first one until we think of something smart to do. - if (reloadReport == null) - reloadReport = report; + reloadReports.addAll(reports); } + reloadReport = (await Future.wait(reloadReports)).first; + if (!validateReloadReport(reloadReport)) { // Reload failed. flutterUsage.sendEvent('hot', 'reload-reject'); @@ -476,6 +466,7 @@ class HotRunner extends ResidentRunner { reloadMessage = '$loadedLibraryCount of $finalLibraryCount libraries'; } } catch (error, st) { + printError("Hot reload failed: $error\n$st"); final int errorCode = error['code']; final String errorMessage = error['message']; if (errorCode == Isolate.kIsolateReloadBarred) { @@ -498,20 +489,24 @@ class HotRunner extends ResidentRunner { if (benchmarkMode) reassembleTimer.start(); // Reload the isolate. - for (FlutterView view in currentViews) - await view.uiIsolate.reload(); + for (FlutterDevice device in flutterDevices) { + for (FlutterView view in device.views) + await view.uiIsolate.reload(); + } // We are now running from source. _runningFromSnapshot = false; // Check if the isolate is paused. final List reassembleViews = []; - for (FlutterView view in currentViews) { - final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; - if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) { - // Isolate is paused. Don't reassemble. - continue; + for (FlutterDevice device in flutterDevices) { + for (FlutterView view in device.views) { + final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent; + if ((pauseEvent != null) && (pauseEvent.isPauseEvent)) { + // Isolate is paused. Don't reassemble. + continue; + } + reassembleViews.add(view); } - reassembleViews.add(view); } if (reassembleViews.isEmpty) { printTrace('Skipping reassemble because all isolates are paused.'); @@ -579,7 +574,11 @@ class HotRunner extends ResidentRunner { ansiAlternative: '$red$fire$bold To hot reload your app on the fly, ' 'press "r". To restart the app entirely, press "R".$reset' ); - printStatus('The Observatory debugger and profiler is available at: $_observatoryUri'); + for (FlutterDevice device in flutterDevices) { + final String dname = device.device.name; + for (Uri uri in device.observatoryUris) + printStatus('An Observatory debugger and profiler on $dname is available at: $uri'); + } if (details) { printHelpDetails(); printStatus('To repeat this help message, press "h". To quit, press "q".'); diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 3fd1e0fa33..8e5d101272 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -154,11 +154,11 @@ abstract class FlutterCommand extends Command { /// Subclasses must implement this to execute the command. Future runCommand(); - /// Find and return the target [Device] based upon currently connected + /// Find and return all target [Device]s based upon currently connected /// devices and criteria entered by the user on the command line. - /// If a device cannot be found that meets specified criteria, + /// If no device can be found that meets specified criteria, /// then print an error message and return `null`. - Future findTargetDevice() async { + Future> findAllTargetDevices() async { if (!doctor.canLaunchAnything) { printError("Unable to locate a development device; please run 'flutter doctor' " "for information about installing additional components."); @@ -171,6 +171,9 @@ abstract class FlutterCommand extends Command { printStatus("No devices found with name or id " "matching '${deviceManager.specifiedDeviceId}'"); return null; + } else if (devices.isEmpty && deviceManager.hasSpecifiedAllDevices) { + printStatus("No devices found"); + return null; } else if (devices.isEmpty) { printNoConnectedDevices(); return null; @@ -181,20 +184,39 @@ abstract class FlutterCommand extends Command { if (devices.isEmpty) { printStatus('No supported devices connected.'); return null; - } else if (devices.length > 1) { + } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) { if (deviceManager.hasSpecifiedDeviceId) { printStatus("Found ${devices.length} devices with name or id matching " "'${deviceManager.specifiedDeviceId}':"); } else { printStatus("More than one device connected; please specify a device with " - "the '-d ' flag."); + "the '-d ' flag, or use '-d all' to act on all devices."); devices = await deviceManager.getAllConnectedDevices().toList(); } printStatus(''); await Device.printDevices(devices); return null; } - return devices.single; + return devices; + } + + /// Find and return the target [Device] based upon currently connected + /// devices and criteria entered by the user on the command line. + /// If a device cannot be found that meets specified criteria, + /// then print an error message and return `null`. + Future findTargetDevice() async { + List deviceList = await findAllTargetDevices(); + if (deviceList == null) + return null; + if (deviceList.length > 1) { + printStatus("More than one device connected; please specify a device with " + "the '-d ' flag."); + deviceList = await deviceManager.getAllConnectedDevices().toList(); + printStatus(''); + await Device.printDevices(deviceList); + return null; + } + return deviceList.single; } void printNoConnectedDevices() { diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 42bf7eb554..91fe8f86d0 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -127,12 +127,28 @@ class MockPortScanner extends PortScanner { class MockDeviceManager implements DeviceManager { List devices = []; + String _specifiedDeviceId; + @override - String specifiedDeviceId; + String get specifiedDeviceId { + if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') + return null; + return _specifiedDeviceId; + } + + @override + set specifiedDeviceId(String id) { + _specifiedDeviceId = id; + } @override bool get hasSpecifiedDeviceId => specifiedDeviceId != null; + @override + bool get hasSpecifiedAllDevices { + return _specifiedDeviceId != null && _specifiedDeviceId == 'all'; + } + @override Stream getAllConnectedDevices() => new Stream.fromIterable(devices);