Files
openclaw/src/process/exec.windows.integration.test.ts
Peter Steinberger 34c90a8cb3 fix(process): prevent orphaned Windows child process trees (#115535)
* 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>
2026-07-29 00:26:32 -04:00

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,
);
});