

























@@ -11,6 +11,7 @@ const DEFAULT_METHODS = ["health", "config.get"];
1111const DEFAULT_ITERATIONS = 10;
1212export const READY_TIMEOUT_MS = 120_000;
1313export const READY_PROBE_TIMEOUT_MS = 1_000;
14+const PARENT_TERMINATION_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM"];
1415const IS_DIRECT_RUN =
1516typeof process.argv[1] === "string" &&
1617path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
@@ -143,20 +144,117 @@ export async function waitForGatewayReady({
143144throw new Error(`gateway did not become ready after ${readyTimeoutMs}ms\n${stderr.slice(-4000)}`);
144145}
145146146-async function stopGateway(child) {
147-if (child.exitCode !== null || child.signalCode !== null) {
147+function isProcessAlreadyExitedError(error) {
148+return error && typeof error === "object" && error.code === "ESRCH";
149+}
150+151+function defaultKillProcess(pid, signal) {
152+return process.kill(pid, signal);
153+}
154+155+async function defaultOpen(filePath, flags) {
156+return await fs.open(filePath, flags);
157+}
158+159+export function signalGatewayProcess(child, signal, killProcess = defaultKillProcess) {
160+if (process.platform !== "win32" && typeof child.pid === "number") {
161+try {
162+killProcess(-child.pid, signal);
163+return true;
164+} catch (error) {
165+if (isProcessAlreadyExitedError(error)) {
166+return false;
167+}
168+throw error;
169+}
170+}
171+try {
172+return child.kill(signal);
173+} catch (error) {
174+if (isProcessAlreadyExitedError(error)) {
175+return false;
176+}
177+throw error;
178+}
179+}
180+181+export function isGatewayProcessAlive(child, killProcess = defaultKillProcess) {
182+if (process.platform !== "win32" && typeof child.pid === "number") {
183+try {
184+killProcess(-child.pid, 0);
185+return true;
186+} catch (error) {
187+if (isProcessAlreadyExitedError(error)) {
188+return false;
189+}
190+throw error;
191+}
192+}
193+return child.exitCode === null && child.signalCode === null;
194+}
195+196+function signalGatewayProcessForParentExit(child, signal, killProcess) {
197+try {
198+signalGatewayProcess(child, signal, killProcess);
199+} catch {
200+// Parent shutdown cleanup is best effort; the original signal should win.
201+}
202+}
203+204+export function installGatewayParentCleanup(
205+child,
206+{ killProcess = defaultKillProcess, processLike = process } = {},
207+) {
208+const signalHandlers = new Map();
209+const cleanup = (signal) => {
210+signalGatewayProcessForParentExit(child, signal, killProcess);
211+if (process.platform !== "win32") {
212+signalGatewayProcessForParentExit(child, "SIGKILL", killProcess);
213+}
214+};
215+const exitHandler = () => {
216+cleanup("SIGTERM");
217+};
218+const removeHandlers = () => {
219+processLike.off?.("exit", exitHandler);
220+for (const [signal, handler] of signalHandlers) {
221+processLike.off?.(signal, handler);
222+}
223+signalHandlers.clear();
224+};
225+processLike.once("exit", exitHandler);
226+for (const signal of PARENT_TERMINATION_SIGNALS) {
227+const handler = () => {
228+cleanup(signal);
229+removeHandlers();
230+processLike.kill?.(processLike.pid, signal);
231+};
232+signalHandlers.set(signal, handler);
233+processLike.once(signal, handler);
234+}
235+return removeHandlers;
236+}
237+238+async function waitForGatewayExit(child, timeoutMs, killProcess = defaultKillProcess) {
239+const deadline = Date.now() + timeoutMs;
240+while (Date.now() <= deadline) {
241+if (!isGatewayProcessAlive(child, killProcess)) {
242+return true;
243+}
244+await sleep(Math.min(25, Math.max(0, deadline - Date.now())));
245+}
246+return !isGatewayProcessAlive(child, killProcess);
247+}
248+249+export async function stopGateway(child, options = {}) {
250+if (!isGatewayProcessAlive(child, options.killProcess)) {
148251return;
149252}
150-child.kill("SIGTERM");
151-const exited = await new Promise((resolve) => {
152-const timer = setTimeout(() => resolve(false), 1_500);
153-child.once("exit", () => {
154-clearTimeout(timer);
155-resolve(true);
156-});
157-});
158-if (!exited && child.exitCode === null && child.signalCode === null) {
159-child.kill("SIGKILL");
253+const killGraceMs = Math.max(0, options.killGraceMs ?? 1_500);
254+signalGatewayProcess(child, "SIGTERM", options.killProcess);
255+const exited = await waitForGatewayExit(child, killGraceMs, options.killProcess);
256+if (!exited) {
257+signalGatewayProcess(child, "SIGKILL", options.killProcess);
160258}
161259}
162260@@ -171,7 +269,7 @@ async function closeFileHandles(handles) {
171269export async function startGateway({
172270 configPath,
173271 env = process.env,
174- openImpl = fs.open,
272+ openImpl = defaultOpen,
175273 port,
176274 repoRoot,
177275 spawnImpl = spawn,
@@ -207,6 +305,7 @@ export async function startGateway({
207305],
208306{
209307cwd: repoRoot,
308+detached: process.platform !== "win32",
210309env: {
211310 ...env,
212311HOME: path.join(tempRoot, "home"),
@@ -417,6 +516,7 @@ async function main() {
417516const stdoutPath = path.join(tempRoot, "gateway.stdout.log");
418517const stderrPath = path.join(tempRoot, "gateway.stderr.log");
419518let gatewayChild;
519+let removeGatewayParentCleanup = () => {};
420520let status = "fail";
421521let details = "";
422522let measurement;
@@ -449,6 +549,7 @@ async function main() {
449549 tempRoot,
450550 token,
451551});
552+removeGatewayParentCleanup = installGatewayParentCleanup(gatewayChild);
452553await waitForGatewayReady({ child: gatewayChild, port, stderrPath });
453554454555const requireFromOpenClaw = createRequire(path.join(repoRoot, "package.json"));
@@ -534,8 +635,12 @@ async function main() {
534635} catch (error) {
535636details = error instanceof Error ? (error.stack ?? error.message) : String(error);
536637} finally {
537-if (gatewayChild) {
538-await stopGateway(gatewayChild).catch(() => {});
638+try {
639+if (gatewayChild) {
640+await stopGateway(gatewayChild).catch(() => {});
641+}
642+} finally {
643+removeGatewayParentCleanup();
539644}
540645try {
541646await cleanupTempRoot(tempRoot);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。