// 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 'package:path/path.dart' as path; import '../application_package.dart'; import '../base/common.dart'; import '../base/os.dart'; import '../base/process.dart'; import '../build_configuration.dart'; import '../device.dart'; import '../globals.dart'; import '../toolchain.dart'; import 'mac.dart'; const String _ideviceinstallerInstructions = 'To work with iOS devices, please install ideviceinstaller.\n' 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".'; class IOSDevices extends PollingDeviceDiscovery { IOSDevices() : super('IOSDevices'); bool get supportsPlatform => Platform.isMacOS; List pollingGetDevices() => IOSDevice.getAttachedDevices(); } class IOSDevice extends Device { IOSDevice(String id, { this.name }) : super(id) { _installerPath = _checkForCommand('ideviceinstaller'); _listerPath = _checkForCommand('idevice_id'); _informerPath = _checkForCommand('ideviceinfo'); _debuggerPath = _checkForCommand('idevicedebug'); _loggerPath = _checkForCommand('idevicesyslog'); _pusherPath = _checkForCommand( 'ios-deploy', 'To copy files to iOS devices, please install ios-deploy. ' 'You can do this using homebrew as follows:\n' '\$ brew tap flutter/flutter\n' '\$ brew install ios-deploy'); } String _installerPath; String get installerPath => _installerPath; String _listerPath; String get listerPath => _listerPath; String _informerPath; String get informerPath => _informerPath; String _debuggerPath; String get debuggerPath => _debuggerPath; String _loggerPath; String get loggerPath => _loggerPath; String _pusherPath; String get pusherPath => _pusherPath; final String name; _IOSDeviceLogReader _logReader; _IOSDevicePortForwarder _portForwarder; bool get isLocalEmulator => false; bool get supportsStartPaused => false; static List getAttachedDevices([IOSDevice mockIOS]) { if (!doctor.iosWorkflow.hasIdeviceId) return []; List devices = []; for (String id in _getAttachedDeviceIDs(mockIOS)) { String name = _getDeviceName(id, mockIOS); devices.add(new IOSDevice(id, name: name)); } return devices; } static Iterable _getAttachedDeviceIDs([IOSDevice mockIOS]) { String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id'); try { String output = runSync([listerPath, '-l']); return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty); } catch (e) { return []; } } static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) { String informerPath = (mockIOS != null) ? mockIOS.informerPath : _checkForCommand('ideviceinfo'); return runSync([informerPath, '-k', 'DeviceName', '-u', deviceID]).trim(); } static final Map _commandMap = {}; static String _checkForCommand( String command, [ String macInstructions = _ideviceinstallerInstructions ]) { return _commandMap.putIfAbsent(command, () { try { command = runCheckedSync(['which', command]).trim(); } catch (e) { if (Platform.isMacOS) { printError('$command not found. $macInstructions'); } else { printError('Cannot control iOS devices or simulators. $command is not available on your platform.'); } } return command; }); } @override bool installApp(ApplicationPackage app) { try { runCheckedSync([installerPath, '-i', app.localPath]); return true; } catch (e) { return false; } return false; } @override bool isSupported() => true; @override bool isAppInstalled(ApplicationPackage app) { try { String apps = runCheckedSync([installerPath, '--list-apps']); if (new RegExp(app.id, multiLine: true).hasMatch(apps)) { return true; } } catch (e) { return false; } return false; } @override Future startApp( ApplicationPackage app, Toolchain toolchain, { String mainPath, String route, bool checked: true, bool clearLogs: false, bool startPaused: false, int debugPort: observatoryDefaultPort, Map platformArgs }) async { // TODO(chinmaygarde): Use checked, mainPath, route, clearLogs. // TODO(devoncarew): Handle startPaused, debugPort. printTrace('Building ${app.name} for $id'); // Step 1: Install the precompiled application if necessary. bool buildResult = await buildIOSXcodeProject(app, buildForDevice: true); if (!buildResult) { printError('Could not build the precompiled application for the device.'); return false; } // Step 2: Check that the application exists at the specified path. Directory bundle = new Directory(path.join(app.localPath, 'build', 'Release-iphoneos', 'Runner.app')); bool bundleExists = bundle.existsSync(); if (!bundleExists) { printError('Could not find the built application bundle at ${bundle.path}.'); return false; } // Step 3: Attempt to install the application on the device. int installationResult = await runCommandAndStreamOutput([ '/usr/bin/env', 'ios-deploy', '--id', id, '--bundle', bundle.path, ]); if (installationResult != 0) { printError('Could not install ${bundle.path} on $id.'); return false; } printTrace('Installation successful.'); return true; } @override Future 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; } return false; } @override TargetPlatform get platform => TargetPlatform.iOS; DeviceLogReader get logReader { if (_logReader == null) _logReader = new _IOSDeviceLogReader(this); return _logReader; } DevicePortForwarder get portForwarder { if (_portForwarder == null) _portForwarder = new _IOSDevicePortForwarder(this); return _portForwarder; } void clearLogs() { } } class _IOSDeviceLogReader extends DeviceLogReader { _IOSDeviceLogReader(this.device); final IOSDevice device; final StreamController _linesStreamController = new StreamController.broadcast(); Process _process; StreamSubscription _stdoutSubscription; StreamSubscription _stderrSubscription; Stream get lines => _linesStreamController.stream; String get name => device.name; bool get isReading => _process != null; Future get finished => _process != null ? _process.exitCode : new Future.value(0); Future start() async { if (_process != null) { throw new StateError( '_IOSDeviceLogReader must be stopped before it can be started.'); } _process = await runCommand([device.loggerPath]); _stdoutSubscription = _process.stdout.transform(UTF8.decoder) .transform(const LineSplitter()).listen(_onLine); _stderrSubscription = _process.stderr.transform(UTF8.decoder) .transform(const LineSplitter()).listen(_onLine); _process.exitCode.then(_onExit); } Future stop() async { if (_process == null) { throw new StateError( '_IOSDeviceLogReader must be started before it can be stopped.'); } _stdoutSubscription?.cancel(); _stdoutSubscription = null; _stderrSubscription?.cancel(); _stderrSubscription = null; await _process.kill(); _process = null; } void _onExit(int exitCode) { _stdoutSubscription?.cancel(); _stdoutSubscription = null; _stderrSubscription?.cancel(); _stderrSubscription = null; _process = null; } RegExp _runnerRegex = new RegExp(r'Runner'); void _onLine(String line) { if (!_runnerRegex.hasMatch(line)) return; _linesStreamController.add(line); } int get hashCode => name.hashCode; bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other is! _IOSDeviceLogReader) return false; return other.name == name; } } class _IOSDevicePortForwarder extends DevicePortForwarder { _IOSDevicePortForwarder(this.device); final IOSDevice device; List get forwardedPorts { final List ports = []; // TODO(chinmaygarde): Implement. return ports; } Future forward(int devicePort, {int hostPort: null}) async { if ((hostPort == null) || (hostPort == 0)) { // Auto select host port. hostPort = await findAvailablePort(); } // TODO(chinmaygarde): Implement. return hostPort; } Future unforward(ForwardedPort forwardedPort) async { // TODO(chinmaygarde): Implement. } }