Commit 4ec96084 authored by Devon Carew's avatar Devon Carew

Merge pull request #2066 from devoncarew/device_polling_redux

add the ability to start and stop device polling
parents 9e56fc4e 67046f93
...@@ -74,7 +74,7 @@ class Adb { ...@@ -74,7 +74,7 @@ class Adb {
).toList(); ).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 /// 'track-devices' command. Call cancel on the returned stream to stop
/// listening. /// listening.
Stream<List<AdbDevice>> trackDevices() { Stream<List<AdbDevice>> trackDevices() {
......
...@@ -29,17 +29,11 @@ const String _deviceBundlePath = '/data/local/tmp/dev.flx'; ...@@ -29,17 +29,11 @@ const String _deviceBundlePath = '/data/local/tmp/dev.flx';
// Path where the snapshot will be copied on the device. // Path where the snapshot will be copied on the device.
const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin'; const String _deviceSnapshotPath = '/data/local/tmp/dev_snapshot.bin';
class AndroidDeviceDiscovery extends DeviceDiscovery { class AndroidDevices extends PollingDeviceDiscovery {
List<Device> _devices = <Device>[]; AndroidDevices() : super('AndroidDevices');
bool get supportsPlatform => true; bool get supportsPlatform => true;
List<Device> pollingGetDevices() => getAdbDevices();
Future init() {
_devices = getAdbDevices();
return new Future.value();
}
List<Device> get devices => _devices;
} }
class AndroidDevice extends Device { class AndroidDevice extends Device {
......
// 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'; ...@@ -6,15 +6,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import '../android/adb.dart'; import '../android/android_device.dart';
import '../android/android_sdk.dart';
import '../android/device_android.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/device_ios.dart'; import '../ios/devices.dart';
import '../ios/simulator.dart'; import '../ios/simulators.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import 'run.dart'; import 'run.dart';
import 'stop.dart' as stop; import 'stop.dart' as stop;
...@@ -126,10 +124,8 @@ class Daemon { ...@@ -126,10 +124,8 @@ class Daemon {
throw 'no domain for method: $method'; throw 'no domain for method: $method';
_domainMap[prefix].handleCommand(name, id, request['params']); _domainMap[prefix].handleCommand(name, id, request['params']);
} catch (error, trace) { } catch (error) {
_send({'id': id, 'error': _toJsonable(error)}); _send({'id': id, 'error': _toJsonable(error)});
stderr.writeln('error handling $request: $error');
stderr.writeln(trace);
} }
} }
...@@ -170,8 +166,6 @@ abstract class Domain { ...@@ -170,8 +166,6 @@ abstract class Domain {
} }
}).catchError((error, trace) { }).catchError((error, trace) {
_send({'id': id, 'error': _toJsonable(error)}); _send({'id': id, 'error': _toJsonable(error)});
stderr.writeln("error handling '$name.$command': $error");
stderr.writeln(trace);
}); });
} }
...@@ -286,166 +280,65 @@ class AppDomain extends Domain { ...@@ -286,166 +280,65 @@ class AppDomain extends Domain {
/// This domain lets callers list and monitor connected devices. /// This domain lets callers list and monitor connected devices.
/// ///
/// It exports a `getDevices()` call, as well as firing `device.added`, /// It exports a `getDevices()` call, as well as firing `device.added` and
/// `device.removed`, and `device.changed` events. /// `device.removed` events.
class DeviceDomain extends Domain { class DeviceDomain extends Domain {
DeviceDomain(Daemon daemon) : super(daemon, 'device') { DeviceDomain(Daemon daemon) : super(daemon, 'device') {
registerHandler('getDevices', getDevices); registerHandler('getDevices', getDevices);
registerHandler('enable', enable);
registerHandler('disable', disable);
_androidDeviceDiscovery = new AndroidDeviceDiscovery(); PollingDeviceDiscovery deviceDiscovery = new AndroidDevices();
_androidDeviceDiscovery.onAdded.listen((Device device) { if (deviceDiscovery.supportsPlatform)
sendEvent('device.added', _deviceToMap(device)); _discoverers.add(deviceDiscovery);
});
_androidDeviceDiscovery.onRemoved.listen((Device device) {
sendEvent('device.removed', _deviceToMap(device));
});
_androidDeviceDiscovery.onChanged.listen((Device device) {
sendEvent('device.changed', _deviceToMap(device));
});
if (Platform.isMacOS) { deviceDiscovery = new IOSDevices();
_iosSimulatorDeviceDiscovery = new IOSSimulatorDeviceDiscovery(); if (deviceDiscovery.supportsPlatform)
_iosSimulatorDeviceDiscovery.onAdded.listen((Device device) { _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)); sendEvent('device.added', _deviceToMap(device));
}); });
_iosSimulatorDeviceDiscovery.onRemoved.listen((Device device) { discoverer.onRemoved.listen((Device device) {
sendEvent('device.removed', _deviceToMap(device)); sendEvent('device.removed', _deviceToMap(device));
}); });
} }
} }
AndroidDeviceDiscovery _androidDeviceDiscovery; List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
IOSSimulatorDeviceDiscovery _iosSimulatorDeviceDiscovery;
Future<List<Device>> getDevices(dynamic args) { Future<List<Device>> getDevices(dynamic args) {
List<Device> devices = <Device>[]; List<Device> devices = _discoverers.expand((PollingDeviceDiscovery discoverer) {
devices.addAll(_androidDeviceDiscovery.getDevices()); return discoverer.devices;
if (_iosSimulatorDeviceDiscovery != null) }).toList();
devices.addAll(_iosSimulatorDeviceDiscovery.getDevices());
return new Future.value(devices); return new Future.value(devices);
} }
void dispose() { /// Enable device events.
_androidDeviceDiscovery.dispose(); Future enable(dynamic args) {
_iosSimulatorDeviceDiscovery?.dispose(); for (PollingDeviceDiscovery discoverer in _discoverers) {
} discoverer.startPolling();
}
class AndroidDeviceDiscovery {
AndroidDeviceDiscovery() {
_initAdb();
if (_adb != null) {
_subscription = _adb.trackDevices().listen(_handleUpdatedDevices);
}
}
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;
} }
return new Future.value();
} }
void _handleUpdatedDevices(List<AdbDevice> newDevices) { /// Disable device events.
List<AndroidDevice> currentDevices = new List.from(getDevices()); Future disable(dynamic args) {
for (PollingDeviceDiscovery discoverer in _discoverers) {
for (AdbDevice device in newDevices) { discoverer.stopPolling();
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);
} }
return new Future.value();
} }
void dispose() { void dispose() {
_subscription?.cancel(); for (PollingDeviceDiscovery discoverer in _discoverers) {
} discoverer.dispose();
}
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);
}
} }
// 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 { ...@@ -490,7 +383,7 @@ class NotifyingLogger extends Logger {
} }
void printTrace(String message) { 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'; ...@@ -7,7 +7,7 @@ import 'dart:io';
import '../application_package.dart'; import '../application_package.dart';
import '../device.dart'; import '../device.dart';
import '../ios/simulator.dart'; import '../ios/simulators.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
class InstallCommand extends FlutterCommand { class InstallCommand extends FlutterCommand {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'dart:async'; import 'dart:async';
import '../android/device_android.dart'; import '../android/android_device.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../globals.dart'; import '../globals.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
......
...@@ -4,12 +4,14 @@ ...@@ -4,12 +4,14 @@
import 'dart:async'; import 'dart:async';
import 'android/device_android.dart'; import 'android/android_device.dart';
import 'application_package.dart'; import 'application_package.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/utils.dart';
import 'build_configuration.dart'; import 'build_configuration.dart';
import 'globals.dart'; import 'globals.dart';
import 'ios/device_ios.dart'; import 'ios/devices.dart';
import 'ios/simulators.dart';
import 'toolchain.dart'; import 'toolchain.dart';
/// A class to get all available devices. /// A class to get all available devices.
...@@ -18,27 +20,9 @@ class DeviceManager { ...@@ -18,27 +20,9 @@ class DeviceManager {
/// of their methods are invoked. /// of their methods are invoked.
DeviceManager() { DeviceManager() {
// Register the known discoverers. // Register the known discoverers.
_deviceDiscoverers.add(new AndroidDeviceDiscovery()); _deviceDiscoverers.add(new AndroidDevices());
_deviceDiscoverers.add(new IOSDeviceDiscovery()); _deviceDiscoverers.add(new IOSDevices());
_deviceDiscoverers.add(new IOSSimulatorDiscovery()); _deviceDiscoverers.add(new IOSSimulators());
}
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;
} }
List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[]; List<DeviceDiscovery> _deviceDiscoverers = <DeviceDiscovery>[];
...@@ -46,8 +30,6 @@ class DeviceManager { ...@@ -46,8 +30,6 @@ class DeviceManager {
/// A user-specified device ID. /// A user-specified device ID.
String specifiedDeviceId; String specifiedDeviceId;
Completer _initedCompleter;
bool get hasSpecifiedDeviceId => specifiedDeviceId != null; bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
/// Return the device with the matching ID; else, complete the Future with /// Return the device with the matching ID; else, complete the Future with
...@@ -75,8 +57,6 @@ class DeviceManager { ...@@ -75,8 +57,6 @@ class DeviceManager {
/// Return the list of all connected devices. /// Return the list of all connected devices.
Future<List<Device>> getAllConnectedDevices() async { Future<List<Device>> getAllConnectedDevices() async {
await _init();
return _deviceDiscoverers return _deviceDiscoverers
.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform) .where((DeviceDiscovery discoverer) => discoverer.supportsPlatform)
.expand((DeviceDiscovery discoverer) => discoverer.devices) .expand((DeviceDiscovery discoverer) => discoverer.devices)
...@@ -87,10 +67,60 @@ class DeviceManager { ...@@ -87,10 +67,60 @@ class DeviceManager {
/// An abstract class to discover and enumerate a specific type of devices. /// An abstract class to discover and enumerate a specific type of devices.
abstract class DeviceDiscovery { abstract class DeviceDiscovery {
bool get supportsPlatform; bool get supportsPlatform;
Future init();
List<Device> get devices; 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 { abstract class Device {
Device(this.id); Device(this.id);
...@@ -139,6 +169,16 @@ abstract class Device { ...@@ -139,6 +169,16 @@ abstract class Device {
/// Stop an app package on the current device. /// Stop an app package on the current device.
Future<bool> stopApp(ApplicationPackage app); 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'; String toString() => '$runtimeType $id';
} }
......
// 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 @@ ...@@ -2,8 +2,23 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/context.dart';
import '../base/process.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 kXcodeRequiredVersionMajor = 7;
const int kXcodeRequiredVersionMinor = 2; const int kXcodeRequiredVersionMinor = 2;
...@@ -51,3 +66,135 @@ class XCode { ...@@ -51,3 +66,135 @@ class XCode {
return false; 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);
}
// 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';
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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 'package:test/test.dart';
import 'src/context.dart'; import 'src/context.dart';
......
// 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 @@ ...@@ -3,11 +3,14 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/commands/daemon.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/globals.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -31,6 +34,9 @@ defineTests() { ...@@ -31,6 +34,9 @@ defineTests() {
appContext = new AppContext(); appContext = new AppContext();
notifyingLogger = new NotifyingLogger(); notifyingLogger = new NotifyingLogger();
appContext[Logger] = notifyingLogger; appContext[Logger] = notifyingLogger;
appContext[Doctor] = new Doctor();
if (Platform.isMacOS)
appContext[XCode] = new XCode();
}); });
tearDown(() { tearDown(() {
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/application_package.dart';
import 'package:flutter_tools/src/build_configuration.dart'; import 'package:flutter_tools/src/build_configuration.dart';
import 'package:flutter_tools/src/device.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/runner/flutter_command.dart';
import 'package:flutter_tools/src/toolchain.dart'; import 'package:flutter_tools/src/toolchain.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
......
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