Commit 5bce2fbd authored by Devon Carew's avatar Devon Carew

refactor platform specific code out of device.dart

remove device type specific checks
parent 0f505fbf
// 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.
const int minApiLevel = 16;
const String minVersionName = 'Jelly Bean';
const String minVersionText = '4.1.x';
// 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:crypto/crypto.dart';
import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/logging.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../toolchain.dart';
import 'android.dart';
class AndroidDevice extends Device {
static const String _defaultAdbPath = 'adb';
static const int _observatoryPort = 8181;
static final String defaultDeviceID = 'default_android_device';
String productID;
String modelID;
String deviceCodeName;
bool _connected;
String _adbPath;
String get adbPath => _adbPath;
bool _hasAdb = false;
bool _hasValidAndroid = false;
factory AndroidDevice({
String id: null,
String productID: null,
String modelID: null,
String deviceCodeName: null,
bool connected
}) {
AndroidDevice device = Device.unique(id ?? defaultDeviceID, (String id) => new AndroidDevice.fromId(id));
device.productID = productID;
device.modelID = modelID;
device.deviceCodeName = deviceCodeName;
if (connected != null)
device._connected = connected;
return device;
}
/// This constructor is intended as protected access; prefer [AndroidDevice].
AndroidDevice.fromId(id) : super.fromId(id) {
_adbPath = getAdbPath();
_hasAdb = _checkForAdb();
// Checking for [minApiName] only needs to be done if we are starting an
// app, but it has an important side effect, which is to discard any
// progress messages if the adb server is restarted.
_hasValidAndroid = _checkForSupportedAndroidVersion();
if (!_hasAdb || !_hasValidAndroid) {
logging.warning('Unable to run on Android.');
}
}
/// mockAndroid argument is only to facilitate testing with mocks, so that
/// we don't have to rely on the test setup having adb available to it.
static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
List<AndroidDevice> devices = [];
String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : getAdbPath();
try {
runCheckedSync([adbPath, 'version']);
} catch (e) {
logging.severe('Unable to find adb. Is "adb" in your path?');
return devices;
}
List<String> output = runSync([adbPath, 'devices', '-l']).trim().split('\n');
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
RegExp deviceRegex1 = new RegExp(
r'^(\S+)\s+device\s+.*product:(\S+)\s+model:(\S+)\s+device:(\S+)$');
// 0149947A0D01500C device usb:340787200X
RegExp deviceRegex2 = new RegExp(r'^(\S+)\s+device\s+\S+$');
RegExp unauthorizedRegex = new RegExp(r'^(\S+)\s+unauthorized\s+\S+$');
RegExp offlineRegex = new RegExp(r'^(\S+)\s+offline\s+\S+$');
// Skip first line, which is always 'List of devices attached'.
for (String line in output.skip(1)) {
// Skip lines like:
// * daemon not running. starting it now on port 5037 *
// * daemon started successfully *
if (line.startsWith('* daemon '))
continue;
if (line.startsWith('List of devices'))
continue;
if (deviceRegex1.hasMatch(line)) {
Match match = deviceRegex1.firstMatch(line);
String deviceID = match[1];
String productID = match[2];
String modelID = match[3];
String deviceCodeName = match[4];
devices.add(new AndroidDevice(
id: deviceID,
productID: productID,
modelID: modelID,
deviceCodeName: deviceCodeName
));
} else if (deviceRegex2.hasMatch(line)) {
Match match = deviceRegex2.firstMatch(line);
String deviceID = match[1];
devices.add(new AndroidDevice(id: deviceID));
} else if (unauthorizedRegex.hasMatch(line)) {
Match match = unauthorizedRegex.firstMatch(line);
String deviceID = match[1];
logging.warning(
'Device $deviceID is not authorized.\n'
'You might need to check your device for an authorization dialog.'
);
} else if (offlineRegex.hasMatch(line)) {
Match match = offlineRegex.firstMatch(line);
String deviceID = match[1];
logging.warning('Device $deviceID is offline.');
} else {
logging.warning(
'Unexpected failure parsing device information from adb output:\n'
'$line\n'
'Please report a bug at https://github.com/flutter/flutter/issues/new');
}
}
return devices;
}
static String getAndroidSdkPath() {
if (Platform.environment.containsKey('ANDROID_HOME')) {
String androidHomeDir = Platform.environment['ANDROID_HOME'];
if (FileSystemEntity.isDirectorySync(
path.join(androidHomeDir, 'platform-tools'))) {
return androidHomeDir;
} else if (FileSystemEntity.isDirectorySync(
path.join(androidHomeDir, 'sdk', 'platform-tools'))) {
return path.join(androidHomeDir, 'sdk');
} else {
logging.warning('Android SDK not found at $androidHomeDir');
return null;
}
} else {
logging.warning('Android SDK not found. The ANDROID_HOME variable must be set.');
return null;
}
}
static String getAdbPath() {
if (Platform.environment.containsKey('ANDROID_HOME')) {
String androidHomeDir = Platform.environment['ANDROID_HOME'];
String adbPath1 = path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
if (FileSystemEntity.isFileSync(adbPath1)) {
return adbPath1;
} else if (FileSystemEntity.isFileSync(adbPath2)) {
return adbPath2;
} else {
logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' +
'using default path "$_defaultAdbPath"');
return _defaultAdbPath;
}
} else {
return _defaultAdbPath;
}
}
List<String> adbCommandForDevice(List<String> args) {
List<String> result = <String>[adbPath];
if (id != defaultDeviceID) {
result.addAll(['-s', id]);
}
result.addAll(args);
return result;
}
bool _isValidAdbVersion(String adbVersion) {
// Sample output: 'Android Debug Bridge version 1.0.31'
Match versionFields =
new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
if (versionFields != null) {
int majorVersion = int.parse(versionFields[1]);
int minorVersion = int.parse(versionFields[2]);
int patchVersion = int.parse(versionFields[3]);
if (majorVersion > 1) {
return true;
}
if (majorVersion == 1 && minorVersion > 0) {
return true;
}
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
return true;
}
return false;
}
logging.warning(
'Unrecognized adb version string $adbVersion. Skipping version check.');
return true;
}
bool _checkForAdb() {
try {
String adbVersion = runCheckedSync([adbPath, 'version']);
if (_isValidAdbVersion(adbVersion)) {
return true;
}
String locatedAdbPath = runCheckedSync(['which', 'adb']);
logging.severe('"$locatedAdbPath" is too old. '
'Please install version 1.0.32 or later.\n'
'Try setting ANDROID_HOME to the path to your Android SDK install. '
'Android builds are unavailable.');
} catch (e, stack) {
logging.severe('"adb" not found in \$PATH. '
'Please install the Android SDK or set ANDROID_HOME '
'to the path of your Android SDK install.');
logging.info(e);
logging.info(stack);
}
return false;
}
bool _checkForSupportedAndroidVersion() {
try {
// If the server is automatically restarted, then we get irrelevant
// output lines like this, which we want to ignore:
// adb server is out of date. killing..
// * daemon started successfully *
runCheckedSync(adbCommandForDevice(['start-server']));
String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready']));
if (ready.trim() != 'ready') {
logging.info('Android device not found.');
return false;
}
// Sample output: '22'
String sdkVersion =
runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.build.version.sdk']))
.trimRight();
int sdkVersionParsed =
int.parse(sdkVersion, onError: (String source) => null);
if (sdkVersionParsed == null) {
logging.severe('Unexpected response from getprop: "$sdkVersion"');
return false;
}
if (sdkVersionParsed < minApiLevel) {
logging.severe(
'The Android version ($sdkVersion) on the target device is too old. Please '
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
return false;
}
return true;
} catch (e) {
logging.severe('Unexpected failure from adb: ', e);
}
return false;
}
String _getDeviceSha1Path(ApplicationPackage app) {
return '/data/local/tmp/sky.${app.id}.sha1';
}
String _getDeviceApkSha1(ApplicationPackage app) {
return runCheckedSync(adbCommandForDevice(['shell', 'cat', _getDeviceSha1Path(app)]));
}
String _getSourceSha1(ApplicationPackage app) {
var sha1 = new SHA1();
var file = new File(app.localPath);
sha1.add(file.readAsBytesSync());
return CryptoUtils.bytesToHex(sha1.close());
}
String get name => modelID;
@override
bool isAppInstalled(ApplicationPackage app) {
if (!isConnected()) {
return false;
}
if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) == '') {
logging.info(
'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...');
return false;
}
if (_getDeviceApkSha1(app) != _getSourceSha1(app)) {
logging.info(
'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...');
return false;
}
return true;
}
@override
bool installApp(ApplicationPackage app) {
if (!isConnected()) {
logging.info('Android device not connected. Not installing.');
return false;
}
if (!FileSystemEntity.isFileSync(app.localPath)) {
logging.severe('"${app.localPath}" does not exist.');
return false;
}
print('Installing ${app.name} on device.');
runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath]));
runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
return true;
}
void _forwardObservatoryPort() {
// Set up port forwarding for observatory.
String portString = 'tcp:$_observatoryPort';
try {
runCheckedSync(adbCommandForDevice(['forward', portString, portString]));
} catch (e) {
logging.warning('Unable to forward observatory port ($_observatoryPort):\n$e');
}
}
bool startBundle(AndroidApk apk, String bundlePath, {
bool poke: false,
bool checked: true,
bool traceStartup: false,
String route,
bool clearLogs: false
}) {
logging.fine('$this startBundle');
if (!FileSystemEntity.isFileSync(bundlePath)) {
logging.severe('Cannot find $bundlePath');
return false;
}
if (!poke)
_forwardObservatoryPort();
if (clearLogs)
this.clearLogs();
String deviceTmpPath = '/data/local/tmp/dev.flx';
runCheckedSync(adbCommandForDevice(['push', bundlePath, deviceTmpPath]));
List<String> cmd = adbCommandForDevice([
'shell', 'am', 'start',
'-a', 'android.intent.action.RUN',
'-d', deviceTmpPath,
]);
if (checked)
cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
if (traceStartup)
cmd.addAll(['--ez', 'trace-startup', 'true']);
if (route != null)
cmd.addAll(['--es', 'route', route]);
cmd.add(apk.launchActivity);
runCheckedSync(cmd);
return true;
}
@override
Future<bool> startApp(
ApplicationPackage package,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
Map<String, dynamic> platformArgs
}) {
return flx.buildInTempDir(
toolchain,
mainPath: mainPath
).then((flx.DirectoryResult buildResult) {
logging.fine('Starting bundle for $this.');
try {
if (startBundle(
package,
buildResult.localBundlePath,
poke: platformArgs['poke'],
checked: checked,
traceStartup: platformArgs['trace-startup'],
route: route,
clearLogs: platformArgs['clear-logs']
)) {
return true;
} else {
return false;
}
} finally {
buildResult.dispose();
}
});
}
Future<bool> stopApp(ApplicationPackage app) async {
final AndroidApk apk = app;
runSync(adbCommandForDevice(['shell', 'am', 'force-stop', apk.id]));
return true;
}
@override
TargetPlatform get platform => TargetPlatform.android;
void clearLogs() {
runSync(adbCommandForDevice(['logcat', '-c']));
}
Future<int> logs({bool clear: false}) async {
if (!isConnected()) {
return 2;
}
if (clear) {
clearLogs();
}
return await runCommandAndStreamOutput(adbCommandForDevice([
'logcat',
'-v',
'tag', // Only log the tag and the message
'-s',
'flutter:V',
'ActivityManager:W',
'System.err:W',
'*:F',
]), prefix: 'android: ');
}
void startTracing(AndroidApk apk) {
runCheckedSync(adbCommandForDevice([
'shell',
'am',
'broadcast',
'-a',
'${apk.id}.TRACING_START'
]));
}
static String _threeDigits(int n) {
if (n >= 100) return "$n";
if (n >= 10) return "0$n";
return "00$n";
}
static String _twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
static String _logcatDateFormat(DateTime dt) {
// Doing this manually, instead of using package:intl for simplicity.
// adb logcat -T wants "%m-%d %H:%M:%S.%3q"
String m = _twoDigits(dt.month);
String d = _twoDigits(dt.day);
String H = _twoDigits(dt.hour);
String M = _twoDigits(dt.minute);
String S = _twoDigits(dt.second);
String q = _threeDigits(dt.millisecond);
return "$m-$d $H:$M:$S.$q";
}
// TODO(eseidel): This is fragile, there must be a better way!
DateTime timeOnDevice() {
// Careful: Android's date command is super-lame, any arguments are taken as
// attempts to set the timezone and will screw your device.
String output = runCheckedSync(adbCommandForDevice(['shell', 'date'])).trim();
// format: Fri Dec 18 13:22:07 PST 2015
// intl doesn't handle timezones: https://github.com/dart-lang/intl/issues/93
// So we use the local date command to parse dates for us.
String seconds = runSync(['date', '--date', output, '+%s']);
// Although '%s' is supposed to be UTC, date appears to be ignoring the
// timezone in the passed string, so using isUTC: false here.
return new DateTime.fromMillisecondsSinceEpoch(int.parse(seconds) * 1000, isUtc: false);
}
String stopTracing(AndroidApk apk, { String outPath: null }) {
// Workaround for logcat -c not always working:
// http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting
String beforeStop = _logcatDateFormat(timeOnDevice());
runCheckedSync(adbCommandForDevice([
'shell',
'am',
'broadcast',
'-a',
'${apk.id}.TRACING_STOP'
]));
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);
String tracePath = null;
bool isComplete = false;
while (!isComplete) {
String logs = runCheckedSync(adbCommandForDevice(['logcat', '-d', '-T', beforeStop]));
Match fileMatch = traceRegExp.firstMatch(logs);
if (fileMatch != null && fileMatch[1] != null) {
tracePath = fileMatch[1];
}
isComplete = completeRegExp.hasMatch(logs);
}
if (tracePath != null) {
String localPath = (outPath != null) ? outPath : path.basename(tracePath);
runCheckedSync(adbCommandForDevice(['root']));
runSync(adbCommandForDevice(['shell', 'run-as', apk.id, 'chmod', '777', tracePath]));
runCheckedSync(adbCommandForDevice(['pull', tracePath, localPath]));
runSync(adbCommandForDevice(['shell', 'rm', tracePath]));
return localPath;
}
logging.warning('No trace file detected. '
'Did you remember to start the trace before stopping it?');
return null;
}
bool isConnected() => _connected != null ? _connected : _hasValidAndroid;
void setConnected(bool value) {
_connected = value;
}
}
......@@ -9,12 +9,12 @@ import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import '../android/device_android.dart';
import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/logging.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../runner/flutter_command.dart';
import 'start.dart';
......@@ -392,16 +392,13 @@ class ApkCommand extends FlutterCommand {
String mainPath = findMainDartFile(argResults['target']);
// Build the FLX.
int result;
await flx.buildInTempDir(
toolchain,
mainPath: mainPath,
onBundleAvailable: (String localBundlePath) {
result = _buildApk(components, localBundlePath);
}
);
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
return result;
try {
return _buildApk(components, buildResult.localBundlePath);
} finally {
buildResult.dispose();
}
}
}
}
......@@ -7,6 +7,7 @@ import 'dart:convert';
import 'dart:io';
import '../android/adb.dart';
import '../android/device_android.dart';
import '../base/logging.dart';
import '../device.dart';
import '../runner/flutter_command.dart';
......
......@@ -9,10 +9,10 @@ import 'package:args/command_runner.dart';
import 'package:mustache4dart/mustache4dart.dart' as mustache;
import 'package:path/path.dart' as path;
import '../android/android.dart' as android;
import '../artifacts.dart';
import '../base/logging.dart';
import '../base/process.dart';
import '../device.dart';
class InitCommand extends Command {
final String name = 'init';
......@@ -247,7 +247,7 @@ final String _apkManifest = '''
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.{{projectName}}">
<uses-sdk android:minSdkVersion="${AndroidDevice.minApiLevel}" android:targetSdkVersion="21" />
<uses-sdk android:minSdkVersion="${android.minApiLevel}" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name="org.domokit.sky.shell.SkyApplication" android:label="{{projectName}}">
......
......@@ -4,7 +4,8 @@
import 'dart:async';
import '../device.dart';
import '../android/device_android.dart';
import '../ios/device_ios.dart';
import '../runner/flutter_command.dart';
class ListCommand extends FlutterCommand {
......@@ -29,6 +30,8 @@ class ListCommand extends FlutterCommand {
if (details)
print('Android Devices:');
// TODO(devoncarew): We should have a more generic mechanism for device discovery.
// DeviceDiscoveryService? DeviceDiscoveryParticipant?
for (AndroidDevice device in AndroidDevice.getAttachedDevices(devices.android)) {
if (details) {
print('${device.id}\t'
......
......@@ -9,9 +9,7 @@ import 'package:path/path.dart' as path;
import '../application_package.dart';
import '../base/logging.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../runner/flutter_command.dart';
import '../toolchain.dart';
import 'install.dart';
......@@ -138,33 +136,30 @@ Future<int> startApp(
logging.fine('Running build command for $device.');
if (device.platform == TargetPlatform.android) {
await flx.buildInTempDir(
Map<String, dynamic> platformArgs = <String, dynamic>{};
if (poke != null)
platformArgs['poke'] = poke;
if (traceStartup != null)
platformArgs['trace-startup'] = traceStartup;
if (clearLogs != null)
platformArgs['clear-logs'] = clearLogs;
bool result = await device.startApp(
package,
toolchain,
mainPath: mainPath,
onBundleAvailable: (String localBundlePath) {
logging.fine('Starting bundle for $device.');
final AndroidDevice androidDevice = device; // https://github.com/flutter/flutter/issues/1035
if (androidDevice.startBundle(package, localBundlePath,
poke: poke,
checked: checked,
traceStartup: traceStartup,
route: route,
clearLogs: clearLogs
)) {
startedSomething = true;
}
}
checked: checked,
platformArgs: platformArgs
);
} else {
bool result = await device.startApp(package);
if (!result) {
logging.severe('Could not start \'${package.name}\' on \'${device.id}\'');
} else {
startedSomething = true;
}
}
}
if (!startedSomething) {
if (!devices.all.any((device) => device.isConnected())) {
......
......@@ -4,9 +4,9 @@
import 'dart:async';
import '../android/device_android.dart';
import '../application_package.dart';
import '../base/logging.dart';
import '../device.dart';
import '../runner/flutter_command.dart';
class TraceCommand extends FlutterCommand {
......
......@@ -3,21 +3,19 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as path;
import 'android/device_android.dart';
import 'application_package.dart';
import 'base/logging.dart';
import 'base/process.dart';
import 'build_configuration.dart';
import 'ios/device_ios.dart';
import 'toolchain.dart';
abstract class Device {
final String id;
static Map<String, Device> _deviceCache = {};
static Device _unique(String id, Device constructor(String id)) {
static Device unique(String id, Device constructor(String id)) {
return _deviceCache.putIfAbsent(id, () => constructor(id));
}
......@@ -25,7 +23,7 @@ abstract class Device {
_deviceCache.remove(id);
}
Device._(this.id);
Device.fromId(this.id);
String get name;
......@@ -42,984 +40,25 @@ abstract class Device {
Future<int> logs({bool clear: false});
/// Start an app package on the current device
Future<bool> startApp(ApplicationPackage app);
/// Start an app package on the current device.
///
/// [platformArgs] allows callers to pass platform-specific arguments to the
/// start call.
Future<bool> startApp(
ApplicationPackage package,
Toolchain toolchain, {
String mainPath,
String route,
bool checked: true,
Map<String, dynamic> platformArgs
});
/// Stop an app package on the current device
/// Stop an app package on the current device.
Future<bool> stopApp(ApplicationPackage app);
String toString() => '$runtimeType $id';
}
class IOSDevice extends Device {
static final String defaultDeviceID = 'default_ios_id';
static const String _macInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'If you use homebrew, you can install it with '
'"\$ brew install ideviceinstaller".';
static const String _linuxInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'On Ubuntu or Debian, you can install it with '
'"\$ apt-get install ideviceinstaller".';
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;
String _name;
String get name => _name;
factory IOSDevice({String id, String name}) {
IOSDevice device = Device._unique(id ?? defaultDeviceID, (String id) => new IOSDevice._(id));
device._name = name;
return device;
}
IOSDevice._(String id) : 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');
}
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
List<IOSDevice> devices = [];
for (String id in _getAttachedDeviceIDs(mockIOS)) {
String name = _getDeviceName(id, mockIOS);
devices.add(new IOSDevice(id: id, name: name));
}
return devices;
}
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
String listerPath =
(mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
String output;
try {
output = runSync([listerPath, '-l']);
} catch (e) {
return [];
}
return output.trim()
.split('\n')
.where((String s) => s != null && s.length > 0);
}
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
String informerPath = (mockIOS != null)
? mockIOS.informerPath
: _checkForCommand('ideviceinfo');
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
}
static final Map<String, String> _commandMap = {};
static String _checkForCommand(String command,
[String macInstructions = _macInstructions,
String linuxInstructions = _linuxInstructions]) {
return _commandMap.putIfAbsent(command, () {
try {
command = runCheckedSync(['which', command]).trim();
} catch (e) {
if (Platform.isMacOS) {
logging.severe(macInstructions);
} else if (Platform.isLinux) {
logging.severe(linuxInstructions);
} else {
logging.severe('$command is not available on your platform.');
}
}
return command;
});
}
@override
bool installApp(ApplicationPackage app) {
try {
if (id == defaultDeviceID) {
runCheckedSync([installerPath, '-i', app.localPath]);
} else {
runCheckedSync([installerPath, '-u', id, '-i', app.localPath]);
}
return true;
} catch (e) {
return false;
}
return false;
}
@override
bool isConnected() {
Iterable<String> ids = _getAttachedDeviceIDs();
for (String id in ids) {
if (id == this.id || this.id == defaultDeviceID) {
return true;
}
}
return false;
}
@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) async {
logging.fine("Attempting to build and install ${app.name} on $id");
// Step 1: Install the precompiled application if necessary
bool buildResult = await _buildIOSXcodeProject(app, true);
if (!buildResult) {
logging.severe('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 = await bundle.exists();
if (!bundleExists) {
logging.severe('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) {
logging.severe('Could not install ${bundle.path} on $id');
return false;
}
logging.fine('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([
pusherPath,
'-t',
'1',
'--bundle_id',
app.id,
'--upload',
localFile,
'--to',
targetFile
]);
return true;
} else {
return false;
}
return false;
}
@override
TargetPlatform get platform => TargetPlatform.iOS;
/// Note that clear is not supported on iOS at this time.
Future<int> logs({bool clear: false}) async {
if (!isConnected()) {
return 2;
}
return await runCommandAndStreamOutput([loggerPath],
prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*'));
}
}
class IOSSimulator extends Device {
static final String defaultDeviceID = 'default_ios_sim_id';
static const String _macInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'If you use homebrew, you can install it with '
'"\$ brew install ideviceinstaller".';
static String _xcrunPath = path.join('/usr', 'bin', 'xcrun');
String _iOSSimPath;
String get iOSSimPath => _iOSSimPath;
String get xcrunPath => _xcrunPath;
String _name;
String get name => _name;
factory IOSSimulator({String id, String name, String iOSSimulatorPath}) {
IOSSimulator device = Device._unique(id ?? defaultDeviceID, (String id) => new IOSSimulator._(id));
device._name = name;
if (iOSSimulatorPath == null) {
iOSSimulatorPath = path.join(
'/Applications', 'iOS Simulator.app', 'Contents', 'MacOS', 'iOS Simulator'
);
}
device._iOSSimPath = iOSSimulatorPath;
return device;
}
IOSSimulator._(String id) : super._(id);
static _IOSSimulatorInfo _getRunningSimulatorInfo([IOSSimulator mockIOS]) {
String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath;
String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']);
Match match;
// iPhone 6s Plus (8AC808E1-6BAE-4153-BBC5-77F83814D414) (Booted)
Iterable<Match> matches = new RegExp(
r'[\W]*(.*) \(([^\)]+)\) \(Booted\)',
multiLine: true
).allMatches(output);
if (matches.length > 1) {
// More than one simulator is listed as booted, which is not allowed but
// sometimes happens erroneously. Kill them all because we don't know
// which one is actually running.
logging.warning('Multiple running simulators were detected, '
'which is not supposed to happen.');
for (Match match in matches) {
if (match.groupCount > 0) {
// TODO: We're killing simulator devices inside an accessor method;
// we probably shouldn't be changing state here.
logging.warning('Killing simulator ${match.group(1)}');
runSync([xcrunPath, 'simctl', 'shutdown', match.group(2)]);
}
}
} else if (matches.length == 1) {
match = matches.first;
}
if (match != null && match.groupCount > 0) {
return new _IOSSimulatorInfo(match.group(2), match.group(1));
} else {
logging.info('No running simulators found');
return null;
}
}
String _getSimulatorPath() {
String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id;
String homeDirectory = path.absolute(Platform.environment['HOME']);
if (deviceID == null)
return null;
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
}
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
String simulatorPath = _getSimulatorPath();
if (simulatorPath == null)
return null;
return path.join(simulatorPath, 'data');
}
static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
List<IOSSimulator> devices = [];
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(mockIOS);
if (deviceInfo != null)
devices.add(new IOSSimulator(id: deviceInfo.id, name: deviceInfo.name));
return devices;
}
Future<bool> boot() async {
if (!Platform.isMacOS)
return false;
if (isConnected())
return true;
if (id == defaultDeviceID) {
runDetached([iOSSimPath]);
Future<bool> checkConnection([int attempts = 20]) async {
if (attempts == 0) {
logging.info('Timed out waiting for iOS Simulator $id to boot.');
return false;
}
if (!isConnected()) {
logging.info('Waiting for iOS Simulator $id to boot...');
return await new Future.delayed(new Duration(milliseconds: 500),
() => checkConnection(attempts - 1));
}
return true;
}
return await checkConnection();
} else {
try {
runCheckedSync([xcrunPath, 'simctl', 'boot', id]);
} catch (e) {
logging.warning('Unable to boot iOS Simulator $id: ', e);
return false;
}
}
return false;
}
@override
bool installApp(ApplicationPackage app) {
if (!isConnected())
return false;
try {
if (id == defaultDeviceID) {
runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]);
} else {
runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]);
}
return true;
} catch (e) {
return false;
}
}
@override
bool isConnected() {
if (!Platform.isMacOS)
return false;
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo();
if (deviceInfo == null) {
return false;
} else if (deviceInfo.id == defaultDeviceID) {
return true;
} else {
return _getRunningSimulatorInfo()?.id == id;
}
}
@override
bool isAppInstalled(ApplicationPackage app) {
try {
String simulatorHomeDirectory = _getSimulatorAppHomeDirectory(app);
return FileSystemEntity.isDirectorySync(simulatorHomeDirectory);
} catch (e) {
return false;
}
}
@override
Future<bool> startApp(ApplicationPackage app) async {
logging.fine('Building ${app.name} for $id');
// Step 1: Build the Xcode project
bool buildResult = await _buildIOSXcodeProject(app, false);
if (!buildResult) {
logging.severe('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) {
logging.severe('Could not find the built application bundle at ${bundle.path}');
return false;
}
// Step 3: Install the updated bundle to the simulator
int installResult = await runCommandAndStreamOutput([
xcrunPath,
'simctl',
'install',
id == defaultDeviceID ? 'booted' : id,
path.absolute(bundle.path)
]);
if (installResult != 0) {
logging.severe('Could not install the application bundle on the simulator');
return false;
}
// Step 4: Launch the updated application in the simulator
int launchResult = await runCommandAndStreamOutput([
xcrunPath,
'simctl',
'launch',
id == defaultDeviceID ? 'booted' : id,
app.id
]);
if (launchResult != 0) {
logging.severe('Could not launch the freshly installed application on the simulator');
return false;
}
logging.fine('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(['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
return true;
}
return false;
}
@override
TargetPlatform get platform => TargetPlatform.iOSSimulator;
Future<int> logs({bool clear: false}) async {
if (!isConnected())
return 2;
String homeDirectory = path.absolute(Platform.environment['HOME']);
String simulatorDeviceID = _getRunningSimulatorInfo().id;
String logFilePath = path.join(
homeDirectory, 'Library', 'Logs', 'CoreSimulator', simulatorDeviceID, 'system.log'
);
if (clear)
runSync(['rm', logFilePath]);
return await runCommandAndStreamOutput(
['tail', '-f', logFilePath],
prefix: 'iOS sim: ',
filter: new RegExp(r'.*SkyShell.*')
);
}
}
class _IOSSimulatorInfo {
final String id;
final String name;
_IOSSimulatorInfo(this.id, this.name);
}
class AndroidDevice extends Device {
static const int minApiLevel = 16;
static const String minVersionName = 'Jelly Bean';
static const String minVersionText = '4.1.x';
static const String _defaultAdbPath = 'adb';
static const int _observatoryPort = 8181;
static final String defaultDeviceID = 'default_android_device';
String productID;
String modelID;
String deviceCodeName;
bool _connected;
String _adbPath;
String get adbPath => _adbPath;
bool _hasAdb = false;
bool _hasValidAndroid = false;
factory AndroidDevice({
String id: null,
String productID: null,
String modelID: null,
String deviceCodeName: null,
bool connected
}) {
AndroidDevice device = Device._unique(id ?? defaultDeviceID, (String id) => new AndroidDevice._(id));
device.productID = productID;
device.modelID = modelID;
device.deviceCodeName = deviceCodeName;
if (connected != null)
device._connected = connected;
return device;
}
/// mockAndroid argument is only to facilitate testing with mocks, so that
/// we don't have to rely on the test setup having adb available to it.
static List<AndroidDevice> getAttachedDevices([AndroidDevice mockAndroid]) {
List<AndroidDevice> devices = [];
String adbPath = (mockAndroid != null) ? mockAndroid.adbPath : getAdbPath();
try {
runCheckedSync([adbPath, 'version']);
} catch (e) {
logging.severe('Unable to find adb. Is "adb" in your path?');
return devices;
}
List<String> output = runSync([adbPath, 'devices', '-l']).trim().split('\n');
// 015d172c98400a03 device usb:340787200X product:nakasi model:Nexus_7 device:grouper
RegExp deviceRegex1 = new RegExp(
r'^(\S+)\s+device\s+.*product:(\S+)\s+model:(\S+)\s+device:(\S+)$');
// 0149947A0D01500C device usb:340787200X
RegExp deviceRegex2 = new RegExp(r'^(\S+)\s+device\s+\S+$');
RegExp unauthorizedRegex = new RegExp(r'^(\S+)\s+unauthorized\s+\S+$');
RegExp offlineRegex = new RegExp(r'^(\S+)\s+offline\s+\S+$');
// Skip first line, which is always 'List of devices attached'.
for (String line in output.skip(1)) {
// Skip lines like:
// * daemon not running. starting it now on port 5037 *
// * daemon started successfully *
if (line.startsWith('* daemon '))
continue;
if (line.startsWith('List of devices'))
continue;
if (deviceRegex1.hasMatch(line)) {
Match match = deviceRegex1.firstMatch(line);
String deviceID = match[1];
String productID = match[2];
String modelID = match[3];
String deviceCodeName = match[4];
devices.add(new AndroidDevice(
id: deviceID,
productID: productID,
modelID: modelID,
deviceCodeName: deviceCodeName
));
} else if (deviceRegex2.hasMatch(line)) {
Match match = deviceRegex2.firstMatch(line);
String deviceID = match[1];
devices.add(new AndroidDevice(id: deviceID));
} else if (unauthorizedRegex.hasMatch(line)) {
Match match = unauthorizedRegex.firstMatch(line);
String deviceID = match[1];
logging.warning(
'Device $deviceID is not authorized.\n'
'You might need to check your device for an authorization dialog.'
);
} else if (offlineRegex.hasMatch(line)) {
Match match = offlineRegex.firstMatch(line);
String deviceID = match[1];
logging.warning('Device $deviceID is offline.');
} else {
logging.warning(
'Unexpected failure parsing device information from adb output:\n'
'$line\n'
'Please report a bug at https://github.com/flutter/flutter/issues/new');
}
}
return devices;
}
AndroidDevice._(id) : super._(id) {
_adbPath = getAdbPath();
_hasAdb = _checkForAdb();
// Checking for [minApiName] only needs to be done if we are starting an
// app, but it has an important side effect, which is to discard any
// progress messages if the adb server is restarted.
_hasValidAndroid = _checkForSupportedAndroidVersion();
if (!_hasAdb || !_hasValidAndroid) {
logging.warning('Unable to run on Android.');
}
}
static String getAndroidSdkPath() {
if (Platform.environment.containsKey('ANDROID_HOME')) {
String androidHomeDir = Platform.environment['ANDROID_HOME'];
if (FileSystemEntity.isDirectorySync(
path.join(androidHomeDir, 'platform-tools'))) {
return androidHomeDir;
} else if (FileSystemEntity.isDirectorySync(
path.join(androidHomeDir, 'sdk', 'platform-tools'))) {
return path.join(androidHomeDir, 'sdk');
} else {
logging.warning('Android SDK not found at $androidHomeDir');
return null;
}
} else {
logging.warning('Android SDK not found. The ANDROID_HOME variable must be set.');
return null;
}
}
static String getAdbPath() {
if (Platform.environment.containsKey('ANDROID_HOME')) {
String androidHomeDir = Platform.environment['ANDROID_HOME'];
String adbPath1 = path.join(androidHomeDir, 'sdk', 'platform-tools', 'adb');
String adbPath2 = path.join(androidHomeDir, 'platform-tools', 'adb');
if (FileSystemEntity.isFileSync(adbPath1)) {
return adbPath1;
} else if (FileSystemEntity.isFileSync(adbPath2)) {
return adbPath2;
} else {
logging.info('"adb" not found at\n "$adbPath1" or\n "$adbPath2"\n' +
'using default path "$_defaultAdbPath"');
return _defaultAdbPath;
}
} else {
return _defaultAdbPath;
}
}
List<String> adbCommandForDevice(List<String> args) {
List<String> result = <String>[adbPath];
if (id != defaultDeviceID) {
result.addAll(['-s', id]);
}
result.addAll(args);
return result;
}
bool _isValidAdbVersion(String adbVersion) {
// Sample output: 'Android Debug Bridge version 1.0.31'
Match versionFields =
new RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(adbVersion);
if (versionFields != null) {
int majorVersion = int.parse(versionFields[1]);
int minorVersion = int.parse(versionFields[2]);
int patchVersion = int.parse(versionFields[3]);
if (majorVersion > 1) {
return true;
}
if (majorVersion == 1 && minorVersion > 0) {
return true;
}
if (majorVersion == 1 && minorVersion == 0 && patchVersion >= 32) {
return true;
}
return false;
}
logging.warning(
'Unrecognized adb version string $adbVersion. Skipping version check.');
return true;
}
bool _checkForAdb() {
try {
String adbVersion = runCheckedSync([adbPath, 'version']);
if (_isValidAdbVersion(adbVersion)) {
return true;
}
String locatedAdbPath = runCheckedSync(['which', 'adb']);
logging.severe('"$locatedAdbPath" is too old. '
'Please install version 1.0.32 or later.\n'
'Try setting ANDROID_HOME to the path to your Android SDK install. '
'Android builds are unavailable.');
} catch (e, stack) {
logging.severe('"adb" not found in \$PATH. '
'Please install the Android SDK or set ANDROID_HOME '
'to the path of your Android SDK install.');
logging.info(e);
logging.info(stack);
}
return false;
}
bool _checkForSupportedAndroidVersion() {
try {
// If the server is automatically restarted, then we get irrelevant
// output lines like this, which we want to ignore:
// adb server is out of date. killing..
// * daemon started successfully *
runCheckedSync(adbCommandForDevice(['start-server']));
String ready = runSync(adbCommandForDevice(['shell', 'echo', 'ready']));
if (ready.trim() != 'ready') {
logging.info('Android device not found.');
return false;
}
// Sample output: '22'
String sdkVersion =
runCheckedSync(adbCommandForDevice(['shell', 'getprop', 'ro.build.version.sdk']))
.trimRight();
int sdkVersionParsed =
int.parse(sdkVersion, onError: (String source) => null);
if (sdkVersionParsed == null) {
logging.severe('Unexpected response from getprop: "$sdkVersion"');
return false;
}
if (sdkVersionParsed < minApiLevel) {
logging.severe(
'The Android version ($sdkVersion) on the target device is too old. Please '
'use a $minVersionName (version $minApiLevel / $minVersionText) device or later.');
return false;
}
return true;
} catch (e) {
logging.severe('Unexpected failure from adb: ', e);
}
return false;
}
String _getDeviceSha1Path(ApplicationPackage app) {
return '/data/local/tmp/sky.${app.id}.sha1';
}
String _getDeviceApkSha1(ApplicationPackage app) {
return runCheckedSync(adbCommandForDevice(['shell', 'cat', _getDeviceSha1Path(app)]));
}
String _getSourceSha1(ApplicationPackage app) {
var sha1 = new SHA1();
var file = new File(app.localPath);
sha1.add(file.readAsBytesSync());
return CryptoUtils.bytesToHex(sha1.close());
}
String get name => modelID;
@override
bool isAppInstalled(ApplicationPackage app) {
if (!isConnected()) {
return false;
}
if (runCheckedSync(adbCommandForDevice(['shell', 'pm', 'path', app.id])) == '') {
logging.info(
'TODO(iansf): move this log to the caller. ${app.name} is not on the device. Installing now...');
return false;
}
if (_getDeviceApkSha1(app) != _getSourceSha1(app)) {
logging.info(
'TODO(iansf): move this log to the caller. ${app.name} is out of date. Installing now...');
return false;
}
return true;
}
@override
bool installApp(ApplicationPackage app) {
if (!isConnected()) {
logging.info('Android device not connected. Not installing.');
return false;
}
if (!FileSystemEntity.isFileSync(app.localPath)) {
logging.severe('"${app.localPath}" does not exist.');
return false;
}
print('Installing ${app.name} on device.');
runCheckedSync(adbCommandForDevice(['install', '-r', app.localPath]));
runCheckedSync(adbCommandForDevice(['shell', 'echo', '-n', _getSourceSha1(app), '>', _getDeviceSha1Path(app)]));
return true;
}
void _forwardObservatoryPort() {
// Set up port forwarding for observatory.
String portString = 'tcp:$_observatoryPort';
try {
runCheckedSync(adbCommandForDevice(['forward', portString, portString]));
} catch (e) {
logging.warning('Unable to forward observatory port ($_observatoryPort):\n$e');
}
}
bool startBundle(AndroidApk apk, String bundlePath, {
bool poke: false,
bool checked: true,
bool traceStartup: false,
String route,
bool clearLogs: false
}) {
logging.fine('$this startBundle');
if (!FileSystemEntity.isFileSync(bundlePath)) {
logging.severe('Cannot find $bundlePath');
return false;
}
if (!poke)
_forwardObservatoryPort();
if (clearLogs)
this.clearLogs();
String deviceTmpPath = '/data/local/tmp/dev.flx';
runCheckedSync(adbCommandForDevice(['push', bundlePath, deviceTmpPath]));
List<String> cmd = adbCommandForDevice([
'shell', 'am', 'start',
'-a', 'android.intent.action.RUN',
'-d', deviceTmpPath,
]);
if (checked)
cmd.addAll(['--ez', 'enable-checked-mode', 'true']);
if (traceStartup)
cmd.addAll(['--ez', 'trace-startup', 'true']);
if (route != null)
cmd.addAll(['--es', 'route', route]);
cmd.add(apk.launchActivity);
runCheckedSync(cmd);
return true;
}
@override
Future<bool> startApp(ApplicationPackage app) async {
// Android currently has to be started with startBundle(...).
assert(false);
return false;
}
Future<bool> stopApp(ApplicationPackage app) async {
final AndroidApk apk = app;
runSync(adbCommandForDevice(['shell', 'am', 'force-stop', apk.id]));
return true;
}
@override
TargetPlatform get platform => TargetPlatform.android;
void clearLogs() {
runSync(adbCommandForDevice(['logcat', '-c']));
}
Future<int> logs({bool clear: false}) async {
if (!isConnected()) {
return 2;
}
if (clear) {
clearLogs();
}
return await runCommandAndStreamOutput(adbCommandForDevice([
'logcat',
'-v',
'tag', // Only log the tag and the message
'-s',
'flutter:V',
'ActivityManager:W',
'System.err:W',
'*:F',
]), prefix: 'android: ');
}
void startTracing(AndroidApk apk) {
runCheckedSync(adbCommandForDevice([
'shell',
'am',
'broadcast',
'-a',
'${apk.id}.TRACING_START'
]));
}
static String _threeDigits(int n) {
if (n >= 100) return "$n";
if (n >= 10) return "0$n";
return "00$n";
}
static String _twoDigits(int n) {
if (n >= 10) return "$n";
return "0$n";
}
static String _logcatDateFormat(DateTime dt) {
// Doing this manually, instead of using package:intl for simplicity.
// adb logcat -T wants "%m-%d %H:%M:%S.%3q"
String m = _twoDigits(dt.month);
String d = _twoDigits(dt.day);
String H = _twoDigits(dt.hour);
String M = _twoDigits(dt.minute);
String S = _twoDigits(dt.second);
String q = _threeDigits(dt.millisecond);
return "$m-$d $H:$M:$S.$q";
}
// TODO(eseidel): This is fragile, there must be a better way!
DateTime timeOnDevice() {
// Careful: Android's date command is super-lame, any arguments are taken as
// attempts to set the timezone and will screw your device.
String output = runCheckedSync(adbCommandForDevice(['shell', 'date'])).trim();
// format: Fri Dec 18 13:22:07 PST 2015
// intl doesn't handle timezones: https://github.com/dart-lang/intl/issues/93
// So we use the local date command to parse dates for us.
String seconds = runSync(['date', '--date', output, '+%s']);
// Although '%s' is supposed to be UTC, date appears to be ignoring the
// timezone in the passed string, so using isUTC: false here.
return new DateTime.fromMillisecondsSinceEpoch(int.parse(seconds) * 1000, isUtc: false);
}
String stopTracing(AndroidApk apk, { String outPath: null }) {
// Workaround for logcat -c not always working:
// http://stackoverflow.com/questions/25645012/logcat-on-android-l-not-clearing-after-unplugging-and-reconnecting
String beforeStop = _logcatDateFormat(timeOnDevice());
runCheckedSync(adbCommandForDevice([
'shell',
'am',
'broadcast',
'-a',
'${apk.id}.TRACING_STOP'
]));
RegExp traceRegExp = new RegExp(r'Saving trace to (\S+)', multiLine: true);
RegExp completeRegExp = new RegExp(r'Trace complete', multiLine: true);
String tracePath = null;
bool isComplete = false;
while (!isComplete) {
String logs = runCheckedSync(adbCommandForDevice(['logcat', '-d', '-T', beforeStop]));
Match fileMatch = traceRegExp.firstMatch(logs);
if (fileMatch != null && fileMatch[1] != null) {
tracePath = fileMatch[1];
}
isComplete = completeRegExp.hasMatch(logs);
}
if (tracePath != null) {
String localPath = (outPath != null) ? outPath : path.basename(tracePath);
runCheckedSync(adbCommandForDevice(['root']));
runSync(adbCommandForDevice(['shell', 'run-as', apk.id, 'chmod', '777', tracePath]));
runCheckedSync(adbCommandForDevice(['pull', tracePath, localPath]));
runSync(adbCommandForDevice(['shell', 'rm', tracePath]));
return localPath;
}
logging.warning('No trace file detected. '
'Did you remember to start the trace before stopping it?');
return null;
}
bool isConnected() => _connected != null ? _connected : _hasValidAndroid;
void setConnected(bool value) {
_connected = value;
}
}
class DeviceStore {
final AndroidDevice android;
final IOSDevice iOS;
......@@ -1098,17 +137,3 @@ class DeviceStore {
return new DeviceStore(android: android, iOS: iOS, iOSSimulator: iOSSimulator);
}
}
Future<bool> _buildIOSXcodeProject(ApplicationPackage app, bool isDevice) async {
List<String> command = [
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
];
if (!isDevice) {
command.addAll(['-sdk', 'iphonesimulator']);
}
int result = await runCommandAndStreamOutput(command,
workingDirectory: app.localPath);
return result == 0;
}
......@@ -114,14 +114,13 @@ ArchiveFile _createSnapshotFile(String snapshotPath) {
return new ArchiveFile(_kSnapshotKey, content.length, content);
}
Future<int> buildInTempDir(
/// Build the flx in a temp dir and return `localBundlePath` on success.
Future<DirectoryResult> buildInTempDir(
Toolchain toolchain, {
String mainPath: defaultMainPath,
void onBundleAvailable(String bundlePath)
String mainPath: defaultMainPath
}) async {
int result;
Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');
try {
String localBundlePath = path.join(tempDir.path, 'app.flx');
String localSnapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
result = await build(
......@@ -131,11 +130,22 @@ Future<int> buildInTempDir(
mainPath: mainPath
);
if (result == 0)
onBundleAvailable(localBundlePath);
} finally {
tempDir.deleteSync(recursive: true);
return new DirectoryResult(tempDir, localBundlePath);
else
throw result;
}
/// The result from [buildInTempDir]. Note that this object should be disposed after use.
class DirectoryResult {
final Directory directory;
final String localBundlePath;
DirectoryResult(this.directory, this.localBundlePath);
/// Call this to delete the temporary directory.
void dispose() {
directory.deleteSync(recursive: true);
}
return result;
}
Future<int> build(
......
// 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/logging.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../toolchain.dart';
class IOSDevice extends Device {
static final String defaultDeviceID = 'default_ios_id';
static const String _macInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'If you use homebrew, you can install it with '
'"\$ brew install ideviceinstaller".';
static const String _linuxInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'On Ubuntu or Debian, you can install it with '
'"\$ apt-get install ideviceinstaller".';
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;
String _name;
String get name => _name;
factory IOSDevice({String id, String name}) {
IOSDevice device = Device.unique(id ?? defaultDeviceID, (String id) => new IOSDevice.fromId(id));
device._name = name;
return device;
}
IOSDevice.fromId(String id) : super.fromId(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');
}
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
List<IOSDevice> devices = [];
for (String id in _getAttachedDeviceIDs(mockIOS)) {
String name = _getDeviceName(id, mockIOS);
devices.add(new IOSDevice(id: id, name: name));
}
return devices;
}
static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
String listerPath =
(mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
String output;
try {
output = runSync([listerPath, '-l']);
} catch (e) {
return [];
}
return output.trim()
.split('\n')
.where((String s) => s != null && s.length > 0);
}
static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
String informerPath = (mockIOS != null)
? mockIOS.informerPath
: _checkForCommand('ideviceinfo');
return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]);
}
static final Map<String, String> _commandMap = {};
static String _checkForCommand(
String command, [
String macInstructions = _macInstructions,
String linuxInstructions = _linuxInstructions
]) {
return _commandMap.putIfAbsent(command, () {
try {
command = runCheckedSync(['which', command]).trim();
} catch (e) {
if (Platform.isMacOS) {
logging.severe(macInstructions);
} else if (Platform.isLinux) {
logging.severe(linuxInstructions);
} else {
logging.severe('$command is not available on your platform.');
}
}
return command;
});
}
@override
bool installApp(ApplicationPackage app) {
try {
if (id == defaultDeviceID) {
runCheckedSync([installerPath, '-i', app.localPath]);
} else {
runCheckedSync([installerPath, '-u', id, '-i', app.localPath]);
}
return true;
} catch (e) {
return false;
}
return false;
}
@override
bool isConnected() {
Iterable<String> ids = _getAttachedDeviceIDs();
for (String id in ids) {
if (id == this.id || this.id == defaultDeviceID) {
return true;
}
}
return false;
}
@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,
Map<String, dynamic> platformArgs
}) async {
// TODO: Use checked, mainPath, route
logging.fine('Building ${app.name} for $id');
// Step 1: Install the precompiled application if necessary
bool buildResult = await _buildIOSXcodeProject(app, true);
if (!buildResult) {
logging.severe('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 = await bundle.exists();
if (!bundleExists) {
logging.severe('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) {
logging.severe('Could not install ${bundle.path} on $id');
return false;
}
logging.fine('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([
pusherPath,
'-t',
'1',
'--bundle_id',
app.id,
'--upload',
localFile,
'--to',
targetFile
]);
return true;
} else {
return false;
}
return false;
}
@override
TargetPlatform get platform => TargetPlatform.iOS;
/// Note that clear is not supported on iOS at this time.
Future<int> logs({bool clear: false}) async {
if (!isConnected()) {
return 2;
}
return await runCommandAndStreamOutput([loggerPath],
prefix: 'iOS dev: ', filter: new RegExp(r'.*SkyShell.*'));
}
}
class IOSSimulator extends Device {
static final String defaultDeviceID = 'default_ios_sim_id';
static const String _macInstructions =
'To work with iOS devices, please install ideviceinstaller. '
'If you use homebrew, you can install it with '
'"\$ brew install ideviceinstaller".';
static String _xcrunPath = path.join('/usr', 'bin', 'xcrun');
String _iOSSimPath;
String get iOSSimPath => _iOSSimPath;
String get xcrunPath => _xcrunPath;
String _name;
String get name => _name;
factory IOSSimulator({String id, String name, String iOSSimulatorPath}) {
IOSSimulator device = Device.unique(id ?? defaultDeviceID, (String id) => new IOSSimulator.fromId(id));
device._name = name;
if (iOSSimulatorPath == null) {
iOSSimulatorPath = path.join(
'/Applications', 'iOS Simulator.app', 'Contents', 'MacOS', 'iOS Simulator'
);
}
device._iOSSimPath = iOSSimulatorPath;
return device;
}
IOSSimulator.fromId(String id) : super.fromId(id);
static _IOSSimulatorInfo _getRunningSimulatorInfo([IOSSimulator mockIOS]) {
String xcrunPath = mockIOS != null ? mockIOS.xcrunPath : _xcrunPath;
String output = runCheckedSync([xcrunPath, 'simctl', 'list', 'devices']);
Match match;
// iPhone 6s Plus (8AC808E1-6BAE-4153-BBC5-77F83814D414) (Booted)
Iterable<Match> matches = new RegExp(
r'[\W]*(.*) \(([^\)]+)\) \(Booted\)',
multiLine: true
).allMatches(output);
if (matches.length > 1) {
// More than one simulator is listed as booted, which is not allowed but
// sometimes happens erroneously. Kill them all because we don't know
// which one is actually running.
logging.warning('Multiple running simulators were detected, '
'which is not supposed to happen.');
for (Match match in matches) {
if (match.groupCount > 0) {
// TODO: We're killing simulator devices inside an accessor method;
// we probably shouldn't be changing state here.
logging.warning('Killing simulator ${match.group(1)}');
runSync([xcrunPath, 'simctl', 'shutdown', match.group(2)]);
}
}
} else if (matches.length == 1) {
match = matches.first;
}
if (match != null && match.groupCount > 0) {
return new _IOSSimulatorInfo(match.group(2), match.group(1));
} else {
logging.info('No running simulators found');
return null;
}
}
String _getSimulatorPath() {
String deviceID = id == defaultDeviceID ? _getRunningSimulatorInfo()?.id : id;
String homeDirectory = path.absolute(Platform.environment['HOME']);
if (deviceID == null)
return null;
return path.join(homeDirectory, 'Library', 'Developer', 'CoreSimulator', 'Devices', deviceID);
}
String _getSimulatorAppHomeDirectory(ApplicationPackage app) {
String simulatorPath = _getSimulatorPath();
if (simulatorPath == null)
return null;
return path.join(simulatorPath, 'data');
}
static List<IOSSimulator> getAttachedDevices([IOSSimulator mockIOS]) {
List<IOSSimulator> devices = [];
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo(mockIOS);
if (deviceInfo != null)
devices.add(new IOSSimulator(id: deviceInfo.id, name: deviceInfo.name));
return devices;
}
Future<bool> boot() async {
if (!Platform.isMacOS)
return false;
if (isConnected())
return true;
if (id == defaultDeviceID) {
runDetached([iOSSimPath]);
Future<bool> checkConnection([int attempts = 20]) async {
if (attempts == 0) {
logging.info('Timed out waiting for iOS Simulator $id to boot.');
return false;
}
if (!isConnected()) {
logging.info('Waiting for iOS Simulator $id to boot...');
return await new Future.delayed(new Duration(milliseconds: 500),
() => checkConnection(attempts - 1));
}
return true;
}
return await checkConnection();
} else {
try {
runCheckedSync([xcrunPath, 'simctl', 'boot', id]);
} catch (e) {
logging.warning('Unable to boot iOS Simulator $id: ', e);
return false;
}
}
return false;
}
@override
bool installApp(ApplicationPackage app) {
if (!isConnected())
return false;
try {
if (id == defaultDeviceID) {
runCheckedSync([xcrunPath, 'simctl', 'install', 'booted', app.localPath]);
} else {
runCheckedSync([xcrunPath, 'simctl', 'install', id, app.localPath]);
}
return true;
} catch (e) {
return false;
}
}
@override
bool isConnected() {
if (!Platform.isMacOS)
return false;
_IOSSimulatorInfo deviceInfo = _getRunningSimulatorInfo();
if (deviceInfo == null) {
return false;
} else if (deviceInfo.id == defaultDeviceID) {
return true;
} else {
return _getRunningSimulatorInfo()?.id == id;
}
}
@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,
Map<String, dynamic> platformArgs
}) async {
// TODO: Use checked, mainPath, route
logging.fine('Building ${app.name} for $id');
// Step 1: Build the Xcode project
bool buildResult = await _buildIOSXcodeProject(app, false);
if (!buildResult) {
logging.severe('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) {
logging.severe('Could not find the built application bundle at ${bundle.path}');
return false;
}
// Step 3: Install the updated bundle to the simulator
int installResult = await runCommandAndStreamOutput([
xcrunPath,
'simctl',
'install',
id == defaultDeviceID ? 'booted' : id,
path.absolute(bundle.path)
]);
if (installResult != 0) {
logging.severe('Could not install the application bundle on the simulator');
return false;
}
// Step 4: Launch the updated application in the simulator
int launchResult = await runCommandAndStreamOutput([
xcrunPath,
'simctl',
'launch',
id == defaultDeviceID ? 'booted' : id,
app.id
]);
if (launchResult != 0) {
logging.severe('Could not launch the freshly installed application on the simulator');
return false;
}
logging.fine('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(['cp', localFile, path.join(simulatorHomeDirectory, targetFile)]);
return true;
}
return false;
}
@override
TargetPlatform get platform => TargetPlatform.iOSSimulator;
Future<int> logs({bool clear: false}) async {
if (!isConnected())
return 2;
String homeDirectory = path.absolute(Platform.environment['HOME']);
String simulatorDeviceID = _getRunningSimulatorInfo().id;
String logFilePath = path.join(
homeDirectory, 'Library', 'Logs', 'CoreSimulator', simulatorDeviceID, 'system.log'
);
if (clear)
runSync(['rm', logFilePath]);
return await runCommandAndStreamOutput(
['tail', '-f', logFilePath],
prefix: 'iOS sim: ',
filter: new RegExp(r'.*SkyShell.*')
);
}
}
class _IOSSimulatorInfo {
final String id;
final String name;
_IOSSimulatorInfo(this.id, this.name);
}
Future<bool> _buildIOSXcodeProject(ApplicationPackage app, bool isDevice) async {
List<String> command = [
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
];
if (!isDevice) {
command.addAll(['-sdk', 'iphonesimulator']);
}
int result = await runCommandAndStreamOutput(command,
workingDirectory: app.localPath);
return result == 0;
}
......@@ -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/device.dart';
import 'package:flutter_tools/src/android/device_android.dart';
import 'package:test/test.dart';
main() => defineTests();
......
......@@ -2,9 +2,11 @@
// 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/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/runner/flutter_command.dart';
import 'package:flutter_tools/src/toolchain.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