// 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'; }