mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 23:41:35 +00:00
* test(process): reproduce Windows taskkill process-tree leak * fix(process): stop leaked Windows child process trees Escalate only when Windows taskkill reports that graceful process-tree termination failed. Preserve awaited taskkill completion, grace-period fallback, one-shot signaling, and PID-reuse protection. Closes #110789 Supersedes #112202 Co-authored-by: Mohammed Alkindi <alkndymhmd692@gmail.com> --------- Co-authored-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { spawn, spawnSync } from "node:child_process";
|
|
import { once } from "node:events";
|
|
import process from "node:process";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { runUtf8CommandWithTimeout } from "./exec.js";
|
|
import { killProcessTree } from "./kill-tree.js";
|
|
|
|
describe("runUtf8CommandWithTimeout Windows integration", () => {
|
|
it.runIf(process.platform === "win32")(
|
|
"keeps truncated UTF-8 head output on a code point boundary",
|
|
async () => {
|
|
const result = await runUtf8CommandWithTimeout(
|
|
[process.execPath, "-e", "process.stdout.write('a😀z'); process.stderr.write('b😀y')"],
|
|
{
|
|
maxOutputBytes: 3,
|
|
outputCapture: "head",
|
|
timeoutMs: 3_000,
|
|
},
|
|
);
|
|
|
|
expect(result.stdout).toBe("a");
|
|
expect(result.stderr).toBe("b");
|
|
expect(result.stdoutTruncatedBytes).toBe(5);
|
|
expect(result.stderrTruncatedBytes).toBe(5);
|
|
},
|
|
);
|
|
|
|
it.runIf(process.platform === "win32")(
|
|
"force-kills a real Windows process tree when graceful taskkill refuses it",
|
|
async () => {
|
|
const program = [
|
|
'const { spawn } = require("node:child_process");',
|
|
'const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore", windowsHide: true });',
|
|
'child.once("spawn", () => process.stdout.write(String(child.pid) + "\\n"));',
|
|
'child.once("error", () => process.exit(1));',
|
|
"setInterval(() => {}, 1000);",
|
|
].join("\n");
|
|
const parent = spawn(process.execPath, ["-e", program], {
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
windowsHide: true,
|
|
});
|
|
const parentPid = parent.pid;
|
|
const parentStdout = parent.stdout;
|
|
|
|
if (parentPid === undefined || parentStdout === null) {
|
|
parent.kill();
|
|
throw new Error("Could not start the Windows process tree");
|
|
}
|
|
|
|
try {
|
|
const [output] = await once(parentStdout, "data");
|
|
const childPid = Number.parseInt(String(output).trim(), 10);
|
|
expect(Number.isSafeInteger(childPid)).toBe(true);
|
|
expect(() => process.kill(parentPid, 0)).not.toThrow();
|
|
expect(() => process.kill(childPid, 0)).not.toThrow();
|
|
|
|
// An unforced taskkill refuses Node console processes. Cleanup must not
|
|
// depend on this unref'd timer surviving an application shutdown.
|
|
killProcessTree(parentPid, { graceMs: 30_000 });
|
|
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(() => process.kill(parentPid, 0)).toThrow();
|
|
expect(() => process.kill(childPid, 0)).toThrow();
|
|
},
|
|
{ timeout: 5_000, interval: 50 },
|
|
);
|
|
} finally {
|
|
spawnSync("taskkill", ["/F", "/T", "/PID", String(parentPid)], {
|
|
stdio: "ignore",
|
|
timeout: 5_000,
|
|
windowsHide: true,
|
|
});
|
|
parentStdout.destroy();
|
|
}
|
|
},
|
|
15_000,
|
|
);
|
|
});
|