

















@@ -1,6 +1,7 @@
1-import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
1+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22import { tmpdir } from "node:os";
33import path from "node:path";
4+import { setTimeout as delay } from "node:timers/promises";
45import { describe, expect, it, vi } from "vitest";
56import {
67appendBoundedOutput,
@@ -10,12 +11,15 @@ import {
1011fetchJson,
1112findDistCallGatewayModuleFiles,
1213makeEnv,
14+runCommand,
1315sampleProcess,
1416sampleWindowsProcessByPort,
1517summarizeProcessSamples,
1618usesBuiltOpenClawEntry,
1719} from "../../scripts/e2e/kitchen-sink-rpc-walk.mjs";
182021+const posixIt = process.platform === "win32" ? it.skip : it;
22+1923describe("kitchen-sink RPC isolated state", () => {
2024it("cleans up the generated temporary home tree", async () => {
2125const { root, env } = makeEnv();
@@ -42,6 +46,52 @@ describe("kitchen-sink RPC command output capture", () => {
4246const second = appendBoundedOutput(first, "ghij", 5);
4347expect(second).toEqual({ text: "fghij", truncatedChars: 5 });
4448});
49+50+posixIt("kills timed command process groups", async () => {
51+const root = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-rpc-timeout-"));
52+const scriptPath = path.join(root, "trap-term.mjs");
53+const grandchildPidPath = path.join(root, "grandchild.pid");
54+let grandchildPid = 0;
55+56+writeFileSync(
57+scriptPath,
58+`
59+import { spawn } from "node:child_process";
60+import fs from "node:fs";
61+62+const grandchild = spawn(process.execPath, [
63+ "-e",
64+ "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);",
65+], { stdio: "ignore" });
66+fs.writeFileSync(process.argv[2], String(grandchild.pid));
67+process.on("SIGTERM", () => {});
68+setInterval(() => {}, 1000);
69+`,
70+"utf8",
71+);
72+73+const runPromise = runCommand(process.execPath, [scriptPath, grandchildPidPath], {
74+detached: undefined,
75+timeoutKillGraceMs: 50,
76+timeoutMs: 2000,
77+});
78+79+try {
80+await waitFor(() => existsSync(grandchildPidPath));
81+grandchildPid = Number.parseInt(readText(grandchildPidPath), 10);
82+expect(Number.isInteger(grandchildPid)).toBe(true);
83+expect(isProcessAlive(grandchildPid)).toBe(true);
84+85+await expect(runPromise).rejects.toThrow("timed out after 2000ms");
86+await waitFor(() => !isProcessAlive(grandchildPid), 5_000);
87+} finally {
88+await runPromise.catch(() => {});
89+if (grandchildPid && isProcessAlive(grandchildPid)) {
90+process.kill(grandchildPid, "SIGKILL");
91+}
92+rmSync(root, { recursive: true, force: true });
93+}
94+});
4595});
46964797describe("kitchen-sink RPC caller loading", () => {
@@ -324,3 +374,26 @@ describe("kitchen-sink RPC process sampling", () => {
324374expect(() => assertResourceCeiling(null)).toThrow("gateway RSS sample was not captured");
325375});
326376});
377+378+function readText(file: string) {
379+return readFileSync(file, "utf8");
380+}
381+382+async function waitFor(condition: () => boolean, timeoutMs = 3_000) {
383+const startedAt = Date.now();
384+while (!condition()) {
385+if (Date.now() - startedAt > timeoutMs) {
386+throw new Error("timed out waiting for condition");
387+}
388+await delay(25);
389+}
390+}
391+392+function isProcessAlive(pid: number) {
393+try {
394+process.kill(pid, 0);
395+return true;
396+} catch {
397+return false;
398+}
399+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。