From 67124dc3a5dea66084d9c5fefbbb0f09f7439538 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Fri, 5 Feb 2016 02:40:17 -0800 Subject: [PATCH] refactor to parse xcrun simctl list as json add device notifications for the simulator --- .../lib/src/commands/daemon.dart | 68 ++++++- .../lib/src/commands/install.dart | 12 +- .../flutter_tools/lib/src/ios/device_ios.dart | 173 +++--------------- .../flutter_tools/lib/src/ios/simulator.dart | 173 ++++++++++++++++++ 4 files changed, 269 insertions(+), 157 deletions(-) create mode 100644 packages/flutter_tools/lib/src/ios/simulator.dart diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 947fbb94ca1..fef69586710 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -10,6 +10,8 @@ import '../android/adb.dart'; import '../android/device_android.dart'; import '../base/context.dart'; import '../device.dart'; +import '../ios/device_ios.dart'; +import '../ios/simulator.dart'; import '../runner/flutter_command.dart'; import 'start.dart'; import 'stop.dart' as stop; @@ -291,18 +293,32 @@ class DeviceDomain extends Domain { _androidDeviceDiscovery.onChanged.listen((Device device) { sendEvent('device.changed', _deviceToMap(device)); }); + + if (Platform.isMacOS) { + _iosSimulatorDeviceDiscovery = new IOSSimulatorDeviceDiscovery(); + _iosSimulatorDeviceDiscovery.onAdded.listen((Device device) { + sendEvent('device.added', _deviceToMap(device)); + }); + _iosSimulatorDeviceDiscovery.onRemoved.listen((Device device) { + sendEvent('device.removed', _deviceToMap(device)); + }); + } } AndroidDeviceDiscovery _androidDeviceDiscovery; + IOSSimulatorDeviceDiscovery _iosSimulatorDeviceDiscovery; Future> getDevices(dynamic args) { List devices = []; devices.addAll(_androidDeviceDiscovery.getDevices()); + if (_iosSimulatorDeviceDiscovery != null) + devices.addAll(_iosSimulatorDeviceDiscovery.getDevices()); return new Future.value(devices); } void dispose() { _androidDeviceDiscovery.dispose(); + _iosSimulatorDeviceDiscovery?.dispose(); } } @@ -311,7 +327,7 @@ class AndroidDeviceDiscovery { _initAdb(); if (_adb != null) { - _subscription = _adb.trackDevices().listen(_handleNewDevices); + _subscription = _adb.trackDevices().listen(_handleUpdatedDevices); } } @@ -337,7 +353,7 @@ class AndroidDeviceDiscovery { } } - void _handleNewDevices(List newDevices) { + void _handleUpdatedDevices(List newDevices) { List currentDevices = new List.from(getDevices()); for (AdbDevice device in newDevices) { @@ -383,6 +399,54 @@ class AndroidDeviceDiscovery { } } +class IOSSimulatorDeviceDiscovery { + IOSSimulatorDeviceDiscovery() { + _subscription = SimControl.trackDevices().listen(_handleUpdatedDevices); + } + + StreamSubscription> _subscription; + + Map _devices = new Map(); + + StreamController addedController = new StreamController.broadcast(); + StreamController removedController = new StreamController.broadcast(); + + List getDevices() => _devices.values.toList(); + + Stream get onAdded => addedController.stream; + Stream get onRemoved => removedController.stream; + + void _handleUpdatedDevices(List newDevices) { + List currentDevices = new List.from(getDevices()); + + for (SimDevice device in newDevices) { + IOSSimulator androidDevice = _devices[device.udid]; + + if (androidDevice == null) { + // device added + androidDevice = new IOSSimulator(id: device.udid, name: device.name); + _devices[androidDevice.id] = androidDevice; + addedController.add(androidDevice); + } else { + currentDevices.remove(androidDevice); + } + } + + // device removed + for (IOSSimulator device in currentDevices) { + _devices.remove(device.id); + + Device.removeFromCache(device.id); + + removedController.add(device); + } + } + + void dispose() { + _subscription?.cancel(); + } +} + Map _deviceToMap(Device device) { return { 'id': device.id, diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart index 5c3147c5c25..b9f86846f38 100644 --- a/packages/flutter_tools/lib/src/commands/install.dart +++ b/packages/flutter_tools/lib/src/commands/install.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import '../application_package.dart'; import '../device.dart'; +import '../ios/simulator.dart'; import '../runner/flutter_command.dart'; class InstallCommand extends FlutterCommand { @@ -19,7 +21,7 @@ class InstallCommand extends FlutterCommand { @override Future runInProject() async { await downloadApplicationPackagesAndConnectToDevices(); - bool installedAny = installApp( + bool installedAny = await installApp( devices, applicationPackages, boot: argResults['boot'] @@ -28,13 +30,13 @@ class InstallCommand extends FlutterCommand { } } -bool installApp( +Future installApp( DeviceStore devices, ApplicationPackageStore applicationPackages, { bool boot: false -}) { - if (boot) - devices.iOSSimulator?.boot(); +}) async { + if (boot && Platform.isMacOS) + await SimControl.boot(); bool installedSomewhere = false; diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart index b7d8abb94f1..321e02e1388 100644 --- a/packages/flutter_tools/lib/src/ios/device_ios.dart +++ b/packages/flutter_tools/lib/src/ios/device_ios.dart @@ -14,6 +14,11 @@ import '../base/process.dart'; import '../build_configuration.dart'; import '../device.dart'; import '../toolchain.dart'; +import 'simulator.dart'; + +const String _ideviceinstallerInstructions = + 'To work with iOS devices, please install ideviceinstaller.\n' + 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".'; class IOSDeviceDiscovery extends DeviceDiscovery { List _devices = []; @@ -44,11 +49,6 @@ class IOSSimulatorDiscovery extends DeviceDiscovery { class IOSDevice extends Device { static final String defaultDeviceID = 'default_ios_id'; - static const String _macInstructions = - 'To work with iOS devices, please install ideviceinstaller. ' - 'If you use homebrew, you can install it with ' - '"\$ brew install ideviceinstaller".'; - String _installerPath; String get installerPath => _installerPath; @@ -123,7 +123,7 @@ class IOSDevice extends Device { static final Map _commandMap = {}; static String _checkForCommand( String command, [ - String macInstructions = _macInstructions + String macInstructions = _ideviceinstallerInstructions ]) { return _commandMap.putIfAbsent(command, () { try { @@ -263,78 +263,27 @@ class IOSDevice extends Device { } class IOSSimulator extends Device { - static final String defaultDeviceID = 'default_ios_sim_id'; - - static const String _macInstructions = - 'To work with iOS devices, please install ideviceinstaller. ' - 'If you use homebrew, you can install it with ' - '"\$ brew install ideviceinstaller".'; - - static String _xcrunPath = path.join('/usr', 'bin', 'xcrun'); - - String _iOSSimPath; - String get iOSSimPath => _iOSSimPath; - - String get xcrunPath => _xcrunPath; - - String _name; - String get name => _name; - - factory IOSSimulator({String id, String name, String iOSSimulatorPath}) { - IOSSimulator device = Device.unique(id ?? defaultDeviceID, (String id) => new IOSSimulator.fromId(id)); + factory IOSSimulator({String id, String name}) { + IOSSimulator device = Device.unique(id, (String id) => new IOSSimulator.fromId(id)); device._name = name; - if (iOSSimulatorPath == null) { - iOSSimulatorPath = path.join( - '/Applications', 'iOS Simulator.app', 'Contents', 'MacOS', 'iOS Simulator' - ); - } - device._iOSSimPath = iOSSimulatorPath; return device; } + static List getAttachedDevices() { + return SimControl.getConnectedDevices().map((SimDevice device) { + return new IOSSimulator(id: device.udid, name: device.name); + }).toList(); + } + IOSSimulator.fromId(String id) : super.fromId(id); - static _IOSSimulatorInfo _getRunningSimulatorInfo([IOSSimulator mockIOS]) { - String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath; - String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']); + String _name; + String get name => _name; - Match match; - // iPhone 6s Plus (8AC808E1-6BAE-4153-BBC5-77F83814D414) (Booted) - Iterable matches = new RegExp( - r'[\W]*(.*) \(([^\)]+)\) \(Booted\)', - multiLine: true - ).allMatches(output); - if (matches.length > 1) { - // More than one simulator is listed as booted, which is not allowed but - // sometimes happens erroneously. Kill them all because we don't know - // which one is actually running. - printError('Multiple running simulators were detected, ' - 'which is not supposed to happen.'); - for (Match match in matches) { - if (match.groupCount > 0) { - // TODO(devoncarew): We're killing simulator devices inside an accessor - // method; we probably shouldn't be changing state here. - printError('Killing simulator ${match.group(1)}'); - runSync([xcrunPath, 'simctl', 'shutdown', match.group(2)]); - } - } - } else if (matches.length == 1) { - match = matches.first; - } - - if (match != null && match.groupCount > 0) { - return new _IOSSimulatorInfo(match.group(2), match.group(1)); - } else { - printTrace('No running simulators found'); - return null; - } - } + String get xcrunPath => path.join('/usr', 'bin', 'xcrun'); String _getSimulatorPath() { - String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id; - if (deviceID == null) - return null; - return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID); + return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id); } String _getSimulatorAppHomeDirectory(ApplicationPackage app) { @@ -344,59 +293,13 @@ class IOSSimulator extends Device { return path.join(simulatorPath, 'data'); } - static List getAttachedDevices([IOSSimulator mockIOS]) { - List devices = []; - try { - _IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(mockIOS); - if (deviceInfo != null) - devices.add(new IOSSimulator(id: deviceInfo.id, name: deviceInfo.name)); - } catch (e) { - } - return devices; - } - - Future boot() async { - if (!Platform.isMacOS) - return false; - if (isConnected()) - return true; - if (id == defaultDeviceID) { - runDetached([iOSSimPath]); - Future checkConnection([int attempts = 20]) async { - if (attempts == 0) { - printStatus('Timed out waiting for iOS Simulator $id to boot.'); - return false; - } - if (!isConnected()) { - printStatus('Waiting for iOS Simulator $id to boot...'); - return await new Future.delayed(new Duration(milliseconds: 500), - () => checkConnection(attempts - 1)); - } - return true; - } - return await checkConnection(); - } else { - try { - runCheckedSync([xcrunPath, 'simctl', 'boot', id]); - } catch (e) { - printError('Unable to boot iOS Simulator $id: ', e); - return false; - } - } - return false; - } - @override bool installApp(ApplicationPackage app) { if (!isConnected()) return false; try { - if (id == defaultDeviceID) { - runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]); - } else { - runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]); - } + SimControl.install(id, app.localPath); return true; } catch (e) { return false; @@ -407,14 +310,7 @@ class IOSSimulator extends Device { bool isConnected() { if (!Platform.isMacOS) return false; - _IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(); - if (deviceInfo == null) { - return false; - } else if (deviceInfo.id == defaultDeviceID) { - return true; - } else { - return _getRunningSimulatorInfo()?.id == id; - } + return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id); } @override @@ -462,29 +358,13 @@ class IOSSimulator extends Device { } // Step 3: Install the updated bundle to the simulator - int installResult = await runCommandAndStreamOutput([ - xcrunPath, - 'simctl', - 'install', - id == defaultDeviceID ? 'booted' : id, - path.absolute(bundle.path) - ]); - - if (installResult != 0) { - printError('Could not install the application bundle on the simulator'); - return false; - } + SimControl.install(id, path.absolute(bundle.path)); // Step 4: Launch the updated application in the simulator - runCheckedSync([ - xcrunPath, - 'simctl', - 'launch', - id == defaultDeviceID ? 'booted' : id, - app.id - ]); + SimControl.launch(id, app.id); printTrace('Successfully started ${app.name} on $id'); + return true; } @@ -623,13 +503,6 @@ class _IOSSimulatorLogReader extends DeviceLogReader { } } -class _IOSSimulatorInfo { - final String id; - final String name; - - _IOSSimulatorInfo(this.id, this.name); -} - final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.'; diff --git a/packages/flutter_tools/lib/src/ios/simulator.dart b/packages/flutter_tools/lib/src/ios/simulator.dart new file mode 100644 index 00000000000..eecd1afc384 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/simulator.dart @@ -0,0 +1,173 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert' show JSON; +import 'dart:io'; + +import '../base/context.dart'; +import '../base/process.dart'; + +const String _xcrunPath = '/usr/bin/xcrun'; + +const String _simulatorPath = + '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator'; + +/// A wrapper around the `simctl` command line tool. +class SimControl { + static Future boot({String deviceId}) async { + if (_isAnyConnected()) + return true; + + if (deviceId == null) { + runDetached([_simulatorPath]); + Future checkConnection([int attempts = 20]) async { + if (attempts == 0) { + printStatus('Timed out waiting for iOS Simulator to boot.'); + return false; + } + if (!_isAnyConnected()) { + printStatus('Waiting for iOS Simulator to boot...'); + return await new Future.delayed(new Duration(milliseconds: 500), + () => checkConnection(attempts - 1) + ); + } + return true; + } + return await checkConnection(); + } else { + try { + runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]); + return true; + } catch (e) { + printError('Unable to boot iOS Simulator $deviceId: ', e); + return false; + } + } + + return false; + } + + /// Returns a list of all available devices, both potential and connected. + static List getDevices() { + // { + // "devices" : { + // "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [ + // { + // "state" : "Shutdown", + // "availability" : " (unavailable, runtime profile not found)", + // "name" : "iPhone 4s", + // "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B" + // }, + // ... + + List args = ['simctl', 'list', '--json', 'devices']; + printTrace('$_xcrunPath ${args.join(' ')}'); + ProcessResult results = Process.runSync(_xcrunPath, args); + if (results.exitCode != 0) { + printError('Error executing simctl: ${results.exitCode}\n${results.stderr}'); + return []; + } + + List devices = []; + + Map> data = JSON.decode(results.stdout); + Map devicesSection = data['devices']; + + for (String deviceCategory in devicesSection.keys) { + List devicesData = devicesSection[deviceCategory]; + + for (Map data in devicesData) { + devices.add(new SimDevice(deviceCategory, data)); + } + } + + return devices; + } + + /// Returns all the connected simulator devices. + static List getConnectedDevices() { + return getDevices().where((SimDevice device) => device.isBooted).toList(); + } + + static StreamController> _trackDevicesControler; + + /// Listens to changes in the set of connected devices. The implementation + /// currently uses polling. Callers should be careful to call cancel() on any + /// stream subscription when finished. + /// + /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly. + static Stream> trackDevices() { + if (_trackDevicesControler == null) { + Timer timer; + Set deviceIds = new Set(); + + _trackDevicesControler = new StreamController.broadcast( + onListen: () { + timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) { + List devices = getConnectedDevices(); + + if (_updateDeviceIds(devices, deviceIds)) { + _trackDevicesControler.add(devices); + } + }); + }, onCancel: () { + timer?.cancel(); + deviceIds.clear(); + } + ); + } + + return _trackDevicesControler.stream; + } + + /// Update the cached set of device IDs and return whether there were any changes. + static bool _updateDeviceIds(List devices, Set deviceIds) { + Set newIds = new Set.from(devices.map((SimDevice device) => device.udid)); + + bool changed = false; + + for (String id in newIds) { + if (!deviceIds.contains(id)) + changed = true; + } + + for (String id in deviceIds) { + if (!newIds.contains(id)) + changed = true; + } + + deviceIds.clear(); + deviceIds.addAll(newIds); + + return changed; + } + + static bool _isAnyConnected() => getConnectedDevices().isNotEmpty; + + static void install(String deviceId, String appPath) { + runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]); + } + + static void launch(String deviceId, String appIdentifier, [List launchArgs]) { + List args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier]; + if (launchArgs != null) + args.addAll(launchArgs); + runCheckedSync(args); + } +} + +class SimDevice { + SimDevice(this.category, this.data); + + final String category; + final Map data; + + String get state => data['state']; + String get availability => data['availability']; + String get name => data['name']; + String get udid => data['udid']; + + bool get isBooted => state == 'Booted'; +}