All files / lib simulator-xcode-9.js

7.41% Statements 2/27
0% Branches 0/8
0% Functions 0/9
7.41% Lines 2/27
1 2 3 4 5 6 7 8 9 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                  1x 1x                                                                                                                                                                                                                                                            
import SimulatorXcode8 from './simulator-xcode-8';
import AsyncLock from 'async-lock';
import log from './logger';
import { shutdown as simctlShutdown, bootDevice, eraseDevice } from 'node-simctl';
import { waitForCondition } from 'asyncbox';
import { restoreTouchEnrollShortcuts, backupTouchEnrollShortcuts,
         setTouchEnrollKey } from './touch-enroll.js';
 
 
const SIMULATOR_SHUTDOWN_TIMEOUT = 15 * 1000;
const startupLock = new AsyncLock();
 
class SimulatorXcode9 extends SimulatorXcode8 {
  constructor (udid, xcodeVersion) {
    super(udid, xcodeVersion);
  }
 
  /**
   * Executes given Simulator with options. The Simulator will not be restarted if
   * it is already running and the current UI state matches to `isHeadless` option.
   * @override
   *
   * @param {object} opts - One or more of available Simulator options:
   *   - {string} scaleFactor: can be one of ['1.0', '0.75', '0.5', '0.33', '0.25'].
   *   Defines the window scale value for the UI client window for the current Simulator.
   *   Equals to null by default, which keeps the current scale unchanged.
   *   - {boolean} connectHardwareKeyboard: whether to connect the hardware keyboard to the
   *   Simulator UI client. Equals to false by default.
   *   - {boolean} allowTouchEnroll: whether to enroll Touch ID in the Simulator UI client.
   *   Equals to false by default.
   *   - {number} startupTimeout: number of milliseconds to wait until Simulator booting
   *   process is completed. The default timeout will be used if not set explicitly.
   *   - {boolean} isHeadless: whether to start the Simulator in headless mode (with UI
   *   client invisible). `false` by default.
   */
  async run (opts = {}) {
    opts = Object.assign({
      isHeadless: false,
      allowTouchEnroll: false,
      startupTimeout: this.startupTimeout,
    }, opts);
    const bootSimulator = async () => {
      try {
        await bootDevice(this.udid);
      } catch (err) {
        log.warn(`'xcrun simctl boot ${this.udid}' command has returned non-zero code. The problem was: ${err.stderr}`);
      }
    };
    const waitForShutdown = async () => {
      await waitForCondition(async () => {
        const {state} = await this.stat();
        return state === 'Shutdown';
      }, {waitMs: SIMULATOR_SHUTDOWN_TIMEOUT, intervalMs: 500});
    };
    let shouldWaitForBoot = true;
    const startTime = process.hrtime();
    await startupLock.acquire(this.uiClientBundleId, async () => {
      const stat = await this.stat();
      const serverState = stat.state;
      const isServerRunning = serverState === 'Booted';
      const isUIClientRunning = await this.isUIClientRunning();
      if (opts.isHeadless) {
        if (isServerRunning && !isUIClientRunning) {
          log.info(`Simulator with UDID ${this.udid} already booted in headless mode.`);
          shouldWaitForBoot = false;
          return;
        }
        if (await this.killUIClient()) {
          // Stopping the UI client also kills all running servers. Sad but true
          log.info(`Detected the UI client was running and killed it. Verifying the Simulator is in Shutdown state...`);
          await waitForShutdown();
        }
        log.info(`Booting Simulator with UDID ${this.udid} in headless mode. All UI-related capabilities are going to be ignored.`);
        await bootSimulator();
      } else {
        if (isServerRunning && isUIClientRunning) {
          log.info(`Both Simulator with UDID ${this.udid} and the UI client are currently running`);
          shouldWaitForBoot = false;
          return;
        }
        if (['Shutdown', 'Booted'].indexOf(serverState) === -1) {
          log.info(`Simulator ${this.udid} is in '${serverState}' state. Trying to shutdown...`);
          try {
            await this.shutdown();
          } catch (err) {
            log.warn(`Error on Simulator shutdown: ${err.message}`);
          }
          await waitForShutdown();
        }
        // Set the 'Touch ID Enroll' key bindings before the Simulator starts
        if (opts.allowTouchEnroll) {
          await setTouchEnrollKey();
        }
        log.info(`Booting Simulator with UDID ${this.udid}...`);
        await bootSimulator();
        if (!isUIClientRunning) {
          await this.startUIClient(opts);
        }
      }
    });
 
    if (shouldWaitForBoot) {
      await this.waitForBoot(opts.startupTimeout);
      log.info(`Simulator with UDID ${this.udid} booted in ${process.hrtime(startTime)[0]} seconds`);
    }
  }
 
  /**
   * Shut down the current Simulator.
   * @override
   */
  async shutdown () {
    await restoreTouchEnrollShortcuts();
    const {state} = await this.stat();
    if (state === 'Shutdown') {
      return;
    }
    await simctlShutdown(this.udid);
  }
 
  async enrollTouchID () {
    await backupTouchEnrollShortcuts();
    await super.enrollTouchID();
  }
 
  /**
   * Reset the current Simulator to the clean state.
   * @override
   */
  async clean () {
    log.info(`Cleaning simulator ${this.udid}`);
    await eraseDevice(this.udid, 10000);
  }
}
 
export default SimulatorXcode9;