mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge pull request #2066 from devoncarew/device_polling_redux
add the ability to start and stop device polling
This commit is contained in:
commit
4ec9608472
@ -74,7 +74,7 @@ class Adb {
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Listen to device activations and deactivations via the asb server's
|
||||
/// Listen to device activations and deactivations via the adb server's
|
||||
/// 'track-devices' command. Call cancel on the returned stream to stop
|
||||
/// listening.
|
||||
Stream<List<AdbDevice>> trackDevices() {
|
||||
|
||||
@ -29,17 +29,11 @@ const String _deviceBundlePath = '/data/local/tmp/dev.flx';
|
||||
// Path where the snapshot will be copied on the device.
|
||||
const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';
|
||||
|
||||
class AndroidDeviceDiscovery extends DeviceDiscovery {
|
||||
List<Device> _devices = <Device>[];
|
||||
class AndroidDevices extends PollingDeviceDiscovery {
|
||||
AndroidDevices() : super('AndroidDevices');
|
||||
|
||||
bool get supportsPlatform => true;
|
||||
|
||||
Future init() {
|
||||
_devices = getAdbDevices();
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
List<Device> get devices => _devices;
|
||||
List<Device> pollingGetDevices() => getAdbDevices();
|
||||
}
|
||||
|
||||
class AndroidDevice extends Device {
|
||||
48
packages/flutter_tools/lib/src/base/utils.dart
Normal file
48
packages/flutter_tools/lib/src/base/utils.dart
Normal file
@ -0,0 +1,48 @@
|
||||
// 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';
|
||||
|
||||
/// A class to maintain a list of items, fire events when items are added or
|
||||
/// removed, and calculate a diff of changes when a new list of items is
|
||||
/// available.
|
||||
class ItemListNotifier<T> {
|
||||
ItemListNotifier() {
|
||||
_items = new Set<T>();
|
||||
}
|
||||
|
||||
ItemListNotifier.from(List<T> items) {
|
||||
_items = new Set<T>.from(items);
|
||||
}
|
||||
|
||||
Set<T> _items;
|
||||
|
||||
StreamController<T> _addedController = new StreamController<T>.broadcast();
|
||||
StreamController<T> _removedController = new StreamController<T>.broadcast();
|
||||
|
||||
Stream<T> get onAdded => _addedController.stream;
|
||||
Stream<T> get onRemoved => _removedController.stream;
|
||||
|
||||
List<T> get items => _items.toList();
|
||||
|
||||
void updateWithNewList(List<T> updatedList) {
|
||||
Set<T> updatedSet = new Set<T>.from(updatedList);
|
||||
|
||||
Set<T> addedItems = updatedSet.difference(_items);
|
||||
Set<T> removedItems = _items.difference(updatedSet);
|
||||
|
||||
_items = updatedSet;
|
||||
|
||||
for (T item in addedItems)
|
||||
_addedController.add(item);
|
||||
for (T item in removedItems)
|
||||
_removedController.add(item);
|
||||
}
|
||||
|
||||
/// Close the streams.
|
||||
void dispose() {
|
||||
_addedController.close();
|
||||
_removedController.close();
|
||||
}
|
||||
}
|
||||
@ -6,15 +6,13 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import '../android/adb.dart';
|
||||
import '../android/android_sdk.dart';
|
||||
import '../android/device_android.dart';
|
||||
import '../android/android_device.dart';
|
||||
import '../base/context.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart';
|
||||
import '../ios/device_ios.dart';
|
||||
import '../ios/simulator.dart';
|
||||
import '../ios/devices.dart';
|
||||
import '../ios/simulators.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
import 'run.dart';
|
||||
import 'stop.dart' as stop;
|
||||
@ -126,10 +124,8 @@ class Daemon {
|
||||
throw 'no domain for method: $method';
|
||||
|
||||
_domainMap[prefix].handleCommand(name, id, request['params']);
|
||||
} catch (error, trace) {
|
||||
} catch (error) {
|
||||
_send({'id': id, 'error': _toJsonable(error)});
|
||||
stderr.writeln('error handling $request: $error');
|
||||
stderr.writeln(trace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,8 +166,6 @@ abstract class Domain {
|
||||
}
|
||||
}).catchError((error, trace) {
|
||||
_send({'id': id, 'error': _toJsonable(error)});
|
||||
stderr.writeln("error handling '$name.$command': $error");
|
||||
stderr.writeln(trace);
|
||||
});
|
||||
}
|
||||
|
||||
@ -286,166 +280,65 @@ class AppDomain extends Domain {
|
||||
|
||||
/// This domain lets callers list and monitor connected devices.
|
||||
///
|
||||
/// It exports a `getDevices()` call, as well as firing `device.added`,
|
||||
/// `device.removed`, and `device.changed` events.
|
||||
/// It exports a `getDevices()` call, as well as firing `device.added` and
|
||||
/// `device.removed` events.
|
||||
class DeviceDomain extends Domain {
|
||||
DeviceDomain(Daemon daemon) : super(daemon, 'device') {
|
||||
registerHandler('getDevices', getDevices);
|
||||
registerHandler('enable', enable);
|
||||
registerHandler('disable', disable);
|
||||
|
||||
_androidDeviceDiscovery = new AndroidDeviceDiscovery();
|
||||
_androidDeviceDiscovery.onAdded.listen((Device device) {
|
||||
sendEvent('device.added', _deviceToMap(device));
|
||||
});
|
||||
_androidDeviceDiscovery.onRemoved.listen((Device device) {
|
||||
sendEvent('device.removed', _deviceToMap(device));
|
||||
});
|
||||
_androidDeviceDiscovery.onChanged.listen((Device device) {
|
||||
sendEvent('device.changed', _deviceToMap(device));
|
||||
});
|
||||
PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
|
||||
if (deviceDiscovery.supportsPlatform)
|
||||
_discoverers.add(deviceDiscovery);
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
_iosSimulatorDeviceDiscovery = new IOSSimulatorDeviceDiscovery();
|
||||
_iosSimulatorDeviceDiscovery.onAdded.listen((Device device) {
|
||||
deviceDiscovery = new IOSDevices();
|
||||
if (deviceDiscovery.supportsPlatform)
|
||||
_discoverers.add(deviceDiscovery);
|
||||
|
||||
deviceDiscovery = new IOSSimulators();
|
||||
if (deviceDiscovery.supportsPlatform)
|
||||
_discoverers.add(deviceDiscovery);
|
||||
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
discoverer.onAdded.listen((Device device) {
|
||||
sendEvent('device.added', _deviceToMap(device));
|
||||
});
|
||||
_iosSimulatorDeviceDiscovery.onRemoved.listen((Device device) {
|
||||
discoverer.onRemoved.listen((Device device) {
|
||||
sendEvent('device.removed', _deviceToMap(device));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AndroidDeviceDiscovery _androidDeviceDiscovery;
|
||||
IOSSimulatorDeviceDiscovery _iosSimulatorDeviceDiscovery;
|
||||
List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
|
||||
|
||||
Future<List<Device>> getDevices(dynamic args) {
|
||||
List<Device> devices = <Device>[];
|
||||
devices.addAll(_androidDeviceDiscovery.getDevices());
|
||||
if (_iosSimulatorDeviceDiscovery != null)
|
||||
devices.addAll(_iosSimulatorDeviceDiscovery.getDevices());
|
||||
List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
|
||||
return discoverer.devices;
|
||||
}).toList();
|
||||
return new Future.value(devices);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_androidDeviceDiscovery.dispose();
|
||||
_iosSimulatorDeviceDiscovery?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidDeviceDiscovery {
|
||||
AndroidDeviceDiscovery() {
|
||||
_initAdb();
|
||||
|
||||
if (_adb != null) {
|
||||
_subscription = _adb.trackDevices().listen(_handleUpdatedDevices);
|
||||
/// Enable device events.
|
||||
Future enable(dynamic args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
discoverer.startPolling();
|
||||
}
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
Adb _adb;
|
||||
StreamSubscription _subscription;
|
||||
Map<String, AndroidDevice> _devices = new Map<String, AndroidDevice>();
|
||||
|
||||
StreamController<Device> addedController = new StreamController<Device>.broadcast();
|
||||
StreamController<Device> removedController = new StreamController<Device>.broadcast();
|
||||
StreamController<Device> changedController = new StreamController<Device>.broadcast();
|
||||
|
||||
List<Device> getDevices() => _devices.values.toList();
|
||||
|
||||
Stream<Device> get onAdded => addedController.stream;
|
||||
Stream<Device> get onRemoved => removedController.stream;
|
||||
Stream<Device> get onChanged => changedController.stream;
|
||||
|
||||
void _initAdb() {
|
||||
if (_adb == null) {
|
||||
_adb = new Adb(getAdbPath(androidSdk));
|
||||
if (!_adb.exists())
|
||||
_adb = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleUpdatedDevices(List<AdbDevice> newDevices) {
|
||||
List<AndroidDevice> currentDevices = new List.from(getDevices());
|
||||
|
||||
for (AdbDevice device in newDevices) {
|
||||
AndroidDevice androidDevice = _devices[device.id];
|
||||
|
||||
if (androidDevice == null) {
|
||||
// device added
|
||||
androidDevice = new AndroidDevice(
|
||||
device.id,
|
||||
productID: device.productID,
|
||||
modelID: device.modelID,
|
||||
deviceCodeName: device.deviceCodeName,
|
||||
connected: device.isAvailable
|
||||
);
|
||||
_devices[androidDevice.id] = androidDevice;
|
||||
addedController.add(androidDevice);
|
||||
} else {
|
||||
currentDevices.remove(androidDevice);
|
||||
|
||||
// check state
|
||||
if (androidDevice.isConnected() != device.isAvailable) {
|
||||
androidDevice.setConnected(device.isAvailable);
|
||||
changedController.add(androidDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// device removed
|
||||
for (AndroidDevice device in currentDevices) {
|
||||
_devices.remove(device.id);
|
||||
|
||||
removedController.add(device);
|
||||
/// Disable device events.
|
||||
Future disable(dynamic args) {
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
discoverer.stopPolling();
|
||||
}
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
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(device.udid, name: device.name);
|
||||
_devices[androidDevice.id] = androidDevice;
|
||||
addedController.add(androidDevice);
|
||||
} else {
|
||||
currentDevices.remove(androidDevice);
|
||||
}
|
||||
for (PollingDeviceDiscovery discoverer in _discoverers) {
|
||||
discoverer.dispose();
|
||||
}
|
||||
|
||||
// device removed
|
||||
for (IOSSimulator device in currentDevices) {
|
||||
_devices.remove(device.id);
|
||||
|
||||
removedController.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@ -490,7 +383,7 @@ class NotifyingLogger extends Logger {
|
||||
}
|
||||
|
||||
void printTrace(String message) {
|
||||
_messageController.add(new LogMessage('trace', message));
|
||||
// This is a lot of traffic to send over the wire.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import 'dart:io';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../device.dart';
|
||||
import '../ios/simulator.dart';
|
||||
import '../ios/simulators.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
class InstallCommand extends FlutterCommand {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../android/device_android.dart';
|
||||
import '../android/android_device.dart';
|
||||
import '../application_package.dart';
|
||||
import '../globals.dart';
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
@ -4,12 +4,14 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'android/device_android.dart';
|
||||
import 'android/android_device.dart';
|
||||
import 'application_package.dart';
|
||||
import 'base/common.dart';
|
||||
import 'base/utils.dart';
|
||||
import 'build_configuration.dart';
|
||||
import 'globals.dart';
|
||||
import 'ios/device_ios.dart';
|
||||
import 'ios/devices.dart';
|
||||
import 'ios/simulators.dart';
|
||||
import 'toolchain.dart';
|
||||
|
||||
/// A class to get all available devices.
|
||||
@ -18,27 +20,9 @@ class DeviceManager {
|
||||
/// of their methods are invoked.
|
||||
DeviceManager() {
|
||||
// Register the known discoverers.
|
||||
_deviceDiscoverers.add(new AndroidDeviceDiscovery());
|
||||
_deviceDiscoverers.add(new IOSDeviceDiscovery());
|
||||
_deviceDiscoverers.add(new IOSSimulatorDiscovery());
|
||||
}
|
||||
|
||||
Future _init() {
|
||||
if (_initedCompleter == null) {
|
||||
_initedCompleter = new Completer();
|
||||
|
||||
Future.forEach(_deviceDiscoverers, (DeviceDiscovery discoverer) {
|
||||
if (!discoverer.supportsPlatform)
|
||||
return null;
|
||||
return discoverer.init();
|
||||
}).then((_) {
|
||||
_initedCompleter.complete();
|
||||
}).catchError((error, stackTrace) {
|
||||
_initedCompleter.completeError(error, stackTrace);
|
||||
});
|
||||
}
|
||||
|
||||
return _initedCompleter.future;
|
||||
_deviceDiscoverers.add(new AndroidDevices());
|
||||
_deviceDiscoverers.add(new IOSDevices());
|
||||
_deviceDiscoverers.add(new IOSSimulators());
|
||||
}
|
||||
|
||||
List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
|
||||
@ -46,8 +30,6 @@ class DeviceManager {
|
||||
/// A user-specified device ID.
|
||||
String specifiedDeviceId;
|
||||
|
||||
Completer _initedCompleter;
|
||||
|
||||
bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
|
||||
|
||||
/// Return the device with the matching ID; else, complete the Future with
|
||||
@ -75,8 +57,6 @@ class DeviceManager {
|
||||
|
||||
/// Return the list of all connected devices.
|
||||
Future<List<Device>> getAllConnectedDevices() async {
|
||||
await _init();
|
||||
|
||||
return _deviceDiscoverers
|
||||
.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform)
|
||||
.expand((DeviceDiscovery discoverer) => discoverer.devices)
|
||||
@ -87,10 +67,60 @@ class DeviceManager {
|
||||
/// An abstract class to discover and enumerate a specific type of devices.
|
||||
abstract class DeviceDiscovery {
|
||||
bool get supportsPlatform;
|
||||
Future init();
|
||||
List<Device> get devices;
|
||||
}
|
||||
|
||||
/// A [DeviceDiscovery] implementation that uses polling to discover device adds
|
||||
/// and removals.
|
||||
abstract class PollingDeviceDiscovery extends DeviceDiscovery {
|
||||
PollingDeviceDiscovery(this.name);
|
||||
|
||||
static const Duration _pollingDuration = const Duration(seconds: 4);
|
||||
|
||||
final String name;
|
||||
ItemListNotifier<Device> _items;
|
||||
Timer _timer;
|
||||
|
||||
List<Device> pollingGetDevices();
|
||||
|
||||
void startPolling() {
|
||||
if (_timer == null) {
|
||||
if (_items == null)
|
||||
_items = new ItemListNotifier<Device>();
|
||||
_timer = new Timer.periodic(_pollingDuration, (Timer timer) {
|
||||
_items.updateWithNewList(pollingGetDevices());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void stopPolling() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
List<Device> get devices {
|
||||
if (_items == null)
|
||||
_items = new ItemListNotifier<Device>.from(pollingGetDevices());
|
||||
return _items.items;
|
||||
}
|
||||
|
||||
Stream<Device> get onAdded {
|
||||
if (_items == null)
|
||||
_items = new ItemListNotifier<Device>();
|
||||
return _items.onAdded;
|
||||
}
|
||||
|
||||
Stream<Device> get onRemoved {
|
||||
if (_items == null)
|
||||
_items = new ItemListNotifier<Device>();
|
||||
return _items.onRemoved;
|
||||
}
|
||||
|
||||
void dispose() => stopPolling();
|
||||
|
||||
String toString() => '$name device discovery';
|
||||
}
|
||||
|
||||
abstract class Device {
|
||||
Device(this.id);
|
||||
|
||||
@ -139,6 +169,16 @@ abstract class Device {
|
||||
/// Stop an app package on the current device.
|
||||
Future<bool> stopApp(ApplicationPackage app);
|
||||
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! Device)
|
||||
return false;
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
String toString() => '$runtimeType $id';
|
||||
}
|
||||
|
||||
|
||||
@ -1,712 +0,0 @@
|
||||
// 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';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart';
|
||||
import '../ios/setup_xcodeproj.dart';
|
||||
import '../services.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>[];
|
||||
|
||||
bool get supportsPlatform => Platform.isMacOS;
|
||||
|
||||
Future init() {
|
||||
_devices = IOSDevice.getAttachedDevices();
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
List<Device> get devices => _devices;
|
||||
}
|
||||
|
||||
class IOSSimulatorDiscovery extends DeviceDiscovery {
|
||||
List<Device> _devices = <Device>[];
|
||||
|
||||
bool get supportsPlatform => Platform.isMacOS;
|
||||
|
||||
Future init() {
|
||||
_devices = IOSSimulator.getAttachedDevices();
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
List<Device> get devices => _devices;
|
||||
}
|
||||
|
||||
class IOSDevice extends Device {
|
||||
IOSDevice(String id, { this.name }) : super(id) {
|
||||
_installerPath = _checkForCommand('ideviceinstaller');
|
||||
_listerPath = _checkForCommand('idevice_id');
|
||||
_informerPath = _checkForCommand('ideviceinfo');
|
||||
_debuggerPath = _checkForCommand('idevicedebug');
|
||||
_loggerPath = _checkForCommand('idevicesyslog');
|
||||
_pusherPath = _checkForCommand(
|
||||
'ios-deploy',
|
||||
'To copy files to iOS devices, please install ios-deploy. '
|
||||
'You can do this using homebrew as follows:\n'
|
||||
'\$ brew tap flutter/flutter\n'
|
||||
'\$ brew install ios-deploy');
|
||||
}
|
||||
|
||||
String _installerPath;
|
||||
String get installerPath => _installerPath;
|
||||
|
||||
String _listerPath;
|
||||
String get listerPath => _listerPath;
|
||||
|
||||
String _informerPath;
|
||||
String get informerPath => _informerPath;
|
||||
|
||||
String _debuggerPath;
|
||||
String get debuggerPath => _debuggerPath;
|
||||
|
||||
String _loggerPath;
|
||||
String get loggerPath => _loggerPath;
|
||||
|
||||
String _pusherPath;
|
||||
String get pusherPath => _pusherPath;
|
||||
|
||||
final String name;
|
||||
|
||||
bool get supportsStartPaused => false;
|
||||
|
||||
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
|
||||
if (!doctor.iosWorkflow.hasIdeviceId)
|
||||
return <IOSDevice>[];
|
||||
|
||||
List<IOSDevice> devices = [];
|
||||
for (String id in _getAttachedDeviceIDs(mockIOS)) {
|
||||
String name = _getDeviceName(id, mockIOS);
|
||||
devices.add(new IOSDevice(id, name: name));
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
|
||||
String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
|
||||
try {
|
||||
String output = runSync([listerPath, '-l']);
|
||||
return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
|
||||
} catch (e) {
|
||||
return <String>[];
|
||||
}
|
||||
}
|
||||
|
||||
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
|
||||
String informerPath = (mockIOS != null)
|
||||
? mockIOS.informerPath
|
||||
: _checkForCommand('ideviceinfo');
|
||||
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
|
||||
}
|
||||
|
||||
static final Map<String, String> _commandMap = {};
|
||||
static String _checkForCommand(
|
||||
String command, [
|
||||
String macInstructions = _ideviceinstallerInstructions
|
||||
]) {
|
||||
return _commandMap.putIfAbsent(command, () {
|
||||
try {
|
||||
command = runCheckedSync(['which', command]).trim();
|
||||
} catch (e) {
|
||||
if (Platform.isMacOS) {
|
||||
printError('$command not found. $macInstructions');
|
||||
} else {
|
||||
printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
try {
|
||||
runCheckedSync([installerPath, '-i', app.localPath]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() => _getAttachedDeviceIDs().contains(id);
|
||||
|
||||
@override
|
||||
bool isSupported() => true;
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String apps = runCheckedSync([installerPath, '--list-apps']);
|
||||
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
bool clearLogs: false,
|
||||
bool startPaused: false,
|
||||
int debugPort: observatoryDefaultPort,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
|
||||
// TODO(devoncarew): Handle startPaused, debugPort.
|
||||
printTrace('Building ${app.name} for $id');
|
||||
|
||||
// Step 1: Install the precompiled application if necessary.
|
||||
bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: true);
|
||||
if (!buildResult) {
|
||||
printError('Could not build the precompiled application for the device.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Check that the application exists at the specified path.
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
|
||||
bool bundleExists = bundle.existsSync();
|
||||
if (!bundleExists) {
|
||||
printError('Could not find the built application bundle at ${bundle.path}.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Attempt to install the application on the device.
|
||||
int installationResult = await runCommandAndStreamOutput([
|
||||
'/usr/bin/env',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
id,
|
||||
'--bundle',
|
||||
bundle.path,
|
||||
]);
|
||||
|
||||
if (installationResult != 0) {
|
||||
printError('Could not install ${bundle.path} on $id.');
|
||||
return false;
|
||||
}
|
||||
|
||||
printTrace('Installation successful.');
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
runSync(<String>[
|
||||
pusherPath,
|
||||
'-t',
|
||||
'1',
|
||||
'--bundle_id',
|
||||
app.id,
|
||||
'--upload',
|
||||
localFile,
|
||||
'--to',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOS;
|
||||
|
||||
DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
|
||||
}
|
||||
|
||||
class IOSSimulator extends Device {
|
||||
IOSSimulator(String id, { this.name }) : super(id);
|
||||
|
||||
static List<IOSSimulator> getAttachedDevices() {
|
||||
if (!xcode.isInstalledAndMeetsVersionCheck)
|
||||
return <IOSSimulator>[];
|
||||
|
||||
return SimControl.getConnectedDevices().map((SimDevice device) {
|
||||
return new IOSSimulator(device.udid, name: device.name);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final String name;
|
||||
|
||||
String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
|
||||
|
||||
String _getSimulatorPath() {
|
||||
return path.join(_homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
|
||||
}
|
||||
|
||||
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
|
||||
String simulatorPath = _getSimulatorPath();
|
||||
if (simulatorPath == null)
|
||||
return null;
|
||||
return path.join(simulatorPath, 'data');
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected())
|
||||
return false;
|
||||
|
||||
try {
|
||||
SimControl.install(id, app.localPath);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id);
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported() {
|
||||
if (!Platform.isMacOS) {
|
||||
_supportMessage = "Not supported on a non Mac host";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 1: Check if the device is part of a blacklisted category.
|
||||
// We do not support WatchOS or tvOS devices.
|
||||
|
||||
RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);
|
||||
|
||||
if (blacklist.hasMatch(name)) {
|
||||
_supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Check if the device must be rejected because of its version.
|
||||
// There is an artitifical check on older simulators where arm64
|
||||
// targetted applications cannot be run (even though the
|
||||
// Flutter runner on the simulator is completely different).
|
||||
|
||||
RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
|
||||
Match match = versionExp.firstMatch(name);
|
||||
|
||||
if (match == null) {
|
||||
// Not an iPhone. All available non-iPhone simulators are compatible.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (int.parse(match.group(1)) > 5) {
|
||||
// iPhones 6 and above are always fine.
|
||||
return true;
|
||||
}
|
||||
|
||||
// The 's' subtype of 5 is compatible.
|
||||
if (name.contains('iPhone 5s')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
|
||||
return false;
|
||||
}
|
||||
|
||||
String _supportMessage;
|
||||
|
||||
@override
|
||||
String supportMessage() {
|
||||
if (isSupported()) {
|
||||
return "Supported";
|
||||
}
|
||||
|
||||
return _supportMessage != null ? _supportMessage : "Unknown";
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
bool clearLogs: false,
|
||||
bool startPaused: false,
|
||||
int debugPort: observatoryDefaultPort,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO(chinmaygarde): Use mainPath, route.
|
||||
printTrace('Building ${app.name} for $id.');
|
||||
|
||||
if (clearLogs)
|
||||
this.clearLogs();
|
||||
|
||||
// Step 1: Build the Xcode project.
|
||||
bool buildResult = await _buildIOSXcodeProject(app, buildForDevice: false);
|
||||
if (!buildResult) {
|
||||
printError('Could not build the application for the simulator.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Assert that the Xcode project was successfully built.
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
|
||||
bool bundleExists = await bundle.exists();
|
||||
if (!bundleExists) {
|
||||
printError('Could not find the built application bundle at ${bundle.path}.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Install the updated bundle to the simulator.
|
||||
SimControl.install(id, path.absolute(bundle.path));
|
||||
|
||||
// Step 4: Prepare launch arguments.
|
||||
List<String> args = <String>[];
|
||||
|
||||
if (checked)
|
||||
args.add("--enable-checked-mode");
|
||||
|
||||
if (startPaused)
|
||||
args.add("--start-paused");
|
||||
|
||||
if (debugPort != observatoryDefaultPort)
|
||||
args.add("--observatory-port=$debugPort");
|
||||
|
||||
// Step 5: Launch the updated application in the simulator.
|
||||
try {
|
||||
SimControl.launch(id, app.id, args);
|
||||
} catch (error) {
|
||||
printError('$error');
|
||||
return false;
|
||||
}
|
||||
|
||||
printTrace('Successfully started ${app.name} on $id.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String get logFilePath {
|
||||
return path.join(_homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOSSimulator;
|
||||
|
||||
DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
|
||||
|
||||
void clearLogs() {
|
||||
File logFile = new File(logFilePath);
|
||||
if (logFile.existsSync()) {
|
||||
RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
|
||||
randomFile.truncateSync(0);
|
||||
randomFile.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
void ensureLogsExists() {
|
||||
File logFile = new File(logFilePath);
|
||||
if (!logFile.existsSync())
|
||||
logFile.writeAsBytesSync(<int>[]);
|
||||
}
|
||||
}
|
||||
|
||||
class _IOSDeviceLogReader extends DeviceLogReader {
|
||||
_IOSDeviceLogReader(this.device);
|
||||
|
||||
final IOSDevice device;
|
||||
|
||||
String get name => device.name;
|
||||
|
||||
// TODO(devoncarew): Support [clear].
|
||||
Future<int> logs({ bool clear: false }) async {
|
||||
if (!device.isConnected())
|
||||
return 2;
|
||||
|
||||
return await runCommandAndStreamOutput(
|
||||
<String>[device.loggerPath],
|
||||
prefix: '[$name] ',
|
||||
filter: new RegExp(r'Runner')
|
||||
);
|
||||
}
|
||||
|
||||
int get hashCode => name.hashCode;
|
||||
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! _IOSDeviceLogReader)
|
||||
return false;
|
||||
return other.name == name;
|
||||
}
|
||||
}
|
||||
|
||||
class _IOSSimulatorLogReader extends DeviceLogReader {
|
||||
_IOSSimulatorLogReader(this.device);
|
||||
|
||||
final IOSSimulator device;
|
||||
|
||||
bool _lastWasFiltered = false;
|
||||
|
||||
String get name => device.name;
|
||||
|
||||
Future<int> logs({ bool clear: false }) async {
|
||||
if (!device.isConnected())
|
||||
return 2;
|
||||
|
||||
if (clear)
|
||||
device.clearLogs();
|
||||
|
||||
device.ensureLogsExists();
|
||||
|
||||
// Match the log prefix (in order to shorten it):
|
||||
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
|
||||
RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
|
||||
// Jan 31 19:23:28 --- last message repeated 1 time ---
|
||||
RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
|
||||
|
||||
// This filter matches many Flutter lines in the log:
|
||||
// new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
|
||||
// a fair number, including ones that would be useful in diagnosing crashes.
|
||||
// For now, we're not filtering the log file (but do clear it with each run).
|
||||
|
||||
Future<int> result = runCommandAndStreamOutput(
|
||||
<String>['tail', '-n', '+0', '-F', device.logFilePath],
|
||||
prefix: '[$name] ',
|
||||
mapFunction: (String string) {
|
||||
Match match = mapRegex.matchAsPrefix(string);
|
||||
if (match != null) {
|
||||
_lastWasFiltered = true;
|
||||
|
||||
// Filter out some messages that clearly aren't related to Flutter.
|
||||
if (string.contains(': could not find icon for representation -> com.apple.'))
|
||||
return null;
|
||||
String category = match.group(1);
|
||||
String content = match.group(2);
|
||||
if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
|
||||
category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
|
||||
category == 'searchd')
|
||||
return null;
|
||||
|
||||
_lastWasFiltered = false;
|
||||
|
||||
if (category == 'Runner')
|
||||
return content;
|
||||
return '$category: $content';
|
||||
}
|
||||
match = lastMessageRegex.matchAsPrefix(string);
|
||||
if (match != null && !_lastWasFiltered)
|
||||
return '(${match.group(1)})';
|
||||
return string;
|
||||
}
|
||||
);
|
||||
|
||||
// Track system.log crashes.
|
||||
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
|
||||
runCommandAndStreamOutput(
|
||||
<String>['tail', '-F', '/private/var/log/system.log'],
|
||||
prefix: '[$name] ',
|
||||
filter: new RegExp(r' FlutterRunner\[\d+\] '),
|
||||
mapFunction: (String string) {
|
||||
Match match = mapRegex.matchAsPrefix(string);
|
||||
return match == null ? string : '${match.group(1)}: ${match.group(2)}';
|
||||
}
|
||||
);
|
||||
|
||||
return await result;
|
||||
}
|
||||
|
||||
int get hashCode => device.logFilePath.hashCode;
|
||||
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! _IOSSimulatorLogReader)
|
||||
return false;
|
||||
return other.device.logFilePath == device.logFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
|
||||
final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.';
|
||||
|
||||
String get _homeDirectory => path.absolute(Platform.environment['HOME']);
|
||||
|
||||
bool _checkXcodeVersion() {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
try {
|
||||
String version = runCheckedSync(<String>['xcodebuild', '-version']);
|
||||
Match match = _xcodeVersionRegExp.firstMatch(version);
|
||||
if (int.parse(match[1]) < 7) {
|
||||
printError('Found "${match[0]}". $_xcodeRequirement');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
printError('Cannot find "xcodebuid". $_xcodeRequirement');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _validateEngineRevision(ApplicationPackage app) {
|
||||
String skyRevision = ArtifactStore.engineRevision;
|
||||
String iosRevision = _getIOSEngineRevision(app);
|
||||
|
||||
if (iosRevision != skyRevision) {
|
||||
printError("Error: incompatible sky_engine revision.");
|
||||
printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
|
||||
return false;
|
||||
} else {
|
||||
printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String _getIOSEngineRevision(ApplicationPackage app) {
|
||||
File revisionFile = new File(path.join(app.localPath, 'REVISION'));
|
||||
if (revisionFile.existsSync()) {
|
||||
return revisionFile.readAsStringSync().trim();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async {
|
||||
String flutterProjectPath = Directory.current.path;
|
||||
|
||||
if (xcodeProjectRequiresUpdate()) {
|
||||
printTrace('Initializing the Xcode project.');
|
||||
if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) {
|
||||
printError('Could not initialize the Xcode project.');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
updateXcodeLocalProperties(flutterProjectPath);
|
||||
}
|
||||
|
||||
if (!_validateEngineRevision(app))
|
||||
return false;
|
||||
|
||||
if (!_checkXcodeVersion())
|
||||
return false;
|
||||
|
||||
// Before the build, all service definitions must be updated and the dylibs
|
||||
// copied over to a location that is suitable for Xcodebuild to find them.
|
||||
|
||||
await _addServicesToBundle(new Directory(app.localPath));
|
||||
|
||||
List<String> commands = <String>[
|
||||
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
|
||||
];
|
||||
|
||||
if (buildForDevice) {
|
||||
commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
|
||||
} else {
|
||||
commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
|
||||
}
|
||||
|
||||
try {
|
||||
runCheckedSync(commands, workingDirectory: app.localPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future _addServicesToBundle(Directory bundle) async {
|
||||
List<Map<String, String>> services = [];
|
||||
printTrace("Trying to resolve native pub services.");
|
||||
|
||||
// Step 1: Parse the service configuration yaml files present in the service
|
||||
// pub packages.
|
||||
await parseServiceConfigs(services);
|
||||
printTrace("Found ${services.length} service definition(s).");
|
||||
|
||||
// Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
|
||||
Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks"));
|
||||
await _copyServiceFrameworks(services, frameworksDirectory);
|
||||
|
||||
// Step 3: Copy the service definitions manifest at the correct spot for
|
||||
// xcodebuild to pick up.
|
||||
File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json"));
|
||||
_copyServiceDefinitionsManifest(services, manifestFile);
|
||||
}
|
||||
|
||||
Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
|
||||
printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'.");
|
||||
frameworksDirectory.createSync(recursive: true);
|
||||
for (Map<String, String> service in services) {
|
||||
String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
|
||||
File dylib = new File(dylibPath);
|
||||
printTrace("Copying ${dylib.path} into bundle.");
|
||||
if (!dylib.existsSync()) {
|
||||
printError("The service dylib '${dylib.path}' does not exist.");
|
||||
continue;
|
||||
}
|
||||
// Shell out so permissions on the dylib are preserved.
|
||||
runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]);
|
||||
}
|
||||
}
|
||||
|
||||
void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
|
||||
printTrace("Creating service definitions manifest at '${manifest.path}'");
|
||||
List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => {
|
||||
'name': service['name'],
|
||||
// Since we have already moved it to the Frameworks directory. Strip away
|
||||
// the directory and basenames.
|
||||
'framework': path.basenameWithoutExtension(service['ios-framework'])
|
||||
}).toList();
|
||||
Map<String, dynamic> json = { 'services' : jsonServices };
|
||||
manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
|
||||
}
|
||||
255
packages/flutter_tools/lib/src/ios/devices.dart
Normal file
255
packages/flutter_tools/lib/src/ios/devices.dart
Normal file
@ -0,0 +1,255 @@
|
||||
// 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:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart';
|
||||
import '../toolchain.dart';
|
||||
import 'mac.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 IOSDevices extends PollingDeviceDiscovery {
|
||||
IOSDevices() : super('IOSDevices');
|
||||
|
||||
bool get supportsPlatform => Platform.isMacOS;
|
||||
List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices();
|
||||
}
|
||||
|
||||
class IOSDevice extends Device {
|
||||
IOSDevice(String id, { this.name }) : super(id) {
|
||||
_installerPath = _checkForCommand('ideviceinstaller');
|
||||
_listerPath = _checkForCommand('idevice_id');
|
||||
_informerPath = _checkForCommand('ideviceinfo');
|
||||
_debuggerPath = _checkForCommand('idevicedebug');
|
||||
_loggerPath = _checkForCommand('idevicesyslog');
|
||||
_pusherPath = _checkForCommand(
|
||||
'ios-deploy',
|
||||
'To copy files to iOS devices, please install ios-deploy. '
|
||||
'You can do this using homebrew as follows:\n'
|
||||
'\$ brew tap flutter/flutter\n'
|
||||
'\$ brew install ios-deploy');
|
||||
}
|
||||
|
||||
String _installerPath;
|
||||
String get installerPath => _installerPath;
|
||||
|
||||
String _listerPath;
|
||||
String get listerPath => _listerPath;
|
||||
|
||||
String _informerPath;
|
||||
String get informerPath => _informerPath;
|
||||
|
||||
String _debuggerPath;
|
||||
String get debuggerPath => _debuggerPath;
|
||||
|
||||
String _loggerPath;
|
||||
String get loggerPath => _loggerPath;
|
||||
|
||||
String _pusherPath;
|
||||
String get pusherPath => _pusherPath;
|
||||
|
||||
final String name;
|
||||
|
||||
bool get supportsStartPaused => false;
|
||||
|
||||
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
|
||||
if (!doctor.iosWorkflow.hasIdeviceId)
|
||||
return <IOSDevice>[];
|
||||
|
||||
List<IOSDevice> devices = [];
|
||||
for (String id in _getAttachedDeviceIDs(mockIOS)) {
|
||||
String name = _getDeviceName(id, mockIOS);
|
||||
devices.add(new IOSDevice(id, name: name));
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
|
||||
String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
|
||||
try {
|
||||
String output = runSync([listerPath, '-l']);
|
||||
return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
|
||||
} catch (e) {
|
||||
return <String>[];
|
||||
}
|
||||
}
|
||||
|
||||
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
|
||||
String informerPath = (mockIOS != null)
|
||||
? mockIOS.informerPath
|
||||
: _checkForCommand('ideviceinfo');
|
||||
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
|
||||
}
|
||||
|
||||
static final Map<String, String> _commandMap = {};
|
||||
static String _checkForCommand(
|
||||
String command, [
|
||||
String macInstructions = _ideviceinstallerInstructions
|
||||
]) {
|
||||
return _commandMap.putIfAbsent(command, () {
|
||||
try {
|
||||
command = runCheckedSync(['which', command]).trim();
|
||||
} catch (e) {
|
||||
if (Platform.isMacOS) {
|
||||
printError('$command not found. $macInstructions');
|
||||
} else {
|
||||
printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
try {
|
||||
runCheckedSync([installerPath, '-i', app.localPath]);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() => _getAttachedDeviceIDs().contains(id);
|
||||
|
||||
@override
|
||||
bool isSupported() => true;
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String apps = runCheckedSync([installerPath, '--list-apps']);
|
||||
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
bool clearLogs: false,
|
||||
bool startPaused: false,
|
||||
int debugPort: observatoryDefaultPort,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO(chinmaygarde): Use checked, mainPath, route, clearLogs.
|
||||
// TODO(devoncarew): Handle startPaused, debugPort.
|
||||
printTrace('Building ${app.name} for $id');
|
||||
|
||||
// Step 1: Install the precompiled application if necessary.
|
||||
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true);
|
||||
if (!buildResult) {
|
||||
printError('Could not build the precompiled application for the device.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Check that the application exists at the specified path.
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app'));
|
||||
bool bundleExists = bundle.existsSync();
|
||||
if (!bundleExists) {
|
||||
printError('Could not find the built application bundle at ${bundle.path}.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Attempt to install the application on the device.
|
||||
int installationResult = await runCommandAndStreamOutput([
|
||||
'/usr/bin/env',
|
||||
'ios-deploy',
|
||||
'--id',
|
||||
id,
|
||||
'--bundle',
|
||||
bundle.path,
|
||||
]);
|
||||
|
||||
if (installationResult != 0) {
|
||||
printError('Could not install ${bundle.path} on $id.');
|
||||
return false;
|
||||
}
|
||||
|
||||
printTrace('Installation successful.');
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
runSync(<String>[
|
||||
pusherPath,
|
||||
'-t',
|
||||
'1',
|
||||
'--bundle_id',
|
||||
app.id,
|
||||
'--upload',
|
||||
localFile,
|
||||
'--to',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOS;
|
||||
|
||||
DeviceLogReader createLogReader() => new _IOSDeviceLogReader(this);
|
||||
}
|
||||
|
||||
class _IOSDeviceLogReader extends DeviceLogReader {
|
||||
_IOSDeviceLogReader(this.device);
|
||||
|
||||
final IOSDevice device;
|
||||
|
||||
String get name => device.name;
|
||||
|
||||
// TODO(devoncarew): Support [clear].
|
||||
Future<int> logs({ bool clear: false }) async {
|
||||
if (!device.isConnected())
|
||||
return 2;
|
||||
|
||||
return await runCommandAndStreamOutput(
|
||||
<String>[device.loggerPath],
|
||||
prefix: '[$name] ',
|
||||
filter: new RegExp(r'Runner')
|
||||
);
|
||||
}
|
||||
|
||||
int get hashCode => name.hashCode;
|
||||
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! _IOSDeviceLogReader)
|
||||
return false;
|
||||
return other.name == name;
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,23 @@
|
||||
// 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 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../artifacts.dart';
|
||||
import '../base/context.dart';
|
||||
import '../base/process.dart';
|
||||
import '../globals.dart';
|
||||
import '../services.dart';
|
||||
import 'setup_xcodeproj.dart';
|
||||
|
||||
String get homeDirectory => path.absolute(Platform.environment['HOME']);
|
||||
|
||||
// TODO(devoncarew): Refactor functionality into XCode.
|
||||
|
||||
const int kXcodeRequiredVersionMajor = 7;
|
||||
const int kXcodeRequiredVersionMinor = 2;
|
||||
@ -51,3 +66,135 @@ class XCode {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async {
|
||||
String flutterProjectPath = Directory.current.path;
|
||||
|
||||
if (xcodeProjectRequiresUpdate()) {
|
||||
printTrace('Initializing the Xcode project.');
|
||||
if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) {
|
||||
printError('Could not initialize the Xcode project.');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
updateXcodeLocalProperties(flutterProjectPath);
|
||||
}
|
||||
|
||||
if (!_validateEngineRevision(app))
|
||||
return false;
|
||||
|
||||
if (!_checkXcodeVersion())
|
||||
return false;
|
||||
|
||||
// Before the build, all service definitions must be updated and the dylibs
|
||||
// copied over to a location that is suitable for Xcodebuild to find them.
|
||||
|
||||
await _addServicesToBundle(new Directory(app.localPath));
|
||||
|
||||
List<String> commands = <String>[
|
||||
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
|
||||
];
|
||||
|
||||
if (buildForDevice) {
|
||||
commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
|
||||
} else {
|
||||
commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
|
||||
}
|
||||
|
||||
try {
|
||||
runCheckedSync(commands, workingDirectory: app.localPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
|
||||
final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.';
|
||||
|
||||
bool _checkXcodeVersion() {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
try {
|
||||
String version = runCheckedSync(<String>['xcodebuild', '-version']);
|
||||
Match match = _xcodeVersionRegExp.firstMatch(version);
|
||||
if (int.parse(match[1]) < 7) {
|
||||
printError('Found "${match[0]}". $_xcodeRequirement');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
printError('Cannot find "xcodebuid". $_xcodeRequirement');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _validateEngineRevision(ApplicationPackage app) {
|
||||
String skyRevision = ArtifactStore.engineRevision;
|
||||
String iosRevision = _getIOSEngineRevision(app);
|
||||
|
||||
if (iosRevision != skyRevision) {
|
||||
printError("Error: incompatible sky_engine revision.");
|
||||
printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
|
||||
return false;
|
||||
} else {
|
||||
printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String _getIOSEngineRevision(ApplicationPackage app) {
|
||||
File revisionFile = new File(path.join(app.localPath, 'REVISION'));
|
||||
if (revisionFile.existsSync()) {
|
||||
return revisionFile.readAsStringSync().trim();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future _addServicesToBundle(Directory bundle) async {
|
||||
List<Map<String, String>> services = [];
|
||||
printTrace("Trying to resolve native pub services.");
|
||||
|
||||
// Step 1: Parse the service configuration yaml files present in the service
|
||||
// pub packages.
|
||||
await parseServiceConfigs(services);
|
||||
printTrace("Found ${services.length} service definition(s).");
|
||||
|
||||
// Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
|
||||
Directory frameworksDirectory = new Directory(path.join(bundle.path, "Frameworks"));
|
||||
await _copyServiceFrameworks(services, frameworksDirectory);
|
||||
|
||||
// Step 3: Copy the service definitions manifest at the correct spot for
|
||||
// xcodebuild to pick up.
|
||||
File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json"));
|
||||
_copyServiceDefinitionsManifest(services, manifestFile);
|
||||
}
|
||||
|
||||
Future _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
|
||||
printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'.");
|
||||
frameworksDirectory.createSync(recursive: true);
|
||||
for (Map<String, String> service in services) {
|
||||
String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
|
||||
File dylib = new File(dylibPath);
|
||||
printTrace("Copying ${dylib.path} into bundle.");
|
||||
if (!dylib.existsSync()) {
|
||||
printError("The service dylib '${dylib.path}' does not exist.");
|
||||
continue;
|
||||
}
|
||||
// Shell out so permissions on the dylib are preserved.
|
||||
runCheckedSync(['/bin/cp', dylib.path, frameworksDirectory.path]);
|
||||
}
|
||||
}
|
||||
|
||||
void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
|
||||
printTrace("Creating service definitions manifest at '${manifest.path}'");
|
||||
List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => {
|
||||
'name': service['name'],
|
||||
// Since we have already moved it to the Frameworks directory. Strip away
|
||||
// the directory and basenames.
|
||||
'framework': path.basenameWithoutExtension(service['ios-framework'])
|
||||
}).toList();
|
||||
Map<String, dynamic> json = { 'services' : jsonServices };
|
||||
manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
|
||||
}
|
||||
|
||||
@ -1,173 +0,0 @@
|
||||
// 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/process.dart';
|
||||
import '../globals.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';
|
||||
}
|
||||
488
packages/flutter_tools/lib/src/ios/simulators.dart
Normal file
488
packages/flutter_tools/lib/src/ios/simulators.dart
Normal file
@ -0,0 +1,488 @@
|
||||
// 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 'package:path/path.dart' as path;
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/process.dart';
|
||||
import '../build_configuration.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart';
|
||||
import '../toolchain.dart';
|
||||
import 'mac.dart';
|
||||
|
||||
const String _xcrunPath = '/usr/bin/xcrun';
|
||||
|
||||
const String _simulatorPath =
|
||||
'/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';
|
||||
|
||||
class IOSSimulators extends PollingDeviceDiscovery {
|
||||
IOSSimulators() : super('IOSSimulators');
|
||||
|
||||
bool get supportsPlatform => Platform.isMacOS;
|
||||
List<Device> pollingGetDevices() => IOSSimulator.getAttachedDevices();
|
||||
}
|
||||
|
||||
/// 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';
|
||||
}
|
||||
|
||||
class IOSSimulator extends Device {
|
||||
IOSSimulator(String id, { this.name }) : super(id);
|
||||
|
||||
static List<IOSSimulator> getAttachedDevices() {
|
||||
if (!xcode.isInstalledAndMeetsVersionCheck)
|
||||
return <IOSSimulator>[];
|
||||
|
||||
return SimControl.getConnectedDevices().map((SimDevice device) {
|
||||
return new IOSSimulator(device.udid, name: device.name);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final String name;
|
||||
|
||||
String get xcrunPath => path.join('/usr', 'bin', 'xcrun');
|
||||
|
||||
String _getSimulatorPath() {
|
||||
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', id);
|
||||
}
|
||||
|
||||
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
|
||||
String simulatorPath = _getSimulatorPath();
|
||||
if (simulatorPath == null)
|
||||
return null;
|
||||
return path.join(simulatorPath, 'data');
|
||||
}
|
||||
|
||||
@override
|
||||
bool installApp(ApplicationPackage app) {
|
||||
if (!isConnected())
|
||||
return false;
|
||||
|
||||
try {
|
||||
SimControl.install(id, app.localPath);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isConnected() {
|
||||
if (!Platform.isMacOS)
|
||||
return false;
|
||||
return SimControl.getConnectedDevices().any((SimDevice device) => device.udid == id);
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported() {
|
||||
if (!Platform.isMacOS) {
|
||||
_supportMessage = "Not supported on a non Mac host";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 1: Check if the device is part of a blacklisted category.
|
||||
// We do not support WatchOS or tvOS devices.
|
||||
|
||||
RegExp blacklist = new RegExp(r'Apple (TV|Watch)', caseSensitive: false);
|
||||
|
||||
if (blacklist.hasMatch(name)) {
|
||||
_supportMessage = "Flutter does not support either the Apple TV or Watch. Choose an iPhone 5s or above.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Check if the device must be rejected because of its version.
|
||||
// There is an artitifical check on older simulators where arm64
|
||||
// targetted applications cannot be run (even though the
|
||||
// Flutter runner on the simulator is completely different).
|
||||
|
||||
RegExp versionExp = new RegExp(r'iPhone ([0-9])+');
|
||||
Match match = versionExp.firstMatch(name);
|
||||
|
||||
if (match == null) {
|
||||
// Not an iPhone. All available non-iPhone simulators are compatible.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (int.parse(match.group(1)) > 5) {
|
||||
// iPhones 6 and above are always fine.
|
||||
return true;
|
||||
}
|
||||
|
||||
// The 's' subtype of 5 is compatible.
|
||||
if (name.contains('iPhone 5s')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_supportMessage = "The simulator version is too old. Choose an iPhone 5s or above.";
|
||||
return false;
|
||||
}
|
||||
|
||||
String _supportMessage;
|
||||
|
||||
@override
|
||||
String supportMessage() {
|
||||
if (isSupported()) {
|
||||
return "Supported";
|
||||
}
|
||||
|
||||
return _supportMessage != null ? _supportMessage : "Unknown";
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAppInstalled(ApplicationPackage app) {
|
||||
try {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startApp(
|
||||
ApplicationPackage app,
|
||||
Toolchain toolchain, {
|
||||
String mainPath,
|
||||
String route,
|
||||
bool checked: true,
|
||||
bool clearLogs: false,
|
||||
bool startPaused: false,
|
||||
int debugPort: observatoryDefaultPort,
|
||||
Map<String, dynamic> platformArgs
|
||||
}) async {
|
||||
// TODO(chinmaygarde): Use mainPath, route.
|
||||
printTrace('Building ${app.name} for $id.');
|
||||
|
||||
if (clearLogs)
|
||||
this.clearLogs();
|
||||
|
||||
// Step 1: Build the Xcode project.
|
||||
bool buildResult = await buildIOSXcodeProject(app, buildForDevice: false);
|
||||
if (!buildResult) {
|
||||
printError('Could not build the application for the simulator.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Assert that the Xcode project was successfully built.
|
||||
Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphonesimulator', 'Runner.app'));
|
||||
bool bundleExists = await bundle.exists();
|
||||
if (!bundleExists) {
|
||||
printError('Could not find the built application bundle at ${bundle.path}.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Install the updated bundle to the simulator.
|
||||
SimControl.install(id, path.absolute(bundle.path));
|
||||
|
||||
// Step 4: Prepare launch arguments.
|
||||
List<String> args = <String>[];
|
||||
|
||||
if (checked)
|
||||
args.add("--enable-checked-mode");
|
||||
|
||||
if (startPaused)
|
||||
args.add("--start-paused");
|
||||
|
||||
if (debugPort != observatoryDefaultPort)
|
||||
args.add("--observatory-port=$debugPort");
|
||||
|
||||
// Step 5: Launch the updated application in the simulator.
|
||||
try {
|
||||
SimControl.launch(id, app.id, args);
|
||||
} catch (error) {
|
||||
printError('$error');
|
||||
return false;
|
||||
}
|
||||
|
||||
printTrace('Successfully started ${app.name} on $id.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) async {
|
||||
// Currently we don't have a way to stop an app running on iOS.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> pushFile(
|
||||
ApplicationPackage app, String localFile, String targetFile) async {
|
||||
if (Platform.isMacOS) {
|
||||
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
|
||||
runCheckedSync(<String>['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String get logFilePath {
|
||||
return path.join(homeDirectory, 'Library', 'Logs', 'CoreSimulator', id, 'system.log');
|
||||
}
|
||||
|
||||
@override
|
||||
TargetPlatform get platform => TargetPlatform.iOSSimulator;
|
||||
|
||||
DeviceLogReader createLogReader() => new _IOSSimulatorLogReader(this);
|
||||
|
||||
void clearLogs() {
|
||||
File logFile = new File(logFilePath);
|
||||
if (logFile.existsSync()) {
|
||||
RandomAccessFile randomFile = logFile.openSync(mode: FileMode.WRITE);
|
||||
randomFile.truncateSync(0);
|
||||
randomFile.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
void ensureLogsExists() {
|
||||
File logFile = new File(logFilePath);
|
||||
if (!logFile.existsSync())
|
||||
logFile.writeAsBytesSync(<int>[]);
|
||||
}
|
||||
}
|
||||
|
||||
class _IOSSimulatorLogReader extends DeviceLogReader {
|
||||
_IOSSimulatorLogReader(this.device);
|
||||
|
||||
final IOSSimulator device;
|
||||
|
||||
bool _lastWasFiltered = false;
|
||||
|
||||
String get name => device.name;
|
||||
|
||||
Future<int> logs({ bool clear: false }) async {
|
||||
if (!device.isConnected())
|
||||
return 2;
|
||||
|
||||
if (clear)
|
||||
device.clearLogs();
|
||||
|
||||
device.ensureLogsExists();
|
||||
|
||||
// Match the log prefix (in order to shorten it):
|
||||
// 'Jan 29 01:31:44 devoncarew-macbookpro3 SpringBoard[96648]: ...'
|
||||
RegExp mapRegex = new RegExp(r'\S+ +\S+ +\S+ \S+ (.+)\[\d+\]\)?: (.*)$');
|
||||
// Jan 31 19:23:28 --- last message repeated 1 time ---
|
||||
RegExp lastMessageRegex = new RegExp(r'\S+ +\S+ +\S+ --- (.*) ---$');
|
||||
|
||||
// This filter matches many Flutter lines in the log:
|
||||
// new RegExp(r'(FlutterRunner|flutter.runner.Runner|$id)'), but it misses
|
||||
// a fair number, including ones that would be useful in diagnosing crashes.
|
||||
// For now, we're not filtering the log file (but do clear it with each run).
|
||||
|
||||
Future<int> result = runCommandAndStreamOutput(
|
||||
<String>['tail', '-n', '+0', '-F', device.logFilePath],
|
||||
prefix: '[$name] ',
|
||||
mapFunction: (String string) {
|
||||
Match match = mapRegex.matchAsPrefix(string);
|
||||
if (match != null) {
|
||||
_lastWasFiltered = true;
|
||||
|
||||
// Filter out some messages that clearly aren't related to Flutter.
|
||||
if (string.contains(': could not find icon for representation -> com.apple.'))
|
||||
return null;
|
||||
String category = match.group(1);
|
||||
String content = match.group(2);
|
||||
if (category == 'Game Center' || category == 'itunesstored' || category == 'nanoregistrylaunchd' ||
|
||||
category == 'mstreamd' || category == 'syncdefaultsd' || category == 'companionappd' ||
|
||||
category == 'searchd')
|
||||
return null;
|
||||
|
||||
_lastWasFiltered = false;
|
||||
|
||||
if (category == 'Runner')
|
||||
return content;
|
||||
return '$category: $content';
|
||||
}
|
||||
match = lastMessageRegex.matchAsPrefix(string);
|
||||
if (match != null && !_lastWasFiltered)
|
||||
return '(${match.group(1)})';
|
||||
return string;
|
||||
}
|
||||
);
|
||||
|
||||
// Track system.log crashes.
|
||||
// ReportCrash[37965]: Saved crash report for FlutterRunner[37941]...
|
||||
runCommandAndStreamOutput(
|
||||
<String>['tail', '-F', '/private/var/log/system.log'],
|
||||
prefix: '[$name] ',
|
||||
filter: new RegExp(r' FlutterRunner\[\d+\] '),
|
||||
mapFunction: (String string) {
|
||||
Match match = mapRegex.matchAsPrefix(string);
|
||||
return match == null ? string : '${match.group(1)}: ${match.group(2)}';
|
||||
}
|
||||
);
|
||||
|
||||
return await result;
|
||||
}
|
||||
|
||||
int get hashCode => device.logFilePath.hashCode;
|
||||
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! _IOSSimulatorLogReader)
|
||||
return false;
|
||||
return other.device.logFilePath == device.logFilePath;
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_tools/src/android/device_android.dart';
|
||||
import 'package:flutter_tools/src/android/android_device.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'src/context.dart';
|
||||
|
||||
37
packages/flutter_tools/test/base_utils_test.dart
Normal file
37
packages/flutter_tools/test/base_utils_test.dart
Normal file
@ -0,0 +1,37 @@
|
||||
// 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 'package:flutter_tools/src/base/utils.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() => defineTests();
|
||||
|
||||
defineTests() {
|
||||
group('ItemListNotifier', () {
|
||||
test('sends notifications', () async {
|
||||
ItemListNotifier<String> list = new ItemListNotifier<String>();
|
||||
expect(list.items, isEmpty);
|
||||
|
||||
Future<List<String>> addedStreamItems = list.onAdded.toList();
|
||||
Future<List<String>> removedStreamItems = list.onRemoved.toList();
|
||||
|
||||
list.updateWithNewList(<String>['aaa']);
|
||||
list.updateWithNewList(<String>['aaa', 'bbb']);
|
||||
list.updateWithNewList(<String>['bbb']);
|
||||
list.dispose();
|
||||
|
||||
List<String> addedItems = await addedStreamItems;
|
||||
List<String> removedItems = await removedStreamItems;
|
||||
|
||||
expect(addedItems.length, 2);
|
||||
expect(addedItems.first, 'aaa');
|
||||
expect(addedItems[1], 'bbb');
|
||||
|
||||
expect(removedItems.length, 1);
|
||||
expect(removedItems.first, 'aaa');
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -3,11 +3,14 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_tools/src/base/context.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/commands/daemon.dart';
|
||||
import 'package:flutter_tools/src/doctor.dart';
|
||||
import 'package:flutter_tools/src/globals.dart';
|
||||
import 'package:flutter_tools/src/ios/mac.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@ -31,6 +34,9 @@ defineTests() {
|
||||
appContext = new AppContext();
|
||||
notifyingLogger = new NotifyingLogger();
|
||||
appContext[Logger] = notifyingLogger;
|
||||
appContext[Doctor] = new Doctor();
|
||||
if (Platform.isMacOS)
|
||||
appContext[XCode] = new XCode();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_tools/src/android/device_android.dart';
|
||||
import 'package:flutter_tools/src/android/android_device.dart';
|
||||
import 'package:flutter_tools/src/application_package.dart';
|
||||
import 'package:flutter_tools/src/build_configuration.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/ios/device_ios.dart';
|
||||
import 'package:flutter_tools/src/ios/devices.dart';
|
||||
import 'package:flutter_tools/src/ios/simulators.dart';
|
||||
import 'package:flutter_tools/src/runner/flutter_command.dart';
|
||||
import 'package:flutter_tools/src/toolchain.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user