1import { execSync } from 'child_process';
2
3import * as Log from '../../../log';
4import { CommandError } from '../../../utils/errors';
5import * as SimControl from './simctl';
6
7type DeviceContext = Partial<Pick<SimControl.Device, 'osType'>>;
8
9/**
10 * Returns the default device stored in the Simulator.app settings.
11 * This helps us to get the device that the user opened most recently regardless of which tool they used.
12 */
13function getDefaultSimulatorDeviceUDID() {
14  try {
15    const defaultDeviceUDID = execSync(
16      `defaults read com.apple.iphonesimulator CurrentDeviceUDID`,
17      { stdio: 'pipe' }
18    ).toString();
19    return defaultDeviceUDID.trim();
20  } catch {
21    return null;
22  }
23}
24
25export async function getBestBootedSimulatorAsync({
26  osType,
27}: DeviceContext = {}): Promise<SimControl.Device | null> {
28  const [simulatorOpenedByApp] = await SimControl.getBootedSimulatorsAsync();
29  // This should prevent opening a second simulator in the chance that default
30  // simulator doesn't match what the Simulator app would open by default.
31  if (
32    simulatorOpenedByApp?.udid &&
33    (!osType || (osType && simulatorOpenedByApp.osType === osType))
34  ) {
35    Log.debug(`First booted simulator: ${simulatorOpenedByApp?.windowName}`);
36    return simulatorOpenedByApp;
37  }
38
39  Log.debug(`No booted simulator matching requirements (osType: ${osType}).`);
40  return null;
41}
42
43/**
44 * Returns the most preferred simulator UDID without booting anything.
45 *
46 * 1. If the simulator app defines a default simulator and the osType is not defined.
47 * 2. If the osType is defined, then check if the default udid matches the osType.
48 * 3. If all else fails, return the first found simulator.
49 */
50export async function getBestUnbootedSimulatorAsync({ osType }: DeviceContext = {}): Promise<
51  string | null
52> {
53  const defaultId = getDefaultSimulatorDeviceUDID();
54  Log.debug(`Default simulator ID: ${defaultId}`);
55
56  if (defaultId && !osType) {
57    return defaultId;
58  }
59
60  const simulators = await getSelectableSimulatorsAsync({ osType });
61
62  if (!simulators.length) {
63    // TODO: Prompt to install the simulators
64    throw new CommandError(
65      'UNSUPPORTED_OS_TYPE',
66      `No ${osType || 'iOS'} devices available in Simulator.app`
67    );
68  }
69
70  // If the default udid is defined, then check to ensure its osType matches the required os.
71  if (defaultId) {
72    const defaultSimulator = simulators.find((device) => device.udid === defaultId);
73    if (defaultSimulator?.osType === osType) {
74      return defaultId;
75    }
76  }
77
78  // Return first selectable device.
79  return simulators[0]?.udid ?? null;
80}
81
82/**
83 * Get all simulators supported by Expo Go (iOS only).
84 */
85export async function getSelectableSimulatorsAsync({ osType = 'iOS' }: DeviceContext = {}): Promise<
86  SimControl.Device[]
87> {
88  const simulators = await SimControl.getDevicesAsync();
89  return simulators.filter((device) => device.isAvailable && device.osType === osType);
90}
91
92/**
93 * Get 'best' simulator for the user based on:
94 * 1. Currently booted simulator.
95 * 2. Last simulator that was opened.
96 * 3. First simulator that was opened.
97 */
98export async function getBestSimulatorAsync({ osType }: DeviceContext): Promise<string | null> {
99  const simulatorOpenedByApp = await getBestBootedSimulatorAsync({ osType });
100
101  if (simulatorOpenedByApp) {
102    return simulatorOpenedByApp.udid;
103  }
104
105  return await getBestUnbootedSimulatorAsync({ osType });
106}
107