fix(emulator): run Android commands through the process host (#35599)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-29 15:00:48 -07:00
committed by GitHub
co-authored by OrcaWin
parent b0e2bf6f81
commit 2d6ee2ab9b
3 changed files with 55 additions and 34 deletions
@@ -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
@@ -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 })
})
@@ -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
}