// 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/platform.dart'; import '../base/port_scanner.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; import '../device.dart'; import '../globals.dart'; import '../protocol_discovery.dart'; import 'ios_workflow.dart'; import 'mac.dart'; const String _kIdeviceinstallerInstructions = 'To work with iOS devices, please install ideviceinstaller. To install, run:\n' 'brew install ideviceinstaller.'; const Duration kPortForwardTimeout = const Duration(seconds: 10); class IOSDevices extends PollingDeviceDiscovery { IOSDevices() : super('iOS devices'); @override bool get supportsPlatform => platform.isMacOS; @override bool get canListAnything => iosWorkflow.canListDevices; @override Future> pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _iproxyPath = _checkForCommand('iproxy'); _pusherPath = _checkForCommand( 'ios-deploy', 'To copy files to iOS devices, please install ios-deploy. To install, run:\n' 'brew install ios-deploy' ); } String _installerPath; String _iproxyPath; String _pusherPath; final String _sdkVersion; @override bool get supportsHotMode => true; @override final String name; Map _logReaders; _IOSDevicePortForwarder _portForwarder; @override Future get isLocalEmulator async => false; @override bool get supportsStartPaused => false; static Future> getAttachedDevices() async { if (!iMobileDevice.isInstalled) return []; final List devices = []; for (String id in (await iMobileDevice.getAvailableDeviceIDs()).split('\n')) { id = id.trim(); if (id.isEmpty) continue; final String deviceName = await iMobileDevice.getInfoForDevice(id, 'DeviceName'); final String sdkVersion = await iMobileDevice.getInfoForDevice(id, 'ProductVersion'); devices.add(new IOSDevice(id, name: deviceName, sdkVersion: sdkVersion)); } return devices; } static String _checkForCommand( String command, [ String macInstructions = _kIdeviceinstallerInstructions ]) { 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 null; } return command; } @override Future isAppInstalled(ApplicationPackage app) async { try { final RunResult apps = await runCheckedAsync([_installerPath, '--list-apps']); if (new RegExp(app.id, multiLine: true).hasMatch(apps.stdout)) { return true; } } catch (e) { return false; } return false; } @override Future isLatestBuildInstalled(ApplicationPackage app) async => false; @override Future installApp(ApplicationPackage app) async { final IOSApp iosApp = app; final 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 { await runCheckedAsync([_installerPath, '-i', iosApp.deviceBundlePath]); return true; } catch (e) { return false; } } @override Future uninstallApp(ApplicationPackage app) async { try { await runCheckedAsync([_installerPath, '-U', app.id]); return true; } catch (e) { return false; } } @override bool isSupported() => true; @override Future startApp( ApplicationPackage app, { String mainPath, String route, DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication: false, bool previewDart2: false, bool applicationNeedsRebuild: false, bool usesTerminalUi: true, }) async { if (!prebuiltApplication) { // TODO(chinmaygarde): Use mainPath, route. printTrace('Building ${app.name} for $id'); // Step 1: Build the precompiled/DBC application if necessary. final XcodeBuildResult buildResult = await buildXcodeProject( app: app, buildInfo: debuggingOptions.buildInfo, target: mainPath, buildForDevice: true, usesTerminalUi: usesTerminalUi, ); if (!buildResult.success) { printError('Could not build the precompiled application for the device.'); await diagnoseXcodeBuildFailure(buildResult, app); printError(''); return new LaunchResult.failed(); } } else { if (!await installApp(app)) return new LaunchResult.failed(); } // Step 2: Check that the application exists at the specified path. final IOSApp iosApp = app; final 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. final List launchArguments = ['--enable-dart-profiling']; if (debuggingOptions.startPaused) launchArguments.add('--start-paused'); if (debuggingOptions.useTestFonts) launchArguments.add('--use-test-fonts'); 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 (debuggingOptions.enableSoftwareRendering) launchArguments.add('--enable-software-rendering'); if (debuggingOptions.traceSkia) launchArguments.add('--trace-skia'); if (platformArgs['trace-startup'] ?? false) launchArguments.add('--trace-startup'); final List launchCommand = [ '/usr/bin/env', 'ios-deploy', '--id', id, '--bundle', bundle.path, '--no-wifi', '--justlaunch', ]; if (launchArguments.isNotEmpty) { launchCommand.add('--args'); launchCommand.add('${launchArguments.join(" ")}'); } int installationResult = -1; Uri localObservatoryUri; 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 server port post launch. printTrace('Debugging is enabled, connecting to observatory'); // 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. final ProtocolDiscovery observatoryDiscovery = new ProtocolDiscovery.observatory( getLogReader(app: app), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort); final Future forwardObservatoryUri = observatoryDiscovery.uri; final Future launch = runCommandAndStreamOutput(launchCommand, trace: true); localObservatoryUri = await launch.then((int result) async { installationResult = result; if (result != 0) { printTrace('Failed to launch the application on device.'); return null; } printTrace('Application launched on the device. Waiting for observatory port.'); return await forwardObservatoryUri; }).whenComplete(() { observatoryDiscovery.cancel(); }); } 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.xcworkspace'); printError(''); return new LaunchResult.failed(); } return new LaunchResult.succeeded(observatoryUri: localObservatoryUri); } @override Future stopApp(ApplicationPackage app) async { // Currently we don't have a way to stop an app running on iOS. return false; } Future 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; } } @override Future get targetPlatform async => TargetPlatform.ios; @override Future get sdkNameAndVersion async => 'iOS $_sdkVersion'; @override DeviceLogReader getLogReader({ApplicationPackage app}) { _logReaders ??= {}; return _logReaders.putIfAbsent(app, () => new _IOSDeviceLogReader(this, app)); } @override DevicePortForwarder get portForwarder => _portForwarder ??= new _IOSDevicePortForwarder(this); @override void clearLogs() { } @override bool get supportsScreenshot => iMobileDevice.isInstalled; @override Future takeScreenshot(File outputFile) => iMobileDevice.takeScreenshot(outputFile); } class _IOSDeviceLogReader extends DeviceLogReader { RegExp _lineRegex; _IOSDeviceLogReader(this.device, ApplicationPackage app) { _linesController = new StreamController.broadcast( onListen: _start, onCancel: _stop ); // Match for lines for the runner in syslog. // // iOS 9 format: Runner[297] : // iOS 10 format: Runner(Flutter)[297] : final String appName = app == null ? '' : app.name.replaceAll('.app', ''); _lineRegex = new RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '); } final IOSDevice device; StreamController _linesController; Process _process; @override Stream get logLines => _linesController.stream; @override String get name => device.name; void _start() { iMobileDevice.startLogger().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.whenComplete(() { if (_linesController.hasListener) _linesController.close(); }); }); } void _onLine(String line) { final Match match = _lineRegex.firstMatch(line); if (match != null) { final String logLine = line.substring(match.end); // Only display the log line after the initial device and executable information. _linesController.add(logLine); } } void _stop() { _process?.kill(); } } class _IOSDevicePortForwarder extends DevicePortForwarder { _IOSDevicePortForwarder(this.device) : _forwardedPorts = []; final IOSDevice device; final List _forwardedPorts; @override List get forwardedPorts => _forwardedPorts; @override Future forward(int devicePort, {int hostPort}) async { if ((hostPort == null) || (hostPort == 0)) { // Auto select host port. hostPort = await portScanner.findAvailablePort(); } // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID final Process process = await runCommand([ device._iproxyPath, hostPort.toString(), devicePort.toString(), device.id, ]); final ForwardedPort forwardedPort = new ForwardedPort.withContext(hostPort, devicePort, process); printTrace('Forwarded port $forwardedPort'); _forwardedPorts.add(forwardedPort); return hostPort; } @override Future unforward(ForwardedPort forwardedPort) async { if (!_forwardedPorts.remove(forwardedPort)) { // Not in list. Nothing to remove. return null; } printTrace('Unforwarding port $forwardedPort'); final Process process = forwardedPort.context; if (process != null) { processManager.killPid(process.pid); } else { printError('Forwarded port did not have a valid process'); } return null; } }