1import * as osascript from '@expo/osascript';
2import spawnAsync from '@expo/spawn-async';
3
4import { Device } from './simctl';
5import * as Log from '../../../log';
6import { waitForActionAsync } from '../../../utils/delay';
7import { CommandError } from '../../../utils/errors';
8
9/** Open the Simulator.app and return when the system registers it as 'open'. */
10export async function ensureSimulatorAppRunningAsync(
11  device: Partial<Pick<Device, 'udid'>>,
12  {
13    maxWaitTime,
14  }: {
15    maxWaitTime?: number;
16  } = {}
17): Promise<void> {
18  if (await isSimulatorAppRunningAsync()) {
19    return;
20  }
21
22  Log.log(`\u203A Opening the iOS simulator, this might take a moment.`);
23
24  // In theory this would ensure the correct simulator is booted as well.
25  // This isn't theory though, this is Xcode.
26  await openSimulatorAppAsync(device);
27
28  if (!(await waitForSimulatorAppToStart({ maxWaitTime }))) {
29    throw new CommandError(
30      'SIMULATOR_TIMEOUT',
31      `Simulator app did not open fast enough. Try opening Simulator first, then running your app.`
32    );
33  }
34}
35
36async function waitForSimulatorAppToStart({
37  maxWaitTime,
38}: { maxWaitTime?: number } = {}): Promise<boolean> {
39  return waitForActionAsync<boolean>({
40    interval: 50,
41    maxWaitTime,
42    action: isSimulatorAppRunningAsync,
43  });
44}
45
46// I think the app can be open while no simulators are booted.
47async function isSimulatorAppRunningAsync(): Promise<boolean> {
48  try {
49    const zeroMeansNo = (
50      await osascript.execAsync(
51        'tell app "System Events" to count processes whose name is "Simulator"'
52      )
53    ).trim();
54    if (zeroMeansNo === '0') {
55      return false;
56    }
57  } catch (error: any) {
58    if (error.message.includes('Application isn’t running')) {
59      return false;
60    }
61    throw error;
62  }
63
64  return true;
65}
66
67async function openSimulatorAppAsync(device: { udid?: string }) {
68  const args = ['-a', 'Simulator'];
69  if (device.udid) {
70    // This has no effect if the app is already running.
71    args.push('--args', '-CurrentDeviceUDID', device.udid);
72  }
73  await spawnAsync('open', args);
74}
75