Commit d38b9a02 authored by Devon Carew's avatar Devon Carew

Merge pull request #1634 from devoncarew/watch_sim_devices

Watch sim devices
parents 8999886f 67124dc3
......@@ -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<List<Device>> getDevices(dynamic args) {
List<Device> devices = <Device>[];
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<AdbDevice> newDevices) {
void _handleUpdatedDevices(List<AdbDevice> newDevices) {
List<AndroidDevice> 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<List<SimDevice>> _subscription;
Map<String, IOSSimulator> _devices = new Map<String, IOSSimulator>();
StreamController<Device> addedController = new StreamController<Device>.broadcast();
StreamController<Device> removedController = new StreamController<Device>.broadcast();
List<Device> getDevices() => _devices.values.toList();
Stream<Device> get onAdded => addedController.stream;
Stream<Device> get onRemoved => removedController.stream;
void _handleUpdatedDevices(List<SimDevice> newDevices) {
List<IOSSimulator> 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<String, dynamic> _deviceToMap(Device device) {
return <String, dynamic>{
'id': device.id,
......
......@@ -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<int> 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<bool> installApp(
DeviceStore devices,
ApplicationPackageStore applicationPackages, {
bool boot: false
}) {
if (boot)
devices.iOSSimulator?.boot();
}) async {
if (boot && Platform.isMacOS)
await SimControl.boot();
bool installedSomewhere = false;
......
......@@ -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<Device> _devices = <Device>[];
......@@ -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<String, String> _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<IOSSimulator> 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']);
Match match;
// iPhone 6s Plus (8AC808E1-6BAE-4153-BBC5-77F83814D414) (Booted)
Iterable<Match> 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;
}
String _name;
String get name => _name;
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<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
List<IOSSimulator> devices = [];
try {
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(mockIOS);
if (deviceInfo != null)
devices.add(new IOSSimulator(id: deviceInfo.id, name: deviceInfo.name));
} catch (e) {
}
return devices;
}
Future<bool> boot() async {
if (!Platform.isMacOS)
return false;
if (isConnected())
return true;
if (id == defaultDeviceID) {
runDetached([iOSSimPath]);
Future<bool> 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.';
......
// 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<bool> boot({String deviceId}) async {
if (_isAnyConnected())
return true;
if (deviceId == null) {
runDetached([_simulatorPath]);
Future<bool> 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<SimDevice> 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<String> args = <String>['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 <SimDevice>[];
}
List<SimDevice> devices = <SimDevice>[];
Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
Map<String, dynamic> devicesSection = data['devices'];
for (String deviceCategory in devicesSection.keys) {
List<dynamic> devicesData = devicesSection[deviceCategory];
for (Map<String, String> data in devicesData) {
devices.add(new SimDevice(deviceCategory, data));
}
}
return devices;
}
/// Returns all the connected simulator devices.
static List<SimDevice> getConnectedDevices() {
return getDevices().where((SimDevice device) => device.isBooted).toList();
}
static StreamController<List<SimDevice>> _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<List<SimDevice>> trackDevices() {
if (_trackDevicesControler == null) {
Timer timer;
Set<String> deviceIds = new Set<String>();
_trackDevicesControler = new StreamController.broadcast(
onListen: () {
timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
List<SimDevice> 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<SimDevice> devices, Set<String> deviceIds) {
Set<String> newIds = new Set<String>.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<String> launchArgs]) {
List<String> 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<String, String> 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';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment