#!/usr/bin/env node import os from "node:os"; import process from "node:process"; import { spawn } from "node:child_process"; const TIMEOUT_EXIT_CODE = 124; const SPAWN_ERROR_EXIT_CODE = 127; const INVALID_ARGUMENT_EXIT_CODE = 2; const DEFAULT_GRACE_SECONDS = 2; const MAX_TIMEOUT_SECONDS = 86_400; const MAX_GRACE_SECONDS = 60; const STAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u; function failArguments() { process.stderr.write(`[docker-smoke] FAIL stage=timeout-runner reason=invalid_arguments exit_code=${INVALID_ARGUMENT_EXIT_CODE}\n`); process.exit(INVALID_ARGUMENT_EXIT_CODE); } function parseSeconds(value, { allowZero, maximum }) { if (typeof value !== "string" || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value)) { failArguments(); } const parsed = Number(value); const validLowerBound = allowZero ? parsed >= 0 : parsed > 0; if (!Number.isFinite(parsed) || !validLowerBound || parsed > maximum) { failArguments(); } return parsed; } function parseArguments(argv) { let stage; let timeoutSeconds; let graceSeconds = DEFAULT_GRACE_SECONDS; let sawGraceSeconds = false; let command; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--") { command = argv.slice(index + 1); break; } if (argument === "--stage" && stage === undefined) { stage = argv[index + 1]; index += 1; continue; } if (argument === "--timeout-seconds" && timeoutSeconds === undefined) { timeoutSeconds = argv[index + 1]; index += 1; continue; } if (argument === "--grace-seconds" && !sawGraceSeconds) { graceSeconds = argv[index + 1]; sawGraceSeconds = true; index += 1; continue; } failArguments(); } if (!STAGE_PATTERN.test(stage ?? "") || timeoutSeconds === undefined || !command?.length) { failArguments(); } return { stage, timeoutSeconds: parseSeconds(timeoutSeconds, { allowZero: false, maximum: MAX_TIMEOUT_SECONDS }), graceSeconds: parseSeconds(String(graceSeconds), { allowZero: true, maximum: MAX_GRACE_SECONDS }), command, }; } function signalExitCode(signal) { const signalNumber = os.constants.signals[signal]; return typeof signalNumber === "number" ? 128 + signalNumber : 1; } const { stage, timeoutSeconds, graceSeconds, command } = parseArguments(process.argv.slice(2)); const startedAt = performance.now(); const timeoutMilliseconds = timeoutSeconds * 1_000; const graceMilliseconds = graceSeconds * 1_000; process.stderr.write(`[docker-smoke] START stage=${stage} timeout_seconds=${timeoutSeconds} grace_seconds=${graceSeconds}\n`); const child = spawn(command[0], command.slice(1), { detached: true, env: process.env, shell: false, stdio: "inherit", }); let childClosed = false; let finished = false; let termination; let timeoutTimer; let forceKillTimer; function durationMilliseconds() { return Math.max(0, Math.round(performance.now() - startedAt)); } function processGroupExists() { if (!child.pid) { return false; } try { process.kill(-child.pid, 0); return true; } catch (error) { return error?.code === "EPERM"; } } function signalProcessGroup(signal) { if (!child.pid) { return; } try { process.kill(-child.pid, signal); } catch (error) { if (error?.code !== "ESRCH") { try { child.kill(signal); } catch { // The child may have exited between the process-group and direct kill. } } } } function clearTimers() { if (timeoutTimer) { clearTimeout(timeoutTimer); } if (forceKillTimer) { clearTimeout(forceKillTimer); } } function finish(exitCode, outcome, extraMetadata = "") { if (finished) { return; } finished = true; clearTimers(); process.removeListener("SIGINT", handleInterrupt); process.removeListener("SIGTERM", handleTerminate); const suffix = extraMetadata ? ` ${extraMetadata}` : ""; process.stderr.write(`[docker-smoke] ${outcome} stage=${stage} duration_ms=${durationMilliseconds()}${suffix} exit_code=${exitCode}\n`); process.exitCode = exitCode; } function finishTerminationIfReady() { if (!termination || (!childClosed && !termination.forceKillSent)) { return; } if (processGroupExists() && !termination.forceKillSent) { return; } if (termination.kind === "timeout") { finish(TIMEOUT_EXIT_CODE, "TIMEOUT"); return; } const exitCode = signalExitCode(termination.signal); finish(exitCode, "FAIL", `signal=${termination.signal}`); } function forceKillTermination() { if (!termination || finished) { return; } termination.forceKillSent = true; signalProcessGroup("SIGKILL"); child.unref(); finishTerminationIfReady(); } function beginTermination(nextTermination) { if (finished) { return; } if (termination) { forceKillTermination(); return; } termination = { ...nextTermination, forceKillSent: false }; if (timeoutTimer) { clearTimeout(timeoutTimer); } signalProcessGroup("SIGTERM"); forceKillTimer = setTimeout(forceKillTermination, graceMilliseconds); finishTerminationIfReady(); } function handleInterrupt() { beginTermination({ kind: "signal", signal: "SIGINT" }); } function handleTerminate() { beginTermination({ kind: "signal", signal: "SIGTERM" }); } process.on("SIGINT", handleInterrupt); process.on("SIGTERM", handleTerminate); child.once("error", () => { childClosed = true; if (termination) { finishTerminationIfReady(); return; } finish(SPAWN_ERROR_EXIT_CODE, "FAIL", "reason=spawn_error"); }); child.once("close", (code, signal) => { childClosed = true; if (termination) { finishTerminationIfReady(); return; } if (signal) { const exitCode = signalExitCode(signal); finish(exitCode, "FAIL", `signal=${signal}`); return; } const exitCode = typeof code === "number" ? code : 1; finish(exitCode, exitCode === 0 ? "PASS" : "FAIL"); }); timeoutTimer = setTimeout(() => { beginTermination({ kind: "timeout" }); }, timeoutMilliseconds);