485 lines
14 KiB
Dart
485 lines
14 KiB
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 'dart:convert';
|
|
|
|
import '../application_package.dart';
|
|
import '../base/file_system.dart';
|
|
import '../base/io.dart';
|
|
import '../base/os.dart';
|
|
import '../base/process.dart';
|
|
import '../base/process_manager.dart';
|
|
import '../build_info.dart';
|
|
import '../device.dart';
|
|
import '../doctor.dart';
|
|
import '../globals.dart';
|
|
import '../protocol_discovery.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".';
|
|
|
|
const Duration kPortForwardTimeout = const Duration(seconds: 10);
|
|
|
|
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');
|
|
_iproxyPath = _checkForCommand('iproxy');
|
|
_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 _iproxyPath;
|
|
String get iproxyPath => _iproxyPath;
|
|
|
|
String _debuggerPath;
|
|
String get debuggerPath => _debuggerPath;
|
|
|
|
String _loggerPath;
|
|
String get loggerPath => _loggerPath;
|
|
|
|
String _pusherPath;
|
|
String get pusherPath => _pusherPath;
|
|
|
|
@override
|
|
bool get supportsHotMode => true;
|
|
|
|
@override
|
|
final String name;
|
|
|
|
Map<ApplicationPackage, _IOSDeviceLogReader> _logReaders;
|
|
|
|
_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 = IOSDevice._getDeviceInfo(id, 'DeviceName', 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 _getDeviceInfo(String deviceID, String infoKey, [IOSDevice mockIOS]) {
|
|
String informerPath = (mockIOS != null)
|
|
? mockIOS.informerPath
|
|
: _checkForCommand('ideviceinfo');
|
|
return runSync(<String>[informerPath, '-k', infoKey, '-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 = fs.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,
|
|
bool prebuiltApplication: false
|
|
}) async {
|
|
if (!prebuiltApplication) {
|
|
// TODO(chinmaygarde): Use checked, mainPath, route.
|
|
// TODO(devoncarew): Handle startPaused, debugPort.
|
|
printTrace('Building ${app.name} for $id');
|
|
|
|
// Step 1: Build the precompiled/DBC application if necessary.
|
|
XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true);
|
|
if (!buildResult.success) {
|
|
printError('Could not build the precompiled application for the device.');
|
|
diagnoseXcodeBuildFailure(buildResult);
|
|
printError('');
|
|
return new LaunchResult.failed();
|
|
}
|
|
} else {
|
|
if (!installApp(app))
|
|
return new LaunchResult.failed();
|
|
}
|
|
|
|
// Step 2: Check that the application exists at the specified path.
|
|
IOSApp iosApp = app;
|
|
Directory bundle = fs.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.
|
|
List<String> launchArguments = <String>["--enable-dart-profiling"];
|
|
|
|
if (debuggingOptions.startPaused)
|
|
launchArguments.add("--start-paused");
|
|
|
|
if (debuggingOptions.debuggingEnabled) {
|
|
launchArguments.add("--enable-checked-mode");
|
|
|
|
// Note: We do NOT need to set the observatory port since this is going to
|
|
// be setup on the device. Let it pick a port automatically. We will check
|
|
// the port picked and scrape that later.
|
|
}
|
|
|
|
if (platformArgs['trace-startup'] ?? false)
|
|
launchArguments.add('--trace-startup');
|
|
|
|
List<String> launchCommand = <String>[
|
|
'/usr/bin/env',
|
|
'ios-deploy',
|
|
'--id',
|
|
id,
|
|
'--bundle',
|
|
bundle.path,
|
|
'--justlaunch',
|
|
];
|
|
|
|
if (launchArguments.length > 0) {
|
|
launchCommand.add('--args');
|
|
launchCommand.add('${launchArguments.join(" ")}');
|
|
}
|
|
|
|
int installationResult = -1;
|
|
Uri localObsUri;
|
|
Uri localDiagUri;
|
|
|
|
if (!debuggingOptions.debuggingEnabled) {
|
|
// If debugging is not enabled, just launch the application and continue.
|
|
printTrace("Debugging is not enabled");
|
|
installationResult = await runCommandAndStreamOutput(launchCommand, trace: true);
|
|
} else {
|
|
// Debugging is enabled, look for the observatory and diagnostic server
|
|
// ports post launch.
|
|
printTrace("Debugging is enabled, connecting to observatory and the diagnostic server");
|
|
|
|
// TODO(danrubel): The Android device class does something similar to this code below.
|
|
// The various Device subclasses should be refactored and common code moved into the superclass.
|
|
ProtocolDiscovery observatoryDiscovery = new ProtocolDiscovery.observatory(
|
|
getLogReader(app: app), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort);
|
|
ProtocolDiscovery diagnosticDiscovery = new ProtocolDiscovery.diagnosticService(
|
|
getLogReader(app: app), portForwarder: portForwarder, hostPort: debuggingOptions.diagnosticPort);
|
|
|
|
Future<Uri> forwardObsUri = observatoryDiscovery.nextUri();
|
|
Future<Uri> forwardDiagUri;
|
|
if (debuggingOptions.buildMode == BuildMode.debug) {
|
|
forwardDiagUri = diagnosticDiscovery.nextUri();
|
|
} else {
|
|
forwardDiagUri = new Future<Uri>.value(null);
|
|
}
|
|
|
|
Future<int> launch = runCommandAndStreamOutput(launchCommand, trace: true);
|
|
|
|
List<Uri> uris = await launch.then((int result) async {
|
|
installationResult = result;
|
|
|
|
if (result != 0) {
|
|
printTrace("Failed to launch the application on device.");
|
|
return <Uri>[null, null];
|
|
}
|
|
|
|
printTrace("Application launched on the device. Attempting to forward ports.");
|
|
return await Future.wait(<Future<Uri>>[forwardObsUri, forwardDiagUri]);
|
|
}).whenComplete(() {
|
|
observatoryDiscovery.cancel();
|
|
diagnosticDiscovery.cancel();
|
|
});
|
|
|
|
localObsUri = uris[0];
|
|
localDiagUri = uris[1];
|
|
}
|
|
|
|
if (installationResult != 0) {
|
|
printError('Could not install ${bundle.path} on $id.');
|
|
printError("Try launching XCode and selecting 'Product > Run' to fix the problem:");
|
|
printError(" open ios/Runner.xcodeproj");
|
|
printError('');
|
|
return new LaunchResult.failed();
|
|
}
|
|
|
|
return new LaunchResult.succeeded(observatoryUri: localObsUri, diagnosticUri: localDiagUri);
|
|
}
|
|
|
|
@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;
|
|
}
|
|
}
|
|
|
|
@override
|
|
TargetPlatform get platform => TargetPlatform.ios;
|
|
|
|
@override
|
|
String get sdkNameAndVersion => 'iOS $_sdkVersion ($_buildVersion)';
|
|
|
|
String get _sdkVersion => _getDeviceInfo(id, 'ProductVersion');
|
|
|
|
String get _buildVersion => _getDeviceInfo(id, 'BuildVersion');
|
|
|
|
@override
|
|
DeviceLogReader getLogReader({ApplicationPackage app}) {
|
|
_logReaders ??= <ApplicationPackage, _IOSDeviceLogReader>{};
|
|
return _logReaders.putIfAbsent(app, () => new _IOSDeviceLogReader(this, app));
|
|
}
|
|
|
|
@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 {
|
|
RegExp _lineRegex;
|
|
|
|
_IOSDeviceLogReader(this.device, ApplicationPackage app) {
|
|
_linesController = new StreamController<String>.broadcast(
|
|
onListen: _start,
|
|
onCancel: _stop
|
|
);
|
|
|
|
// Match for lines for the runner in syslog.
|
|
//
|
|
// iOS 9 format: Runner[297] <Notice>:
|
|
// iOS 10 format: Runner(libsystem_asl.dylib)[297] <Notice>:
|
|
String appName = app == null ? '' : app.name.replaceAll('.app', '');
|
|
_lineRegex = new RegExp(appName + r'(\(.*\))?\[[\d]+\] <[A-Za-z]+>: ');
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
}
|
|
|
|
void _onLine(String line) {
|
|
Match match = _lineRegex.firstMatch(line);
|
|
|
|
if (match != null) {
|
|
// Only display the log line after the initial device and executable information.
|
|
_linesController.add(line.substring(match.end));
|
|
}
|
|
}
|
|
|
|
void _stop() {
|
|
_process?.kill();
|
|
}
|
|
}
|
|
|
|
class _IOSDevicePortForwarder extends DevicePortForwarder {
|
|
_IOSDevicePortForwarder(this.device) : _forwardedPorts = new List<ForwardedPort>();
|
|
|
|
final IOSDevice device;
|
|
|
|
final List<ForwardedPort> _forwardedPorts;
|
|
|
|
@override
|
|
List<ForwardedPort> get forwardedPorts => _forwardedPorts;
|
|
|
|
@override
|
|
Future<int> forward(int devicePort, {int hostPort: null}) async {
|
|
if ((hostPort == null) || (hostPort == 0)) {
|
|
// Auto select host port.
|
|
hostPort = await findAvailablePort();
|
|
}
|
|
|
|
// Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
|
|
Process process = await runCommand(<String>[
|
|
device.iproxyPath,
|
|
hostPort.toString(),
|
|
devicePort.toString(),
|
|
device.id,
|
|
]);
|
|
|
|
ForwardedPort forwardedPort = new ForwardedPort.withContext(hostPort,
|
|
devicePort, process);
|
|
|
|
printTrace("Forwarded port $forwardedPort");
|
|
|
|
_forwardedPorts.add(forwardedPort);
|
|
|
|
return hostPort;
|
|
}
|
|
|
|
@override
|
|
Future<Null> unforward(ForwardedPort forwardedPort) async {
|
|
if (!_forwardedPorts.remove(forwardedPort)) {
|
|
// Not in list. Nothing to remove.
|
|
return null;
|
|
}
|
|
|
|
printTrace("Unforwarding port $forwardedPort");
|
|
|
|
Process process = forwardedPort.context;
|
|
|
|
if (process != null) {
|
|
processManager.killPid(process.pid);
|
|
} else {
|
|
printError("Forwarded port did not have a valid process");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|