// 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/common.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; 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 isConnected() => _getAttachedDeviceIDs().contains(id); @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 createLogReader() => new _IOSDeviceLogReader(this); } class _IOSDeviceLogReader extends DeviceLogReader { _IOSDeviceLogReader(this.device); final IOSDevice device; String get name => device.name; // TODO(devoncarew): Support [clear]. Future logs({ bool clear: false }) async { if (!device.isConnected()) return 2; return await runCommandAndStreamOutput( [device.loggerPath], prefix: '[$name] ', filter: new RegExp(r'Runner') ); } 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; } }