| import type { ChildProcessWithoutNullStreams, SpawnOptions } from "node:child_process"; |
| import { killProcessTree } from "../../kill-tree.js"; |
| import { spawnWithFallback } from "../../spawn-utils.js"; |
| import { resolveWindowsCommandShim } from "../../windows-command.js"; |
| import type { ManagedRunStdin, SpawnProcessAdapter } from "../types.js"; |
| import { toStringEnv } from "./env.js"; |
|
|
| function resolveCommand(command: string): string { |
| return resolveWindowsCommandShim({ |
| command, |
| cmdCommands: ["npm", "pnpm", "yarn", "npx"], |
| }); |
| } |
|
|
| export type ChildAdapter = SpawnProcessAdapter<NodeJS.Signals | null>; |
|
|
| function isServiceManagedRuntime(): boolean { |
| return Boolean(process.env.OPENCLAW_SERVICE_MARKER?.trim()); |
| } |
|
|
| export async function createChildAdapter(params: { |
| argv: string[]; |
| cwd?: string; |
| env?: NodeJS.ProcessEnv; |
| windowsVerbatimArguments?: boolean; |
| input?: string; |
| stdinMode?: "inherit" | "pipe-open" | "pipe-closed"; |
| }): Promise<ChildAdapter> { |
| const resolvedArgv = [...params.argv]; |
| resolvedArgv[0] = resolveCommand(resolvedArgv[0] ?? ""); |
|
|
| const stdinMode = params.stdinMode ?? (params.input !== undefined ? "pipe-closed" : "inherit"); |
|
|
| |
| |
| |
| const useDetached = process.platform !== "win32" && !isServiceManagedRuntime(); |
|
|
| const options: SpawnOptions = { |
| cwd: params.cwd, |
| env: params.env ? toStringEnv(params.env) : undefined, |
| stdio: ["pipe", "pipe", "pipe"], |
| detached: useDetached, |
| windowsHide: true, |
| windowsVerbatimArguments: params.windowsVerbatimArguments, |
| }; |
| if (stdinMode === "inherit") { |
| options.stdio = ["inherit", "pipe", "pipe"]; |
| } else { |
| options.stdio = ["pipe", "pipe", "pipe"]; |
| } |
|
|
| const spawned = await spawnWithFallback({ |
| argv: resolvedArgv, |
| options, |
| fallbacks: useDetached |
| ? [ |
| { |
| label: "no-detach", |
| options: { detached: false }, |
| }, |
| ] |
| : [], |
| }); |
|
|
| const child = spawned.child as ChildProcessWithoutNullStreams; |
| if (child.stdin) { |
| if (params.input !== undefined) { |
| child.stdin.write(params.input); |
| child.stdin.end(); |
| } else if (stdinMode === "pipe-closed") { |
| child.stdin.end(); |
| } |
| } |
|
|
| const stdin: ManagedRunStdin | undefined = child.stdin |
| ? { |
| destroyed: false, |
| write: (data: string, cb?: (err?: Error | null) => void) => { |
| try { |
| child.stdin.write(data, cb); |
| } catch (err) { |
| cb?.(err as Error); |
| } |
| }, |
| end: () => { |
| try { |
| child.stdin.end(); |
| } catch { |
| |
| } |
| }, |
| destroy: () => { |
| try { |
| child.stdin.destroy(); |
| } catch { |
| |
| } |
| }, |
| } |
| : undefined; |
|
|
| const onStdout = (listener: (chunk: string) => void) => { |
| child.stdout.on("data", (chunk) => { |
| listener(chunk.toString()); |
| }); |
| }; |
|
|
| const onStderr = (listener: (chunk: string) => void) => { |
| child.stderr.on("data", (chunk) => { |
| listener(chunk.toString()); |
| }); |
| }; |
|
|
| const wait = async () => |
| await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => { |
| child.once("error", reject); |
| child.once("close", (code, signal) => { |
| resolve({ code, signal }); |
| }); |
| }); |
|
|
| const kill = (signal?: NodeJS.Signals) => { |
| const pid = child.pid ?? undefined; |
| if (signal === undefined || signal === "SIGKILL") { |
| if (pid) { |
| killProcessTree(pid); |
| } else { |
| try { |
| child.kill("SIGKILL"); |
| } catch { |
| |
| } |
| } |
| return; |
| } |
| try { |
| child.kill(signal); |
| } catch { |
| |
| } |
| }; |
|
|
| const dispose = () => { |
| child.removeAllListeners(); |
| }; |
|
|
| return { |
| pid: child.pid ?? undefined, |
| stdin, |
| onStdout, |
| onStderr, |
| wait, |
| kill, |
| dispose, |
| }; |
| } |
|
|