devices.dart 9.52 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 9
import 'dart:io';

import '../application_package.dart';
10
import '../base/os.dart';
11
import '../base/process.dart';
12
import '../build_info.dart';
13 14
import '../device.dart';
import '../globals.dart';
15
import '../observatory.dart';
16 17 18 19 20 21 22 23 24
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');

25
  @override
26
  bool get supportsPlatform => Platform.isMacOS;
27 28

  @override
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  List<Device> 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;

65
  @override
66 67
  final String name;

68 69
  _IOSDeviceLogReader _logReader;

70 71
  _IOSDevicePortForwarder _portForwarder;

72
  @override
73 74
  bool get isLocalEmulator => false;

75
  @override
76 77 78
  bool get supportsStartPaused => false;

  static List<IOSDevice> getAttachedDevices([IOSDevice mockIOS]) {
79
    if (!doctor.iosWorkflow.hasIDeviceId)
80 81
      return <IOSDevice>[];

82
    List<IOSDevice> devices = <IOSDevice>[];
83 84 85 86 87 88 89 90 91 92
    for (String id in _getAttachedDeviceIDs(mockIOS)) {
      String name = _getDeviceName(id, mockIOS);
      devices.add(new IOSDevice(id, name: name));
    }
    return devices;
  }

  static Iterable<String> _getAttachedDeviceIDs([IOSDevice mockIOS]) {
    String listerPath = (mockIOS != null) ? mockIOS.listerPath : _checkForCommand('idevice_id');
    try {
93
      String output = runSync(<String>[listerPath, '-l']);
94 95 96 97 98 99 100 101 102 103
      return output.trim().split('\n').where((String s) => s != null && s.isNotEmpty);
    } catch (e) {
      return <String>[];
    }
  }

  static String _getDeviceName(String deviceID, [IOSDevice mockIOS]) {
    String informerPath = (mockIOS != null)
        ? mockIOS.informerPath
        : _checkForCommand('ideviceinfo');
104
    return runSync(<String>[informerPath, '-k', 'DeviceName', '-u', deviceID]).trim();
105 106
  }

107
  static final Map<String, String> _commandMap = <String, String>{};
108 109 110 111 112 113
  static String _checkForCommand(
    String command, [
    String macInstructions = _ideviceinstallerInstructions
  ]) {
    return _commandMap.putIfAbsent(command, () {
      try {
114
        command = runCheckedSync(<String>['which', command]).trim();
115 116 117 118 119 120 121 122 123 124 125 126
      } 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
127
  bool isAppInstalled(ApplicationPackage app) {
128
    try {
129 130 131 132
      String apps = runCheckedSync(<String>[installerPath, '--list-apps']);
      if (new RegExp(app.id, multiLine: true).hasMatch(apps)) {
        return true;
      }
133 134 135 136 137 138 139
    } catch (e) {
      return false;
    }
    return false;
  }

  @override
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
  bool installApp(ApplicationPackage app) {
    IOSApp iosApp = app;
    Directory bundle = new Directory(iosApp.deviceBundlePath);
    if (!bundle.existsSync()) {
      printError("Could not find application bundle at ${bundle.path}; have you run 'flutter build ios'?");
      return false;
    }

    try {
      runCheckedSync(<String>[installerPath, '-i', iosApp.deviceBundlePath]);
      return true;
    } catch (e) {
      return false;
    }
  }
155 156

  @override
157
  bool uninstallApp(ApplicationPackage app) {
158
    try {
159 160
      runCheckedSync(<String>[installerPath, '-U', app.id]);
      return true;
161 162 163 164 165
    } catch (e) {
      return false;
    }
  }

166 167 168
  @override
  bool isSupported() => true;

169
  @override
Devon Carew's avatar
Devon Carew committed
170
  Future<LaunchResult> startApp(
171 172
    ApplicationPackage app,
    BuildMode mode, {
173 174
    String mainPath,
    String route,
Devon Carew's avatar
Devon Carew committed
175
    DebuggingOptions debuggingOptions,
176 177
    Map<String, dynamic> platformArgs
  }) async {
Devon Carew's avatar
Devon Carew committed
178
    // TODO(chinmaygarde): Use checked, mainPath, route.
179 180 181
    // TODO(devoncarew): Handle startPaused, debugPort.
    printTrace('Building ${app.name} for $id');

182 183
    // Step 1: Install the precompiled/DBC application if necessary.
    bool buildResult = await buildIOSXcodeProject(app, mode, buildForDevice: true);
184 185
    if (!buildResult) {
      printError('Could not build the precompiled application for the device.');
Devon Carew's avatar
Devon Carew committed
186
      return new LaunchResult.failed();
187 188 189
    }

    // Step 2: Check that the application exists at the specified path.
190 191
    IOSApp iosApp = app;
    Directory bundle = new 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
    int installationResult = await runCommandAndStreamOutput(<String>[
199 200 201 202 203 204
      '/usr/bin/env',
      'ios-deploy',
      '--id',
      id,
      '--bundle',
      bundle.path,
205
      '--justlaunch',
206 207 208 209
    ]);

    if (installationResult != 0) {
      printError('Could not install ${bundle.path} on $id.');
Devon Carew's avatar
Devon Carew committed
210
      return new LaunchResult.failed();
211 212
    }

Devon Carew's avatar
Devon Carew committed
213
    return new LaunchResult.succeeded();
214 215
  }

216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
  @override
  Future<bool> restartApp(
    ApplicationPackage package,
    LaunchResult result, {
    String mainPath,
    Observatory observatory
  }) async {
    return observatory.isolateReload(observatory.firstIsolateId).then((Response response) {
      return true;
    }).catchError((dynamic error) {
      printError('Error restarting app: $error');
      return false;
    });
  }

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
  @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 {
    if (Platform.isMacOS) {
      runSync(<String>[
        pusherPath,
        '-t',
        '1',
        '--bundle_id',
        app.id,
        '--upload',
        localFile,
        '--to',
        targetFile
      ]);
      return true;
    } else {
      return false;
    }
    return false;
  }

  @override
258
  TargetPlatform get platform => TargetPlatform.ios;
259

260
  @override
261 262 263 264 265 266 267
  DeviceLogReader get logReader {
    if (_logReader == null)
      _logReader = new _IOSDeviceLogReader(this);

    return _logReader;
  }

268
  @override
269 270 271 272 273 274 275
  DevicePortForwarder get portForwarder {
    if (_portForwarder == null)
      _portForwarder = new _IOSDevicePortForwarder(this);

    return _portForwarder;
  }

276
  @override
277 278
  void clearLogs() {
  }
Devon Carew's avatar
Devon Carew committed
279 280 281 282 283 284 285 286 287 288 289 290

  @override
  bool get supportsScreenshot => false;

  @override
  Future<bool> takeScreenshot(File outputFile) {
    // We could use idevicescreenshot here (installed along with the brew
    // ideviceinstaller tools). It however requires a developer disk image on
    // the device.

    return new Future<bool>.value(false);
  }
291 292 293
}

class _IOSDeviceLogReader extends DeviceLogReader {
Devon Carew's avatar
Devon Carew committed
294 295 296 297 298 299
  _IOSDeviceLogReader(this.device) {
    _linesController = new StreamController<String>.broadcast(
     onListen: _start,
     onCancel: _stop
   );
  }
300 301 302

  final IOSDevice device;

Devon Carew's avatar
Devon Carew committed
303
  StreamController<String> _linesController;
304 305
  Process _process;

306
  @override
Devon Carew's avatar
Devon Carew committed
307
  Stream<String> get logLines => _linesController.stream;
308

309
  @override
310 311
  String get name => device.name;

Devon Carew's avatar
Devon Carew committed
312 313 314 315 316
  void _start() {
    runCommand(<String>[device.loggerPath]).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);
317

Devon Carew's avatar
Devon Carew committed
318 319 320 321 322
      _process.exitCode.then((int code) {
        if (_linesController.hasListener)
          _linesController.close();
      });
    });
323 324
  }

Devon Carew's avatar
Devon Carew committed
325
  static final RegExp _runnerRegex = new RegExp(r'FlutterRunner');
326 327

  void _onLine(String line) {
Devon Carew's avatar
Devon Carew committed
328 329
    if (_runnerRegex.hasMatch(line))
      _linesController.add(line);
330 331
  }

Devon Carew's avatar
Devon Carew committed
332 333
  void _stop() {
    _process?.kill();
334 335
  }
}
336 337 338 339 340 341

class _IOSDevicePortForwarder extends DevicePortForwarder {
  _IOSDevicePortForwarder(this.device);

  final IOSDevice device;

342
  @override
343 344 345 346 347 348
  List<ForwardedPort> get forwardedPorts {
    final List<ForwardedPort> ports = <ForwardedPort>[];
    // TODO(chinmaygarde): Implement.
    return ports;
  }

349
  @override
350 351 352 353 354 355 356 357 358
  Future<int> forward(int devicePort, {int hostPort: null}) async {
    if ((hostPort == null) || (hostPort == 0)) {
      // Auto select host port.
      hostPort = await findAvailablePort();
    }
    // TODO(chinmaygarde): Implement.
    return hostPort;
  }

359
  @override
Ian Hickson's avatar
Ian Hickson committed
360
  Future<Null> unforward(ForwardedPort forwardedPort) async {
361 362 363
    // TODO(chinmaygarde): Implement.
  }
}