mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 18:42:25 +00:00
fix(qa-matrix): clean up killed CLI process groups
This commit is contained in:
@@ -378,4 +378,57 @@ describe("Matrix QA CLI runtime", () => {
|
||||
await rm(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("kills ignored-stdio descendants after manual CLI session kill", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await mkdtemp(
|
||||
path.join(resolvePreferredOpenClawTmpDir(), "matrix-qa-cli-session-kill-ignored-stdio-"),
|
||||
);
|
||||
const childPidPath = path.join(root, "child.pid");
|
||||
const grandchildPidPath = path.join(root, "grandchild.pid");
|
||||
let childPid: number | undefined;
|
||||
let grandchildPid: number | undefined;
|
||||
try {
|
||||
await mkdir(path.join(root, "dist"));
|
||||
await writeFile(
|
||||
path.join(root, "dist", "index.mjs"),
|
||||
[
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`writeFileSync(${JSON.stringify(childPidPath)}, String(process.pid));`,
|
||||
"const grandchild = spawn(process.execPath, ['-e', 'process.on(\\'SIGTERM\\', () => {}); setInterval(() => {}, 1000);'], { stdio: 'ignore' });",
|
||||
"grandchild.unref();",
|
||||
`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(grandchild.pid));`,
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const session = startMatrixQaOpenClawCli({
|
||||
args: ["matrix", "verify", "self"],
|
||||
cwd: root,
|
||||
env: process.env,
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
await waitForFile(grandchildPidPath, 2_000);
|
||||
await sleep(300);
|
||||
|
||||
session.kill();
|
||||
await sleep(500);
|
||||
|
||||
childPid = Number(await readFile(childPidPath, "utf8"));
|
||||
grandchildPid = Number(await readFile(grandchildPidPath, "utf8"));
|
||||
expect(isProcessRunning(childPid)).toBe(false);
|
||||
expect(isProcessRunning(grandchildPid)).toBe(false);
|
||||
} finally {
|
||||
for (const pid of [grandchildPid, childPid]) {
|
||||
if (pid && isProcessRunning(pid)) {
|
||||
process.kill(pid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
await rm(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,7 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
let closed = false;
|
||||
let closeError: Error | undefined;
|
||||
let closeResult: MatrixQaCliRunResult | undefined;
|
||||
let killRequested = false;
|
||||
let timedOut = false;
|
||||
let forceKillTimeout: NodeJS.Timeout | undefined;
|
||||
let forceSettleTimeout: NodeJS.Timeout | undefined;
|
||||
@@ -187,6 +188,13 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
const finishTimeout = (result: MatrixQaCliRunResult) => {
|
||||
finish(result, new Error(formatMatrixQaCliTimeoutError(result, params.timeoutMs)));
|
||||
};
|
||||
const finishResult = (result: MatrixQaCliRunResult) => {
|
||||
if (result.exitCode !== 0 && params.allowNonZero !== true) {
|
||||
finish(result, new Error(formatMatrixQaCliExitError(result)));
|
||||
return;
|
||||
}
|
||||
finish(result);
|
||||
};
|
||||
const clearForcedTimeouts = () => {
|
||||
if (forceKillTimeout) {
|
||||
clearTimeout(forceKillTimeout);
|
||||
@@ -197,16 +205,23 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
forceSettleTimeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
killMatrixQaCliChild(child, "SIGTERM");
|
||||
const finishForcedCleanup = (result: MatrixQaCliRunResult) => {
|
||||
if (timedOut) {
|
||||
finishTimeout(result);
|
||||
return;
|
||||
}
|
||||
finishResult(result);
|
||||
};
|
||||
const scheduleForcedCleanup = () => {
|
||||
if (forceKillTimeout || forceSettleTimeout) {
|
||||
return;
|
||||
}
|
||||
forceKillTimeout = setTimeout(() => {
|
||||
forceKillTimeout = undefined;
|
||||
killMatrixQaCliChild(child, "SIGKILL");
|
||||
forceSettleTimeout = setTimeout(() => {
|
||||
forceSettleTimeout = undefined;
|
||||
finishTimeout(
|
||||
finishForcedCleanup(
|
||||
buildMatrixQaCliResult({
|
||||
args: params.args,
|
||||
exitCode: 1,
|
||||
@@ -215,6 +230,12 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
);
|
||||
}, MATRIX_QA_CLI_TIMEOUT_FORCE_SETTLE_MS);
|
||||
}, MATRIX_QA_CLI_TIMEOUT_KILL_GRACE_MS);
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
killMatrixQaCliChild(child, "SIGTERM");
|
||||
scheduleForcedCleanup();
|
||||
}, params.timeoutMs);
|
||||
|
||||
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
||||
@@ -241,20 +262,17 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
exitCode: exitCode ?? 1,
|
||||
output: readOutput(),
|
||||
});
|
||||
if (timedOut) {
|
||||
if (timedOut || killRequested) {
|
||||
// A closed parent is not proof that detached, ignored-stdio descendants are gone.
|
||||
if (isMatrixQaCliChildProcessGroupRunning(child)) {
|
||||
return;
|
||||
}
|
||||
clearForcedTimeouts();
|
||||
finishTimeout(result);
|
||||
finishForcedCleanup(result);
|
||||
return;
|
||||
}
|
||||
clearForcedTimeouts();
|
||||
if (result.exitCode !== 0 && params.allowNonZero !== true) {
|
||||
finish(result, new Error(formatMatrixQaCliExitError(result)));
|
||||
return;
|
||||
}
|
||||
finish(result);
|
||||
finishResult(result);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -310,7 +328,10 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
},
|
||||
kill: () => {
|
||||
if (!closed) {
|
||||
clearTimeout(timeout);
|
||||
killRequested = true;
|
||||
killMatrixQaCliChild(child, "SIGTERM");
|
||||
scheduleForcedCleanup();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user