blob: eecd1afc3848066280b87c79f449e2d7e2b4b45c [file]
// 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';
}