@@ -4,7 +4,7 @@ import path from 'node:path';
44import { fileURLToPath } from 'node:url' ;
55import { AppError } from '../../utils/errors.ts' ;
66import { runCmd , runCmdStreaming , runCmdBackground , type ExecResult , type ExecBackgroundResult } from '../../utils/exec.ts' ;
7- import { withRetry } from '../../utils/retry.ts' ;
7+ import { Deadline , retryWithPolicy , withRetry } from '../../utils/retry.ts' ;
88import type { DeviceInfo } from '../../utils/device.ts' ;
99import net from 'node:net' ;
1010import { bootFailureHint , classifyBootFailure } from '../boot-diagnostics.ts' ;
@@ -522,28 +522,52 @@ async function waitForRunner(
522522 timeoutMs : number = RUNNER_STARTUP_TIMEOUT_MS ,
523523) : Promise < Response > {
524524 let endpoints = await resolveRunnerCommandEndpoints ( device , port ) ;
525- let nextEndpointRefreshAt = Date . now ( ) + 1_000 ;
526- const start = Date . now ( ) ;
527525 let lastError : unknown = null ;
528- while ( Date . now ( ) - start < timeoutMs ) {
529- if ( device . kind === 'device' && Date . now ( ) >= nextEndpointRefreshAt ) {
530- endpoints = await resolveRunnerCommandEndpoints ( device , port ) ;
531- nextEndpointRefreshAt = Date . now ( ) + 1_000 ;
532- }
533- for ( const endpoint of endpoints ) {
534- try {
535- const response = await fetchWithTimeout ( endpoint , {
536- method : 'POST' ,
537- headers : { 'Content-Type' : 'application/json' } ,
538- body : JSON . stringify ( command ) ,
539- } , 1_000 ) ;
540- return response ;
541- } catch ( err ) {
542- lastError = err ;
543- }
526+ const deadline = Deadline . fromTimeoutMs ( timeoutMs ) ;
527+ const maxAttempts = Math . max ( 1 , Math . ceil ( timeoutMs / 250 ) ) ;
528+ try {
529+ return await retryWithPolicy (
530+ async ( ) => {
531+ if ( device . kind === 'device' ) {
532+ endpoints = await resolveRunnerCommandEndpoints ( device , port ) ;
533+ }
534+ for ( const endpoint of endpoints ) {
535+ try {
536+ const response = await fetchWithTimeout (
537+ endpoint ,
538+ {
539+ method : 'POST' ,
540+ headers : { 'Content-Type' : 'application/json' } ,
541+ body : JSON . stringify ( command ) ,
542+ } ,
543+ 1_000 ,
544+ ) ;
545+ return response ;
546+ } catch ( err ) {
547+ lastError = err ;
548+ }
549+ }
550+ throw new AppError ( 'COMMAND_FAILED' , 'Runner endpoint probe failed' , {
551+ port,
552+ endpoints,
553+ lastError : lastError ? String ( lastError ) : undefined ,
554+ } ) ;
555+ } ,
556+ {
557+ maxAttempts,
558+ baseDelayMs : 100 ,
559+ maxDelayMs : 500 ,
560+ jitter : 0.2 ,
561+ shouldRetry : ( ) => true ,
562+ } ,
563+ { deadline, phase : 'ios_runner_connect' } ,
564+ ) ;
565+ } catch ( error ) {
566+ if ( ! lastError ) {
567+ lastError = error ;
544568 }
545- await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
546569 }
570+
547571 if ( device . kind === 'simulator' ) {
548572 const simResponse = await postCommandViaSimulator ( device . id , port , command ) ;
549573 return new Response ( simResponse . body , { status : simResponse . status } ) ;
0 commit comments