devices.dart 12.8 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:convert';
7 8

import '../application_package.dart';
9
import '../base/file_system.dart';
10
import '../base/io.dart';
11
import '../base/platform.dart';
12
import '../base/port_scanner.dart';
13
import '../base/process.dart';
14
import '../base/process_manager.dart';
15
import '../build_info.dart';
16 17
import '../device.dart';
import '../globals.dart';
18
import '../protocol_discovery.dart';
19
import 'ios_workflow.dart';
20 21
import 'mac.dart';

22
const String _kIdeviceinstallerInstructions =
23 24
    'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
    'brew install ideviceinstaller.';
25

26 27
const Duration kPortForwardTimeout = const Duration(seconds: 10);

28
class IOSDevices extends PollingDeviceDiscovery {
29
  IOSDevices() : super('iOS devices');
30

31
  @override
32
  bool get supportsPlatform => platform.isMacOS;
33

34
  @override
35
  bool get canListAnything => iosWorkflow.canListDevices;
36

37
  @override
38
  Future<List<Device>> pollingGetDevices() => IOSDevice.getAttachedDevices();
39 40 41
}

class IOSDevice extends Device {
42
  IOSDevice(String id, { this.name, String sdkVersion }) : _sdkVersion = sdkVersion, super(id) {
43
    _installerPath = _checkForCommand('ideviceinstaller');
44
    _iproxyPath = _checkForCommand('iproxy');
45
    _pusherPath = _checkForCommand(
46 47 48 49
      'ios-deploy',
      'To copy files to iOS devices, please install ios-deploy. To install, run:\n'
      'brew install ios-deploy'
    );
50 51 52
  }

  String _installerPath;
53
  String _iproxyPath;
54 55
  String _pusherPath;

56 57
  final String _sdkVersion;

58 59 60
  @override
  bool get supportsHotMode => true;

61
  @override
62 63
  final String name;

64
  Map<ApplicationPackage, _IOSDeviceLogReader> _logReaders;
65

66 67
  _IOSDevicePortForwarder _portForwarder;

68
  @override
69
  Future<bool> get isLocalEmulator async => false;
70

71
  @override
72 73
  bool get supportsStartPaused => false;

74
  static Future<List<IOSDevice>> getAttachedDevices() async {
75
    if (!iMobileDevice.isInstalled)
76 77
      return <IOSDevice>[];

78
    final List<IOSDevice> devices = <IOSDevice>[];
79 80 81 82 83 84 85 86
    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));
87 88 89 90 91 92
    }
    return devices;
  }

  static String _checkForCommand(
    String command, [
93
    String macInstructions = _kIdeviceinstallerInstructions
94
  ]) {
95 96 97
    try {
      command = runCheckedSync(<String>['which', command]).trim();
    } catch (e) {
98
      if (platform.isMacOS) {
99 100 101
        printError('$command not found. $macInstructions');
      } else {
        printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
102
      }
103 104 105
      return null;
    }
    return command;
106 107 108
  }

  @override
109
  Future<bool> isAppInstalled(ApplicationPackage app) async {
110
    try {
111
      final RunResult apps = await runCheckedAsync(<String>[_installerPath, '--list-apps']);
112
      if (new RegExp(app.id, multiLine: true).hasMatch(apps.stdout)) {
113 114
        return true;
      }
115 116 117 118 119 120
    } catch (e) {
      return false;
    }
    return false;
  }

121
  @override
122
  Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => false;
123

124
  @override
125
  Future<bool> installApp(ApplicationPackage app) async {
126 127
    final IOSApp iosApp = app;
    final Directory bundle = fs.directory(iosApp.deviceBundlePath);
128
    if (!bundle.existsSync()) {
129
      printError('Could not find application bundle at ${bundle.path}; have you run "flutter build ios"?');
130 131 132 133
      return false;
    }

    try {
134
      await runCheckedAsync(<String>[_installerPath, '-i', iosApp.deviceBundlePath]);
135 136 137 138 139
      return true;
    } catch (e) {
      return false;
    }
  }
140 141

  @override
142
  Future<bool> uninstallApp(ApplicationPackage app) async {
143
    try {
144
      await runCheckedAsync(<String>[_installerPath, '-U', app.id]);
145
      return true;
146 147 148 149 150
    } catch (e) {
      return false;
    }
  }

151 152 153
  @override
  bool isSupported() => true;

154
  @override
Devon Carew's avatar
Devon Carew committed
155
  Future<LaunchResult> startApp(
156
    ApplicationPackage app, {
157 158
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
159
    DebuggingOptions debuggingOptions,
160
    Map<String, dynamic> platformArgs,
161
    bool prebuiltApplication: false,
162
    bool previewDart2: false,
163
    bool applicationNeedsRebuild: false,
164
    bool usesTerminalUi: true,
165
  }) async {
166
    if (!prebuiltApplication) {
167
      // TODO(chinmaygarde): Use mainPath, route.
168 169 170
      printTrace('Building ${app.name} for $id');

      // Step 1: Build the precompiled/DBC application if necessary.
171 172
      final XcodeBuildResult buildResult = await buildXcodeProject(
          app: app,
173
          buildInfo: debuggingOptions.buildInfo,
174 175 176 177
          target: mainPath,
          buildForDevice: true,
          usesTerminalUi: usesTerminalUi,
      );
178 179
      if (!buildResult.success) {
        printError('Could not build the precompiled application for the device.');
180
        await diagnoseXcodeBuildFailure(buildResult, app);
181 182 183
        printError('');
        return new LaunchResult.failed();
      }
184
    } else {
185
      if (!await installApp(app))
186
        return new LaunchResult.failed();
187 188 189
    }

    // Step 2: Check that the application exists at the specified path.
190 191
    final IOSApp iosApp = app;
    final Directory bundle = fs.directory(iosApp.deviceBundlePath);
192
    if (!bundle.existsSync()) {
193
      printError('Could not find the built application bundle at ${bundle.path}.');
Devon Carew's avatar
Devon Carew committed
194
      return new LaunchResult.failed();
195 196 197
    }

    // Step 3: Attempt to install the application on the device.
198
    final List<String> launchArguments = <String>['--enable-dart-profiling'];
199 200

    if (debuggingOptions.startPaused)
201
      launchArguments.add('--start-paused');
202

203
    if (debuggingOptions.useTestFonts)
204
      launchArguments.add('--use-test-fonts');
205

206
    if (debuggingOptions.debuggingEnabled) {
207
      launchArguments.add('--enable-checked-mode');
208 209 210 211 212 213

      // 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.
    }

214
    if (debuggingOptions.enableSoftwareRendering)
215 216
      launchArguments.add('--enable-software-rendering');

217 218 219
    if (debuggingOptions.traceSkia)
      launchArguments.add('--trace-skia');

220 221 222
    if (platformArgs['trace-startup'] ?? false)
      launchArguments.add('--trace-startup');

223
    final List<String> launchCommand = <String>[
224 225 226 227 228 229
      '/usr/bin/env',
      'ios-deploy',
      '--id',
      id,
      '--bundle',
      bundle.path,
230
      '--no-wifi',
231
      '--justlaunch',
232 233
    ];

234
    if (launchArguments.isNotEmpty) {
235
      launchCommand.add('--args');
236
      launchCommand.add('${launchArguments.join(" ")}');
237 238 239
    }

    int installationResult = -1;
240
    Uri localObservatoryUri;
241

242
    if (!debuggingOptions.debuggingEnabled) {
243
      // If debugging is not enabled, just launch the application and continue.
244
      printTrace('Debugging is not enabled');
245 246
      installationResult = await runCommandAndStreamOutput(launchCommand, trace: true);
    } else {
247 248
      // Debugging is enabled, look for the observatory server port post launch.
      printTrace('Debugging is enabled, connecting to observatory');
249

250 251
      // 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.
252
      final ProtocolDiscovery observatoryDiscovery = new ProtocolDiscovery.observatory(
253 254
        getLogReader(app: app), portForwarder: portForwarder, hostPort: debuggingOptions.observatoryPort);

255
      final Future<Uri> forwardObservatoryUri = observatoryDiscovery.uri;
256

257
      final Future<int> launch = runCommandAndStreamOutput(launchCommand, trace: true);
258

259
      localObservatoryUri = await launch.then<Uri>((int result) async {
260 261 262
        installationResult = result;

        if (result != 0) {
263
          printTrace('Failed to launch the application on device.');
264
          return null;
265 266
        }

267
        printTrace('Application launched on the device. Attempting to forward ports.');
268
        return await forwardObservatoryUri;
269 270
      }).whenComplete(() {
        observatoryDiscovery.cancel();
271 272
      });
    }
273 274 275

    if (installationResult != 0) {
      printError('Could not install ${bundle.path} on $id.');
276
      printError('Try launching Xcode and selecting "Product > Run" to fix the problem:');
277
      printError('  open ios/Runner.xcworkspace');
278
      printError('');
Devon Carew's avatar
Devon Carew committed
279
      return new LaunchResult.failed();
280 281
    }

282
    return new LaunchResult.succeeded(observatoryUri: localObservatoryUri);
283 284
  }

285 286 287 288 289 290 291
  @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 {
292
    if (platform.isMacOS) {
293
      runSync(<String>[
294
        _pusherPath,
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
        '-t',
        '1',
        '--bundle_id',
        app.id,
        '--upload',
        localFile,
        '--to',
        targetFile
      ]);
      return true;
    } else {
      return false;
    }
  }

  @override
311
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
312

313
  @override
314
  Future<String> get sdkNameAndVersion async => 'iOS $_sdkVersion';
315

316
  @override
317 318 319
  DeviceLogReader getLogReader({ApplicationPackage app}) {
    _logReaders ??= <ApplicationPackage, _IOSDeviceLogReader>{};
    return _logReaders.putIfAbsent(app, () => new _IOSDeviceLogReader(this, app));
320 321
  }

322
  @override
323
  DevicePortForwarder get portForwarder => _portForwarder ??= new _IOSDevicePortForwarder(this);
324

325
  @override
326 327
  void clearLogs() {
  }
Devon Carew's avatar
Devon Carew committed
328 329

  @override
330
  bool get supportsScreenshot => iMobileDevice.isInstalled;
Devon Carew's avatar
Devon Carew committed
331 332

  @override
333
  Future<Null> takeScreenshot(File outputFile) => iMobileDevice.takeScreenshot(outputFile);
334 335 336
}

class _IOSDeviceLogReader extends DeviceLogReader {
337 338 339
  RegExp _lineRegex;

  _IOSDeviceLogReader(this.device, ApplicationPackage app) {
Devon Carew's avatar
Devon Carew committed
340
    _linesController = new StreamController<String>.broadcast(
341 342 343 344 345 346 347
      onListen: _start,
      onCancel: _stop
    );

    // Match for lines for the runner in syslog.
    //
    // iOS 9 format:  Runner[297] <Notice>:
348
    // iOS 10 format: Runner(Flutter)[297] <Notice>:
349
    final String appName = app == null ? '' : app.name.replaceAll('.app', '');
350
    _lineRegex = new RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: ');
Devon Carew's avatar
Devon Carew committed
351
  }
352 353 354

  final IOSDevice device;

Devon Carew's avatar
Devon Carew committed
355
  StreamController<String> _linesController;
356
  Process _process;
357

358
  @override
Devon Carew's avatar
Devon Carew committed
359
  Stream<String> get logLines => _linesController.stream;
360

361
  @override
362 363
  String get name => device.name;

Devon Carew's avatar
Devon Carew committed
364
  void _start() {
365
    iMobileDevice.startLogger().then<Null>((Process process) {
Devon Carew's avatar
Devon Carew committed
366 367 368
      _process = process;
      _process.stdout.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
      _process.stderr.transform(UTF8.decoder).transform(const LineSplitter()).listen(_onLine);
369
      _process.exitCode.whenComplete(() {
Devon Carew's avatar
Devon Carew committed
370 371 372 373
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
374 375 376
  }

  void _onLine(String line) {
377
    final Match match = _lineRegex.firstMatch(line);
378 379

    if (match != null) {
380
      final String logLine = line.substring(match.end);
381 382
      // Only display the log line after the initial device and executable information.
      _linesController.add(logLine);
383
    }
384 385
  }

Devon Carew's avatar
Devon Carew committed
386 387
  void _stop() {
    _process?.kill();
388 389
  }
}
390 391

class _IOSDevicePortForwarder extends DevicePortForwarder {
392
  _IOSDevicePortForwarder(this.device) : _forwardedPorts = <ForwardedPort>[];
393 394 395

  final IOSDevice device;

396 397
  final List<ForwardedPort> _forwardedPorts;

398
  @override
399
  List<ForwardedPort> get forwardedPorts => _forwardedPorts;
400

401
  @override
402
  Future<int> forward(int devicePort, {int hostPort}) async {
403 404
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
405
      hostPort = await portScanner.findAvailablePort();
406
    }
407 408

    // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
409
    final Process process = await runCommand(<String>[
410
      device._iproxyPath,
411 412 413 414 415
      hostPort.toString(),
      devicePort.toString(),
      device.id,
    ]);

416
    final ForwardedPort forwardedPort = new ForwardedPort.withContext(hostPort,
417 418
        devicePort, process);

419
    printTrace('Forwarded port $forwardedPort');
420 421 422

    _forwardedPorts.add(forwardedPort);

423
    return hostPort;
424 425
  }

426
  @override
Ian Hickson's avatar
Ian Hickson committed
427
  Future<Null> unforward(ForwardedPort forwardedPort) async {
428 429 430 431 432
    if (!_forwardedPorts.remove(forwardedPort)) {
      // Not in list. Nothing to remove.
      return null;
    }

433
    printTrace('Unforwarding port $forwardedPort');
434

435
    final Process process = forwardedPort.context;
436 437

    if (process != null) {
438
      processManager.killPid(process.pid);
439
    } else {
440
      printError('Forwarded port did not have a valid process');
441 442 443
    }

    return null;
444 445
  }
}