simulator.dart 5.19 KB
Newer Older
1 2 3 4 5 6 7 8
// 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' show JSON;
import 'dart:io';

9
import '../base/globals.dart';
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
import '../base/process.dart';

const String _xcrunPath = '/usr/bin/xcrun';

const String _simulatorPath =
  '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator';

/// A wrapper around the `simctl` command line tool.
class SimControl {
  static Future<bool> boot({String deviceId}) async {
    if (_isAnyConnected())
      return true;

    if (deviceId == null) {
      runDetached([_simulatorPath]);
      Future<bool> checkConnection([int attempts = 20]) async {
        if (attempts == 0) {
          printStatus('Timed out waiting for iOS Simulator to boot.');
          return false;
        }
        if (!_isAnyConnected()) {
          printStatus('Waiting for iOS Simulator to boot...');
          return await new Future.delayed(new Duration(milliseconds: 500),
            () => checkConnection(attempts - 1)
          );
        }
        return true;
      }
      return await checkConnection();
    } else {
      try {
        runCheckedSync([_xcrunPath, 'simctl', 'boot', deviceId]);
        return true;
      } catch (e) {
        printError('Unable to boot iOS Simulator $deviceId: ', e);
        return false;
      }
    }

    return false;
  }

  /// Returns a list of all available devices, both potential and connected.
  static List<SimDevice> getDevices() {
    // {
    //   "devices" : {
    //     "com.apple.CoreSimulator.SimRuntime.iOS-8-2" : [
    //       {
    //         "state" : "Shutdown",
    //         "availability" : " (unavailable, runtime profile not found)",
    //         "name" : "iPhone 4s",
    //         "udid" : "1913014C-6DCB-485D-AC6B-7CD76D322F5B"
    //       },
    //       ...

    List<String> args = <String>['simctl', 'list', '--json', 'devices'];
    printTrace('$_xcrunPath ${args.join(' ')}');
    ProcessResult results = Process.runSync(_xcrunPath, args);
    if (results.exitCode != 0) {
      printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
      return <SimDevice>[];
    }

    List<SimDevice> devices = <SimDevice>[];

    Map<String, Map<String, dynamic>> data = JSON.decode(results.stdout);
    Map<String, dynamic> devicesSection = data['devices'];

    for (String deviceCategory in devicesSection.keys) {
      List<dynamic> devicesData = devicesSection[deviceCategory];

      for (Map<String, String> data in devicesData) {
        devices.add(new SimDevice(deviceCategory, data));
      }
    }

    return devices;
  }

  /// Returns all the connected simulator devices.
  static List<SimDevice> getConnectedDevices() {
    return getDevices().where((SimDevice device) => device.isBooted).toList();
  }

  static StreamController<List<SimDevice>> _trackDevicesControler;

  /// Listens to changes in the set of connected devices. The implementation
  /// currently uses polling. Callers should be careful to call cancel() on any
  /// stream subscription when finished.
  ///
  /// TODO(devoncarew): We could investigate using the usbmuxd protocol directly.
  static Stream<List<SimDevice>> trackDevices() {
    if (_trackDevicesControler == null) {
      Timer timer;
      Set<String> deviceIds = new Set<String>();

      _trackDevicesControler = new StreamController.broadcast(
        onListen: () {
          timer = new Timer.periodic(new Duration(seconds: 4), (Timer timer) {
            List<SimDevice> devices = getConnectedDevices();

            if (_updateDeviceIds(devices, deviceIds)) {
              _trackDevicesControler.add(devices);
            }
          });
        }, onCancel: () {
          timer?.cancel();
          deviceIds.clear();
        }
      );
    }

    return _trackDevicesControler.stream;
  }

  /// Update the cached set of device IDs and return whether there were any changes.
  static bool _updateDeviceIds(List<SimDevice> devices, Set<String> deviceIds) {
    Set<String> newIds = new Set<String>.from(devices.map((SimDevice device) => device.udid));

    bool changed = false;

    for (String id in newIds) {
      if (!deviceIds.contains(id))
        changed = true;
    }

    for (String id in deviceIds) {
      if (!newIds.contains(id))
        changed = true;
    }

    deviceIds.clear();
    deviceIds.addAll(newIds);

    return changed;
  }

  static bool _isAnyConnected() => getConnectedDevices().isNotEmpty;

  static void install(String deviceId, String appPath) {
    runCheckedSync([_xcrunPath, 'simctl', 'install', deviceId, appPath]);
  }

  static void launch(String deviceId, String appIdentifier, [List<String> launchArgs]) {
    List<String> args = [_xcrunPath, 'simctl', 'launch', deviceId, appIdentifier];
    if (launchArgs != null)
      args.addAll(launchArgs);
    runCheckedSync(args);
  }
}

class SimDevice {
  SimDevice(this.category, this.data);

  final String category;
  final Map<String, String> data;

  String get state => data['state'];
  String get availability => data['availability'];
  String get name => data['name'];
  String get udid => data['udid'];

  bool get isBooted => state == 'Booted';
}