UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

473 lines (472 loc) 16.6 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { r as markOpenClawExecEnv } from "./openclaw-exec-env-48iH8Lwg.js"; import { t as getWindowsInstallRoots } from "./windows-install-roots-jk5k_ejK.js"; import { a as shouldLogVerbose, t as danger } from "./globals-GTrXU4s9.js"; import { a as decodeWindowsOutputBuffer, n as resolveCommandStdio, o as resolveWindowsConsoleEncoding, t as resolveWindowsCommandShim } from "./windows-command-CggaAJAY.js"; import { n as logError, t as logDebug } from "./logger-lqqYRtFw.js"; import { t as killProcessTree } from "./kill-tree-kSm0C74g.js"; import process from "node:process"; import fs from "node:fs"; import path from "node:path"; import { execFile, spawn } from "node:child_process"; import { promisify } from "node:util"; //#region src/process/exec.ts const execFileAsync = promisify(execFile); const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>^%\r\n]/; function isWindowsBatchCommand(resolvedCommand) { if (process.platform !== "win32") return false; const ext = normalizeLowercaseStringOrEmpty(path.extname(resolvedCommand)); return ext === ".cmd" || ext === ".bat"; } function escapeForCmdExe(arg) { if (WINDOWS_UNSAFE_CMD_CHARS_RE.test(arg)) throw new Error(`Unsafe Windows cmd.exe argument detected: ${JSON.stringify(arg)}. Pass an explicit shell-wrapper argv at the call site instead.`); if (!arg.includes(" ") && !arg.includes("\"")) return arg; return `"${arg.replace(/"/g, "\"\"")}"`; } function buildCmdExeCommandLine(resolvedCommand, args) { return [escapeForCmdExe(resolvedCommand), ...args.map(escapeForCmdExe)].join(" "); } function resolveTrustedWindowsCmdExe() { if (process.platform !== "win32") return "cmd.exe"; return path.win32.join(getWindowsInstallRoots().systemRoot, "System32", "cmd.exe"); } function assignChildEnvValue(params) { if (params.value === void 0) return; if (params.platform === "win32") { const normalizedKey = params.key.toLowerCase(); for (const existingKey of Object.keys(params.env)) if (existingKey.toLowerCase() === normalizedKey && existingKey !== params.key) delete params.env[existingKey]; } params.env[params.key] = params.value; } function mergeChildEnv(params) { const resolvedEnv = {}; for (const [key, value] of Object.entries(params.baseEnv)) assignChildEnvValue({ env: resolvedEnv, key, platform: params.platform, value }); for (const [key, value] of Object.entries(params.env ?? {})) assignChildEnvValue({ env: resolvedEnv, key, platform: params.platform, value }); return resolvedEnv; } /** * On Windows, Node 18.20.2+ (CVE-2024-27980) rejects spawning .cmd/.bat directly * without shell, causing EINVAL. Resolve npm/npx to node + cli script so we * spawn node.exe instead of npm.cmd. */ function resolveNpmArgvForWindows(argv) { if (process.platform !== "win32" || argv.length === 0) return null; const basename = normalizeLowercaseStringOrEmpty(path.basename(argv[0])).replace(/\.(cmd|exe|bat)$/, ""); const cliName = basename === "npx" ? "npx-cli.js" : basename === "npm" ? "npm-cli.js" : null; if (!cliName) return null; const nodeDir = path.dirname(process.execPath); const cliPath = path.join(nodeDir, "node_modules", "npm", "bin", cliName); if (!fs.existsSync(cliPath)) { const command = argv[0] ?? ""; return [normalizeLowercaseStringOrEmpty(path.extname(command)) ? command : `${command}.cmd`, ...argv.slice(1)]; } return [ process.execPath, cliPath, ...argv.slice(1) ]; } /** * Resolves a command for Windows compatibility. * On Windows, non-.exe commands (like pnpm, yarn) are resolved to .cmd; npm/npx * are handled by resolveNpmArgvForWindows to avoid spawn EINVAL (no direct .cmd). */ function resolveCommand(command) { return resolveWindowsCommandShim({ command, cmdCommands: [ "corepack", "pnpm", "yarn" ] }); } function resolveChildProcessInvocation(params) { const finalArgv = process.platform === "win32" ? resolveNpmArgvForWindows(params.argv) ?? params.argv : params.argv; const resolvedCommand = finalArgv !== params.argv ? finalArgv[0] ?? "" : resolveCommand(params.argv[0] ?? ""); const useCmdWrapper = isWindowsBatchCommand(resolvedCommand); return { command: useCmdWrapper ? resolveTrustedWindowsCmdExe() : resolvedCommand, args: useCmdWrapper ? [ "/d", "/s", "/c", buildCmdExeCommandLine(resolvedCommand, finalArgv.slice(1)) ] : finalArgv.slice(1), usesWindowsExitCodeShim: process.platform === "win32" && (useCmdWrapper || finalArgv !== params.argv), windowsHide: true, windowsVerbatimArguments: useCmdWrapper ? true : params.windowsVerbatimArguments }; } function shouldSpawnWithShell(params) { return false; } async function runExec(command, args, opts = 1e4) { const options = typeof opts === "number" ? { timeout: opts, encoding: "buffer" } : { timeout: opts.timeoutMs, maxBuffer: opts.maxBuffer, cwd: opts.cwd, encoding: "buffer" }; try { const invocation = resolveChildProcessInvocation({ argv: [command, ...args] }); const { stdout, stderr } = await execFileAsync(invocation.command, invocation.args, { ...options, windowsHide: invocation.windowsHide, windowsVerbatimArguments: invocation.windowsVerbatimArguments }); const windowsEncoding = resolveWindowsConsoleEncoding(); const decodedStdout = decodeWindowsOutputBuffer({ buffer: stdout, windowsEncoding }); const decodedStderr = decodeWindowsOutputBuffer({ buffer: stderr, windowsEncoding }); if (shouldLogVerbose()) { if (decodedStdout.trim()) logDebug(decodedStdout.trim()); if (decodedStderr.trim()) logError(decodedStderr.trim()); } return { stdout: decodedStdout, stderr: decodedStderr }; } catch (err) { const windowsEncoding = resolveWindowsConsoleEncoding(); if (err && typeof err === "object") { const errorWithOutput = err; if (Buffer.isBuffer(errorWithOutput.stdout)) errorWithOutput.stdout = decodeWindowsOutputBuffer({ buffer: errorWithOutput.stdout, windowsEncoding }); if (Buffer.isBuffer(errorWithOutput.stderr)) errorWithOutput.stderr = decodeWindowsOutputBuffer({ buffer: errorWithOutput.stderr, windowsEncoding }); } if (shouldLogVerbose()) logError(danger(`Command failed: ${command} ${args.join(" ")}`)); throw err; } } const WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS = 250; const WINDOWS_CLOSE_STATE_POLL_MS = 10; const COMMAND_PROCESS_TREE_KILL_GRACE_MS = 300; const TIMEOUT_EXIT_CODE = 124; const DEFAULT_COMMAND_OUTPUT_MAX_BYTES = 16 * 1024 * 1024; function normalizeMaxOutputBytes(value) { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return DEFAULT_COMMAND_OUTPUT_MAX_BYTES; return Math.max(1, Math.floor(value)); } function appendCapturedOutput(capture, chunk, maxBytes) { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); if (buffer.byteLength >= maxBytes) { capture.chunks = [Buffer.from(buffer.subarray(buffer.byteLength - maxBytes))]; capture.truncatedBytes += capture.bytes + buffer.byteLength - maxBytes; capture.bytes = maxBytes; return; } capture.chunks.push(buffer); capture.bytes += buffer.byteLength; while (capture.bytes > maxBytes && capture.chunks.length > 0) { const first = capture.chunks[0]; const overflow = capture.bytes - maxBytes; if (first.byteLength <= overflow) { capture.chunks.shift(); capture.bytes -= first.byteLength; capture.truncatedBytes += first.byteLength; } else { capture.chunks[0] = Buffer.from(first.subarray(overflow)); capture.bytes -= overflow; capture.truncatedBytes += overflow; } } } function resolveProcessExitCode(params) { return params.explicitCode ?? params.childExitCode ?? (params.usesWindowsExitCodeShim && params.resolvedSignal == null && !params.timedOut && !params.noOutputTimedOut && !params.killIssuedByTimeout && !params.killIssuedByAbort ? 0 : null); } function resolveCommandEnv(params) { const baseEnv = params.baseEnv ?? process.env; const platform = params.platform ?? process.platform; const argv = params.argv; const shouldSuppressNpmFund = (() => { const cmd = path.basename(argv[0] ?? ""); if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") return true; if (cmd === "node" || cmd === "node.exe") return (argv[1] ?? "").includes("npm-cli.js"); return false; })(); const resolvedEnv = mergeChildEnv({ baseEnv, env: params.env, platform }); if (shouldSuppressNpmFund) { if (resolvedEnv.NPM_CONFIG_FUND == null) resolvedEnv.NPM_CONFIG_FUND = "false"; if (resolvedEnv.npm_config_fund == null) resolvedEnv.npm_config_fund = "false"; } return markOpenClawExecEnv(resolvedEnv); } async function runCommandWithTimeout(argv, optionsOrTimeout) { const options = typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; const { timeoutMs, cwd, input, baseEnv, env, noOutputTimeoutMs, signal, killProcessTree: killProcessTree$1 } = options; const hasInput = input !== void 0; const resolvedEnv = resolveCommandEnv({ argv, baseEnv, env }); const stdio = resolveCommandStdio({ hasInput, preferInherit: true }); const invocation = resolveChildProcessInvocation({ argv, windowsVerbatimArguments: options.windowsVerbatimArguments }); if (signal?.aborted) return { stdout: "", stderr: "", code: null, signal: null, killed: false, termination: "signal", noOutputTimedOut: false }; const child = spawn(invocation.command, invocation.args, { stdio, cwd, env: resolvedEnv, ...killProcessTree$1 && process.platform !== "win32" ? { detached: true } : {}, windowsHide: invocation.windowsHide, windowsVerbatimArguments: invocation.windowsVerbatimArguments, ...shouldSpawnWithShell({ resolvedCommand: invocation.command, platform: process.platform }) ? { shell: true } : {} }); return await new Promise((resolve, reject) => { const stdoutCapture = { chunks: [], bytes: 0, truncatedBytes: 0 }; const stderrCapture = { chunks: [], bytes: 0, truncatedBytes: 0 }; const maxOutputBytes = normalizeMaxOutputBytes(options.maxOutputBytes); const windowsEncoding = resolveWindowsConsoleEncoding(); let settled = false; let timedOut = false; let noOutputTimedOut = false; let killIssuedByTimeout = false; let killIssuedByAbort = false; let childExitState = null; let closeFallbackTimer = null; let processTreeForceKillTimer = null; let noOutputTimer = null; const shouldTrackOutputTimeout = typeof noOutputTimeoutMs === "number" && Number.isFinite(noOutputTimeoutMs) && noOutputTimeoutMs > 0; let removeAbortListener = null; const clearNoOutputTimer = () => { if (!noOutputTimer) return; clearTimeout(noOutputTimer); noOutputTimer = null; }; const clearCloseFallbackTimer = () => { if (!closeFallbackTimer) return; clearTimeout(closeFallbackTimer); closeFallbackTimer = null; }; const clearProcessTreeForceKillTimer = () => { if (!processTreeForceKillTimer) return; clearTimeout(processTreeForceKillTimer); processTreeForceKillTimer = null; }; const killChild = (byTimeout = true) => { if (settled || typeof child?.kill !== "function") return; if (byTimeout) killIssuedByTimeout = true; else killIssuedByAbort = true; if (killProcessTree$1 && typeof child.pid === "number" && child.pid > 0) { if (process.platform === "win32") try { spawn("taskkill", [ "/PID", String(child.pid), "/T" ], { stdio: "ignore", windowsHide: true }); if (!processTreeForceKillTimer) { processTreeForceKillTimer = setTimeout(() => { processTreeForceKillTimer = null; if (settled || childExitState != null || child.exitCode != null || child.signalCode != null) return; try { spawn("taskkill", [ "/PID", String(child.pid), "/T", "/F" ], { stdio: "ignore", windowsHide: true }); } catch { child.kill("SIGKILL"); } }, COMMAND_PROCESS_TREE_KILL_GRACE_MS); processTreeForceKillTimer.unref(); } return; } catch {} killProcessTree(child.pid, { graceMs: COMMAND_PROCESS_TREE_KILL_GRACE_MS }); return; } if (process.platform === "win32" && typeof child.pid === "number" && child.pid > 0) try { spawn("taskkill", [ "/PID", String(child.pid), "/T", "/F" ], { stdio: "ignore", windowsHide: true }); return; } catch {} child.kill("SIGKILL"); }; const armNoOutputTimer = () => { if (!shouldTrackOutputTimeout || settled) return; clearNoOutputTimer(); noOutputTimer = setTimeout(() => { if (settled) return; noOutputTimedOut = true; killChild(); }, Math.floor(noOutputTimeoutMs)); }; const timer = setTimeout(() => { timedOut = true; killChild(); }, timeoutMs); armNoOutputTimer(); if (signal) { const onAbort = () => killChild(false); signal.addEventListener("abort", onAbort, { once: true }); removeAbortListener = () => signal.removeEventListener("abort", onAbort); } if (hasInput && child.stdin) { child.stdin.on("error", () => {}); child.stdin.write(input ?? ""); child.stdin.end(); } child.stdout?.on("data", (d) => { appendCapturedOutput(stdoutCapture, d, maxOutputBytes); armNoOutputTimer(); }); child.stderr?.on("data", (d) => { appendCapturedOutput(stderrCapture, d, maxOutputBytes); armNoOutputTimer(); }); child.on("error", (err) => { if (settled) return; settled = true; clearTimeout(timer); clearNoOutputTimer(); clearCloseFallbackTimer(); clearProcessTreeForceKillTimer(); removeAbortListener?.(); removeAbortListener = null; reject(err); }); child.on("exit", (code, signalResult) => { childExitState = { code, signal: signalResult }; clearProcessTreeForceKillTimer(); if (settled || closeFallbackTimer) return; closeFallbackTimer = setTimeout(() => { if (settled) return; child.stdout?.destroy(); child.stderr?.destroy(); }, 250); }); const resolveFromClose = (code, signalValue) => { if (settled) return; settled = true; clearTimeout(timer); clearNoOutputTimer(); clearCloseFallbackTimer(); clearProcessTreeForceKillTimer(); removeAbortListener?.(); removeAbortListener = null; const resolvedSignal = childExitState?.signal ?? signalValue ?? child.signalCode ?? null; const resolvedCode = resolveProcessExitCode({ explicitCode: childExitState?.code ?? code, childExitCode: child.exitCode, resolvedSignal, usesWindowsExitCodeShim: invocation.usesWindowsExitCodeShim, timedOut, noOutputTimedOut, killIssuedByTimeout, killIssuedByAbort }); const termination = noOutputTimedOut ? "no-output-timeout" : timedOut ? "timeout" : resolvedSignal != null || killIssuedByAbort ? "signal" : "exit"; const normalizedCode = termination === "timeout" || termination === "no-output-timeout" ? resolvedCode == null || resolvedCode === 0 ? TIMEOUT_EXIT_CODE : resolvedCode : resolvedCode; resolve({ pid: child.pid ?? void 0, stdout: decodeWindowsOutputBuffer({ buffer: Buffer.concat(stdoutCapture.chunks, stdoutCapture.bytes), windowsEncoding }), stderr: decodeWindowsOutputBuffer({ buffer: Buffer.concat(stderrCapture.chunks, stderrCapture.bytes), windowsEncoding }), stdoutTruncatedBytes: stdoutCapture.truncatedBytes || void 0, stderrTruncatedBytes: stderrCapture.truncatedBytes || void 0, code: normalizedCode, signal: resolvedSignal, killed: child.killed, termination, noOutputTimedOut }); }; child.on("close", (code, signalLocal) => { if (process.platform !== "win32" || childExitState != null || code != null || signalLocal != null || child.exitCode != null || child.signalCode != null) { resolveFromClose(code, signalLocal); return; } const startedAt = Date.now(); const waitForExitState = () => { if (settled) return; if (childExitState != null || child.exitCode != null || child.signalCode != null) { resolveFromClose(code, signalLocal); return; } if (Date.now() - startedAt >= WINDOWS_CLOSE_STATE_SETTLE_TIMEOUT_MS) { resolveFromClose(code, signalLocal); return; } setTimeout(waitForExitState, WINDOWS_CLOSE_STATE_POLL_MS); }; waitForExitState(); }); }); } //#endregion export { shouldSpawnWithShell as a, runExec as i, resolveProcessExitCode as n, runCommandWithTimeout as r, resolveCommandEnv as t };