Files
SenMeshNet/electron/bridges/systemManager/execOnSession.cjs
zhaolei 2f9f8da48e
Some checks failed
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
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

249 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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,
});
}
// [NETWORK-DEVICE GATE] 网络设备(华为 VRP、H3C Comware 等)SSH server 通常
// 不支持在同一连接上开多个 channel——exec channel 会触发整个连接被断开。
// 从 SSH banner (remoteSshVersion) 检测,如果是网络设备就直接拒绝。
const remoteVer = String(session?.remoteSshVersion || '').toLowerCase();
const isNetworkDeviceSsh = /^H3C[-_\s]/i.test(session?.remoteSshVersion || '')
|| /^Comware-/i.test(session?.remoteSshVersion || '')
|| /^HUAWEI[-_]/i.test(session?.remoteSshVersion || '')
|| /^VRP-/i.test(session?.remoteSshVersion || '')
|| (session?.remoteSshVersion === '-')
|| /^Cisco[-_]|^CiscoIOS_/i.test(session?.remoteSshVersion || '');
if (isNetworkDeviceSsh) {
return { success: false, error: "SSH exec channel not supported on network device (Huawei/H3C/VRP)" };
}
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 };