




















@@ -43,6 +43,9 @@ const DEFAULT_PREFLIGHT_RUN_TIMEOUT_MS = 60_000;
4343const CLEANUP_SMOKE_NAME = "cleanup-smoke";
4444export const SHELL_CAPTURE_MAX_CHARS = 1024 * 1024;
4545export const LOG_TAIL_MAX_BYTES = 1024 * 1024;
46+const SHELL_TIMEOUT_KILL_GRACE_MS = 10_000;
47+const SHELL_POST_FORCE_KILL_WAIT_MS = 1_000;
48+const SHELL_PROCESS_GROUP_EXIT_POLL_MS = 25;
4649const DEFAULT_TIMINGS_FILE = path.join(ROOT_DIR, ".artifacts/docker-tests/lane-timings.json");
4750const DEFAULT_GITHUB_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml";
4851const IS_MAIN = process.argv[1]
@@ -551,7 +554,15 @@ export function dockerPreflightSmokeCommand(arch = process.arch) {
551554return `docker run --rm --platform ${shellQuote(platform)} alpine:3.20 true`;
552555}
553556554-export function runShellCommand({ command, env, label, logFile, timeoutMs, noOutputTimeoutMs }) {
557+export function runShellCommand({
558+ command,
559+ env,
560+ label,
561+ logFile,
562+ timeoutMs,
563+ noOutputTimeoutMs,
564+ timeoutKillGraceMs = SHELL_TIMEOUT_KILL_GRACE_MS,
565+}) {
555566return new Promise((resolve) => {
556567const pipeOutput = Boolean(logFile || noOutputTimeoutMs > 0);
557568const child = spawn("bash", ["-c", command], {
@@ -564,6 +575,7 @@ export function runShellCommand({ command, env, label, logFile, timeoutMs, noOut
564575let timedOut = false;
565576let noOutputTimedOut = false;
566577let killTimer;
578+let killAt;
567579let stream;
568580let noOutputTimer;
569581const terminateForTimeout = (message, options = {}) => {
@@ -578,7 +590,8 @@ export function runShellCommand({ command, env, label, logFile, timeoutMs, noOut
578590console.error(`==> [${label}] ${message}; sending SIGTERM`);
579591}
580592terminateChild(child, "SIGTERM");
581-killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), 10_000);
593+killAt = Date.now() + timeoutKillGraceMs;
594+killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), timeoutKillGraceMs);
582595killTimer.unref?.();
583596};
584597const resetNoOutputTimer = () => {
@@ -627,23 +640,31 @@ export function runShellCommand({ command, env, label, logFile, timeoutMs, noOut
627640if (noOutputTimer) {
628641clearTimeout(noOutputTimer);
629642}
643+const finish = () => {
644+if (killTimer) {
645+clearTimeout(killTimer);
646+}
647+killAt = undefined;
648+activeChildren.delete(child);
649+const exitCode = typeof status === "number" ? status : signal ? 128 : 1;
650+if (stream) {
651+stream.write(
652+`\n==> [${label}] finished: ${utcStamp()} status=${exitCode}${
653+ noOutputTimedOut ? " noOutputTimedOut=true" : ""
654+ }\n`,
655+);
656+stream.end();
657+}
658+resolve({ signal, status: exitCode, timedOut, noOutputTimedOut });
659+};
630660if (timedOut) {
631-terminateChild(child, "SIGKILL");
632-}
633-if (killTimer) {
634-clearTimeout(killTimer);
635-}
636-activeChildren.delete(child);
637-const exitCode = typeof status === "number" ? status : signal ? 128 : 1;
638-if (stream) {
639-stream.write(
640-`\n==> [${label}] finished: ${utcStamp()} status=${exitCode}${
641- noOutputTimedOut ? " noOutputTimedOut=true" : ""
642- }\n`,
661+void finishTimedOutShellProcessTree(child, { killAt, timeoutKillGraceMs }).then(
662+finish,
663+finish,
643664);
644-stream.end();
665+return;
645666}
646-resolve({ signal, status: exitCode, timedOut, noOutputTimedOut });
667+finish();
647668});
648669});
649670}
@@ -656,7 +677,13 @@ export function appendBoundedShellCapture(current, chunk, maxChars = SHELL_CAPTU
656677return { text: combined.slice(-maxChars), truncated: true };
657678}
658679659-export function runShellCaptureCommand({ command, env, label, timeoutMs }) {
680+export function runShellCaptureCommand({
681+ command,
682+ env,
683+ label,
684+ timeoutMs,
685+ timeoutKillGraceMs = SHELL_TIMEOUT_KILL_GRACE_MS,
686+}) {
660687return new Promise((resolve) => {
661688const child = spawn("bash", ["-c", command], {
662689cwd: ROOT_DIR,
@@ -671,12 +698,14 @@ export function runShellCaptureCommand({ command, env, label, timeoutMs }) {
671698let stderrTruncated = false;
672699let timedOut = false;
673700let killTimer;
701+let killAt;
674702const timeoutTimer =
675703timeoutMs > 0
676704 ? setTimeout(() => {
677705timedOut = true;
678706terminateChild(child, "SIGTERM");
679-killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), 10_000);
707+killAt = Date.now() + timeoutKillGraceMs;
708+killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), timeoutKillGraceMs);
680709killTimer.unref?.();
681710}, timeoutMs)
682711 : undefined;
@@ -695,24 +724,32 @@ export function runShellCaptureCommand({ command, env, label, timeoutMs }) {
695724if (timeoutTimer) {
696725clearTimeout(timeoutTimer);
697726}
727+const finish = () => {
728+if (killTimer) {
729+clearTimeout(killTimer);
730+}
731+killAt = undefined;
732+activeChildren.delete(child);
733+const exitCode = typeof status === "number" ? status : signal ? 128 : 1;
734+resolve({
735+ label,
736+ signal,
737+status: exitCode,
738+ stderr,
739+ stderrTruncated,
740+ stdout,
741+ stdoutTruncated,
742+ timedOut,
743+});
744+};
698745if (timedOut) {
699-terminateChild(child, "SIGKILL");
700-}
701-if (killTimer) {
702-clearTimeout(killTimer);
746+void finishTimedOutShellProcessTree(child, { killAt, timeoutKillGraceMs }).then(
747+finish,
748+finish,
749+);
750+return;
703751}
704-activeChildren.delete(child);
705-const exitCode = typeof status === "number" ? status : signal ? 128 : 1;
706-resolve({
707- label,
708- signal,
709-status: exitCode,
710- stderr,
711- stderrTruncated,
712- stdout,
713- stdoutTruncated,
714- timedOut,
715-});
752+finish();
716753});
717754});
718755}
@@ -1225,6 +1262,47 @@ async function printFailureSummary(failures, tailLines) {
12251262}
1226126312271264const activeChildren = new Set();
1265+1266+function shellProcessGroupAlive(child) {
1267+if (process.platform === "win32" || !child.pid) {
1268+return false;
1269+}
1270+try {
1271+process.kill(-child.pid, 0);
1272+return true;
1273+} catch (error) {
1274+return error?.code === "EPERM";
1275+}
1276+}
1277+1278+async function waitForShellProcessGroupExit(child, timeoutMs) {
1279+const deadlineAt = Date.now() + timeoutMs;
1280+while (Date.now() < deadlineAt) {
1281+if (!shellProcessGroupAlive(child)) {
1282+return true;
1283+}
1284+await new Promise((resolvePoll) => {
1285+setTimeout(resolvePoll, SHELL_PROCESS_GROUP_EXIT_POLL_MS);
1286+});
1287+}
1288+return !shellProcessGroupAlive(child);
1289+}
1290+1291+async function finishTimedOutShellProcessTree(child, { killAt, timeoutKillGraceMs }) {
1292+if (!shellProcessGroupAlive(child)) {
1293+return;
1294+}
1295+const graceRemainingMs =
1296+killAt === undefined ? timeoutKillGraceMs : Math.max(0, killAt - Date.now());
1297+if (graceRemainingMs > 0) {
1298+await waitForShellProcessGroupExit(child, graceRemainingMs);
1299+}
1300+if (shellProcessGroupAlive(child)) {
1301+terminateChild(child, "SIGKILL");
1302+}
1303+await waitForShellProcessGroupExit(child, SHELL_POST_FORCE_KILL_WAIT_MS);
1304+}
1305+12281306function terminateChild(child, signal) {
12291307if (process.platform !== "win32" && child.pid) {
12301308try {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。