mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 10:31:33 +00:00
* 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>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
import { EventEmitter } from "node:events";
|
|
import os from "node:os";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
|
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
|
|
import { maybeWrapCommandWithShellSnapshot } from "./shell-snapshot.js";
|
|
|
|
const { killProcessTreeMock, spawnMock } = vi.hoisted(() => ({
|
|
killProcessTreeMock: vi.fn(),
|
|
spawnMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("node:child_process", async () => {
|
|
const { mockNodeBuiltinModule } = await import("openclaw/plugin-sdk/test-node-mocks");
|
|
return mockNodeBuiltinModule(
|
|
() => vi.importActual<typeof import("node:child_process")>("node:child_process"),
|
|
{ spawn: spawnMock },
|
|
);
|
|
});
|
|
|
|
vi.mock("../process/kill-tree.js", () => ({
|
|
killProcessTree: killProcessTreeMock,
|
|
}));
|
|
|
|
describe.skipIf(process.platform === "win32")("shell snapshot subprocesses", () => {
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
let envSnapshot: ReturnType<typeof captureEnv>;
|
|
|
|
beforeEach(() => {
|
|
envSnapshot = captureEnv(["HOME", "OPENCLAW_EXEC_SHELL_SNAPSHOT", "OPENCLAW_STATE_DIR"]);
|
|
spawnMock.mockReset();
|
|
killProcessTreeMock.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
envSnapshot.restore();
|
|
});
|
|
|
|
it("does not create output pipes for status-only shell commands", async () => {
|
|
const child = Object.assign(new EventEmitter(), { pid: 4242 }) as ChildProcess;
|
|
spawnMock.mockImplementation(() => {
|
|
queueMicrotask(() => child.emit("close", 1));
|
|
return child;
|
|
});
|
|
|
|
const home = tempDirs.make("openclaw-snapshot-spawn-home-");
|
|
const stateDir = tempDirs.make("openclaw-snapshot-spawn-state-");
|
|
setTestEnvValue("HOME", home);
|
|
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
|
|
|
|
const command = "echo unchanged";
|
|
await expect(
|
|
maybeWrapCommandWithShellSnapshot({
|
|
command,
|
|
shell: "/bin/bash",
|
|
shellArgs: ["-c"],
|
|
cwd: os.tmpdir(),
|
|
env: { ...process.env },
|
|
}),
|
|
).resolves.toBe(command);
|
|
|
|
const options = spawnMock.mock.calls[0]?.[2] as SpawnOptions | undefined;
|
|
expect(options?.stdio).toBe("ignore");
|
|
expect(killProcessTreeMock).toHaveBeenCalledWith(4242, { graceMs: 0, detached: true });
|
|
});
|
|
});
|