blob: 972c186325fd35d7fa39d24231fb6206caf2a32b [file] [log] [blame]
Devon Carew67046f92016-02-20 22:00:11 -08001// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
John McCutchan8803cec2016-03-07 13:52:27 -08006import 'dart:convert';
Devon Carew67046f92016-02-20 22:00:11 -08007import 'dart:io';
8
Devon Carew67046f92016-02-20 22:00:11 -08009import '../application_package.dart';
John McCutchan5e140b72016-03-10 15:48:37 -080010import '../base/os.dart';
Devon Carew67046f92016-02-20 22:00:11 -080011import '../base/process.dart';
Jason Simmonsa590ee22016-05-12 12:22:15 -070012import '../build_info.dart';
Devon Carew67046f92016-02-20 22:00:11 -080013import '../device.dart';
14import '../globals.dart';
Chinmay Garde9782d972016-06-14 11:47:51 -070015import '../protocol_discovery.dart';
Devon Carew67046f92016-02-20 22:00:11 -080016import 'mac.dart';
17
18const String _ideviceinstallerInstructions =
19 'To work with iOS devices, please install ideviceinstaller.\n'
20 'If you use homebrew, you can install it with "\$ brew install ideviceinstaller".';
21
22class IOSDevices extends PollingDeviceDiscovery {
23 IOSDevices() : super('IOSDevices');
24
Hixie797e27e2016-03-14 13:31:43 -070025 @override
Devon Carew67046f92016-02-20 22:00:11 -080026 bool get supportsPlatform => Platform.isMacOS;
Hixie797e27e2016-03-14 13:31:43 -070027
28 @override
Devon Carew67046f92016-02-20 22:00:11 -080029 List<Device> pollingGetDevices() => IOSDevice.getAttachedDevices();
30}
31
32class IOSDevice extends Device {
33 IOSDevice(String id, { this.name }) : super(id) {
34 _installerPath = _checkForCommand('ideviceinstaller');
35 _listerPath = _checkForCommand('idevice_id');
36 _informerPath = _checkForCommand('ideviceinfo');
Chinmay Garde9782d972016-06-14 11:47:51 -070037 _iproxyPath = _checkForCommand('iproxy');
Devon Carew67046f92016-02-20 22:00:11 -080038 _debuggerPath = _checkForCommand('idevicedebug');
39 _loggerPath = _checkForCommand('idevicesyslog');
40 _pusherPath = _checkForCommand(
41 'ios-deploy',
42 'To copy files to iOS devices, please install ios-deploy. '
43 'You can do this using homebrew as follows:\n'
44 '\$ brew tap flutter/flutter\n'
45 '\$ brew install ios-deploy');
46 }
47
48 String _installerPath;
49 String get installerPath => _installerPath;
50
51 String _listerPath;
52 String get listerPath => _listerPath;
53
54 String _informerPath;
55 String get informerPath => _informerPath;
56
Chinmay Garde9782d972016-06-14 11:47:51 -070057 String _iproxyPath;
58 String get iproxyPath => _iproxyPath;
59
Devon Carew67046f92016-02-20 22:00:11 -080060 String _debuggerPath;
61 String get debuggerPath => _debuggerPath;
62
63 String _loggerPath;
64 String get loggerPath => _loggerPath;
65
66 String _pusherPath;
67 String get pusherPath => _pusherPath;
68
Hixie797e27e2016-03-14 13:31:43 -070069 @override
John McCutchana8198122016-08-09 13:02:15 -070070 bool get supportsHotMode => true;
71
72 @override
Devon Carew67046f92016-02-20 22:00:11 -080073 final String name;
74
John McCutchan8803cec2016-03-07 13:52:27 -080075 _IOSDeviceLogReader _logReader;
76
John McCutchan5e140b72016-03-10 15:48:37 -080077 _IOSDevicePortForwarder _portForwarder;
78
Hixie797e27e2016-03-14 13:31:43 -070079 @override
Yegor Jbanov677e63b2016-02-25 15:58:09 -080080 bool get isLocalEmulator => false;
81
Hixie797e27e2016-03-14 13:31:43 -070082 @override
Devon Carew67046f92016-02-20 22:00:11 -080083 bool get supportsStartPaused => false;
84
85 static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
Devon Carew25f332d2016-03-23 16:59:56 -070086 if (!doctor.iosWorkflow.hasIDeviceId)
Devon Carew67046f92016-02-20 22:00:11 -080087 return <IOSDevice>[];
88
Devon Carewc9010c92016-05-03 09:09:00 -070089 List<IOSDevice> devices = <IOSDevice>[];
Devon Carew67046f92016-02-20 22:00:11 -080090 for (String id in _getAttachedDeviceIDs(mockIOS)) {
Dan Rubel0b49d812016-10-27 09:07:21 +010091 String name = IOSDevice._getDeviceInfo(id, 'DeviceName', mockIOS);
Devon Carew67046f92016-02-20 22:00:11 -080092 devices.add(new IOSDevice(id, name: name));
93 }
94 return devices;
95 }
96
97 static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
98 String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
99 try {
Devon Carewc9010c92016-05-03 09:09:00 -0700100 String output = runSync(<String>[listerPath, '-l']);
Devon Carew67046f92016-02-20 22:00:11 -0800101 return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
102 } catch (e) {
103 return <String>[];
104 }
105 }
106
Dan Rubel0b49d812016-10-27 09:07:21 +0100107 static String _getDeviceInfo(String deviceID, String infoKey, [IOSDevice mockIOS]) {
Devon Carew67046f92016-02-20 22:00:11 -0800108 String informerPath = (mockIOS != null)
109 ? mockIOS.informerPath
110 : _checkForCommand('ideviceinfo');
Dan Rubel0b49d812016-10-27 09:07:21 +0100111 return runSync(<String>[informerPath, '-k', infoKey, '-u', deviceID]).trim();
Devon Carew67046f92016-02-20 22:00:11 -0800112 }
113
Devon Carewc9010c92016-05-03 09:09:00 -0700114 static final Map<String, String> _commandMap = <String, String>{};
Devon Carew67046f92016-02-20 22:00:11 -0800115 static String _checkForCommand(
116 String command, [
117 String macInstructions = _ideviceinstallerInstructions
118 ]) {
119 return _commandMap.putIfAbsent(command, () {
120 try {
Devon Carewc9010c92016-05-03 09:09:00 -0700121 command = runCheckedSync(<String>['which', command]).trim();
Devon Carew67046f92016-02-20 22:00:11 -0800122 } catch (e) {
123 if (Platform.isMacOS) {
124 printError('$command not found. $macInstructions');
125 } else {
126 printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
127 }
128 }
129 return command;
130 });
131 }
132
133 @override
Devon Carew67046f92016-02-20 22:00:11 -0800134 bool isAppInstalled(ApplicationPackage app) {
135 try {
Devon Carewc9010c92016-05-03 09:09:00 -0700136 String apps = runCheckedSync(<String>[installerPath, '--list-apps']);
Devon Carew67046f92016-02-20 22:00:11 -0800137 if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
138 return true;
139 }
140 } catch (e) {
141 return false;
142 }
143 return false;
144 }
145
146 @override
Todd Volkert51d4ab32016-05-27 11:05:10 -0700147 bool installApp(ApplicationPackage app) {
148 IOSApp iosApp = app;
149 Directory bundle = new Directory(iosApp.deviceBundlePath);
150 if (!bundle.existsSync()) {
151 printError("Could not find application bundle at ${bundle.path}; have you run 'flutter build ios'?");
152 return false;
153 }
154
155 try {
156 runCheckedSync(<String>[installerPath, '-i', iosApp.deviceBundlePath]);
157 return true;
158 } catch (e) {
159 return false;
160 }
161 }
162
163 @override
164 bool uninstallApp(ApplicationPackage app) {
165 try {
166 runCheckedSync(<String>[installerPath, '-U', app.id]);
167 return true;
168 } catch (e) {
169 return false;
170 }
171 }
172
173 @override
174 bool isSupported() => true;
175
176 @override
Devon Carewb0dca792016-04-27 14:43:42 -0700177 Future<LaunchResult> startApp(
Chinmay Garde66fee3a2016-05-23 12:58:42 -0700178 ApplicationPackage app,
179 BuildMode mode, {
Devon Carew67046f92016-02-20 22:00:11 -0800180 String mainPath,
181 String route,
Devon Carewb0dca792016-04-27 14:43:42 -0700182 DebuggingOptions debuggingOptions,
John McCutchanca8070f2016-09-28 08:46:16 -0700183 Map<String, dynamic> platformArgs,
184 bool prebuiltApplication: false
Devon Carew67046f92016-02-20 22:00:11 -0800185 }) async {
Todd Volkert904d5242016-10-13 16:17:50 -0700186 if (!prebuiltApplication) {
187 // TODO(chinmaygarde): Use checked, mainPath, route.
188 // TODO(devoncarew): Handle startPaused, debugPort.
189 printTrace('Building ${app.name} for $id');
Devon Carew67046f92016-02-20 22:00:11 -0800190
Todd Volkert904d5242016-10-13 16:17:50 -0700191 // Step 1: Build the precompiled/DBC application if necessary.
192 XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true);
193 if (!buildResult.success) {
194 printError('Could not build the precompiled application for the device.');
195 diagnoseXcodeBuildFailure(buildResult);
196 printError('');
197 return new LaunchResult.failed();
198 }
Devon Carew67046f92016-02-20 22:00:11 -0800199 }
200
201 // Step 2: Check that the application exists at the specified path.
Todd Volkertcc8c78a2016-05-24 16:25:40 -0700202 IOSApp iosApp = app;
203 Directory bundle = new Directory(iosApp.deviceBundlePath);
Todd Volkert51d4ab32016-05-27 11:05:10 -0700204 if (!bundle.existsSync()) {
Devon Carew67046f92016-02-20 22:00:11 -0800205 printError('Could not find the built application bundle at ${bundle.path}.');
Devon Carewb0dca792016-04-27 14:43:42 -0700206 return new LaunchResult.failed();
Devon Carew67046f92016-02-20 22:00:11 -0800207 }
208
209 // Step 3: Attempt to install the application on the device.
Chinmay Garde9782d972016-06-14 11:47:51 -0700210 List<String> launchArguments = <String>[];
211
212 if (debuggingOptions.startPaused)
213 launchArguments.add("--start-paused");
214
215 if (debuggingOptions.debuggingEnabled) {
216 launchArguments.add("--enable-checked-mode");
217
218 // Note: We do NOT need to set the observatory port since this is going to
219 // be setup on the device. Let it pick a port automatically. We will check
220 // the port picked and scrape that later.
221 }
222
Yegora0aa0ed2016-08-09 14:12:15 -0700223 if (platformArgs['trace-startup'] ?? false)
224 launchArguments.add('--trace-startup');
225
Chinmay Garde9782d972016-06-14 11:47:51 -0700226 List<String> launchCommand = <String>[
Devon Carew67046f92016-02-20 22:00:11 -0800227 '/usr/bin/env',
228 'ios-deploy',
229 '--id',
230 id,
231 '--bundle',
232 bundle.path,
Todd Volkertcc8c78a2016-05-24 16:25:40 -0700233 '--justlaunch',
Chinmay Garde9782d972016-06-14 11:47:51 -0700234 ];
235
236 if (launchArguments.length > 0) {
237 launchCommand.add('--args');
Chinmay Garde1852fdc2016-08-24 12:57:29 -0700238 launchCommand.add('${launchArguments.join(" ")}');
Chinmay Garde9782d972016-06-14 11:47:51 -0700239 }
240
241 int installationResult = -1;
242 int localObsPort;
243 int localDiagPort;
244
Ryan Macnak932059b2016-07-19 12:46:17 -0700245 if (!debuggingOptions.debuggingEnabled) {
Chinmay Garde9782d972016-06-14 11:47:51 -0700246 // If debugging is not enabled, just launch the application and continue.
247 printTrace("Debugging is not enabled");
248 installationResult = await runCommandAndStreamOutput(launchCommand, trace: true);
249 } else {
250 // Debugging is enabled, look for the observatory and diagnostic server
251 // ports post launch.
252 printTrace("Debugging is enabled, connecting to observatory and the diagnostic server");
253
Chris Brackenc5567a52016-09-26 17:12:20 -0700254 Future<int> forwardObsPort = _acquireAndForwardPort(ProtocolDiscovery.kObservatoryService,
255 debuggingOptions.observatoryPort);
256 Future<int> forwardDiagPort;
257 if (debuggingOptions.buildMode == BuildMode.debug) {
258 forwardDiagPort = _acquireAndForwardPort(ProtocolDiscovery.kDiagnosticService,
259 debuggingOptions.diagnosticPort);
260 } else {
261 forwardDiagPort = new Future<int>.value(null);
262 }
263
Chinmay Garde9782d972016-06-14 11:47:51 -0700264 Future<int> launch = runCommandAndStreamOutput(launchCommand, trace: true);
265
266 List<int> ports = await launch.then((int result) async {
267 installationResult = result;
268
269 if (result != 0) {
270 printTrace("Failed to launch the application on device.");
271 return <int>[null, null];
272 }
273
274 printTrace("Application launched on the device. Attempting to forward ports.");
Ryan Macnak932059b2016-07-19 12:46:17 -0700275 return Future.wait(<Future<int>>[forwardObsPort, forwardDiagPort]);
Chinmay Garde9782d972016-06-14 11:47:51 -0700276 });
277
278 printTrace("Local Observatory Port: ${ports[0]}");
279 printTrace("Local Diagnostic Server Port: ${ports[1]}");
280
281 localObsPort = ports[0];
282 localDiagPort = ports[1];
283 }
Devon Carew67046f92016-02-20 22:00:11 -0800284
285 if (installationResult != 0) {
286 printError('Could not install ${bundle.path} on $id.');
Dan Rubel573eaf02016-09-16 17:59:43 -0400287 printError("Try launching XCode and selecting 'Product > Run' to fix the problem:");
288 printError(" open ios/Runner.xcodeproj");
289 printError('');
Devon Carewb0dca792016-04-27 14:43:42 -0700290 return new LaunchResult.failed();
Devon Carew67046f92016-02-20 22:00:11 -0800291 }
292
Chinmay Garde9782d972016-06-14 11:47:51 -0700293 return new LaunchResult.succeeded(observatoryPort: localObsPort, diagnosticPort: localDiagPort);
294 }
295
296 Future<int> _acquireAndForwardPort(String serviceName, int localPort) async {
Chinmay Gardef3ca1102016-10-21 16:07:39 -0700297 Duration stepTimeout = const Duration(seconds: 60);
Chinmay Garde9782d972016-06-14 11:47:51 -0700298
299 Future<int> remote = new ProtocolDiscovery(logReader, serviceName).nextPort();
300
301 int remotePort = await remote.timeout(stepTimeout,
302 onTimeout: () {
303 printTrace("Timeout while attempting to retrieve remote port for $serviceName");
304 return null;
305 });
306
307 if (remotePort == null) {
308 printTrace("Could not read port on device for $serviceName");
309 return null;
310 }
311
312 if ((localPort == null) || (localPort == 0)) {
313 localPort = await findAvailablePort();
314 printTrace("Auto selected local port to $localPort");
315 }
316
317 int forwardResult = await portForwarder.forward(remotePort,
318 hostPort: localPort).timeout(stepTimeout, onTimeout: () {
319 printTrace("Timeout while atempting to foward port for $serviceName");
320 return null;
321 });
322
323 if (forwardResult == null) {
324 printTrace("Could not foward remote $serviceName port $remotePort to local port $localPort");
325 return null;
326 }
327
Ryan Macnake42be3c2016-07-18 12:54:08 -0700328 printStatus('$serviceName listening on http://127.0.0.1:$localPort');
Chinmay Garde9782d972016-06-14 11:47:51 -0700329 return localPort;
330 }
331
332 @override
Devon Carew67046f92016-02-20 22:00:11 -0800333 Future<bool> stopApp(ApplicationPackage app) async {
334 // Currently we don't have a way to stop an app running on iOS.
335 return false;
336 }
337
338 Future<bool> pushFile(ApplicationPackage app, String localFile, String targetFile) async {
339 if (Platform.isMacOS) {
340 runSync(<String>[
341 pusherPath,
342 '-t',
343 '1',
344 '--bundle_id',
345 app.id,
346 '--upload',
347 localFile,
348 '--to',
349 targetFile
350 ]);
351 return true;
352 } else {
353 return false;
354 }
Devon Carew67046f92016-02-20 22:00:11 -0800355 }
356
357 @override
Chinmay Gardec8377d72016-03-21 14:41:26 -0700358 TargetPlatform get platform => TargetPlatform.ios;
Devon Carew67046f92016-02-20 22:00:11 -0800359
Hixie797e27e2016-03-14 13:31:43 -0700360 @override
Dan Rubel0b49d812016-10-27 09:07:21 +0100361 String get sdkNameAndVersion => 'iOS $_sdkVersion ($_buildVersion)';
362
363 String get _sdkVersion => _getDeviceInfo(id, 'ProductVersion');
364
365 String get _buildVersion => _getDeviceInfo(id, 'BuildVersion');
366
367 @override
John McCutchan8803cec2016-03-07 13:52:27 -0800368 DeviceLogReader get logReader {
369 if (_logReader == null)
370 _logReader = new _IOSDeviceLogReader(this);
371
372 return _logReader;
373 }
374
Hixie797e27e2016-03-14 13:31:43 -0700375 @override
John McCutchan5e140b72016-03-10 15:48:37 -0800376 DevicePortForwarder get portForwarder {
377 if (_portForwarder == null)
378 _portForwarder = new _IOSDevicePortForwarder(this);
379
380 return _portForwarder;
381 }
382
Hixie797e27e2016-03-14 13:31:43 -0700383 @override
John McCutchan8803cec2016-03-07 13:52:27 -0800384 void clearLogs() {
385 }
Devon Carew15b9e1d2016-03-25 16:04:22 -0700386
387 @override
388 bool get supportsScreenshot => false;
389
390 @override
391 Future<bool> takeScreenshot(File outputFile) {
392 // We could use idevicescreenshot here (installed along with the brew
393 // ideviceinstaller tools). It however requires a developer disk image on
394 // the device.
395
396 return new Future<bool>.value(false);
397 }
Devon Carew67046f92016-02-20 22:00:11 -0800398}
399
400class _IOSDeviceLogReader extends DeviceLogReader {
Devon Carewb0dca792016-04-27 14:43:42 -0700401 _IOSDeviceLogReader(this.device) {
402 _linesController = new StreamController<String>.broadcast(
403 onListen: _start,
404 onCancel: _stop
405 );
406 }
Devon Carew67046f92016-02-20 22:00:11 -0800407
408 final IOSDevice device;
409
Devon Carewb0dca792016-04-27 14:43:42 -0700410 StreamController<String> _linesController;
John McCutchan8803cec2016-03-07 13:52:27 -0800411 Process _process;
John McCutchan8803cec2016-03-07 13:52:27 -0800412
Hixie797e27e2016-03-14 13:31:43 -0700413 @override
Devon Carewb0dca792016-04-27 14:43:42 -0700414 Stream<String> get logLines => _linesController.stream;
John McCutchan8803cec2016-03-07 13:52:27 -0800415
Hixie797e27e2016-03-14 13:31:43 -0700416 @override
Devon Carew67046f92016-02-20 22:00:11 -0800417 String get name => device.name;
418
Devon Carewb0dca792016-04-27 14:43:42 -0700419 void _start() {
420 runCommand(<String>[device.loggerPath]).then((Process process) {
421 _process = process;
422 _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
423 _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
John McCutchan8803cec2016-03-07 13:52:27 -0800424
Devon Carewb0dca792016-04-27 14:43:42 -0700425 _process.exitCode.then((int code) {
426 if (_linesController.hasListener)
427 _linesController.close();
428 });
429 });
Ian Hicksond745e202016-03-12 00:32:34 -0800430 }
John McCutchan8803cec2016-03-07 13:52:27 -0800431
Chris Brackenfea4a912016-09-23 16:59:03 -0700432 // Match for lines for the runner in syslog.
433 //
434 // iOS 9 format: Runner[297] <Notice>:
435 // iOS 10 format: Runner(libsystem_asl.dylib)[297] <Notice>:
Chris Brackenc5567a52016-09-26 17:12:20 -0700436 static final RegExp _runnerRegex = new RegExp(r'Runner(\(.*\))?\[[\d]+\] <[A-Za-z]+>: ');
John McCutchan8803cec2016-03-07 13:52:27 -0800437
438 void _onLine(String line) {
Chinmay Garde9782d972016-06-14 11:47:51 -0700439 Match match = _runnerRegex.firstMatch(line);
440
441 if (match != null) {
442 // Only display the log line after the initial device and executable information.
443 _linesController.add(line.substring(match.end));
444 }
Devon Carew67046f92016-02-20 22:00:11 -0800445 }
446
Devon Carewb0dca792016-04-27 14:43:42 -0700447 void _stop() {
448 _process?.kill();
Devon Carew67046f92016-02-20 22:00:11 -0800449 }
450}
John McCutchan5e140b72016-03-10 15:48:37 -0800451
452class _IOSDevicePortForwarder extends DevicePortForwarder {
Chinmay Garde9782d972016-06-14 11:47:51 -0700453 _IOSDevicePortForwarder(this.device) : _forwardedPorts = new List<ForwardedPort>();
John McCutchan5e140b72016-03-10 15:48:37 -0800454
455 final IOSDevice device;
456
Chinmay Garde9782d972016-06-14 11:47:51 -0700457 final List<ForwardedPort> _forwardedPorts;
458
Hixie797e27e2016-03-14 13:31:43 -0700459 @override
Chinmay Garde9782d972016-06-14 11:47:51 -0700460 List<ForwardedPort> get forwardedPorts => _forwardedPorts;
John McCutchan5e140b72016-03-10 15:48:37 -0800461
Hixie797e27e2016-03-14 13:31:43 -0700462 @override
John McCutchan5e140b72016-03-10 15:48:37 -0800463 Future<int> forward(int devicePort, {int hostPort: null}) async {
464 if ((hostPort == null) || (hostPort == 0)) {
465 // Auto select host port.
466 hostPort = await findAvailablePort();
467 }
Chinmay Garde9782d972016-06-14 11:47:51 -0700468
469 // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
470 Process process = await runCommand(<String>[
471 device.iproxyPath,
472 hostPort.toString(),
473 devicePort.toString(),
474 device.id,
475 ]);
476
477 ForwardedPort forwardedPort = new ForwardedPort.withContext(hostPort,
478 devicePort, process);
479
480 printTrace("Forwarded port $forwardedPort");
481
482 _forwardedPorts.add(forwardedPort);
483
484 return 1;
John McCutchan5e140b72016-03-10 15:48:37 -0800485 }
486
Hixie797e27e2016-03-14 13:31:43 -0700487 @override
Ian Hicksond745e202016-03-12 00:32:34 -0800488 Future<Null> unforward(ForwardedPort forwardedPort) async {
Chinmay Garde9782d972016-06-14 11:47:51 -0700489 if (!_forwardedPorts.remove(forwardedPort)) {
490 // Not in list. Nothing to remove.
491 return null;
492 }
493
494 printTrace("Unforwarding port $forwardedPort");
495
496 Process process = forwardedPort.context;
497
498 if (process != null) {
499 Process.killPid(process.pid);
500 } else {
501 printError("Forwarded port did not have a valid process");
502 }
503
504 return null;
John McCutchan5e140b72016-03-10 15:48:37 -0800505 }
506}