Skip to content

Commit 75aff8e

Browse files
committed
fix: android race condition for snapshot dump
1 parent 4aed3fb commit 75aff8e

1 file changed

Lines changed: 45 additions & 4 deletions

File tree

src/platforms/android/index.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,12 +449,52 @@ async function dumpUiHierarchy(device: DeviceInfo): Promise<string> {
449449
}
450450

451451
async 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 = /dumped to:\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

460500
function 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

Comments
 (0)