



























@@ -15,6 +15,10 @@ const LOG_SCAN_BYTES = readPositiveInt(
1515process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_LOG_SCAN_BYTES,
1616256 * 1024,
1717);
18+const GATEWAY_LOG_CAPTURE_BYTES = readPositiveInt(
19+process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_GATEWAY_LOG_BYTES,
20+16 * 1024 * 1024,
21+);
1822const WATCHDOG_MS = readPositiveInt(process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_WATCHDOG_MS, 1000);
1923const READY_TIMEOUT_MS = readPositiveInt(
2024process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_READY_MS,
@@ -33,14 +37,26 @@ const HTTP_PROBE_TIMEOUT_MS = readPositiveInt(
3337process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_HTTP_MS,
34385000,
3539);
40+const GATEWAY_TEARDOWN_GRACE_MS = readPositiveInt(
41+process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_TEARDOWN_GRACE_MS,
42+10000,
43+);
44+const GATEWAY_TEARDOWN_KILL_GRACE_MS = readPositiveInt(
45+process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_TEARDOWN_KILL_GRACE_MS,
46+1000,
47+);
3648const GATEWAY_READY_LOG_NEEDLE = Buffer.from("[gateway] ready");
3749const READY_OFFSET_LOG_NEEDLES = [
3850GATEWAY_READY_LOG_NEEDLE,
3951Buffer.from("listening on ws://"),
4052Buffer.from("[gateway] http server listening"),
4153];
54+const GATEWAY_LOG_TRUNCATED_NEEDLE = "[gateway log truncated after ";
4255const FORBIDDEN_POST_READY_DEPS_WORK = [/\b(?:npm|pnpm|yarn|corepack) install\b/iu];
4356const isolatedStateRoots = new WeakMap();
57+const activeGatewayChildren = new Set();
58+const parentSignalHandlers = new Map();
59+let gatewayExitCleanupInstalled = false;
44604561function readPositiveInt(raw, fallback) {
4662const text = String(raw ?? "").trim();
@@ -319,6 +335,44 @@ function formatCapturedOutput(label, buffer) {
319335return `${prefix}${buffer.text}`;
320336}
321337338+function createBoundedGatewayLog(logPath) {
339+fs.mkdirSync(path.dirname(logPath), { recursive: true });
340+const fd = fs.openSync(logPath, "w");
341+let bytes = 0;
342+let closed = false;
343+let truncated = false;
344+const marker = Buffer.from(
345+`\n[gateway log truncated after ${String(GATEWAY_LOG_CAPTURE_BYTES)} bytes]\n`,
346+);
347+return {
348+append(chunk) {
349+if (closed || truncated) {
350+return;
351+}
352+const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
353+const remaining = GATEWAY_LOG_CAPTURE_BYTES - bytes;
354+if (buffer.length <= remaining) {
355+fs.writeSync(fd, buffer);
356+bytes += buffer.length;
357+return;
358+}
359+if (remaining > 0) {
360+fs.writeSync(fd, buffer.subarray(0, remaining));
361+}
362+fs.writeSync(fd, marker);
363+bytes = GATEWAY_LOG_CAPTURE_BYTES;
364+truncated = true;
365+},
366+close() {
367+if (closed) {
368+return;
369+}
370+closed = true;
371+fs.closeSync(fd);
372+},
373+};
374+}
375+322376export function runCommand(command, args, options = {}) {
323377return new Promise((resolve, reject) => {
324378const { timeoutMs = COMMAND_TIMEOUT_MS, ...spawnOptions } = options;
@@ -384,8 +438,8 @@ export function runCommand(command, args, options = {}) {
384438});
385439}
386440387-function startGateway(params) {
388-const log = fs.openSync(params.logPath, "w");
441+export function startGateway(params) {
442+const log = createBoundedGatewayLog(params.logPath);
389443const child = childProcess.spawn(
390444"node",
391445[
@@ -405,29 +459,125 @@ function startGateway(params) {
405459OPENCLAW_SKIP_CHANNELS: params.skipChannels ? "1" : "0",
406460OPENCLAW_SKIP_PROVIDERS: "0",
407461},
408-stdio: ["ignore", log, log],
409-detached: false,
462+stdio: ["ignore", "pipe", "pipe"],
463+detached: process.platform !== "win32",
410464},
411465);
412-fs.closeSync(log);
466+child.stdout?.on("data", (chunk) => log.append(chunk));
467+child.stderr?.on("data", (chunk) => log.append(chunk));
468+child.once("error", () => log.close());
469+child.once("close", () => log.close());
470+trackGatewayChild(child);
413471return child;
414472}
415473416474export function hasChildExited(child) {
417475return child.exitCode !== null || (child.signalCode ?? null) !== null;
418476}
419477478+function trackGatewayChild(child) {
479+activeGatewayChildren.add(child);
480+const untrack = () => {
481+if (!processTreeIsAlive(child)) {
482+activeGatewayChildren.delete(child);
483+}
484+};
485+child.once("error", untrack);
486+child.once("close", untrack);
487+installGatewayParentCleanup();
488+}
489+490+function installGatewayParentCleanup() {
491+if (!gatewayExitCleanupInstalled) {
492+gatewayExitCleanupInstalled = true;
493+process.once("exit", () => {
494+cleanupActiveGatewayChildren("SIGTERM");
495+});
496+}
497+for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) {
498+if (parentSignalHandlers.has(signal)) {
499+continue;
500+}
501+const handler = () => {
502+cleanupActiveGatewayChildren(signal);
503+for (const [registeredSignal, registeredHandler] of parentSignalHandlers) {
504+process.off(registeredSignal, registeredHandler);
505+}
506+parentSignalHandlers.clear();
507+process.kill(process.pid, signal);
508+};
509+parentSignalHandlers.set(signal, handler);
510+process.once(signal, handler);
511+}
512+}
513+514+function cleanupActiveGatewayChildren(signal) {
515+for (const child of activeGatewayChildren) {
516+signalGateway(child, signal);
517+if (process.platform !== "win32") {
518+signalGateway(child, "SIGKILL");
519+}
520+}
521+}
522+420523export async function stopGateway(child) {
421-if (!child || hasChildExited(child)) {
524+if (!child || !processTreeIsAlive(child)) {
422525return;
423526}
424-child.kill("SIGTERM");
425-const started = Date.now();
426-while (!hasChildExited(child) && Date.now() - started < 10000) {
427-await delay(100);
527+const waitForExit = async (ms) => {
528+const deadline = Date.now() + ms;
529+while (Date.now() < deadline) {
530+if (!processTreeIsAlive(child)) {
531+return true;
532+}
533+await delay(100);
534+}
535+return !processTreeIsAlive(child);
536+};
537+538+signalGateway(child, "SIGTERM");
539+if (await waitForExit(GATEWAY_TEARDOWN_GRACE_MS)) {
540+return;
541+}
542+signalGateway(child, "SIGKILL");
543+await waitForExit(GATEWAY_TEARDOWN_KILL_GRACE_MS);
544+}
545+546+function processTreeIsAlive(child) {
547+if (!child || typeof child.pid !== "number") {
548+return !hasChildExited(child);
549+}
550+if (process.platform === "win32") {
551+return !hasChildExited(child);
552+}
553+try {
554+process.kill(-child.pid, 0);
555+return true;
556+} catch (error) {
557+if (error?.code === "EPERM") {
558+return true;
559+}
560+return false;
428561}
429-if (!hasChildExited(child)) {
430-child.kill("SIGKILL");
562+}
563+564+function signalGateway(child, signal) {
565+if (process.platform !== "win32" && typeof child.pid === "number") {
566+try {
567+process.kill(-child.pid, signal);
568+return;
569+} catch (error) {
570+if (error?.code === "ESRCH") {
571+return;
572+}
573+}
574+}
575+try {
576+child.kill(signal);
577+} catch (error) {
578+if (error?.code !== "ESRCH") {
579+throw error;
580+}
431581}
432582}
433583@@ -842,6 +992,7 @@ async function runWatchdog(options) {
842992);
843993}
844994await retryRpcCall("health", {}, options);
995+assertGatewayLogNotTruncated(options.logPath);
845996assertNoPostReadyRuntimeDepsWork(options.logPath, readyOffset);
846997await assertNoPackageManagerChildren(options.child.pid);
847998}
@@ -850,6 +1001,16 @@ export function findReadyLogOffset(logPath) {
8501001return findFirstNeedleOffset(logPath, READY_OFFSET_LOG_NEEDLES);
8511002}
85210031004+export function assertGatewayLogNotTruncated(logPath) {
1005+if (readFileTail(logPath).includes(GATEWAY_LOG_TRUNCATED_NEEDLE)) {
1006+throw new Error(
1007+`gateway log exceeded ${String(
1008+ GATEWAY_LOG_CAPTURE_BYTES,
1009+ )} bytes; runtime smoke cannot validate complete post-ready output`,
1010+);
1011+}
1012+}
1013+8531014export function assertNoPostReadyRuntimeDepsWork(logPath, readyOffset) {
8541015let stat;
8551016try {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。