openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
238 lines (237 loc) • 9.92 kB
JavaScript
import { m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs";
import { n as getCommandPathWithRootOptions } from "./argv-D_Nxd_3s.mjs";
import { r as isForegroundGatewayRunArgv } from "./gateway-run-argv-DFDqy1EF.mjs";
import { t as resolveCliArgvInvocation } from "./argv-invocation-CV_-dmd8.mjs";
import { n as isTruthyEnvValue } from "./env-BBJTTIuE.mjs";
import { t as normalizeWindowsArgv } from "./windows-argv-DUGbJuRb.mjs";
import { t as attachChildProcessBridge } from "./child-process-bridge-CFJsa4sQ.mjs";
import { r as signalProcessTree } from "./kill-tree-ecObdMeW.mjs";
import { t as resolveNodeStartupTlsEnvironment } from "./node-startup-env-CnxxjID9.mjs";
import path from "node:path";
import { spawn } from "node:child_process";
//#region src/cli/respawn-policy.ts
const INTERACTIVE_TTY_COMMANDS = /* @__PURE__ */ new Set([
"tui",
"terminal",
"chat"
]);
/** Gmail owns a shutdown grace period longer than the generic respawn wrapper allows. */
function isForegroundGmailRunArgv(argv) {
return getCommandPathWithRootOptions(argv, 3).join(" ") === "webhooks gmail run";
}
function isNativeHookRelayArgv(argv) {
const { commandPath } = resolveCliArgvInvocation(argv);
return commandPath[0] === "hooks" && commandPath[1] === "relay";
}
function shouldKeepNativeHookRelayInProcess(argv, platform) {
return platform !== "win32" && isNativeHookRelayArgv(argv);
}
function isTerminalInteractiveRespawnArgv(argv) {
const invocation = resolveCliArgvInvocation(argv);
if (invocation.hasHelpOrVersion) return false;
return invocation.primary === null || INTERACTIVE_TTY_COMMANDS.has(invocation.primary);
}
/** Returns whether CLI startup should avoid the general respawn wrapper for this argv. */
function shouldSkipRespawnForArgv(argv, platform = process.platform) {
const invocation = resolveCliArgvInvocation(argv);
const isGatewayStatus = invocation.commandPath.length === 2 && invocation.commandPath[0] === "gateway" && invocation.commandPath[1] === "status";
return invocation.hasHelpOrVersion || invocation.primary !== null && INTERACTIVE_TTY_COMMANDS.has(invocation.primary) || isForegroundGmailRunArgv(argv) || shouldKeepNativeHookRelayInProcess(argv, platform) || isGatewayStatus || invocation.primary === "gateway" && isForegroundGatewayRunArgv(argv);
}
/** Returns whether startup-environment respawn should be skipped without suppressing TUI respawn policy. */
function shouldSkipStartupEnvironmentRespawnForArgv(argv, platform = process.platform) {
const invocation = resolveCliArgvInvocation(argv);
return invocation.hasHelpOrVersion || isForegroundGmailRunArgv(argv) || shouldKeepNativeHookRelayInProcess(argv, platform) || invocation.primary === "gateway" && isForegroundGatewayRunArgv(argv);
}
//#endregion
//#region src/process/respawn-child-runner.ts
const RESPAWN_SIGNAL_EXIT_GRACE_MS = 1e3;
const RESPAWN_SIGNAL_FORCE_KILL_GRACE_MS = 1e3;
const RESPAWN_SIGNAL_HARD_EXIT_GRACE_MS = 1e3;
function runRespawnChildWithSignalBridge(params) {
const { command, args, env, runtime, onError } = params;
const stdioIsTerminal = params.stdioIsTerminal ?? (process.stdin.isTTY || process.stdout.isTTY);
const detachForProcessTree = params.detachForProcessTree === true && process.platform !== "win32" && !stdioIsTerminal;
const child = runtime.spawn(command, args, {
stdio: "inherit",
env,
detached: detachForProcessTree,
windowsHide: !stdioIsTerminal
});
let signalExitTimer;
let signalForceKillTimer;
let signalHardExitTimer;
let parentSignalReceived = false;
let firstForwardedSignal;
let hardKillBackstopStarted = false;
const clearSignalTimers = () => {
clearTimeout(signalExitTimer);
clearTimeout(signalForceKillTimer);
clearTimeout(signalHardExitTimer);
signalExitTimer = void 0;
signalForceKillTimer = void 0;
signalHardExitTimer = void 0;
};
const signalChild = (signal) => {
if (detachForProcessTree && typeof child.pid === "number" && child.pid > 0) {
signalProcessTree(child.pid, signal, { detached: true });
return;
}
child.kill(signal === "SIGKILL" && process.platform === "win32" ? "SIGTERM" : signal);
};
const forceKillChild = () => {
try {
signalChild("SIGKILL");
} catch {}
};
const requestChildTermination = () => {
try {
signalChild("SIGTERM");
} catch {}
signalForceKillTimer = setTimeout(() => {
hardKillBackstopStarted = true;
forceKillChild();
signalHardExitTimer = setTimeout(() => {
runtime.exit(1);
}, RESPAWN_SIGNAL_HARD_EXIT_GRACE_MS);
signalHardExitTimer.unref?.();
}, RESPAWN_SIGNAL_FORCE_KILL_GRACE_MS);
signalForceKillTimer.unref?.();
};
const scheduleParentExit = (signal) => {
parentSignalReceived = true;
firstForwardedSignal ??= signal;
if (signalExitTimer) return;
signalExitTimer = setTimeout(() => {
requestChildTermination();
}, RESPAWN_SIGNAL_EXIT_GRACE_MS);
signalExitTimer.unref?.();
};
runtime.attachChildProcessBridge(child, { onSignal: scheduleParentExit });
child.once("exit", (code, signal) => {
if (parentSignalReceived && detachForProcessTree) forceKillChild();
clearSignalTimers();
if (signal) {
if (process.platform !== "win32") {
process.kill(process.pid, signal);
return;
}
const forwardedSignalExitCode = !hardKillBackstopStarted && signal === firstForwardedSignal ? signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : void 0 : void 0;
runtime.exit(forwardedSignalExitCode ?? 1);
return;
}
runtime.exit(code ?? 1);
});
child.on("error", (error) => {
if (child.pid !== void 0) return;
clearSignalTimers();
const reporting = onError(error);
const exit = () => runtime.exit(1);
if (reporting) reporting.then(exit, exit);
else exit();
});
return child;
}
//#endregion
//#region src/entry.respawn.ts
const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning";
const OPENCLAW_NODE_OPTIONS_READY = "OPENCLAW_NODE_OPTIONS_READY";
const OPENCLAW_NODE_EXTRA_CA_CERTS_READY = "OPENCLAW_NODE_EXTRA_CA_CERTS_READY";
const WINDOWS_STACK_SIZE_FLAG = "--stack-size=8192";
function pathModuleForPlatform(platform) {
return platform === "win32" ? path.win32 : path.posix;
}
function resolveCliRespawnCommand(params) {
const basename = pathModuleForPlatform(params.platform ?? process.platform).basename(params.execPath).toLowerCase();
if (basename === "volta-shim" || basename === "volta-shim.exe") return "node";
return params.execPath;
}
function hasExperimentalWarningSuppressed(params = {}) {
const env = params.env ?? process.env;
const execArgv = params.execArgv ?? process.execArgv;
const nodeOptions = env.NODE_OPTIONS ?? "";
if (nodeOptions.includes(EXPERIMENTAL_WARNING_FLAG) || nodeOptions.includes("--no-warnings")) return true;
return execArgv.some((arg) => arg === EXPERIMENTAL_WARNING_FLAG || arg === "--no-warnings");
}
function hasStackSizeConfigured(execArgv) {
return execArgv.some((arg) => arg === "--stack-size" || arg.startsWith("--stack-size=") || arg === "--stack_size" || arg.startsWith("--stack_size="));
}
function buildCliRespawnPlan(params = {}) {
const argv = params.argv ?? process.argv;
const env = params.env ?? process.env;
const execArgv = params.execArgv ?? process.execArgv;
const execPath = params.execPath ?? process.execPath;
const platform = params.platform ?? process.platform;
const normalizedArgv = platform === "win32" ? normalizeWindowsArgv(argv, {
platform,
execPath
}) : argv;
if (shouldSkipStartupEnvironmentRespawnForArgv(normalizedArgv, platform) || isTruthyEnvValue(env.OPENCLAW_NO_RESPAWN)) return null;
const childEnv = { ...env };
if (!readNonBlankString(childEnv.NODE_EXTRA_CA_CERTS)) delete childEnv.NODE_EXTRA_CA_CERTS;
const childExecArgv = [...execArgv];
let needsRespawn = false;
if (platform === "win32") {
if (!hasStackSizeConfigured(childExecArgv)) {
childExecArgv.unshift(WINDOWS_STACK_SIZE_FLAG);
needsRespawn = true;
}
if (!needsRespawn) return null;
return {
command: resolveCliRespawnCommand({
execPath,
platform
}),
argv: [...childExecArgv, ...normalizedArgv.slice(1)],
env: childEnv,
detachForProcessTree: false
};
}
const autoNodeExtraCaCerts = params.autoNodeExtraCaCerts ?? resolveNodeStartupTlsEnvironment({
env,
execPath,
includeDarwinDefaults: false
}).NODE_EXTRA_CA_CERTS;
if (autoNodeExtraCaCerts && !isTruthyEnvValue(env[OPENCLAW_NODE_EXTRA_CA_CERTS_READY]) && !childEnv.NODE_EXTRA_CA_CERTS) {
childEnv.NODE_EXTRA_CA_CERTS = autoNodeExtraCaCerts;
childEnv[OPENCLAW_NODE_EXTRA_CA_CERTS_READY] = "1";
needsRespawn = true;
}
if (!shouldSkipRespawnForArgv(argv, platform) && !isTruthyEnvValue(env[OPENCLAW_NODE_OPTIONS_READY]) && !hasExperimentalWarningSuppressed({
env,
execArgv
})) {
childEnv[OPENCLAW_NODE_OPTIONS_READY] = "1";
childExecArgv.unshift(EXPERIMENTAL_WARNING_FLAG);
needsRespawn = true;
}
if (!needsRespawn) return null;
return {
command: resolveCliRespawnCommand({
execPath,
platform
}),
argv: [...childExecArgv, ...argv.slice(1)],
env: childEnv,
detachForProcessTree: !isTerminalInteractiveRespawnArgv(argv)
};
}
function runCliRespawnPlan(plan, runtime, writeError = (message, error) => console.error(message, error)) {
const resolvedRuntime = runtime ?? {
spawn,
attachChildProcessBridge,
exit: process.exit.bind(process),
writeError
};
return runRespawnChildWithSignalBridge({
command: plan.command,
args: plan.argv,
env: plan.env,
detachForProcessTree: plan.detachForProcessTree,
runtime: resolvedRuntime,
onError: (error) => {
return resolvedRuntime.writeError("[openclaw] Failed to respawn CLI:", error instanceof Error ? error.stack ?? error.message : error);
}
});
}
//#endregion
export { isNativeHookRelayArgv as a, isForegroundGmailRunArgv as i, runCliRespawnPlan as n, isTerminalInteractiveRespawnArgv as o, runRespawnChildWithSignalBridge as r, shouldKeepNativeHookRelayInProcess as s, buildCliRespawnPlan as t };