158 lines
4.1 KiB
TypeScript
158 lines
4.1 KiB
TypeScript
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");
|
|
|
|
// 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,
|
|
};
|
|
}
|