Files

235 lines
8.1 KiB
JavaScript
Raw Permalink Normal View History

/* eslint-disable no-undef */
const { isSshConnAlive, isTransportExecError } = require("./execConnHealth.cjs");
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
function createExecOnSessionApi(ctx) {
with (ctx) {
const DEFAULT_LOCAL_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
function normalizeExecMaxBuffer(value, fallback = DEFAULT_LOCAL_EXEC_MAX_BUFFER) {
const numeric = Number(value);
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : fallback;
}
function isExecMaxBufferError(err) {
const code = String(err?.code || "");
const message = String(err?.message || "");
return code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || /maxBuffer/i.test(message);
}
/** Serialize remote exec per session to avoid SSH channel storms. */
const execQueues = new Map();
function getSession(sessionId) {
return sessions?.get?.(sessionId) ?? null;
}
function enqueueExec(sessionId, task) {
let state = execQueues.get(sessionId);
if (!state) {
state = { running: false, pending: [] };
execQueues.set(sessionId, state);
}
return new Promise((resolve) => {
state.pending.push({ task, resolve });
void drainExecQueue(sessionId);
});
}
async function drainExecQueue(sessionId) {
const state = execQueues.get(sessionId);
if (!state || state.running) return;
state.running = true;
while (state.pending.length > 0) {
const job = state.pending.shift();
if (!job) continue;
try {
const result = await job.task();
job.resolve(result);
} catch (err) {
job.resolve({ success: false, error: err?.message || String(err) });
}
}
state.running = false;
if (state.pending.length === 0) {
execQueues.delete(sessionId);
}
}
async function ensureMoshCompanion(session, sessionId, event) {
if (session?.type !== "mosh" || typeof ensureMoshStatsConnection !== "function") {
return;
}
if (session.moshStatsConn && isSshConnAlive(session.moshStatsConn)) {
return;
}
if (session.moshStatsConn && !isSshConnAlive(session.moshStatsConn)) {
session.moshStatsConn = null;
}
if (!session.moshStatsConn && !session.moshStatsConnFailed) {
await ensureMoshStatsConnection(session, sessionId, event?.sender);
}
}
async function resolveExecConnection(session, sessionId, event) {
if (!session) return null;
await ensureMoshCompanion(session, sessionId, event);
const conn = session.conn || session.moshStatsConn;
if (!conn) return null;
if (!isSshConnAlive(conn)) {
if (session.moshStatsConn === conn) {
session.moshStatsConn = null;
await ensureMoshCompanion(session, sessionId, event);
return session.conn || session.moshStatsConn;
}
return null;
}
return conn;
}
function execOnConnection(conn, command, timeoutMs, execOptions = {}) {
const maxBuffer = normalizeExecMaxBuffer(execOptions.maxBuffer);
return executeBoundedSshCommand(conn, command, {
openingTimeoutMs: timeoutMs,
runTimeoutMs: timeoutMs,
maxOutputBytes: maxBuffer,
onStream(stream) {
if (typeof execOptions.stdin === "string") {
stream.write(execOptions.stdin);
stream.end();
}
},
}).then(
({ stdout, stderr, code }) => ({
success: true,
stdout,
stderr,
code: code ?? 0,
}),
(error) => ({
success: false,
error: error?.code === "SSH_EXEC_OUTPUT_LIMIT"
? "SSH command maxBuffer exceeded"
: error?.message || String(error),
stdout: "",
stderr: "",
code: 1,
}),
);
}
async function execOnSshSession(session, sessionId, command, timeoutMs, event, execOptions = {}, allowCompanionRetry = true) {
if (session?.type === "et") {
if (typeof execOnEtSession !== "function") {
return { success: false, error: "ET command executor unavailable" };
}
return execOnEtSession(session, command, timeoutMs, {
requireTrustedHost: true,
knownHosts: session.etStatsAuth?.knownHosts,
stdin: execOptions.stdin,
maxBuffer: execOptions.maxBuffer,
});
}
const conn = await resolveExecConnection(session, sessionId, event);
if (!conn) {
if (session?.type === "mosh" && !session.moshStatsAuth && !session.moshStatsConnFailed) {
return { success: false, pending: true, error: "Mosh handshake in progress" };
}
return { success: false, error: "Session not found or not connected" };
}
const result = await execOnConnection(conn, command, timeoutMs, execOptions);
if (
allowCompanionRetry
&& !result.success
&& session.moshStatsConn
&& isTransportExecError(result.error)
) {
session.moshStatsConn = null;
return execOnSshSession(session, sessionId, command, timeoutMs, event, execOptions, false);
}
return result;
}
async function execOnLocalMachine(command, timeoutMs, execOptions = {}) {
const { execFile } = require("node:child_process");
const platform = process.platform;
if (platform === "win32") {
return new Promise((resolve) => {
const child = execFile(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-Command", command],
{ timeout: timeoutMs, maxBuffer: normalizeExecMaxBuffer(execOptions.maxBuffer) },
(err, stdout, stderr) => {
if (err && (isExecMaxBufferError(err) || !stdout)) {
resolve({ success: false, error: err.message || String(err), stdout: "", stderr: String(stderr || "") });
return;
}
resolve({ success: true, stdout: String(stdout || ""), stderr: String(stderr || ""), code: err?.code ?? 0 });
},
);
if (typeof execOptions.stdin === "string") {
child.stdin?.end(execOptions.stdin);
}
});
}
return new Promise((resolve) => {
const child = execFile(
"sh",
["-c", command],
{ timeout: timeoutMs, maxBuffer: normalizeExecMaxBuffer(execOptions.maxBuffer) },
(err, stdout, stderr) => {
if (err && (isExecMaxBufferError(err) || !stdout)) {
resolve({ success: false, error: err.message || String(err), stdout: "", stderr: String(stderr || "") });
return;
}
resolve({ success: true, stdout: String(stdout || ""), stderr: String(stderr || ""), code: err?.code ?? 0 });
},
);
if (typeof execOptions.stdin === "string") {
child.stdin?.end(execOptions.stdin);
}
});
}
async function execOnSessionInner(event, sessionId, command, timeoutMs = 8000, execOptions = {}) {
const session = getSession(sessionId);
if (!session) {
execQueues.delete(sessionId);
return { success: false, error: "Session not found" };
}
if (session.protocol === "local" || session.type === "local") {
return execOnLocalMachine(command, timeoutMs, execOptions);
}
if (session.conn || session.type === "mosh" || session.type === "et") {
return execOnSshSession(session, sessionId, command, timeoutMs, event, execOptions);
}
return { success: false, error: "Session not supported for system management" };
}
async function execOnSession(event, sessionId, command, timeoutMs = 8000, execOptions = {}) {
return enqueueExec(sessionId, () => execOnSessionInner(event, sessionId, command, timeoutMs, execOptions));
}
function isLocalSession(sessionId) {
const session = getSession(sessionId);
return !!(session?.protocol === "local" || session?.type === "local");
}
return { execOnSession, execOnLocalMachine, isLocalSession, getSession };
}
}
module.exports = { createExecOnSessionApi };