18d307f52SEvan Baconimport { execSync } from 'child_process';
28d307f52SEvan Bacon
38d307f52SEvan Baconimport * as SimControl from './simctl';
4*8a424bebSJames Ideimport { CommandError } from '../../../utils/errors';
58d307f52SEvan Bacon
6474a7a4bSEvan Baconconst debug = require('debug')('expo:start:platforms:ios:getBestSimulator') as typeof console.log;
7474a7a4bSEvan Bacon
88d307f52SEvan Bacontype DeviceContext = Partial<Pick<SimControl.Device, 'osType'>>;
98d307f52SEvan Bacon
108d307f52SEvan Bacon/**
118d307f52SEvan Bacon * Returns the default device stored in the Simulator.app settings.
128d307f52SEvan Bacon * This helps us to get the device that the user opened most recently regardless of which tool they used.
138d307f52SEvan Bacon */
148d307f52SEvan Baconfunction getDefaultSimulatorDeviceUDID() {
158d307f52SEvan Bacon  try {
168d307f52SEvan Bacon    const defaultDeviceUDID = execSync(
178d307f52SEvan Bacon      `defaults read com.apple.iphonesimulator CurrentDeviceUDID`,
188d307f52SEvan Bacon      { stdio: 'pipe' }
198d307f52SEvan Bacon    ).toString();
208d307f52SEvan Bacon    return defaultDeviceUDID.trim();
218d307f52SEvan Bacon  } catch {
228d307f52SEvan Bacon    return null;
238d307f52SEvan Bacon  }
248d307f52SEvan Bacon}
258d307f52SEvan Bacon
268d307f52SEvan Baconexport async function getBestBootedSimulatorAsync({
278d307f52SEvan Bacon  osType,
288d307f52SEvan Bacon}: DeviceContext = {}): Promise<SimControl.Device | null> {
298d307f52SEvan Bacon  const [simulatorOpenedByApp] = await SimControl.getBootedSimulatorsAsync();
308d307f52SEvan Bacon  // This should prevent opening a second simulator in the chance that default
318d307f52SEvan Bacon  // simulator doesn't match what the Simulator app would open by default.
328d307f52SEvan Bacon  if (
338d307f52SEvan Bacon    simulatorOpenedByApp?.udid &&
348d307f52SEvan Bacon    (!osType || (osType && simulatorOpenedByApp.osType === osType))
358d307f52SEvan Bacon  ) {
36474a7a4bSEvan Bacon    debug(`First booted simulator: ${simulatorOpenedByApp?.windowName}`);
378d307f52SEvan Bacon    return simulatorOpenedByApp;
388d307f52SEvan Bacon  }
398d307f52SEvan Bacon
40474a7a4bSEvan Bacon  debug(`No booted simulator matching requirements (osType: ${osType}).`);
418d307f52SEvan Bacon  return null;
428d307f52SEvan Bacon}
438d307f52SEvan Bacon
448d307f52SEvan Bacon/**
458d307f52SEvan Bacon * Returns the most preferred simulator UDID without booting anything.
468d307f52SEvan Bacon *
478d307f52SEvan Bacon * 1. If the simulator app defines a default simulator and the osType is not defined.
488d307f52SEvan Bacon * 2. If the osType is defined, then check if the default udid matches the osType.
498d307f52SEvan Bacon * 3. If all else fails, return the first found simulator.
508d307f52SEvan Bacon */
518d307f52SEvan Baconexport async function getBestUnbootedSimulatorAsync({ osType }: DeviceContext = {}): Promise<
528d307f52SEvan Bacon  string | null
538d307f52SEvan Bacon> {
548d307f52SEvan Bacon  const defaultId = getDefaultSimulatorDeviceUDID();
55474a7a4bSEvan Bacon  debug(`Default simulator ID: ${defaultId}`);
568d307f52SEvan Bacon
578d307f52SEvan Bacon  if (defaultId && !osType) {
588d307f52SEvan Bacon    return defaultId;
598d307f52SEvan Bacon  }
608d307f52SEvan Bacon
618d307f52SEvan Bacon  const simulators = await getSelectableSimulatorsAsync({ osType });
628d307f52SEvan Bacon
638d307f52SEvan Bacon  if (!simulators.length) {
648d307f52SEvan Bacon    // TODO: Prompt to install the simulators
658d307f52SEvan Bacon    throw new CommandError(
668d307f52SEvan Bacon      'UNSUPPORTED_OS_TYPE',
678d307f52SEvan Bacon      `No ${osType || 'iOS'} devices available in Simulator.app`
688d307f52SEvan Bacon    );
698d307f52SEvan Bacon  }
708d307f52SEvan Bacon
718d307f52SEvan Bacon  // If the default udid is defined, then check to ensure its osType matches the required os.
728d307f52SEvan Bacon  if (defaultId) {
738d307f52SEvan Bacon    const defaultSimulator = simulators.find((device) => device.udid === defaultId);
748d307f52SEvan Bacon    if (defaultSimulator?.osType === osType) {
758d307f52SEvan Bacon      return defaultId;
768d307f52SEvan Bacon    }
778d307f52SEvan Bacon  }
788d307f52SEvan Bacon
798d307f52SEvan Bacon  // Return first selectable device.
808d307f52SEvan Bacon  return simulators[0]?.udid ?? null;
818d307f52SEvan Bacon}
828d307f52SEvan Bacon
838d307f52SEvan Bacon/**
848d307f52SEvan Bacon * Get all simulators supported by Expo Go (iOS only).
858d307f52SEvan Bacon */
868d307f52SEvan Baconexport async function getSelectableSimulatorsAsync({ osType = 'iOS' }: DeviceContext = {}): Promise<
878d307f52SEvan Bacon  SimControl.Device[]
888d307f52SEvan Bacon> {
898d307f52SEvan Bacon  const simulators = await SimControl.getDevicesAsync();
908d307f52SEvan Bacon  return simulators.filter((device) => device.isAvailable && device.osType === osType);
918d307f52SEvan Bacon}
928d307f52SEvan Bacon
938d307f52SEvan Bacon/**
948d307f52SEvan Bacon * Get 'best' simulator for the user based on:
958d307f52SEvan Bacon * 1. Currently booted simulator.
968d307f52SEvan Bacon * 2. Last simulator that was opened.
978d307f52SEvan Bacon * 3. First simulator that was opened.
988d307f52SEvan Bacon */
9929975bfdSEvan Baconexport async function getBestSimulatorAsync({ osType }: DeviceContext): Promise<string | null> {
1008d307f52SEvan Bacon  const simulatorOpenedByApp = await getBestBootedSimulatorAsync({ osType });
1018d307f52SEvan Bacon
1028d307f52SEvan Bacon  if (simulatorOpenedByApp) {
1038d307f52SEvan Bacon    return simulatorOpenedByApp.udid;
1048d307f52SEvan Bacon  }
1058d307f52SEvan Bacon
1068d307f52SEvan Bacon  return await getBestUnbootedSimulatorAsync({ osType });
1078d307f52SEvan Bacon}
108