@@ -2,11 +2,14 @@ import { runCmd, whichCmd } from '../../utils/exec.ts';
22import type { ExecResult } from '../../utils/exec.ts' ;
33import { AppError , asAppError } from '../../utils/errors.ts' ;
44import type { DeviceInfo } from '../../utils/device.ts' ;
5- import { Deadline , retryWithPolicy } from '../../utils/retry.ts' ;
6- import { classifyBootFailure } from '../boot-diagnostics.ts' ;
5+ import { Deadline , retryWithPolicy , type RetryTelemetryEvent } from '../../utils/retry.ts' ;
6+ import { bootFailureHint , classifyBootFailure } from '../boot-diagnostics.ts' ;
77
88const EMULATOR_SERIAL_PREFIX = 'emulator-' ;
99const ANDROID_BOOT_POLL_MS = 1000 ;
10+ const RETRY_LOGS_ENABLED = [ '1' , 'true' , 'yes' , 'on' ] . includes (
11+ ( process . env . AGENT_DEVICE_RETRY_LOGS ?? '' ) . toLowerCase ( ) ,
12+ ) ;
1013
1114function adbArgs ( serial : string , args : string [ ] ) : string [ ] {
1215 return [ '-s' , serial , ...args ] ;
@@ -79,8 +82,9 @@ export async function isAndroidBooted(serial: string): Promise<boolean> {
7982}
8083
8184export async function waitForAndroidBoot ( serial : string , timeoutMs = 60000 ) : Promise < void > {
82- const deadline = Deadline . fromTimeoutMs ( timeoutMs ) ;
83- const maxAttempts = Math . max ( 1 , Math . ceil ( timeoutMs / ANDROID_BOOT_POLL_MS ) ) ;
85+ const timeoutBudget = timeoutMs ;
86+ const deadline = Deadline . fromTimeoutMs ( timeoutBudget ) ;
87+ const maxAttempts = Math . max ( 1 , Math . ceil ( timeoutBudget / ANDROID_BOOT_POLL_MS ) ) ;
8488 let lastBootResult : ExecResult | undefined ;
8589 let timedOut = false ;
8690 try {
@@ -115,11 +119,26 @@ export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Pro
115119 error,
116120 stdout : lastBootResult ?. stdout ,
117121 stderr : lastBootResult ?. stderr ,
122+ context : { platform : 'android' , phase : 'boot' } ,
118123 } ) ;
119- return reason !== 'PERMISSION_DENIED' && reason !== 'TOOL_MISSING' && reason !== 'BOOT_TIMEOUT' ;
124+ return reason !== 'ADB_TRANSPORT_UNAVAILABLE' && reason !== 'ANDROID_BOOT_TIMEOUT' ;
125+ } ,
126+ } ,
127+ {
128+ deadline,
129+ phase : 'boot' ,
130+ classifyReason : ( error ) =>
131+ classifyBootFailure ( {
132+ error,
133+ stdout : lastBootResult ?. stdout ,
134+ stderr : lastBootResult ?. stderr ,
135+ context : { platform : 'android' , phase : 'boot' } ,
136+ } ) ,
137+ onEvent : ( event : RetryTelemetryEvent ) => {
138+ if ( ! RETRY_LOGS_ENABLED ) return ;
139+ process . stderr . write ( `[agent-device][retry] ${ JSON . stringify ( event ) } \n` ) ;
120140 } ,
121141 } ,
122- { deadline } ,
123142 ) ;
124143 } catch ( error ) {
125144 const appErr = asAppError ( error ) ;
@@ -130,26 +149,28 @@ export async function waitForAndroidBoot(serial: string, timeoutMs = 60000): Pro
130149 error,
131150 stdout,
132151 stderr,
152+ context : { platform : 'android' , phase : 'boot' } ,
133153 } ) ;
134154 const baseDetails = {
135155 serial,
136- timeoutMs,
156+ timeoutMs : timeoutBudget ,
137157 elapsedMs : deadline . elapsedMs ( ) ,
138158 reason,
159+ hint : bootFailureHint ( reason ) ,
139160 stdout,
140161 stderr,
141162 exitCode,
142163 } ;
143- if ( timedOut || reason === 'BOOT_TIMEOUT ' ) {
164+ if ( timedOut || reason === 'ANDROID_BOOT_TIMEOUT ' ) {
144165 throw new AppError ( 'COMMAND_FAILED' , 'Android device did not finish booting in time' , baseDetails ) ;
145166 }
146- if ( appErr . code === 'TOOL_MISSING' || reason === 'TOOL_MISSING' ) {
167+ if ( appErr . code === 'TOOL_MISSING' ) {
147168 throw new AppError ( 'TOOL_MISSING' , appErr . message , {
148169 ...baseDetails ,
149170 ...( appErr . details ?? { } ) ,
150171 } ) ;
151172 }
152- if ( reason === 'PERMISSION_DENIED' || reason === 'DEVICE_UNAVAILABLE' || reason === 'DEVICE_OFFLINE ') {
173+ if ( reason === 'ADB_TRANSPORT_UNAVAILABLE ' ) {
153174 throw new AppError ( 'COMMAND_FAILED' , appErr . message , {
154175 ...baseDetails ,
155176 ...( appErr . details ?? { } ) ,
0 commit comments