fix(crabbox): kill telegram proof trees on windows

This commit is contained in:
Vincent Koc
2026-06-21 06:47:25 +02:00
parent 0030a192c8
commit 38c8b0c196
2 changed files with 62 additions and 4 deletions

View File

@@ -1,7 +1,12 @@
#!/usr/bin/env -S node --import tsx
// Telegram User Crabbox Proof script supports OpenClaw repository automation.
import { type ChildProcess, spawn, type SpawnOptionsWithoutStdio } from "node:child_process";
import {
type ChildProcess,
spawn,
spawnSync,
type SpawnOptionsWithoutStdio,
} from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
@@ -587,13 +592,37 @@ function timedOutError(message: string) {
const activeCommandChildren = new Set<ChildProcess>();
let commandCleanupHandlersInstalled = false;
function signalCommandTree(child: ChildProcess, signal: NodeJS.Signals) {
if (child.pid && process.platform !== "win32") {
type CommandTreeTarget = Pick<ChildProcess, "kill" | "pid">;
export function signalCommandTree(
child: CommandTreeTarget,
signal: NodeJS.Signals,
{
platform = process.platform,
runTaskkill = spawnSync,
useProcessGroup = platform !== "win32",
}: {
platform?: NodeJS.Platform;
runTaskkill?: typeof spawnSync;
useProcessGroup?: boolean;
} = {},
) {
if (child.pid && useProcessGroup) {
try {
process.kill(-child.pid, signal);
return;
} catch {}
}
if (platform === "win32" && typeof child.pid === "number") {
const args = ["/PID", String(child.pid), "/T"];
if (signal === "SIGKILL") {
args.push("/F");
}
const result = runTaskkill("taskkill", args, { stdio: "ignore" });
if (!result.error && result.status === 0) {
return;
}
}
child.kill(signal);
}

View File

@@ -18,6 +18,7 @@ import {
renderRemoteSetup,
renderSelectDesktopChat,
runCommand,
signalCommandTree,
stageFullSessionArtifacts,
startLocalSut,
waitForLog,
@@ -217,7 +218,10 @@ describe("telegram user Crabbox proof log polling", () => {
fs.mkdirSync(publishDir);
fs.writeFileSync(path.join(publishDir, "stale.txt"), "stale");
fs.mkdirSync(path.join(outputDir, "publish-gif-only"));
fs.writeFileSync(path.join(outputDir, "session.json"), '{"sshKey":"/private/tmp/openclaw/key"}');
fs.writeFileSync(
path.join(outputDir, "session.json"),
'{"sshKey":"/private/tmp/openclaw/key"}',
);
fs.writeFileSync(path.join(outputDir, "lease.json"), '{"token":"secret"}');
fs.writeFileSync(path.join(outputDir, "status.json"), '{"ok":true}');
fs.writeFileSync(path.join(outputDir, "probe.json"), '{"ok":true}');
@@ -349,6 +353,31 @@ setInterval(() => {}, 1000);
}
});
it("signals Windows proof command process trees with taskkill", () => {
const child = {
kill: vi.fn(),
pid: 12345,
};
const runTaskkill = vi.fn(() => ({ error: undefined, status: 0 }));
signalCommandTree(child, "SIGTERM", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
stdio: "ignore",
});
signalCommandTree(child, "SIGKILL", {
platform: "win32",
runTaskkill,
});
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
stdio: "ignore",
});
expect(child.kill).not.toHaveBeenCalled();
});
posixIt("lets timed-out command descendants exit during kill grace", async () => {
const root = makeTempDir();
const scriptPath = path.join(root, "trap-term-grace.mjs");