Files
NetMesh/electron/bridges/systemManager/execOnSession.cjs
zhaolei c64e00d835 feat: 新增设备巡检模块实现完整的巡检告警功能
新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
2026-10-09 10:37:48 +08:00

249 lines
9.0 KiB
JavaScript
Raw 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 };