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