

























1-import { execFile } from "node:child_process";
1+import { execFile, spawn } from "node:child_process";
2+import { existsSync, readFileSync } from "node:fs";
23import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
34import { tmpdir } from "node:os";
45import path from "node:path";
6+import { pathToFileURL } from "node:url";
57import { afterEach, describe, expect, it } from "vitest";
68import {
79downloadUrl,
@@ -33,6 +35,63 @@ async function missing(file: string): Promise<boolean> {
3335);
3436}
353738+function isProcessAlive(pid: number): boolean {
39+try {
40+process.kill(pid, 0);
41+return true;
42+} catch {
43+return false;
44+}
45+}
46+47+async function sleep(ms: number): Promise<void> {
48+await new Promise((resolve) => {
49+setTimeout(resolve, ms);
50+});
51+}
52+53+async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
54+const deadline = Date.now() + timeoutMs;
55+while (Date.now() < deadline) {
56+if (existsSync(filePath)) {
57+return;
58+}
59+await sleep(25);
60+}
61+throw new Error(`timeout waiting for ${filePath}`);
62+}
63+64+async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
65+const deadline = Date.now() + timeoutMs;
66+while (Date.now() < deadline) {
67+if (!isProcessAlive(pid)) {
68+return;
69+}
70+await sleep(25);
71+}
72+throw new Error(`process still alive: ${pid}`);
73+}
74+75+async function waitForExit(
76+child: ReturnType<typeof spawn>,
77+timeoutMs: number,
78+): Promise<{ signal: NodeJS.Signals | null; status: number | null }> {
79+return await new Promise((resolve, reject) => {
80+const timeout = setTimeout(
81+() => reject(new Error("timeout waiting for child exit")),
82+timeoutMs,
83+);
84+child.on("close", (status, signal) => {
85+clearTimeout(timeout);
86+resolve({ signal, status });
87+});
88+child.on("error", (error) => {
89+clearTimeout(timeout);
90+reject(error);
91+});
92+});
93+}
94+3695afterEach(async () => {
3796await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
3897});
@@ -161,6 +220,96 @@ describe("resolve-openclaw-package-candidate", () => {
161220).rejects.toThrow(/produced more than \d+ captured stdout chars/u);
162221});
163222223+it("kills timed-out package runner process groups", async () => {
224+if (process.platform === "win32") {
225+return;
226+}
227+228+const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-runner-timeout-"));
229+tempDirs.push(dir);
230+const childPidPath = path.join(dir, "child.pid");
231+let childPid: number | undefined;
232+233+try {
234+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
235+const parentScript = [
236+"const { spawn } = require('node:child_process');",
237+"const fs = require('node:fs');",
238+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
239+"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));",
240+"setInterval(() => {}, 1000);",
241+].join("");
242+243+const timeoutAssertion = expect(
244+runCommandForTest(process.execPath, ["-e", parentScript], {
245+env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath },
246+killAfterMs: 25,
247+timeoutMs: 500,
248+}),
249+).rejects.toThrow(/timed out after 500ms/u);
250+251+await waitForFile(childPidPath, 2_000);
252+childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
253+await timeoutAssertion;
254+await waitForDead(childPid, 2_000);
255+} finally {
256+if (childPid !== undefined && isProcessAlive(childPid)) {
257+process.kill(childPid, "SIGKILL");
258+}
259+}
260+});
261+262+it("forwards external termination to package runner process groups", async () => {
263+if (process.platform === "win32") {
264+return;
265+}
266+267+const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-runner-signal-"));
268+tempDirs.push(dir);
269+const childPidPath = path.join(dir, "child.pid");
270+const scriptUrl = pathToFileURL(
271+path.resolve("scripts/resolve-openclaw-package-candidate.mjs"),
272+).href;
273+let childPid: number | undefined;
274+let runnerPid: number | undefined;
275+276+try {
277+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
278+const parentScript = [
279+"const { spawn } = require('node:child_process');",
280+"const fs = require('node:fs');",
281+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
282+"fs.writeFileSync(process.env.OPENCLAW_TEST_CHILD_PID, String(child.pid));",
283+"setInterval(() => {}, 1000);",
284+].join("");
285+const runnerScript = [
286+`import { runCommandForTest } from ${JSON.stringify(scriptUrl)};`,
287+`await runCommandForTest(process.execPath, ['-e', ${JSON.stringify(parentScript)}], { timeoutMs: 60000 });`,
288+].join("\n");
289+const runner = spawn(process.execPath, ["--input-type=module", "-e", runnerScript], {
290+cwd: process.cwd(),
291+env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath },
292+stdio: ["ignore", "ignore", "pipe"],
293+});
294+runnerPid = runner.pid;
295+296+await waitForFile(childPidPath, 2_000);
297+childPid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
298+runner.kill("SIGTERM");
299+const result = await waitForExit(runner, 7_000);
300+301+expect(result).toEqual({ signal: null, status: 143 });
302+await waitForDead(childPid, 2_000);
303+} finally {
304+if (runnerPid !== undefined && isProcessAlive(runnerPid)) {
305+process.kill(runnerPid, "SIGKILL");
306+}
307+if (childPid !== undefined && isProcessAlive(childPid)) {
308+process.kill(childPid, "SIGKILL");
309+}
310+}
311+});
312+164313it("loads named trusted package URL source policies", async () => {
165314const dir = await mkdtemp(path.join(tmpdir(), "openclaw-trusted-package-source-"));
166315tempDirs.push(dir);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。