

























11// Docker All Scheduler tests cover docker all scheduler script behavior.
2-import { spawnSync } from "node:child_process";
2+import { spawn, spawnSync } from "node:child_process";
33import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
44import { tmpdir } from "node:os";
55import path from "node:path";
@@ -68,6 +68,20 @@ async function waitFor(predicate: () => boolean, timeoutMs = 5_000): Promise<voi
6868throw new Error("condition was not met before timeout");
6969}
707071+async function waitForChildClose(child: ReturnType<typeof spawn>, timeoutMs = 5_000) {
72+return await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
73+(resolve, reject) => {
74+const timeout = setTimeout(() => {
75+reject(new Error("child did not close before timeout"));
76+}, timeoutMs);
77+child.once("close", (code, signal) => {
78+clearTimeout(timeout);
79+resolve({ code, signal });
80+});
81+},
82+);
83+}
84+7185describe("scripts/test-docker-all scheduler", () => {
7286it("parses the supported CLI options", () => {
7387expect(parseDockerAllCliArgs([])).toEqual({
@@ -591,6 +605,117 @@ setInterval(() => {}, 1000);
591605expect(readFileSync(donePath, "utf8")).toBe("done");
592606});
593607608+posixIt("cleans active shell command groups before parent signal exit", async () => {
609+const root = createTempDir("openclaw-docker-all-parent-signal-");
610+const leaderPath = path.join(root, "leader-exits.mjs");
611+const runnerPath = path.join(root, "runner.mjs");
612+const grandchildPidPath = path.join(root, "grandchild.pid");
613+const readyPath = path.join(root, "ready");
614+const secondGrandchildPidPath = path.join(root, "second-grandchild.pid");
615+const secondReadyPath = path.join(root, "second-ready");
616+let grandchildPid = 0;
617+let secondGrandchildPid = 0;
618+let runner: ReturnType<typeof spawn> | undefined;
619+const childScript = [
620+"const fs = require('node:fs');",
621+"process.on('SIGTERM', () => {});",
622+"process.on('SIGHUP', () => {});",
623+`fs.writeFileSync(${JSON.stringify(readyPath)}, 'ready');`,
624+"setInterval(() => {}, 1000);",
625+].join("\n");
626+627+writeFileSync(
628+leaderPath,
629+`
630+import { spawn } from "node:child_process";
631+import fs from "node:fs";
632+633+const grandchild = spawn(process.execPath, ["-e", ${JSON.stringify(childScript)}], {
634+ stdio: "ignore",
635+});
636+fs.writeFileSync(${JSON.stringify(grandchildPidPath)}, String(grandchild.pid));
637+process.on("SIGTERM", () => process.exit(0));
638+setInterval(() => {}, 1000);
639+`,
640+"utf8",
641+);
642+writeFileSync(
643+runnerPath,
644+`
645+import { runShellCommand } from ${JSON.stringify(
646+ new URL("../../scripts/test-docker-all.mjs", import.meta.url).href,
647+ )};
648+649+await runShellCommand({
650+ command: ${JSON.stringify(`exec ${JSON.stringify(process.execPath)} ${JSON.stringify(leaderPath)}`)},
651+ env: process.env,
652+ label: "parent-signal-cleanup",
653+ timeoutKillGraceMs: 100,
654+ timeoutMs: 30_000,
655+});
656+657+await runShellCommand({
658+ command: ${JSON.stringify(
659+ [
660+ "exec",
661+ JSON.stringify(process.execPath),
662+ "-e",
663+ JSON.stringify(
664+ [
665+ "const { spawn } = require('node:child_process');",
666+ "const fs = require('node:fs');",
667+ "const child = spawn(process.execPath, ['-e', \"process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);\"], { stdio: 'ignore' });",
668+ `fs.writeFileSync(${JSON.stringify(secondGrandchildPidPath)}, String(child.pid));`,
669+ `fs.writeFileSync(${JSON.stringify(secondReadyPath)}, 'ready');`,
670+ "setInterval(() => {}, 1000);",
671+ ].join("\n"),
672+ ),
673+ ].join(" "),
674+ )},
675+ env: process.env,
676+ label: "parent-signal-second-command",
677+ timeoutKillGraceMs: 100,
678+ timeoutMs: 30_000,
679+});
680+`,
681+"utf8",
682+);
683+684+try {
685+runner = spawn(process.execPath, [runnerPath], {
686+cwd: process.cwd(),
687+stdio: ["ignore", "ignore", "pipe"],
688+});
689+await waitFor(() => existsSync(readyPath) && existsSync(grandchildPidPath));
690+grandchildPid = Number.parseInt(readFileSync(grandchildPidPath, "utf8"), 10);
691+expect(Number.isInteger(grandchildPid)).toBe(true);
692+expect(isProcessAlive(grandchildPid)).toBe(true);
693+694+runner.kill("SIGTERM");
695+696+await expect(waitForChildClose(runner, 15_000)).resolves.toEqual({
697+code: 143,
698+signal: null,
699+});
700+await waitFor(() => !isProcessAlive(grandchildPid));
701+expect(existsSync(secondReadyPath)).toBe(false);
702+if (existsSync(secondGrandchildPidPath)) {
703+secondGrandchildPid = Number.parseInt(readFileSync(secondGrandchildPidPath, "utf8"), 10);
704+}
705+expect(secondGrandchildPid).toBe(0);
706+} finally {
707+if (grandchildPid && isProcessAlive(grandchildPid)) {
708+process.kill(grandchildPid, "SIGKILL");
709+}
710+if (secondGrandchildPid && isProcessAlive(secondGrandchildPid)) {
711+process.kill(secondGrandchildPid, "SIGKILL");
712+}
713+if (runner?.pid && isProcessAlive(runner.pid)) {
714+runner.kill("SIGKILL");
715+}
716+}
717+});
718+594719it("describes effective scheduler limits for operator errors", () => {
595720expect(describeDockerSchedulerLimits(2, limits)).toBe(
596721"parallelism=2 weightLimit=2 resources=docker=2 npm=2",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。