11import { runCmd } from '../../utils/exec.ts' ;
22import { AppError } from '../../utils/errors.ts' ;
33import type { DeviceInfo } from '../../utils/device.ts' ;
4+ import { Deadline , retryWithPolicy } from '../../utils/retry.ts' ;
5+ import { classifyBootFailure } from '../boot-diagnostics.ts' ;
46
57const ALIASES : Record < string , string > = {
68 settings : 'com.apple.Preferences' ,
79} ;
810
11+ const IOS_BOOT_TIMEOUT_MS = resolveTimeoutMs ( process . env . AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS , 120_000 , 5_000 ) ;
12+
913export async function resolveIosApp ( device : DeviceInfo , app : string ) : Promise < string > {
1014 const trimmed = app . trim ( ) ;
1115 if ( trimmed . includes ( '.' ) ) return trimmed ;
@@ -207,8 +211,82 @@ export async function ensureBootedSimulator(device: DeviceInfo): Promise<void> {
207211 if ( device . kind !== 'simulator' ) return ;
208212 const state = await getSimulatorState ( device . id ) ;
209213 if ( state === 'Booted' ) return ;
210- await runCmd ( 'xcrun' , [ 'simctl' , 'boot' , device . id ] , { allowFailure : true } ) ;
211- await runCmd ( 'xcrun' , [ 'simctl' , 'bootstatus' , device . id , '-b' ] , { allowFailure : true } ) ;
214+ const deadline = Deadline . fromTimeoutMs ( IOS_BOOT_TIMEOUT_MS ) ;
215+ let bootResult : { stdout : string ; stderr : string ; exitCode : number } | null = null ;
216+ let bootStatusResult : { stdout : string ; stderr : string ; exitCode : number } | null = null ;
217+ try {
218+ await retryWithPolicy (
219+ async ( ) => {
220+ const currentState = await getSimulatorState ( device . id ) ;
221+ if ( currentState === 'Booted' ) return ;
222+ bootResult = await runCmd ( 'xcrun' , [ 'simctl' , 'boot' , device . id ] , { allowFailure : true } ) ;
223+ const bootOutput = `${ bootResult . stdout } \n${ bootResult . stderr } ` . toLowerCase ( ) ;
224+ const bootAlreadyDone =
225+ bootOutput . includes ( 'already booted' ) || bootOutput . includes ( 'current state: booted' ) ;
226+ if ( bootResult . exitCode !== 0 && ! bootAlreadyDone ) {
227+ throw new AppError ( 'COMMAND_FAILED' , 'simctl boot failed' , {
228+ stdout : bootResult . stdout ,
229+ stderr : bootResult . stderr ,
230+ exitCode : bootResult . exitCode ,
231+ } ) ;
232+ }
233+ bootStatusResult = await runCmd ( 'xcrun' , [ 'simctl' , 'bootstatus' , device . id , '-b' ] , {
234+ allowFailure : true ,
235+ } ) ;
236+ if ( bootStatusResult . exitCode !== 0 ) {
237+ throw new AppError ( 'COMMAND_FAILED' , 'simctl bootstatus failed' , {
238+ stdout : bootStatusResult . stdout ,
239+ stderr : bootStatusResult . stderr ,
240+ exitCode : bootStatusResult . exitCode ,
241+ } ) ;
242+ }
243+ const nextState = await getSimulatorState ( device . id ) ;
244+ if ( nextState !== 'Booted' ) {
245+ throw new AppError ( 'COMMAND_FAILED' , 'Simulator is still booting' , {
246+ state : nextState ,
247+ } ) ;
248+ }
249+ } ,
250+ {
251+ maxAttempts : 3 ,
252+ baseDelayMs : 500 ,
253+ maxDelayMs : 2000 ,
254+ jitter : 0.2 ,
255+ shouldRetry : ( error ) => {
256+ const reason = classifyBootFailure ( {
257+ error,
258+ stdout : bootStatusResult ?. stdout ?? bootResult ?. stdout ,
259+ stderr : bootStatusResult ?. stderr ?? bootResult ?. stderr ,
260+ } ) ;
261+ return reason !== 'PERMISSION_DENIED' && reason !== 'TOOL_MISSING' ;
262+ } ,
263+ } ,
264+ { deadline } ,
265+ ) ;
266+ } catch ( error ) {
267+ const reason = classifyBootFailure ( {
268+ error,
269+ stdout : bootStatusResult ?. stdout ?? bootResult ?. stdout ,
270+ stderr : bootStatusResult ?. stderr ?? bootResult ?. stderr ,
271+ } ) ;
272+ throw new AppError ( 'COMMAND_FAILED' , 'iOS simulator failed to boot' , {
273+ platform : 'ios' ,
274+ deviceId : device . id ,
275+ timeoutMs : IOS_BOOT_TIMEOUT_MS ,
276+ elapsedMs : deadline . elapsedMs ( ) ,
277+ reason,
278+ boot : bootResult
279+ ? { exitCode : bootResult . exitCode , stdout : bootResult . stdout , stderr : bootResult . stderr }
280+ : undefined ,
281+ bootstatus : bootStatusResult
282+ ? {
283+ exitCode : bootStatusResult . exitCode ,
284+ stdout : bootStatusResult . stdout ,
285+ stderr : bootStatusResult . stderr ,
286+ }
287+ : undefined ,
288+ } ) ;
289+ }
212290}
213291
214292async function getSimulatorState ( udid : string ) : Promise < string | null > {
@@ -229,3 +307,10 @@ async function getSimulatorState(udid: string): Promise<string | null> {
229307 }
230308 return null ;
231309}
310+
311+ function resolveTimeoutMs ( raw : string | undefined , fallback : number , min : number ) : number {
312+ if ( ! raw ) return fallback ;
313+ const parsed = Number ( raw ) ;
314+ if ( ! Number . isFinite ( parsed ) ) return fallback ;
315+ return Math . max ( min , Math . floor ( parsed ) ) ;
316+ }
0 commit comments