mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
b06fe2a673
* fix(kill-tree): verify process group leader before group kill to prevent gateway SIGTERM (#76259) - Add isProcessGroupLeader() to killProcessTree/signalProcessTree: ps -p <pid> -o pgid= primary check with /proc/<pid>/stat fallback on Linux. Group kill only when the PID is its own process group leader; non-leaders fall back to single-pid kill, preventing accidental gateway SIGTERM when a non-detached child shares the gateway's process group. - Propagate detached: true to all detached-spawn cleanup callers (exec-termination, agent-bundle LSP, mcp-stdio, bash, supervisor pty, agent-core nodejs) so detached group cleanup survives leader exit. - Gateway/daemon cleanup paths (schtasks, restart-health) keep the leader-checked default (detached omitted). Closes #76259 Co-Authored-By: Claude <noreply@anthropic.com> * refactor(process): tighten process-group ownership checks * refactor(daemon): split restart diagnostics * refactor(daemon): isolate restart health types --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import type { IPty } from "@lydell/node-pty";
|
|
import { signalProcessTree } from "./kill-tree.js";
|
|
import {
|
|
buildWindowsCmdExeCommandLine,
|
|
isWindowsBatchCommand,
|
|
resolveTrustedWindowsCmdExe,
|
|
} from "./windows-command.js";
|
|
|
|
/** Live PTY handle shared by gateway terminals and node-host commands. */
|
|
type TerminalPtyHandle = {
|
|
pid: number;
|
|
write(data: string): void;
|
|
resize(cols: number, rows: number): void;
|
|
pause(): void;
|
|
resume(): void;
|
|
onData(listener: (chunk: string) => void): void;
|
|
onExit(listener: (event: { exitCode: number; signal?: number }) => void): void;
|
|
kill(signal?: string): void;
|
|
};
|
|
|
|
function resolveTerminalPtyInvocation(params: {
|
|
file: string;
|
|
args: string[];
|
|
platform?: NodeJS.Platform;
|
|
comSpec?: string;
|
|
}): { file: string; args: string[] } {
|
|
const platform = params.platform ?? process.platform;
|
|
if (!isWindowsBatchCommand(params.file, platform)) {
|
|
return { file: params.file, args: params.args };
|
|
}
|
|
return {
|
|
file: params.comSpec?.trim() || resolveTrustedWindowsCmdExe(platform),
|
|
args: ["/d", "/s", "/c", buildWindowsCmdExeCommandLine(params.file, params.args)],
|
|
};
|
|
}
|
|
|
|
export async function spawnTerminalPty(params: {
|
|
file: string;
|
|
args: string[];
|
|
cwd?: string;
|
|
env: Record<string, string>;
|
|
cols: number;
|
|
rows: number;
|
|
}): Promise<TerminalPtyHandle> {
|
|
const { spawn } = await import("@lydell/node-pty");
|
|
const env = { ...params.env };
|
|
// Ambient TERM=dumb describes the gateway/node host, not this real PTY.
|
|
// Passing it through makes interactive CLIs refuse to start in the web terminal.
|
|
const inheritedTerm = env.TERM?.trim();
|
|
env.TERM =
|
|
!inheritedTerm || inheritedTerm.toLowerCase() === "dumb" ? "xterm-256color" : inheritedTerm;
|
|
const comSpec = env.ComSpec ?? env.COMSPEC;
|
|
const invocation = resolveTerminalPtyInvocation({
|
|
file: params.file,
|
|
args: params.args,
|
|
...(comSpec ? { comSpec } : {}),
|
|
});
|
|
const pty = spawn(invocation.file, invocation.args, {
|
|
name: env.TERM,
|
|
cols: params.cols,
|
|
rows: params.rows,
|
|
cwd: params.cwd,
|
|
env,
|
|
});
|
|
return {
|
|
get pid() {
|
|
return pty.pid;
|
|
},
|
|
write: (data) => pty.write(data),
|
|
resize: (cols, rows) => pty.resize(cols, rows),
|
|
pause: () => pty.pause(),
|
|
resume: () => pty.resume(),
|
|
onData: (listener) => {
|
|
pty.onData(listener);
|
|
},
|
|
onExit: (listener) => {
|
|
pty.onExit(listener);
|
|
},
|
|
kill: (signal) => killPtyTree(pty, signal),
|
|
} satisfies TerminalPtyHandle;
|
|
}
|
|
|
|
// A long-running child of the interactive shell must not survive terminal
|
|
// close. Signal the process tree, matching the process supervisor contract.
|
|
function killPtyTree(pty: Pick<IPty, "pid" | "kill">, signal?: string): void {
|
|
const sig = (signal ?? "SIGKILL") as NodeJS.Signals;
|
|
try {
|
|
if ((sig === "SIGKILL" || sig === "SIGTERM") && typeof pty.pid === "number" && pty.pid > 0) {
|
|
// forkpty creates a new session/process group; retain descendant cleanup
|
|
// after the shell exits and only its group remains.
|
|
signalProcessTree(pty.pid, sig, { detached: true });
|
|
} else if (process.platform === "win32") {
|
|
pty.kill();
|
|
} else {
|
|
pty.kill(sig);
|
|
}
|
|
} catch {
|
|
// Process may already be gone; teardown is best-effort.
|
|
}
|
|
}
|