[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
160
electron/bridges/externalMcp/boundedCliCommand.cjs
Normal file
160
electron/bridges/externalMcp/boundedCliCommand.cjs
Normal file
@@ -0,0 +1,160 @@
|
||||
"use strict";
|
||||
|
||||
const { StringDecoder } = require("node:string_decoder");
|
||||
|
||||
const DEFAULT_EXTERNAL_MCP_CLI_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_EXTERNAL_MCP_CLI_MAX_OUTPUT_BYTES = 1024 * 1024;
|
||||
const DEFAULT_EXTERNAL_MCP_CLI_KILL_GRACE_MS = 750;
|
||||
|
||||
function runBoundedCliCommand(deps, command, args = [], options = {}) {
|
||||
const timeoutMs = Math.max(
|
||||
1,
|
||||
Number(options.timeoutMs) || DEFAULT_EXTERNAL_MCP_CLI_TIMEOUT_MS,
|
||||
);
|
||||
const maxOutputBytes = Math.max(
|
||||
1,
|
||||
Number(options.maxOutputBytes) || DEFAULT_EXTERNAL_MCP_CLI_MAX_OUTPUT_BYTES,
|
||||
);
|
||||
const killGraceMs = Math.max(
|
||||
1,
|
||||
Number(options.killGraceMs) || DEFAULT_EXTERNAL_MCP_CLI_KILL_GRACE_MS,
|
||||
);
|
||||
const signal = options.signal || null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let child;
|
||||
let settled = false;
|
||||
let closed = false;
|
||||
let timeoutTimer = null;
|
||||
let forceKillTimer = null;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let outputBytes = 0;
|
||||
const stdoutDecoder = new StringDecoder("utf8");
|
||||
const stderrDecoder = new StringDecoder("utf8");
|
||||
let decodersEnded = false;
|
||||
|
||||
const clearTimeoutTimer = () => {
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
};
|
||||
const clearForceKillTimer = () => {
|
||||
if (forceKillTimer) clearTimeout(forceKillTimer);
|
||||
forceKillTimer = null;
|
||||
};
|
||||
const removeDataListeners = () => {
|
||||
child?.stdout?.removeListener?.("data", onStdout);
|
||||
child?.stderr?.removeListener?.("data", onStderr);
|
||||
};
|
||||
const detachAbort = () => signal?.removeEventListener?.("abort", onAbort);
|
||||
const armForcedKill = () => {
|
||||
if (!child || closed || forceKillTimer) return;
|
||||
try { child.kill?.("SIGTERM"); } catch { /* ignore */ }
|
||||
forceKillTimer = setTimeout(() => {
|
||||
if (closed) return;
|
||||
try { child.kill?.("SIGKILL"); } catch { /* ignore */ }
|
||||
}, killGraceMs);
|
||||
forceKillTimer.unref?.();
|
||||
};
|
||||
const rejectAndTerminate = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeoutTimer();
|
||||
detachAbort();
|
||||
removeDataListeners();
|
||||
armForcedKill();
|
||||
reject(error);
|
||||
};
|
||||
const append = (target, chunk) => {
|
||||
if (settled) return;
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
||||
const remaining = Math.max(0, maxOutputBytes - outputBytes);
|
||||
if (remaining > 0) {
|
||||
const accepted = buffer.length <= remaining ? buffer : buffer.subarray(0, remaining);
|
||||
if (target === "stdout") stdout += stdoutDecoder.write(accepted);
|
||||
else stderr += stderrDecoder.write(accepted);
|
||||
outputBytes += accepted.length;
|
||||
}
|
||||
if (buffer.length > remaining) {
|
||||
const error = new Error(`CLI output exceeded ${maxOutputBytes} bytes`);
|
||||
error.code = "CLI_OUTPUT_LIMIT";
|
||||
rejectAndTerminate(error);
|
||||
}
|
||||
};
|
||||
const onStdout = (chunk) => append("stdout", chunk);
|
||||
const onStderr = (chunk) => append("stderr", chunk);
|
||||
const onAbort = () => {
|
||||
const reason = signal?.reason;
|
||||
const error = reason instanceof Error ? reason : new Error("CLI command was cancelled");
|
||||
if (!error.code) error.code = "ABORT_ERR";
|
||||
rejectAndTerminate(error);
|
||||
};
|
||||
const onError = (error) => {
|
||||
closed = true;
|
||||
clearTimeoutTimer();
|
||||
clearForceKillTimer();
|
||||
detachAbort();
|
||||
removeDataListeners();
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(error);
|
||||
};
|
||||
const onClose = (exitCode) => {
|
||||
closed = true;
|
||||
clearTimeoutTimer();
|
||||
clearForceKillTimer();
|
||||
detachAbort();
|
||||
removeDataListeners();
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (!decodersEnded) {
|
||||
decodersEnded = true;
|
||||
stdout += stdoutDecoder.end();
|
||||
stderr += stderrDecoder.end();
|
||||
}
|
||||
resolve({
|
||||
exitCode,
|
||||
stdout: deps.stripAnsi(stdout),
|
||||
stderr: deps.stripAnsi(stderr),
|
||||
});
|
||||
};
|
||||
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const spawnSpec = deps.prepareCommandForSpawn(command, args);
|
||||
child = deps.spawn(spawnSpec.command, spawnSpec.args || [], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
cwd: options.cwd || undefined,
|
||||
env: options.env || process.env,
|
||||
shell: spawnSpec.shell,
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch (error) {
|
||||
settled = true;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
child.stdout?.on?.("data", onStdout);
|
||||
child.stderr?.on?.("data", onStderr);
|
||||
child.once?.("error", onError);
|
||||
child.once?.("close", onClose);
|
||||
signal?.addEventListener?.("abort", onAbort, { once: true });
|
||||
timeoutTimer = setTimeout(() => {
|
||||
const error = new Error(`CLI command timed out after ${timeoutMs} ms`);
|
||||
error.code = "CLI_TIMEOUT";
|
||||
rejectAndTerminate(error);
|
||||
}, timeoutMs);
|
||||
if (signal?.aborted) onAbort();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_EXTERNAL_MCP_CLI_TIMEOUT_MS,
|
||||
DEFAULT_EXTERNAL_MCP_CLI_MAX_OUTPUT_BYTES,
|
||||
DEFAULT_EXTERNAL_MCP_CLI_KILL_GRACE_MS,
|
||||
runBoundedCliCommand,
|
||||
};
|
||||
Reference in New Issue
Block a user