blob: df8d16b0e4d2195b764ebcaad8c73db9d3045c25 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../application_package.dart';
import '../base/os.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../device.dart';
import '../globals.dart';
import '../observatory.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');
@override
bool get supportsPlatform => Platform.isMacOS;
@override
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;
@override
final String name;
_IOSDeviceLogReader _logReader;
_IOSDevicePortForwarder _portForwarder;
@override
bool get isLocalEmulator => false;
@override
bool get supportsStartPaused => false;
static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
if (!doctor.iosWorkflow.hasIDeviceId)
return <IOSDevice>[];
List<IOSDevice> devices = <IOSDevice>[];
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(<String>[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(<String>[informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
}
static final Map<String, String> _commandMap = <String, String>{};
static String _checkForCommand(
String command, [
String macInstructions = _ideviceinstallerInstructions
]) {
return _commandMap.putIfAbsent(command, () {
try {
command = runCheckedSync(<String>['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 isAppInstalled(ApplicationPackage app) {
try {
String apps = runCheckedSync(<String>[installerPath, '--list-apps']);
if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
return true;
}
} catch (e) {
return false;
}
return false;
}
@override
bool installApp(ApplicationPackage app) {
IOSApp iosApp = app;
Directory bundle = new Directory(iosApp.deviceBundlePath);
if (!bundle.existsSync()) {
printError("Could not find application bundle at ${bundle.path}; have you run 'flutter build ios'?");
return false;
}
try {
runCheckedSync(<String>[installerPath, '-i', iosApp.deviceBundlePath]);
return true;
} catch (e) {
return false;
}
}
@override
bool uninstallApp(ApplicationPackage app) {
try {
runCheckedSync(<String>[installerPath, '-U', app.id]);
return true;
} catch (e) {
return false;
}
}
@override
bool isSupported() => true;
@override
Future<LaunchResult> startApp(
ApplicationPackage app,
BuildMode mode, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, dynamic> platformArgs
}) async {
// TODO(chinmaygarde): Use checked, mainPath, route.
// TODO(devoncarew): Handle startPaused, debugPort.
printTrace('Building ${app.name} for $id');
// Step 1: Install the precompiled/DBC application if necessary.
XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, buildForDevice: true);
if (!buildResult.success) {
printError('Could not build the precompiled application for the device.');
return new LaunchResult.failed();
}
// Step 2: Check that the application exists at the specified path.
IOSApp iosApp = app;
Directory bundle = new Directory(iosApp.deviceBundlePath);
if (!bundle.existsSync()) {
printError('Could not find the built application bundle at ${bundle.path}.');
return new LaunchResult.failed();
}
// Step 3: Attempt to install the application on the device.
int installationResult = await runCommandAndStreamOutput(<String>[
'/usr/bin/env',
'ios-deploy',
'--id',
id,
'--bundle',
bundle.path,
'--justlaunch',
]);
if (installationResult != 0) {
printError('Could not install ${bundle.path} on $id.');
return new LaunchResult.failed();
}
return new LaunchResult.succeeded();
}
@override
Future<bool> restartApp(
ApplicationPackage package,
LaunchResult result, {
String mainPath,
Observatory observatory
}) async {
return observatory.isolateReload(observatory.firstIsolateId).then((Response response) {
return true;
}).catchError((dynamic error) {
printError('Error restarting app: $error');
return false;
});
}
@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;
@override
DeviceLogReader get logReader {
if (_logReader == null)
_logReader = new _IOSDeviceLogReader(this);
return _logReader;
}
@override
DevicePortForwarder get portForwarder {
if (_portForwarder == null)
_portForwarder = new _IOSDevicePortForwarder(this);
return _portForwarder;
}
@override
void clearLogs() {
}
@override
bool get supportsScreenshot => false;
@override
Future<bool> takeScreenshot(File outputFile) {
// We could use idevicescreenshot here (installed along with the brew
// ideviceinstaller tools). It however requires a developer disk image on
// the device.
return new Future<bool>.value(false);
}
}
class _IOSDeviceLogReader extends DeviceLogReader {
_IOSDeviceLogReader(this.device) {
_linesController = new StreamController<String>.broadcast(
onListen: _start,
onCancel: _stop
);
}
final IOSDevice device;
StreamController<String> _linesController;
Process _process;
@override
Stream<String> get logLines => _linesController.stream;
@override
String get name => device.name;
void _start() {
runCommand(<String>[device.loggerPath]).then((Process process) {
_process = process;
_process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
_process.exitCode.then((int code) {
if (_linesController.hasListener)
_linesController.close();
});
});
}
static final RegExp _runnerRegex = new RegExp(r'FlutterRunner');
void _onLine(String line) {
if (_runnerRegex.hasMatch(line))
_linesController.add(line);
}
void _stop() {
_process?.kill();
}
}
class _IOSDevicePortForwarder extends DevicePortForwarder {
_IOSDevicePortForwarder(this.device);
final IOSDevice device;
@override
List<ForwardedPort> get forwardedPorts {
final List<ForwardedPort> ports = <ForwardedPort>[];
// TODO(chinmaygarde): Implement.
return ports;
}
@override
Future<int> forward(int devicePort, {int hostPort: null}) async {
if ((hostPort == null) || (hostPort == 0)) {
// Auto select host port.
hostPort = await findAvailablePort();
}
// TODO(chinmaygarde): Implement.
return hostPort;
}
@override
Future<Null> unforward(ForwardedPort forwardedPort) async {
// TODO(chinmaygarde): Implement.
}
}