@@ -449,12 +449,52 @@ async function dumpUiHierarchy(device: DeviceInfo): Promise<string> {
449449}
450450
451451async function dumpUiHierarchyOnce ( device : DeviceInfo ) : Promise < string > {
452- await runCmd (
452+ // Preferred: stream XML directly to stdout, avoiding file I/O race conditions.
453+ const streamed = await runCmd (
454+ 'adb' ,
455+ adbArgs ( device , [ 'exec-out' , 'uiautomator' , 'dump' , '/dev/tty' ] ) ,
456+ { allowFailure : true } ,
457+ ) ;
458+ if ( streamed . exitCode === 0 ) {
459+ const fromStream = extractUiDumpXml ( streamed . stdout , streamed . stderr ) ;
460+ if ( fromStream ) return fromStream ;
461+ }
462+
463+ // Fallback: dump to file and read back.
464+ // If `cat` fails with "no such file", the outer withRetry (via isRetryableAdbError) handles it.
465+ const dumpPath = '/sdcard/window_dump.xml' ;
466+ const dumpResult = await runCmd (
453467 'adb' ,
454- adbArgs ( device , [ 'shell' , 'uiautomator' , 'dump' , '/sdcard/window_dump.xml' ] ) ,
468+ adbArgs ( device , [ 'shell' , 'uiautomator' , 'dump' , dumpPath ] ) ,
455469 ) ;
456- const result = await runCmd ( 'adb' , adbArgs ( device , [ 'shell' , 'cat' , '/sdcard/window_dump.xml' ] ) ) ;
457- return result . stdout ;
470+ const actualPath = resolveDumpPath ( dumpPath , dumpResult . stdout , dumpResult . stderr ) ;
471+
472+ const result = await runCmd ( 'adb' , adbArgs ( device , [ 'shell' , 'cat' , actualPath ] ) ) ;
473+ const xml = extractUiDumpXml ( result . stdout , result . stderr ) ;
474+ if ( ! xml ) {
475+ throw new AppError ( 'COMMAND_FAILED' , 'uiautomator dump did not return XML' , {
476+ stdout : result . stdout ,
477+ stderr : result . stderr ,
478+ } ) ;
479+ }
480+ return xml ;
481+ }
482+
483+ function resolveDumpPath ( defaultPath : string , stdout : string , stderr : string ) : string {
484+ const text = `${ stdout } \n${ stderr } ` ;
485+ const match = / d u m p e d t o : \s * ( \S + ) / i. exec ( text ) ;
486+ return match ?. [ 1 ] ?? defaultPath ;
487+ }
488+
489+ function extractUiDumpXml ( stdout : string , stderr : string ) : string | null {
490+ const text = `${ stdout } \n${ stderr } ` ;
491+ const start = text . indexOf ( '<?xml' ) ;
492+ const hierarchyStart = start >= 0 ? start : text . indexOf ( '<hierarchy' ) ;
493+ if ( hierarchyStart < 0 ) return null ;
494+ const end = text . lastIndexOf ( '</hierarchy>' ) ;
495+ if ( end < 0 || end < hierarchyStart ) return null ;
496+ const xml = text . slice ( hierarchyStart , end + '</hierarchy>' . length ) . trim ( ) ;
497+ return xml . length > 0 ? xml : null ;
458498}
459499
460500function isRetryableAdbError ( err : unknown ) : boolean {
@@ -467,6 +507,7 @@ function isRetryableAdbError(err: unknown): boolean {
467507 if ( stderr . includes ( 'connection reset' ) ) return true ;
468508 if ( stderr . includes ( 'broken pipe' ) ) return true ;
469509 if ( stderr . includes ( 'timed out' ) ) return true ;
510+ if ( stderr . includes ( 'no such file or directory' ) ) return true ;
470511 return false ;
471512}
472513
0 commit comments