18d307f52SEvan Baconimport * as osascript from '@expo/osascript';
28d307f52SEvan Baconimport spawnAsync from '@expo/spawn-async';
38d307f52SEvan Bacon
4*8a424bebSJames Ideimport { Device } from './simctl';
58d307f52SEvan Baconimport * as Log from '../../../log';
68d307f52SEvan Baconimport { waitForActionAsync } from '../../../utils/delay';
78d307f52SEvan Baconimport { CommandError } from '../../../utils/errors';
88d307f52SEvan Bacon
98d307f52SEvan Bacon/** Open the Simulator.app and return when the system registers it as 'open'. */
108d307f52SEvan Baconexport async function ensureSimulatorAppRunningAsync(
118d307f52SEvan Bacon  device: Partial<Pick<Device, 'udid'>>,
128d307f52SEvan Bacon  {
138d307f52SEvan Bacon    maxWaitTime,
148d307f52SEvan Bacon  }: {
158d307f52SEvan Bacon    maxWaitTime?: number;
168d307f52SEvan Bacon  } = {}
178d307f52SEvan Bacon): Promise<void> {
188d307f52SEvan Bacon  if (await isSimulatorAppRunningAsync()) {
198d307f52SEvan Bacon    return;
208d307f52SEvan Bacon  }
218d307f52SEvan Bacon
228d307f52SEvan Bacon  Log.log(`\u203A Opening the iOS simulator, this might take a moment.`);
238d307f52SEvan Bacon
248d307f52SEvan Bacon  // In theory this would ensure the correct simulator is booted as well.
258d307f52SEvan Bacon  // This isn't theory though, this is Xcode.
268d307f52SEvan Bacon  await openSimulatorAppAsync(device);
278d307f52SEvan Bacon
288d307f52SEvan Bacon  if (!(await waitForSimulatorAppToStart({ maxWaitTime }))) {
298d307f52SEvan Bacon    throw new CommandError(
308d307f52SEvan Bacon      'SIMULATOR_TIMEOUT',
318d307f52SEvan Bacon      `Simulator app did not open fast enough. Try opening Simulator first, then running your app.`
328d307f52SEvan Bacon    );
338d307f52SEvan Bacon  }
348d307f52SEvan Bacon}
358d307f52SEvan Bacon
368d307f52SEvan Baconasync function waitForSimulatorAppToStart({
378d307f52SEvan Bacon  maxWaitTime,
388d307f52SEvan Bacon}: { maxWaitTime?: number } = {}): Promise<boolean> {
398d307f52SEvan Bacon  return waitForActionAsync<boolean>({
408d307f52SEvan Bacon    interval: 50,
418d307f52SEvan Bacon    maxWaitTime,
428d307f52SEvan Bacon    action: isSimulatorAppRunningAsync,
438d307f52SEvan Bacon  });
448d307f52SEvan Bacon}
458d307f52SEvan Bacon
468d307f52SEvan Bacon// I think the app can be open while no simulators are booted.
478d307f52SEvan Baconasync function isSimulatorAppRunningAsync(): Promise<boolean> {
488d307f52SEvan Bacon  try {
498d307f52SEvan Bacon    const zeroMeansNo = (
508d307f52SEvan Bacon      await osascript.execAsync(
518d307f52SEvan Bacon        'tell app "System Events" to count processes whose name is "Simulator"'
528d307f52SEvan Bacon      )
538d307f52SEvan Bacon    ).trim();
548d307f52SEvan Bacon    if (zeroMeansNo === '0') {
558d307f52SEvan Bacon      return false;
568d307f52SEvan Bacon    }
5729975bfdSEvan Bacon  } catch (error: any) {
588d307f52SEvan Bacon    if (error.message.includes('Application isn’t running')) {
598d307f52SEvan Bacon      return false;
608d307f52SEvan Bacon    }
618d307f52SEvan Bacon    throw error;
628d307f52SEvan Bacon  }
638d307f52SEvan Bacon
648d307f52SEvan Bacon  return true;
658d307f52SEvan Bacon}
668d307f52SEvan Bacon
678d307f52SEvan Baconasync function openSimulatorAppAsync(device: { udid?: string }) {
688d307f52SEvan Bacon  const args = ['-a', 'Simulator'];
698d307f52SEvan Bacon  if (device.udid) {
708d307f52SEvan Bacon    // This has no effect if the app is already running.
718d307f52SEvan Bacon    args.push('--args', '-CurrentDeviceUDID', device.udid);
728d307f52SEvan Bacon  }
738d307f52SEvan Bacon  await spawnAsync('open', args);
748d307f52SEvan Bacon}
75