openclaw/scripts/lib/docker-e2e-watchdog.mjs
Peter Steinberger 9491d5a5a3
refactor(scripts): deslop scripts/lib (#159362)
* refactor(scripts): deslop scripts/lib

Consolidate script lifecycle, scenario, planner, report, release and
packaging helpers while retaining CLI and publication contracts.

Keep config-boundary source caches within one scan so repeated in-process
checks observe edits. Carry extracted helpers through selective fixtures
and iOS scope routing. Leave PR tooling and its library import closure intact.

* fix(scripts): retain sanitizer fast path after helper cleanup

* fix(scripts): model watchdog shell consumer in dead-code checks
2026-09-27 17:13:58 -07:00

117 lines
3.4 KiB
JavaScript

// Shared Node fallback for host Docker and in-container E2E command timeouts.
import { pathToFileURL } from "node:url";
export async function runWatchdog(kind, timeoutValue, [command, ...args]) {
const docker = kind === "docker";
const label = docker ? "Docker" : "OpenClaw E2E";
const parseTimeoutMs = (value) => {
const match = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h)?$/u.exec(String(value ?? "").trim());
if (!match) {
throw new Error(`unsupported timeout value: ${value}`);
}
const amount = Number(match[1]);
const unit = match[2] ?? "s";
const multiplier = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : 3_600_000;
return Math.max(1, Math.ceil(amount * multiplier));
};
if (!command) {
console.error("missing command for Node watchdog");
process.exit(1);
}
const { spawn } = await import("node:child_process");
let timeoutMs;
try {
timeoutMs = parseTimeoutMs(timeoutValue);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
const child = spawn(command, args, {
detached: process.platform !== "win32",
stdio: "inherit",
});
let timedOut = false;
let parentSignal = null;
let parentSignalTimer = null;
const signalExitCodes = new Map([
["SIGHUP", 129],
["SIGINT", 130],
["SIGTERM", 143],
]);
const killGraceMs = Number.parseInt(
process.env[
docker ? "OPENCLAW_DOCKER_TIMEOUT_KILL_GRACE_MS" : "OPENCLAW_E2E_TIMEOUT_KILL_GRACE_MS"
] || "30000",
10,
);
const killChild = (signal) => {
if (!child.pid) {
return;
}
const killTarget = process.platform === "win32" ? child.pid : -child.pid;
try {
process.kill(killTarget, signal);
} catch {
try {
child.kill(signal);
} catch {}
}
};
const timer = setTimeout(() => {
timedOut = true;
console.error(`${label} command timed out after ${timeoutValue}`);
killChild("SIGTERM");
setTimeout(() => killChild("SIGKILL"), killGraceMs).unref();
}, timeoutMs);
const forwardSignal = (signal) => {
if (parentSignal) {
killChild("SIGKILL");
process.exit(signalExitCodes.get(signal) ?? 1);
}
parentSignal = signal;
clearTimeout(timer);
killChild(signal);
parentSignalTimer = setTimeout(() => {
killChild("SIGKILL");
process.exit(signalExitCodes.get(signal) ?? 1);
}, killGraceMs);
parentSignalTimer.unref();
};
process.once("SIGINT", forwardSignal);
process.once("SIGTERM", forwardSignal);
process.once("SIGHUP", forwardSignal);
// In-container commands settle their inherited streams; host Docker wrappers settle on exit.
child.on(docker ? "exit" : "close", (code, signal) => {
clearTimeout(timer);
if (parentSignalTimer) {
clearTimeout(parentSignalTimer);
}
if (timedOut) {
process.exit(124);
}
if (parentSignal) {
process.exit(signalExitCodes.get(parentSignal) ?? 1);
}
if (code !== null) {
process.exit(code);
}
if (signal) {
process.kill(process.pid, signal);
}
process.exit(1);
});
child.on("error", (error) => {
clearTimeout(timer);
console.error(error.message);
process.exit(127);
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [kind, timeoutValue, ...args] = process.argv.slice(2);
await runWatchdog(kind, timeoutValue, args);
}