From 2d6ee2ab9bcf04ff85089fbfc2b29543d2d83b41 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Thu, 29 Oct 2026 15:00:48 -0700 Subject: [PATCH] fix(emulator): run Android commands through the process host (#35599) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../process-host-direct-import-baseline.txt | 1 - .../android-command-runner-native.test.ts | 21 ++++++ .../android/android-command-runner.ts | 67 ++++++++++--------- 3 files changed, 55 insertions(+), 34 deletions(-) create mode 100644 src/main/emulator/android/android-command-runner-native.test.ts diff --git a/config/process-host-direct-import-baseline.txt b/config/process-host-direct-import-baseline.txt index f179b6bc8db..9c277c2eb8a 100644 --- a/config/process-host-direct-import-baseline.txt +++ b/config/process-host-direct-import-baseline.txt @@ -33,7 +33,6 @@ src/main/daemon/daemon-process-identity-query.ts src/main/daemon/daemon-process-inspection.ts src/main/daemon/daemon-process-start-time.ts src/main/emulator/android/android-avd-boot.ts -src/main/emulator/android/android-command-runner.ts src/main/emulator/android/scrcpy-stream-session.ts src/main/emulator/serve-sim-helper-processes.ts src/main/emulator/serve-sim-runtime-materializer.ts diff --git a/src/main/emulator/android/android-command-runner-native.test.ts b/src/main/emulator/android/android-command-runner-native.test.ts new file mode 100644 index 00000000000..0567cf49820 --- /dev/null +++ b/src/main/emulator/android/android-command-runner-native.test.ts @@ -0,0 +1,21 @@ +import { expect, it } from 'vitest' +import { execFileAndroidCommandRunner } from './android-command-runner' + +it('preserves argument bytes and returns both output streams and nonzero exits as data', async () => { + const args = ['space text', 'quote\'"', '$() & | ;', '漢🙂'] + const result = await execFileAndroidCommandRunner(process.execPath, [ + '-e', + 'process.stdout.write(JSON.stringify(process.argv.slice(1))); process.stderr.write("stderr"); process.exitCode=7', + ...args + ]) + expect(result).toEqual({ stdout: JSON.stringify(args), stderr: 'stderr', code: 7 }) +}) + +it('fails a command that exceeds its deadline', async () => { + const result = await execFileAndroidCommandRunner( + process.execPath, + ['-e', 'setInterval(()=>{},1000)'], + { timeoutMs: 500 } + ) + expect(result).toEqual({ stdout: '', stderr: '', code: 1 }) +}) diff --git a/src/main/emulator/android/android-command-runner.ts b/src/main/emulator/android/android-command-runner.ts index f4071edd66a..4bc7db8cca4 100644 --- a/src/main/emulator/android/android-command-runner.ts +++ b/src/main/emulator/android/android-command-runner.ts @@ -1,4 +1,4 @@ -import { execFile } from 'node:child_process' +import { runProcess } from '@orca/process-host' import { basename } from 'node:path' import { emulatorProbe, emulatorProbeError } from '../emulator-probe' @@ -14,35 +14,36 @@ export type AndroidCommandRunner = ( const DEFAULT_TIMEOUT_MS = 60_000 const MAX_BUFFER_BYTES = 16 * 1024 * 1024 -export const execFileAndroidCommandRunner: AndroidCommandRunner = (binary, args, options) => - new Promise((resolve) => { - execFile( - binary, - [...args], - { timeout: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, maxBuffer: MAX_BUFFER_BYTES }, - (error, stdout, stderr) => { - const exitCode = - error && typeof (error as { code?: unknown }).code === 'number' - ? (error as { code: number }).code - : error - ? 1 - : 0 - const result = { - stdout: stdout?.toString() ?? '', - stderr: stderr?.toString() ?? '', - code: exitCode - } - if (exitCode === 0) { - emulatorProbe('cmd', { bin: basename(binary), args }) - } else { - emulatorProbeError('cmd.fail', error ?? new Error(result.stderr || 'nonzero exit'), { - bin: basename(binary), - args, - code: exitCode, - stderr: result.stderr.slice(0, 400) - }) - } - resolve(result) - } - ) - }) +export const execFileAndroidCommandRunner: AndroidCommandRunner = async (binary, args, options) => { + const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS + let result: AndroidCommandResult + let failure: unknown + try { + const output = await runProcess({ + program: binary, + args, + timeoutMs: timeoutMs === 0 ? null : timeoutMs, + maxOutputBytes: MAX_BUFFER_BYTES, + killOnOutputLimit: true + }) + result = { + stdout: output.stdout, + stderr: output.stderr, + code: output.timedOut || output.outputTruncated ? 1 : (output.code ?? 1) + } + } catch (error) { + failure = error + result = { stdout: '', stderr: '', code: 1 } + } + if (result.code === 0) { + emulatorProbe('cmd', { bin: basename(binary), args }) + } else { + emulatorProbeError('cmd.fail', failure ?? new Error(result.stderr || 'nonzero exit'), { + bin: basename(binary), + args, + code: result.code, + stderr: result.stderr.slice(0, 400) + }) + } + return result +}