import spawnAsync , { SpawnOptions , SpawnResult } from '@expo/spawn-async' ; import chalk from 'chalk' ; import { execSync } from 'child_process' ; import path from 'path' ; import { waitForActionAsync } from './apple/utils/waitForActionAsync' ; import { AppleDevice , CoreSimulator , Logger , XDLError } from './internal' ; import { profileMethod } from './utils/profileMethod' ; type DeviceState = 'Shutdown' | 'Booted' ; export type SimulatorDevice = { availabilityError : 'runtime profile not found' ; /**
     * '/Users/name/Library/Developer/CoreSimulator/Devices/00E55DC0-0364-49DF-9EC6-77BE587137D4/data'
     */ dataPath : string ; /**
     * '/Users/name/Library/Logs/CoreSimulator/00E55DC0-0364-49DF-9EC6-77BE587137D4'
     */ logPath : string ; /**
     * '00E55DC0-0364-49DF-9EC6-77BE587137D4'
     */ udid : string ; /**
     * com.apple.CoreSimulator.SimRuntime.tvOS-13-4
     */ runtime : string ; isAvailable : boolean ; /**
     * 'com.apple.CoreSimulator.SimDeviceType.Apple-TV-1080p'
     */ deviceTypeIdentifier : string ; state : DeviceState ; /**
     * 'Apple TV'
     */ name : string ; osType : OSType ; /**
     * '13.4'
     */ osVersion : string ; /**
     * 'iPhone 11 (13.6)'
     */ windowName : string ; } ; export type XCTraceDevice = { /**
     * '00E55DC0-0364-49DF-9EC6-77BE587137D4'
     */ udid : string ; /**
     * 'Apple TV'
     */ name : string ; deviceType : 'device' | 'catalyst' ; /**
     * '13.4'
     */ osVersion : string ; } ; type OSType = 'iOS' | 'tvOS' | 'watchOS' | 'macOS' ; type PermissionName = 'all' | 'calendar' | 'contacts-limited' | 'contacts' | 'location' | 'location-always' | 'photos-add' | 'photos' | 'media-library' | 'microphone' | 'motion' | 'reminders' | 'siri' ; type SimulatorDeviceList = { devices : { [ runtime ] : SimulatorDevice [ ] ; } ; } ; export async function getDefaultSimulatorDeviceUDIDAsync ( ) { try { const { stdout : defaultDeviceUDID } = await spawnAsync ( 'defaults' , [ 'read' , 'com.apple.iphonesimulator' , 'CurrentDeviceUDID' , ] ) ; return defaultDeviceUDID . trim ( ) ; } catch ( e ) { return null ; } } param props . udid param props . bundleIdentifier returns export async function getContainerPathAsync ( { udid , bundleIdentifier , } ) { if ( CoreSimulator . isEnabled ( ) ) { return CoreSimulator . getContainerPathAsync ( { udid , bundleIdentifier } ) ; } try { const { stdout } = await xcrunAsync ( [ 'simctl' , 'get_app_container' , deviceUDIDOrBooted ( udid ) , bundleIdentifier , ] ) ; return stdout . trim ( ) ; } catch ( error ) { if ( error . stderr ?. match ( /No such file or directory/ ) ) { return null ; } throw error ; } } export async function waitForDeviceToBootAsync ( { udid , } ) { return waitForActionAsync < SimulatorDevice | null > ( { action : ( ) => bootAsync ( { udid } ) } ) ; } export async function openURLAsync ( options ) { try { await xcrunAsync ( [ 'simctl' , 'openurl' , deviceUDIDOrBooted ( options . udid ) , options . url ] ) ; } catch ( error ) { if ( ! error . stderr ?. match ( /Unable to lookup in current state: Shut/ ) ) { throw error ; } await runBootAsync ( { udid : deviceUDIDOrBooted ( options . udid ) } ) ; return await openURLAsync ( options ) ; } } export async function openBundleIdAsync ( options ) { return xcrunAsync ( [ 'simctl' , 'launch' , deviceUDIDOrBooted ( options . udid ) , options . bundleIdentifier , ] ) ; } export async function bootAsync ( { udid } ) { if ( CoreSimulator . isEnabled ( ) ) { const device = await CoreSimulator . getDeviceInfoAsync ( { udid } ) . catch ( ( ) => null ) ; if ( device ?. state === 'Booted' ) { return device ; } await runBootAsync ( { udid } ) ; return await profileMethod ( CoreSimulator . getDeviceInfoAsync ) ( { udid } ) ; } await runBootAsync ( { udid } ) ; return await isSimulatorBootedAsync ( { udid } ) ; } async function getBootedSimulatorsAsync ( ) { const simulatorDeviceInfo = await listAsync ( 'devices' ) ; return Object . values ( simulatorDeviceInfo . devices ) . reduce ( ( prev , runtime ) => { return prev . concat ( runtime . filter ( device => device . state === 'Booted' ) ) ; } , [ ] ) ; } async function isSimulatorBootedAsync ( { udid , } ) { const devices = await getBootedSimulatorsAsync ( ) ; if ( udid ) { return devices . find ( bootedDevice => bootedDevice . udid === udid ) ?? null ; } else { return devices [ 0 ] ?? null ; } } export async function runBootAsync ( { udid } ) { try { await xcrunAsync ( [ 'simctl' , 'boot' , udid ] ) ; } catch ( error ) { if ( ! error . stderr ?. match ( /Unable to boot device in current state: Booted/ ) ) { throw error ; } } } export async function installAsync ( options ) { return simctlAsync ( [ 'install' , deviceUDIDOrBooted ( options . udid ) , options . dir ] ) ; } export async function uninstallAsync ( options ) { return simctlAsync ( [ 'uninstall' , deviceUDIDOrBooted ( options . udid ) , options . bundleIdentifier ] ) ; } function parseSimControlJSONResults ( input ) { try { return JSON . parse ( input ) ; } catch ( error ) { if ( error . message . match ( 'Unexpected token' ) ) { Logger . global . error ( `Apple's simctl returned malformed JSON:\n${ input }` ) ; } throw error ; } } export async function listAsync ( type , query ? ) { const result = await simctlAsync ( [ 'list' , type , '--json' , query ] ) ; const info = parseSimControlJSONResults ( result . stdout ) as SimulatorDeviceList ; for ( const runtime of Object . keys ( info . devices ) ) { const runtimeSuffix = runtime . split ( 'com.apple.CoreSimulator.SimRuntime.' ) . pop ( ) ! ; const [ osType , ... osVersionComponents ] = runtimeSuffix . split ( '-' ) ; const osVersion = osVersionComponents . join ( '.' ) ; const sims = info . devices [ runtime ] ; for ( const device of sims ) { device . runtime = runtime ; device . osVersion = osVersion ; device . windowName = `${ device . name } (${ osVersion })` ; device . osType = osType as OSType ; } } return info ; } export async function listSimulatorDevicesAsync ( ) { if ( CoreSimulator . isEnabled ( ) ) { return CoreSimulator . listDevicesAsync ( ) ; } const simulatorDeviceInfo = await listAsync ( 'devices' ) ; return Object . values ( simulatorDeviceInfo . devices ) . reduce ( ( prev , runtime ) => { return prev . concat ( runtime ) ; } , [ ] ) ; } /**
 * Get a list of all connected devices.
 */ export async function listDevicesAsync ( ) { if ( AppleDevice . isEnabled ( ) ) { const results = await AppleDevice . getConnectedDevices ( ) ; return results . map ( device => ( { name : device . DeviceName ?? device . ProductType ?? 'unknown ios device' , model : device . ProductType , osVersion : device . ProductVersion , deviceType : 'device' , udid : device . UniqueDeviceID } ) ) ; } const { output } = await xcrunAsync ( [ 'xctrace' , 'list' , 'devices' ] ) ; const text = output . join ( '' ) ; const devices = [ ] ; if ( ! text . includes ( '== Simulators ==' ) ) { return [ ] ; } const lines = text . split ( '\n' ) ; for ( const line of lines ) { if ( line === '== Simulators ==' ) { break ; } const device = line . match ( /(.*?) (\(([0-9.]+)\) )?\(([0-9A-F-]+)\)/i ) ; if ( device ) { const [ , name , , osVersion , udid ] = device ; const metadata = { name , udid , osVersion : osVersion ?? '??' , deviceType : osVersion ? 'device' : 'catalyst' } ; devices . push ( metadata ) ; } } return devices ; } export async function shutdownAsync ( udid ? ) { try { return simctlAsync ( [ 'shutdown' , deviceUDIDOrBooted ( udid ) ] ) ; } catch ( e ) { if ( ! e . message ?. includes ( 'No devices are booted.' ) ) { throw e ; } } return null ; } export async function updatePermissionsAsync ( udid , action , permission , bundleIdentifier ? ) { return simctlAsync ( [ 'privacy' , deviceUDIDOrBooted ( udid ) , action , permission , bundleIdentifier ] ) ; } export async function setAppearanceAsync ( udid , theme ) { return simctlAsync ( [ 'ui' , deviceUDIDOrBooted ( udid ) , theme ] ) ; } export async function eraseAsync ( udid ) { return simctlAsync ( [ 'erase' , deviceUDIDOrBooted ( udid ) ] ) ; } export async function eraseAllAsync ( ) { return simctlAsync ( [ 'erase' , 'all' ] ) ; } export async function addMediaAsync ( udid , mediaPath ) { return simctlAsync ( [ 'addmedia' , deviceUDIDOrBooted ( udid ) , mediaPath ] ) ; } export async function captureScreenAsync ( udid , captureType , outputFilePath ) { return simctlAsync ( [ 'io' , deviceUDIDOrBooted ( udid ) , captureType , `—type=${ path . extname ( outputFilePath ) }` , outputFilePath , ] ) ; } export async function deleteUnavailableAsync ( ) { return simctlAsync ( [ 'delete' , 'unavailable' ] ) ; } export async function simctlAsync ( [ command , ... args ] , options ? ) { return xcrunWithLogging ( [ 'simctl' , command , ... args . filter ( Boolean ) ] , options ) ; } function deviceUDIDOrBooted ( udid ? ) { return udid ? udid : 'booted' ; } export function isLicenseOutOfDate ( text ) { if ( ! text ) { return false ; } const lower = text . toLowerCase ( ) ; return lower . includes ( 'xcode' ) && lower . includes ( 'license' ) ; } export async function isXcrunInstalledAsync ( ) { try { execSync ( 'xcrun --version' , { stdio : 'ignore' } ) ; return true ; } catch { return false ; } } export async function xcrunAsync ( args , options ? ) { Logger . global . debug ( 'Running: xcrun ' + args . join ( ' ' ) ) ; try { return await spawnAsync ( 'xcrun' , args , options ) ; } catch ( e ) { throw parseXcrunError ( e ) ; } } export function parseXcrunError ( e ) { if ( isLicenseOutOfDate ( e . stdout ) || isLicenseOutOfDate ( e . stderr ) ) { return new XDLError ( 'XCODE_LICENSE_NOT_ACCEPTED' , 'Xcode license is not accepted. Please run `sudo xcodebuild -license`.' ) ; } else if ( e . stderr ?. includes ( 'not a developer tool or in PATH' ) ) { return new XDLError ( 'SIMCTL_NOT_AVAILABLE' , `You may need to run ${ chalk . bold ( 'sudo xcode-select -s /Applications/Xcode.app' ) } and try again.` ) ; } if ( Array . isArray ( e . output ) ) { e . message += '\n' + e . output . join ( '\n' ) . trim ( ) ; } else if ( e . stderr ) { e . message += '\n' + e . stderr ; } return e ; } export async function xcrunWithLogging ( args , options ? ) { try { return await xcrunAsync ( args , options ) ; } catch ( e ) { Logger . global . error ( `Error running \`xcrun ${ args . join ( ' ' ) }\`: ${ e . stderr || e . message }` ) ; throw e ; } }