diff --git a/.changeset/windows-appium-cmd.md b/.changeset/windows-appium-cmd.md new file mode 100644 index 000000000..9d6666003 --- /dev/null +++ b/.changeset/windows-appium-cmd.md @@ -0,0 +1,5 @@ +--- +"@qawolf/cli": patch +--- + +Fix Android flows and the `appium` / `uiautomator2-driver` doctor checks on Windows. The CLI resolved `node_modules/.bin/appium`, which is the extension-less POSIX script. Windows cannot execute that file. The CLI now uses the `appium.cmd` wrapper npm writes beside it, with the `shell: true` Node requires for a batch file after CVE-2024-27980. `qawolf install android` gets the same fix for the `sdkmanager.bat` and `avdmanager.bat` wrappers in cmdline-tools. The `adb` and `emulator` paths built from `ANDROID_HOME` now name the `.exe` suffix directly instead of relying on the spawn path search. diff --git a/src/commands/doctor/handler.ts b/src/commands/doctor/handler.ts index eeab98b28..328d48307 100644 --- a/src/commands/doctor/handler.ts +++ b/src/commands/doctor/handler.ts @@ -84,7 +84,7 @@ export async function handleDoctor( androidHome: process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"], checkExists: (path: string) => ctx.fs.existsSync(path), envDir, - resolveAppiumBin, + resolveAppiumBin: (dir) => resolveAppiumBin(dir, process.platform), requiredAvds, platform: process.platform, }); diff --git a/src/commands/install/android.ts b/src/commands/install/android.ts index 44e2d7536..094024214 100644 --- a/src/commands/install/android.ts +++ b/src/commands/install/android.ts @@ -1,5 +1,4 @@ -import { join } from "node:path"; - +import { avdManagerBin, sdkManagerBin } from "~/core/androidBins.js"; import { expandPatterns as defaultExpandPatterns, makePeekFlowMeta, @@ -30,25 +29,13 @@ export async function handleInstallAndroid( arch: process.arch, androidHome, checkExists: (path: string) => fs.existsSync(path), - sdkManagerPath: join( - androidHome, - "cmdline-tools", - "latest", - "bin", - "sdkmanager", - ), - avdManagerPath: join( - androidHome, - "cmdline-tools", - "latest", - "bin", - "avdmanager", - ), + sdkManagerPath: sdkManagerBin(androidHome, process.platform), + avdManagerPath: avdManagerBin(androidHome, process.platform), expandPatterns: (patterns, cwd) => defaultExpandPatterns(patterns, cwd ?? process.cwd(), undefined, fs), peekFlowMeta: makePeekFlowMeta(fs), resolveDepsRoot: async (files) => envDir ?? (await resolveDepsRootHelper({ files, fs })).depsRoot, - resolveAppiumBin, + resolveAppiumBin: (dir) => resolveAppiumBin(dir, process.platform), }); } diff --git a/src/core/androidBins.test.ts b/src/core/androidBins.test.ts new file mode 100644 index 000000000..f73a276ae --- /dev/null +++ b/src/core/androidBins.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "bun:test"; +import { join } from "node:path"; + +import { + adbBin, + avdManagerBin, + emulatorBin, + sdkManagerBin, +} from "./androidBins.js"; + +describe("emulatorBin", () => { + const home = join("/opt", "android-sdk"); + + it("returns the extension-less path on linux and macOS", () => { + expect(emulatorBin(home, "linux")).toBe(join(home, "emulator", "emulator")); + expect(emulatorBin(home, "darwin")).toBe( + join(home, "emulator", "emulator"), + ); + }); + + it("returns emulator.exe on win32", () => { + expect(emulatorBin(home, "win32")).toBe( + join(home, "emulator", "emulator.exe"), + ); + }); + + it("falls back to the bare name on PATH when ANDROID_HOME is unset", () => { + expect(emulatorBin(undefined, "linux")).toBe("emulator"); + expect(emulatorBin(undefined, "win32")).toBe("emulator.exe"); + }); +}); + +describe("adbBin", () => { + const home = join("/opt", "android-sdk"); + + it("returns the extension-less path on linux and macOS", () => { + expect(adbBin(home, "linux")).toBe(join(home, "platform-tools", "adb")); + }); + + it("returns adb.exe on win32", () => { + expect(adbBin(home, "win32")).toBe(join(home, "platform-tools", "adb.exe")); + }); + + it("falls back to the bare name on PATH when ANDROID_HOME is unset", () => { + expect(adbBin(undefined, "linux")).toBe("adb"); + expect(adbBin(undefined, "win32")).toBe("adb.exe"); + }); +}); + +describe("sdkManagerBin and avdManagerBin", () => { + const home = join("/opt", "android-sdk"); + const binDir = join(home, "cmdline-tools", "latest", "bin"); + + it("returns the extension-less scripts on linux and macOS", () => { + expect(sdkManagerBin(home, "linux")).toBe(join(binDir, "sdkmanager")); + expect(avdManagerBin(home, "darwin")).toBe(join(binDir, "avdmanager")); + }); + + it("returns the .bat wrappers on win32", () => { + expect(sdkManagerBin(home, "win32")).toBe(join(binDir, "sdkmanager.bat")); + expect(avdManagerBin(home, "win32")).toBe(join(binDir, "avdmanager.bat")); + }); +}); diff --git a/src/core/androidBins.ts b/src/core/androidBins.ts new file mode 100644 index 000000000..61674e4a4 --- /dev/null +++ b/src/core/androidBins.ts @@ -0,0 +1,41 @@ +import { join } from "node:path"; + +// The SDK ships emulator.exe and adb.exe on Windows. Name the extension rather +// than rely on libuv appending it during the spawn path search. +function withExeSuffix(name: string, platform: NodeJS.Platform): string { + return platform === "win32" ? `${name}.exe` : name; +} + +export function emulatorBin( + home: string | undefined, + platform: NodeJS.Platform, +): string { + const name = withExeSuffix("emulator", platform); + return home ? join(home, "emulator", name) : name; +} + +export function adbBin( + home: string | undefined, + platform: NodeJS.Platform, +): string { + const name = withExeSuffix("adb", platform); + return home ? join(home, "platform-tools", name) : name; +} + +// cmdline-tools ships each command as a POSIX script plus a .bat wrapper. +function cmdlineToolsBin( + home: string, + name: string, + platform: NodeJS.Platform, +): string { + const file = platform === "win32" ? `${name}.bat` : name; + return join(home, "cmdline-tools", "latest", "bin", file); +} + +export function sdkManagerBin(home: string, platform: NodeJS.Platform): string { + return cmdlineToolsBin(home, "sdkmanager", platform); +} + +export function avdManagerBin(home: string, platform: NodeJS.Platform): string { + return cmdlineToolsBin(home, "avdmanager", platform); +} diff --git a/src/domains/doctor/checks/android.fixtures.ts b/src/domains/doctor/checks/android.fixtures.ts index 2a314b89a..43bb43a59 100644 --- a/src/domains/doctor/checks/android.fixtures.ts +++ b/src/domains/doctor/checks/android.fixtures.ts @@ -34,6 +34,7 @@ export function baseDeps( envDir, resolveAppiumBin: (dir: string) => `${dir}/node_modules/.bin/appium`, requiredAvds: [] as readonly string[], + platform: "linux" as NodeJS.Platform, ...over, }; } diff --git a/src/domains/doctor/checks/android.test.ts b/src/domains/doctor/checks/android.test.ts index dc5ea505b..a57d79310 100644 --- a/src/domains/doctor/checks/android.test.ts +++ b/src/domains/doctor/checks/android.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; +import { join } from "node:path"; import type { SpawnFn } from "~/shell/spawn.js"; @@ -179,3 +180,15 @@ describe("checkAndroid: android-avd", () => { expect(avd?.detail).not.toContain("Could not launch emulator"); }); }); + +describe("checkAndroid: Windows binary names", () => { + it("launches adb.exe and emulator.exe on win32", async () => { + const spawn = mock(() => Promise.resolve(success)); + await checkAndroid( + baseDeps({ spawn, platform: "win32", requiredAvds: ["Pixel_9"] }), + ); + const spawned = spawn.mock.calls.map((call) => call[0]); + expect(spawned).toContain(join(sdk, "platform-tools", "adb.exe")); + expect(spawned).toContain(join(sdk, "emulator", "emulator.exe")); + }); +}); diff --git a/src/domains/doctor/checks/android.ts b/src/domains/doctor/checks/android.ts index d34a51014..7c5d24c70 100644 --- a/src/domains/doctor/checks/android.ts +++ b/src/domains/doctor/checks/android.ts @@ -16,6 +16,7 @@ export type CheckAndroidDeps = { readonly envDir: string | undefined; readonly resolveAppiumBin: (envDir: string) => string; readonly requiredAvds: readonly string[]; + readonly platform: NodeJS.Platform; }; export async function checkAndroid( @@ -23,9 +24,9 @@ export async function checkAndroid( ): Promise { const home = checkHome(deps.androidHome, deps.checkExists); const [adb, emulator, avds] = await Promise.all([ - checkAdb(deps.spawn, deps.androidHome), - checkEmulatorBin(deps.spawn, deps.androidHome), - checkAvds(deps.spawn, deps.androidHome, deps.requiredAvds), + checkAdb(deps.spawn, deps.androidHome, deps.platform), + checkEmulatorBin(deps.spawn, deps.androidHome, deps.platform), + checkAvds(deps.spawn, deps.androidHome, deps.requiredAvds, deps.platform), ]); const { appium, bin } = checkAppium( deps.envDir, diff --git a/src/domains/doctor/checks/androidSdk.ts b/src/domains/doctor/checks/androidSdk.ts index beb7f9e24..3b0987e3a 100644 --- a/src/domains/doctor/checks/androidSdk.ts +++ b/src/domains/doctor/checks/androidSdk.ts @@ -1,3 +1,4 @@ +import { adbBin, emulatorBin } from "~/core/androidBins.js"; import { doctorMessages } from "~/core/messages/index.js"; import type { CheckResult } from "~/domains/doctor/types.js"; import type { SpawnFn, SpawnResult } from "~/shell/spawn.js"; @@ -33,8 +34,9 @@ export function checkHome( export async function checkAdb( spawn: SpawnFn, androidHome: string | undefined, + platform: NodeJS.Platform, ): Promise { - const bin = androidHome ? `${androidHome}/platform-tools/adb` : "adb"; + const bin = adbBin(androidHome, platform); const result = await spawn(bin, ["--version"]); if (result.exitCode < 0) { return { @@ -52,8 +54,9 @@ export async function checkAdb( export async function checkEmulatorBin( spawn: SpawnFn, androidHome: string | undefined, + platform: NodeJS.Platform, ): Promise { - const bin = androidHome ? `${androidHome}/emulator/emulator` : "emulator"; + const bin = emulatorBin(androidHome, platform); const result = await spawn(bin, ["-version"]); if (result.exitCode < 0) { return { @@ -79,9 +82,10 @@ export async function checkAvds( spawn: SpawnFn, androidHome: string | undefined, requiredAvds: readonly string[], + platform: NodeJS.Platform, ): Promise { if (requiredAvds.length === 0) return []; - const bin = androidHome ? `${androidHome}/emulator/emulator` : "emulator"; + const bin = emulatorBin(androidHome, platform); const result = await spawn(bin, ["-list-avds"]); if (result.exitCode < 0) { return [ diff --git a/src/domains/doctor/checks/index.ts b/src/domains/doctor/checks/index.ts index a8363d4c5..fd80f571c 100644 --- a/src/domains/doctor/checks/index.ts +++ b/src/domains/doctor/checks/index.ts @@ -64,6 +64,7 @@ export async function runChecks(deps: CheckDeps): Promise { envDir: deps.envDir, resolveAppiumBin: deps.resolveAppiumBin, requiredAvds: deps.requiredAvds, + platform: deps.platform, }) : Promise.resolve([]), ]); diff --git a/src/domains/runner/runAndroidFlowDeps.ts b/src/domains/runner/runAndroidFlowDeps.ts index d942b4c6f..25ab8d6f3 100644 --- a/src/domains/runner/runAndroidFlowDeps.ts +++ b/src/domains/runner/runAndroidFlowDeps.ts @@ -1,6 +1,6 @@ import { createAppiumServer } from "~/shell/appium/createAppiumServer.js"; import { createEmulatorPool } from "~/shell/appium/createEmulatorPool.js"; -import { defaultAdb } from "~/shell/appium/emulatorSetup.js"; +import { defaultAdb } from "~/shell/appium/adb.js"; import type { AppiumDriver } from "~/shell/appium/types.js"; import type { RunAndroidFlowDeps } from "./runAndroidFlow.js"; import { createRunnerDeps } from "./runnerDeps.js"; diff --git a/src/shell/appium/adb.ts b/src/shell/appium/adb.ts new file mode 100644 index 000000000..4ccbc365e --- /dev/null +++ b/src/shell/appium/adb.ts @@ -0,0 +1,14 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +import { adbBin } from "~/core/androidBins.js"; + +const execFileAsync = promisify(execFile); + +export type AdbFn = (args: string[]) => Promise<{ stdout: string }>; + +export const defaultAdb: AdbFn = async (args) => { + const home = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"]; + const { stdout } = await execFileAsync(adbBin(home, process.platform), args); + return { stdout }; +}; diff --git a/src/shell/appium/createAndroidEmulator.test.ts b/src/shell/appium/createAndroidEmulator.test.ts index 593dd2810..7c986236e 100644 --- a/src/shell/appium/createAndroidEmulator.test.ts +++ b/src/shell/appium/createAndroidEmulator.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; -import type { AdbFn, SpawnFn } from "./createAndroidEmulator.js"; +import type { AdbFn } from "./adb.js"; +import type { SpawnFn } from "./createAndroidEmulator.js"; import { createAndroidEmulator } from "./createAndroidEmulator.js"; afterEach(() => { diff --git a/src/shell/appium/createAndroidEmulator.ts b/src/shell/appium/createAndroidEmulator.ts index 1a9d9b53d..3fa9e6e0e 100644 --- a/src/shell/appium/createAndroidEmulator.ts +++ b/src/shell/appium/createAndroidEmulator.ts @@ -1,7 +1,8 @@ -import { execFile, spawn } from "node:child_process"; -import { promisify } from "node:util"; +import { spawn } from "node:child_process"; + +import { emulatorBin } from "~/core/androidBins.js"; +import { defaultAdb, type AdbFn } from "./adb.js"; -const execFileAsync = promisify(execFile); const defaultBootTimeoutMs = 120_000; const pollIntervalMs = 2_000; @@ -9,18 +10,7 @@ function androidHome(): string | undefined { return process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"]; } -function emulatorBin(): string { - const home = androidHome(); - return home ? `${home}/emulator/emulator` : "emulator"; -} - -function adbBin(): string { - const home = androidHome(); - return home ? `${home}/platform-tools/adb` : "adb"; -} - export type SpawnFn = (bin: string, args: string[]) => { stop: () => void }; -export type AdbFn = (args: string[]) => Promise<{ stdout: string }>; const defaultSpawn: SpawnFn = (bin, args) => { const child = spawn(bin, args, { stdio: "ignore" }); @@ -28,11 +18,6 @@ const defaultSpawn: SpawnFn = (bin, args) => { return { stop: () => child.kill() }; }; -const defaultAdb: AdbFn = async (args) => { - const { stdout } = await execFileAsync(adbBin(), args); - return { stdout }; -}; - async function bootSequence( adb: AdbFn, serial: string, @@ -97,7 +82,7 @@ export async function createAndroidEmulator(params: { const timeoutMs = params.options?.bootTimeoutMs ?? defaultBootTimeoutMs; const serial = `emulator-${port}`; - const proc = spawnFn(emulatorBin(), [ + const proc = spawnFn(emulatorBin(androidHome(), process.platform), [ "-avd", avdName, "-no-audio", diff --git a/src/shell/appium/createAndroidLaunchContext.ts b/src/shell/appium/createAndroidLaunchContext.ts index b73c7ffe9..69496ced4 100644 --- a/src/shell/appium/createAndroidLaunchContext.ts +++ b/src/shell/appium/createAndroidLaunchContext.ts @@ -1,7 +1,8 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; import type { EmulatorSlot } from "./createEmulatorPool.js"; // (D2) -import { configureEmulator, defaultAdb } from "./emulatorSetup.js"; +import { defaultAdb } from "./adb.js"; +import { configureEmulator } from "./emulatorSetup.js"; import type { AndroidCleanupResult, AndroidLaunchContext, diff --git a/src/shell/appium/createAppiumServer.ts b/src/shell/appium/createAppiumServer.ts index 140a65629..07c95e592 100644 --- a/src/shell/appium/createAppiumServer.ts +++ b/src/shell/appium/createAppiumServer.ts @@ -89,7 +89,9 @@ export async function createAppiumServer( }> { const spawnFn = params?.deps?.spawn ?? defaultSpawnAppium; const findFreePortFn = params?.deps?.findFreePort ?? findFreePort; - const resolveAppiumBinFn = params?.deps?.resolveAppiumBin ?? resolveAppiumBin; + const resolveAppiumBinFn = + params?.deps?.resolveAppiumBin ?? + ((dir: string) => resolveAppiumBin(dir, process.platform)); const appiumHome = params?.options?.appiumHome ?? join(envPaths("qawolf").data, "appium"); const timeoutMs = params?.options?.startTimeoutMs ?? defaultStartTimeoutMs; diff --git a/src/shell/appium/createEmulatorPool.test.ts b/src/shell/appium/createEmulatorPool.test.ts index 2b6169712..7b7f03412 100644 --- a/src/shell/appium/createEmulatorPool.test.ts +++ b/src/shell/appium/createEmulatorPool.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; -import type { AdbFn, SpawnFn } from "./createAndroidEmulator.js"; +import type { AdbFn } from "./adb.js"; +import type { SpawnFn } from "./createAndroidEmulator.js"; import { createEmulatorPool } from "./createEmulatorPool.js"; import { makeNoopSignals } from "~/shell/signals/createSignalRegistry.fixtures.js"; diff --git a/src/shell/appium/createEmulatorPool.ts b/src/shell/appium/createEmulatorPool.ts index 6b61a6f8a..619a14627 100644 --- a/src/shell/appium/createEmulatorPool.ts +++ b/src/shell/appium/createEmulatorPool.ts @@ -1,9 +1,9 @@ import { createAndroidEmulator, - type AdbFn, type SpawnFn, } from "./createAndroidEmulator.js"; import type { SignalRegistry } from "~/shell/signals/createSignalRegistry.js"; +import type { AdbFn } from "./adb.js"; export type EmulatorSlot = { serial: string; avdName: string }; diff --git a/src/shell/appium/emulatorSetup.ts b/src/shell/appium/emulatorSetup.ts index 696aef4ee..4db8eb49a 100644 --- a/src/shell/appium/emulatorSetup.ts +++ b/src/shell/appium/emulatorSetup.ts @@ -1,20 +1,4 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; -import type { AdbFn } from "./createAndroidEmulator.js"; - -const execFileAsync = promisify(execFile); - -function adbBin(): string { - const home = process.env["ANDROID_HOME"] ?? process.env["ANDROID_SDK_ROOT"]; - return home ? `${home}/platform-tools/adb` : "adb"; -} - -// Duplicates the private defaultAdb in createAndroidEmulator.ts. -// Extract to a shared helper when a third callsite appears. -export const defaultAdb: AdbFn = async (args) => { - const { stdout } = await execFileAsync(adbBin(), args); - return { stdout }; -}; +import type { AdbFn } from "./adb.js"; async function disableAnimations(adb: AdbFn, serial: string): Promise { await Promise.all([ diff --git a/src/shell/appium/resolveAppiumBin.test.ts b/src/shell/appium/resolveAppiumBin.test.ts new file mode 100644 index 000000000..6a494999a --- /dev/null +++ b/src/shell/appium/resolveAppiumBin.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "bun:test"; +import { join } from "node:path"; + +import { resolveAppiumBin } from "./resolveAppiumBin.js"; + +describe("resolveAppiumBin", () => { + const envDir = join("/envs", "node20"); + const binDir = join(envDir, "node_modules", ".bin"); + + it("returns the extension-less shim on linux and macOS", () => { + expect(resolveAppiumBin(envDir, "linux")).toBe(join(binDir, "appium")); + expect(resolveAppiumBin(envDir, "darwin")).toBe(join(binDir, "appium")); + }); + + it("returns the .cmd shim on win32", () => { + expect(resolveAppiumBin(envDir, "win32")).toBe(join(binDir, "appium.cmd")); + }); +}); diff --git a/src/shell/appium/resolveAppiumBin.ts b/src/shell/appium/resolveAppiumBin.ts index 880fd2f32..24fdf69f3 100644 --- a/src/shell/appium/resolveAppiumBin.ts +++ b/src/shell/appium/resolveAppiumBin.ts @@ -1,5 +1,11 @@ import { join } from "node:path"; -export function resolveAppiumBin(envDir: string): string { - return join(envDir, "node_modules", ".bin", "appium"); +// npm writes both an extension-less POSIX shell script and an appium.cmd batch +// wrapper into node_modules/.bin. Windows CreateProcess can only run the .cmd. +export function resolveAppiumBin( + envDir: string, + platform: NodeJS.Platform, +): string { + const name = platform === "win32" ? "appium.cmd" : "appium"; + return join(envDir, "node_modules", ".bin", name); } diff --git a/src/shell/appium/spawnAppium.test.ts b/src/shell/appium/spawnAppium.test.ts new file mode 100644 index 000000000..bee307552 --- /dev/null +++ b/src/shell/appium/spawnAppium.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "bun:test"; + +import { buildAppiumSpawnOptions } from "./spawnAppium.js"; + +describe("buildAppiumSpawnOptions", () => { + const env = { APPIUM_HOME: "/data/appium" }; + + it("pipes stdout and stderr and passes the env through", () => { + expect( + buildAppiumSpawnOptions("/envs/node20/.bin/appium", "linux", env), + ).toEqual({ + stdio: ["ignore", "pipe", "pipe"], + env, + }); + }); + + it("sets shell on win32 so Node will run the .cmd shim", () => { + expect( + buildAppiumSpawnOptions( + "C:\\envs\\node20\\.bin\\appium.cmd", + "win32", + env, + ).shell, + ).toBe(true); + }); +}); diff --git a/src/shell/appium/spawnAppium.ts b/src/shell/appium/spawnAppium.ts index 6825a36a3..f2edb4912 100644 --- a/src/shell/appium/spawnAppium.ts +++ b/src/shell/appium/spawnAppium.ts @@ -1,7 +1,8 @@ -import { spawn } from "node:child_process"; +import { spawn, type SpawnOptions } from "node:child_process"; import net from "node:net"; import { PassThrough } from "node:stream"; +import { buildSpawnOptions } from "~/shell/spawn.js"; import type { AppiumProcess, FindFreePortFn, @@ -18,8 +19,23 @@ export const findFreePort: FindFreePortFn = () => server.on("error", reject); }); +export function buildAppiumSpawnOptions( + bin: string, + platform: NodeJS.Platform, + env: Record, +): SpawnOptions { + return { + stdio: ["ignore", "pipe", "pipe"], + ...buildSpawnOptions(bin, platform, env), + }; +} + export const defaultSpawnAppium: SpawnAppiumFn = (bin, args, env) => { - const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], env }); + const child = spawn( + bin, + args, + buildAppiumSpawnOptions(bin, process.platform, env), + ); const output = new PassThrough(); child.stdout?.pipe(output, { end: false }); child.stderr?.pipe(output, { end: false }); diff --git a/src/shell/appium/types.ts b/src/shell/appium/types.ts index eb7e51abd..55a47a06d 100644 --- a/src/shell/appium/types.ts +++ b/src/shell/appium/types.ts @@ -1,4 +1,4 @@ -import type { AdbFn } from "./createAndroidEmulator.js"; +import type { AdbFn } from "./adb.js"; import type { EmulatorSlot } from "./createEmulatorPool.js"; // (D2) export type AppiumDriver = { diff --git a/src/shell/spawn.ts b/src/shell/spawn.ts index 483659320..d06fb5734 100644 --- a/src/shell/spawn.ts +++ b/src/shell/spawn.ts @@ -23,9 +23,10 @@ export type SpawnFn = ( // Do not pass attacker-controlled values in `cmd` or `args`. Node's // cmd.exe escaping protects against shell-metacharacter injection in // args, but only when those args are passed as separate array elements -// (never concatenated into `cmd`). Today the call sites are playwright.cmd -// (browser names, --version) and npm.cmd (install, ping), all literal args; -// audit any new .cmd/.bat caller before merging. +// (never concatenated into `cmd`). Call sites today: playwright.cmd, +// npm.cmd and appium.cmd with literal args. sdkmanager.bat and +// avdmanager.bat take AVD names and system images from flow metadata in +// the user's own repo. Audit any new .cmd/.bat caller before merging. export function buildSpawnOptions( cmd: string, platform: NodeJS.Platform,