[flutter_tools] Adds some support for '-d all' (#9585)

This commit is contained in:
Zachary Anderson 2017-04-26 21:49:38 -07:00 committed by GitHub
parent 9558ac7d55
commit 0770c3c14f
9 changed files with 652 additions and 346 deletions

View File

@ -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>[flutterDevice],
target: target,
debuggingOptions: options,
usesTerminalUI: false,
@ -368,7 +370,7 @@ class AppDomain extends Domain {
);
} else {
runner = new ColdRunner(
device,
<FlutterDevice>[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<String, dynamic> result = await isolate.invokeFlutterExtensionRpcRaw(methodName, params: params);
if (result == null)
return new OperationResult(1, 'method not available: $methodName');

View File

@ -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<String> fullAddresses = targetPorts.map(
(int p) => '$_address:$p'
).toList();
final List<Uri> 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>[flutterDevice],
debuggingOptions: new DebuggingOptions.enabled(getBuildMode()),
target: _target,
projectRootPath: _fuchsiaProjectPath,
packagesFilePath: _dotPackagesPath
);
final List<Uri> 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<List<int>> _filterPorts(List<int> ports, String isolateFilter) async {
Future<List<int>> _filterPorts(List<int> ports, String viewFilter) async {
final List<int> result = <int>[];
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;

View File

@ -150,17 +150,20 @@ class RunCommand extends RunCommandBase {
};
}
Device device;
List<Device> devices;
@override
Future<String> 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<Null> 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<Null> 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<FlutterDevice> 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,

View File

@ -32,11 +32,26 @@ class DeviceManager {
final List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
/// 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<Device> getDevicesById(String deviceId) async* {
final Stream<Device> devices = getAllConnectedDevices();
deviceId = deviceId.toLowerCase();

View File

@ -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<Uri> observatoryUris;
List<VMService> vmServices;
DevFS devFS;
ApplicationPackage package;
String _viewFilter;
StreamSubscription<String> _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<VMService>(observatoryUris.length);
for (int i = 0; i < observatoryUris.length; i++) {
vmServices[i] = VMService.connect(observatoryUris[i]);
printTrace('Connected to service protocol: ${observatoryUris[i]}');
}
}
Future<Null> refreshViews() async {
if ((vmServices == null) || vmServices.isEmpty)
return;
for (VMService service in vmServices)
await service.vm.refreshViews();
_viewsCache = null;
}
List<FlutterView> _viewsCache;
List<FlutterView> get views {
if (_viewsCache == null) {
if ((vmServices == null) || vmServices.isEmpty)
return null;
final List<FlutterView> result = <FlutterView>[];
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<Null> getVMs() async {
for (VMService service in vmServices)
await service.getVM();
}
Future<Null> waitForViews() async {
// Refresh the view list, and wait a bit for the list to populate.
for (VMService service in vmServices)
await service.waitForViews();
}
Future<Null> stopApps() async {
final List<FlutterView> 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<Null>.delayed(const Duration(milliseconds: 100));
}
Future<Uri> 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<Future<Map<String, dynamic>>> 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<Future<Map<String, dynamic>>> reports = <Future<Map<String, dynamic>>>[];
for (FlutterView view in views) {
final Future<Map<String, dynamic>> report = view.uiIsolate.reloadSources(
pause: pause,
rootLibUri: deviceEntryUri,
packagesUri: devicePackagesUri
);
reports.add(report);
}
return reports;
}
Future<Null> debugDumpApp() async {
for (FlutterView view in views)
await view.uiIsolate.flutterDebugDumpApp();
}
Future<Null> debugDumpRenderTree() async {
for (FlutterView view in views)
await view.uiIsolate.flutterDebugDumpRenderTree();
}
Future<Null> toggleDebugPaintSizeEnabled() async {
for (FlutterView view in views)
await view.uiIsolate.flutterToggleDebugPaintSizeEnabled();
}
Future<String> 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<Null> stopEchoingDeviceLog() async {
if (_loggingSubscription == null)
return;
await _loggingSubscription.cancel();
_loggingSubscription = null;
}
void initLogReader() {
device.getLogReader(app: package).appPid = vmServices.first.vm.pid;
}
Future<int> 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<String, dynamic> platformArgs = <String, dynamic>{};
startEchoingDeviceLog();
// Start the application.
final bool hasDirtyDependencies = hotRunner.hasDirtyDependencies(this);
final Future<LaunchResult> 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 = <Uri>[result.observatoryUri];
return 0;
}
Future<int> 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<String, dynamic> platformArgs;
if (coldRunner.traceStartup != null)
platformArgs = <String, dynamic>{ '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 = <Uri>[result.observatoryUri];
return 0;
}
Future<bool> updateDevFS({
DevFSProgressReporter progressReporter,
AssetBundle bundle,
bool bundleDirty: false,
Set<String> 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<FlutterDevice> 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<VMService> vmServices;
StreamSubscription<String> _loggingSubscription;
/// Start the app and keep the process running during its lifetime.
Future<int> run({
Completer<DebugConnectionInfo> connectionInfoCompleter,
@ -78,25 +381,6 @@ abstract class ResidentRunner {
bool get supportsRestart => false;
String isolateFilter;
List<FlutterView> _currentViewsCache;
List<FlutterView> get currentViews {
if (_currentViewsCache == null) {
if ((vmServices == null) || vmServices.isEmpty)
return null;
if (isolateFilter == null)
return vmServices[0].vm.views.toList();
final List<FlutterView> result = <FlutterView>[];
for (VMService service in vmServices)
result.addAll(service.vm.allViewsWithName(isolateFilter));
_currentViewsCache = result;
}
return _currentViewsCache;
}
FlutterView get currentView => (currentViews != null) ? currentViews[0] : null;
Future<OperationResult> restart({ bool fullRestart: false, bool pauseAfterRestart: false }) {
throw 'unsupported';
}
@ -115,47 +399,49 @@ abstract class ResidentRunner {
}
Future<Null> 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<Null> _debugDumpApp() async {
await refreshViews();
await currentView.uiIsolate.flutterDebugDumpApp();
for (FlutterDevice device in flutterDevices)
await device.debugDumpApp();
}
Future<Null> _debugDumpRenderTree() async {
await refreshViews();
await currentView.uiIsolate.flutterDebugDumpRenderTree();
for (FlutterDevice device in flutterDevices)
await device.debugDumpRenderTree();
}
Future<Null> _debugToggleDebugPaintSizeEnabled() async {
await refreshViews();
await currentView.uiIsolate.flutterToggleDebugPaintSizeEnabled();
for (FlutterDevice device in flutterDevices)
await device.toggleDebugPaintSizeEnabled();
}
Future<Null> _screenshot() async {
final Status status = logger.startProgress('Taking screenshot...');
Future<Null> _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<String> _debugRotatePlatform() async {
Future<Null> _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<Null> 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<Null> stopEchoingDeviceLog() async {
if (_loggingSubscription != null) {
await _loggingSubscription.cancel();
}
_loggingSubscription = null;
for (FlutterDevice device in flutterDevices)
device.stopEchoingDeviceLog();
}
Future<Null> connectToServiceProtocol(
List<Uri> uris, {
String isolateFilter
}) async {
Future<Null> connectToServiceProtocol({String viewFilter}) async {
if (!debuggingOptions.debuggingEnabled) {
return new Future<Null>.error('Error the service protocol is not enabled.');
}
final List<VMService> services = new List<VMService>(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<Null>(
_serviceProtocolDone,
onError: _serviceProtocolError
).whenComplete(_serviceDisconnected);
for (FlutterDevice device in flutterDevices) {
for (VMService service in device.vmServices) {
service.done.then<Null>(
_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<Null> preStop() async { }
Future<Null> 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<Null>.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".');
}

View File

@ -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<FlutterDevice> 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<int> run({
Completer<DebugConnectionInfo> 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<String, dynamic> platformArgs;
if (traceStartup != null)
platformArgs = <String, dynamic>{ '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(<Uri>[_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<Null> 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);
}
}
}

View File

@ -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<FlutterDevice> 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<String> _dartDependencies;
Uri _observatoryUri;
final bool benchmarkMode;
final Map<String, int> benchmarkData = <String, int>{};
@ -87,30 +84,30 @@ class HotRunner extends ResidentRunner {
return true;
}
Future<int> attach(List<Uri> observatoryUris, {
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<Null> 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<Uri> 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<String, dynamic> platformArgs = <String, dynamic>{};
await startEchoingDeviceLog(package);
// Start the application.
final Future<LaunchResult> 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(<Uri>[result.observatoryUri],
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter);
return attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter
);
}
@override
@ -238,15 +209,18 @@ class HotRunner extends ResidentRunner {
}
}
DevFS _devFS;
Future<Uri> _initDevFS() {
Future<List<Uri>> _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<Uri> devFSUris = <Uri>[];
for (FlutterDevice device in flutterDevices) {
final Uri uri = await device.setupDevFS(
fsName,
fs.directory(projectRootPath),
packagesFilePath: packagesFilePath
);
devFSUris.add(uri);
}
return devFSUris;
}
Future<bool> _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<Null> _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<Null> _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<Null> _launchInView(Uri entryUri,
Future<Null> _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<Null> _launchFromDevFS(ApplicationPackage package,
String mainScript) async {
Future<Null> _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<OperationResult> _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<OperationResult> 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<OperationResult> _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<String, dynamic> reloadReport;
for (FlutterView view in currentViews) {
final Map<String, dynamic> report = await view.uiIsolate.reloadSources(
pause: pause,
rootLibUri: deviceEntryUri,
packagesUri: devicePackagesUri
final List<Future<Map<String, dynamic>>> reloadReports = <Future<Map<String, dynamic>>>[];
for (FlutterDevice device in flutterDevices) {
final List<Future<Map<String, dynamic>>> 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<FlutterView> reassembleViews = <FlutterView>[];
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".');

View File

@ -154,11 +154,11 @@ abstract class FlutterCommand extends Command<Null> {
/// Subclasses must implement this to execute the command.
Future<Null> 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<Device> findTargetDevice() async {
Future<List<Device>> 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<Null> {
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<Null> {
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 <deviceId>' flag.");
"the '-d <deviceId>' 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<Device> findTargetDevice() async {
List<Device> 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 <deviceId>' flag.");
deviceList = await deviceManager.getAllConnectedDevices().toList();
printStatus('');
await Device.printDevices(deviceList);
return null;
}
return deviceList.single;
}
void printNoConnectedDevices() {

View File

@ -127,12 +127,28 @@ class MockPortScanner extends PortScanner {
class MockDeviceManager implements DeviceManager {
List<Device> devices = <Device>[];
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<Device> getAllConnectedDevices() => new Stream<Device>.fromIterable(devices);