

























@@ -105,6 +105,8 @@ type CliOptions = {
105105const DEFAULT_RUNS = 5;
106106const DEFAULT_WARMUP = 1;
107107const DEFAULT_TIMEOUT_MS = 30_000;
108+const TIMEOUT_KILL_GRACE_MS = 1_000;
109+const PROCESS_GROUP_EXIT_POLL_MS = 25;
108110const DEFAULT_ENTRY = "openclaw.mjs";
109111const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
110112const VALUE_FLAGS = new Set([
@@ -708,12 +710,16 @@ async function runSample(params: {
708710let stderr = "";
709711let settled = false;
710712let timedOut = false;
713+let forceKillAt: number | null = null;
714+let forceKillTimer: ReturnType<typeof setTimeout> | null = null;
711715const maxOutputLength = 32 * 1024 * 1024;
712716713717try {
714718return await new Promise<Sample>((resolve) => {
719+const useProcessGroup = process.platform !== "win32";
715720const proc = spawn(process.execPath, nodeArgs, {
716721cwd: process.cwd(),
722+detached: useProcessGroup,
717723env: {
718724 ...process.env,
719725HOME: runRoot,
@@ -733,6 +739,10 @@ async function runSample(params: {
733739return;
734740}
735741settled = true;
742+if (forceKillTimer) {
743+clearTimeout(forceKillTimer);
744+forceKillTimer = null;
745+}
736746const ms = Number(process.hrtime.bigint() - started) / 1e6;
737747resolve({
738748 ms,
@@ -751,18 +761,11 @@ async function runSample(params: {
751761752762const timeout = setTimeout(() => {
753763timedOut = true;
754-try {
755-proc.kill("SIGTERM");
756-} catch {
757-// Best-effort timeout cleanup.
758-}
759-setTimeout(() => {
760-try {
761-proc.kill("SIGKILL");
762-} catch {
763-// Best-effort timeout cleanup.
764-}
765-}, 1_000).unref?.();
764+signalSampleProcess(proc, "SIGTERM", useProcessGroup);
765+forceKillAt = Date.now() + TIMEOUT_KILL_GRACE_MS;
766+forceKillTimer = setTimeout(() => {
767+signalSampleProcess(proc, "SIGKILL", useProcessGroup);
768+}, TIMEOUT_KILL_GRACE_MS).unref?.();
766769}, params.timeoutMs);
767770timeout.unref?.();
768771@@ -790,23 +793,108 @@ async function runSample(params: {
790793});
791794proc.once("close", (code, signal) => {
792795clearTimeout(timeout);
793-finish({
794-exitCode: code,
795- signal,
796- ...(code === 0 && signal == null
797- ? {}
798- : {
799-stdoutTail: tailLines(stdout, 20),
800-stderrTail: tailLines(stderr, 20),
801-}),
802-});
796+const complete = () =>
797+finish({
798+exitCode: code,
799+ signal,
800+ ...(code === 0 && signal == null
801+ ? {}
802+ : {
803+stdoutTail: tailLines(stdout, 20),
804+stderrTail: tailLines(stderr, 20),
805+}),
806+});
807+if (timedOut && isSampleProcessGroupAlive(proc, useProcessGroup)) {
808+void finishAfterTimeoutCleanup({
809+ complete,
810+ forceKillAt,
811+ proc,
812+ useProcessGroup,
813+});
814+return;
815+}
816+complete();
803817});
804818});
805819} finally {
806820rmSync(runRoot, { recursive: true, force: true });
807821}
808822}
809823824+async function finishAfterTimeoutCleanup(params: {
825+complete: () => void;
826+forceKillAt: number | null;
827+proc: ReturnType<typeof spawn>;
828+useProcessGroup: boolean;
829+}): Promise<void> {
830+const graceRemainingMs =
831+params.forceKillAt === null
832+ ? TIMEOUT_KILL_GRACE_MS
833+ : Math.max(0, params.forceKillAt - Date.now());
834+if (graceRemainingMs > 0) {
835+await waitForSampleProcessGroupExit(params.proc, params.useProcessGroup, graceRemainingMs);
836+}
837+if (isSampleProcessGroupAlive(params.proc, params.useProcessGroup)) {
838+signalSampleProcess(params.proc, "SIGKILL", params.useProcessGroup);
839+}
840+await waitForSampleProcessGroupExit(params.proc, params.useProcessGroup, TIMEOUT_KILL_GRACE_MS);
841+params.complete();
842+}
843+844+function signalSampleProcess(
845+proc: ReturnType<typeof spawn>,
846+signal: NodeJS.Signals,
847+useProcessGroup: boolean,
848+): void {
849+if (!proc.pid) {
850+return;
851+}
852+try {
853+if (useProcessGroup) {
854+process.kill(-proc.pid, signal);
855+} else {
856+proc.kill(signal);
857+}
858+} catch (error) {
859+const code = (error as NodeJS.ErrnoException | undefined)?.code;
860+if (code !== "ESRCH" && code !== "EPERM") {
861+throw error;
862+}
863+}
864+}
865+866+function isSampleProcessGroupAlive(
867+proc: ReturnType<typeof spawn>,
868+useProcessGroup: boolean,
869+): boolean {
870+if (!useProcessGroup || !proc.pid) {
871+return false;
872+}
873+try {
874+process.kill(-proc.pid, 0);
875+return true;
876+} catch (error) {
877+return (error as NodeJS.ErrnoException | undefined)?.code === "EPERM";
878+}
879+}
880+881+async function waitForSampleProcessGroupExit(
882+proc: ReturnType<typeof spawn>,
883+useProcessGroup: boolean,
884+timeoutMs: number,
885+): Promise<boolean> {
886+const deadlineAt = Date.now() + timeoutMs;
887+while (Date.now() < deadlineAt) {
888+if (!isSampleProcessGroupAlive(proc, useProcessGroup)) {
889+return true;
890+}
891+await new Promise((resolvePoll) => {
892+setTimeout(resolvePoll, PROCESS_GROUP_EXIT_POLL_MS);
893+});
894+}
895+return !isSampleProcessGroupAlive(proc, useProcessGroup);
896+}
897+810898async function runCase(params: {
811899entry: string;
812900commandCase: CommandCase;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。