




















1+// Memory Host SDK real-process tests cover QMD process-tree cleanup.
2+import { spawnSync } from "node:child_process";
3+import fs from "node:fs/promises";
4+import os from "node:os";
5+import path from "node:path";
6+import { describe, expect, it } from "vitest";
7+import { runCliCommand } from "./qmd-process.js";
8+9+type ProcessTreePids = {
10+parent: number;
11+grandchild: number;
12+};
13+14+function isProcessRunning(pid: number): boolean {
15+try {
16+process.kill(pid, 0);
17+return true;
18+} catch (error) {
19+if (error instanceof Error && "code" in error && error.code === "ESRCH") {
20+return false;
21+}
22+throw error;
23+}
24+}
25+26+async function waitUntil(params: {
27+condition: () => boolean | Promise<boolean>;
28+description: string;
29+timeoutMs?: number;
30+}): Promise<void> {
31+const deadline = Date.now() + (params.timeoutMs ?? 5_000);
32+while (!(await params.condition())) {
33+if (Date.now() >= deadline) {
34+throw new Error(`timed out waiting for ${params.description}`);
35+}
36+await new Promise<void>((resolve) => {
37+setTimeout(resolve, 20);
38+});
39+}
40+}
41+42+async function readProcessTreePids(pidFile: string): Promise<ProcessTreePids> {
43+let pids: ProcessTreePids | undefined;
44+await waitUntil({
45+description: "the process-tree PID file",
46+condition: async () => {
47+try {
48+pids = JSON.parse(await fs.readFile(pidFile, "utf8")) as ProcessTreePids;
49+return Number.isInteger(pids.parent) && Number.isInteger(pids.grandchild);
50+} catch (error) {
51+if (error instanceof Error && "code" in error && error.code === "ENOENT") {
52+return false;
53+}
54+throw error;
55+}
56+},
57+});
58+if (!pids) {
59+throw new Error("process-tree PID file was not populated");
60+}
61+return pids;
62+}
63+64+function killProcessTree(parentPid: number): void {
65+if (process.platform === "win32") {
66+const systemRoot = process.env.SystemRoot ?? process.env.WINDIR ?? "C:\\Windows";
67+spawnSync(
68+path.win32.join(systemRoot, "System32", "taskkill.exe"),
69+["/PID", String(parentPid), "/T", "/F"],
70+{ stdio: "ignore", windowsHide: true },
71+);
72+return;
73+}
74+process.kill(-parentPid, "SIGKILL");
75+}
76+77+describe("runCliCommand real process lifecycle", () => {
78+it("kills the command and its descendant when the caller aborts", async () => {
79+const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qmd-abort-"));
80+const pidFile = path.join(tempDir, "pids.json");
81+const controller = new AbortController();
82+const abortError = new Error("memory_search timed out after 15s");
83+let pending: ReturnType<typeof runCliCommand> | undefined;
84+let pids: ProcessTreePids | undefined;
85+86+const childScript = `
87+ const { spawn } = require("node:child_process");
88+ const { renameSync, writeFileSync } = require("node:fs");
89+ const grandchild = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
90+ stdio: "ignore",
91+ });
92+ const pidFile = process.argv[1];
93+ const temporaryPidFile = pidFile + ".tmp";
94+ writeFileSync(temporaryPidFile, JSON.stringify({
95+ parent: process.pid,
96+ grandchild: grandchild.pid,
97+ }));
98+ renameSync(temporaryPidFile, pidFile);
99+ setInterval(() => {}, 1000);
100+ `;
101+102+try {
103+pending = runCliCommand({
104+commandSummary: "real qmd process-tree fixture",
105+spawnInvocation: { command: process.execPath, argv: ["-e", childScript, pidFile] },
106+env: process.env,
107+cwd: tempDir,
108+timeoutMs: 60_000,
109+maxOutputChars: 10_000,
110+signal: controller.signal,
111+});
112+113+pids = await readProcessTreePids(pidFile);
114+expect(isProcessRunning(pids.parent)).toBe(true);
115+expect(isProcessRunning(pids.grandchild)).toBe(true);
116+117+controller.abort(abortError);
118+await expect(pending).rejects.toBe(abortError);
119+await waitUntil({
120+description: "the detached process tree to exit",
121+condition: () =>
122+pids !== undefined &&
123+!isProcessRunning(pids.parent) &&
124+!isProcessRunning(pids.grandchild),
125+});
126+} finally {
127+if (!controller.signal.aborted) {
128+controller.abort(abortError);
129+}
130+await pending?.catch(() => undefined);
131+if (pids?.parent && isProcessRunning(pids.parent)) {
132+try {
133+killProcessTree(pids.parent);
134+} catch {
135+// Best-effort cleanup when an assertion failed after the child exited.
136+}
137+}
138+await fs.rm(tempDir, { recursive: true, force: true });
139+}
140+}, 15_000);
141+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。