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; 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 { const resolvedArgv = [...params.argv]; resolvedArgv[0] = resolveCommand(resolvedArgv[0] ?? ""); const stdinMode = params.stdinMode ?? (params.input !== undefined ? "pipe-closed" : "inherit"); // In service-managed mode keep children attached so systemd/launchd can // stop the full process tree reliably. Outside service mode preserve the // existing POSIX detached behavior. 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 { // ignore close errors } }, destroy: () => { try { child.stdin.destroy(); } catch { // ignore destroy errors } }, } : 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 { // ignore kill errors } } return; } try { child.kill(signal); } catch { // ignore kill errors for non-kill signals } }; const dispose = () => { child.removeAllListeners(); }; return { pid: child.pid ?? undefined, stdin, onStdout, onStderr, wait, kill, dispose, }; }