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