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
507 lines
20 KiB
JavaScript
507 lines
20 KiB
JavaScript
"use strict";
|
||
|
||
/**
|
||
* 巡检执行器 - 用 ssh2 临时建立 SSH 连接执行命令
|
||
*
|
||
* 用于设备巡检的命令执行:每次巡检临时建立连接,执行完即释放,
|
||
* 不占用 terminalWorker 的连接池(巡检是低频后台任务)。
|
||
*/
|
||
|
||
const { Client: SSHClient } = require("ssh2");
|
||
const { buildAlgorithms } = require("../sshAlgorithms.cjs");
|
||
|
||
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
||
const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
|
||
const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024; // 100KB
|
||
|
||
/**
|
||
* 执行单条 SSH 命令(使用 pty shell 方式,兼容交换机等需要交互式 shell 的设备)
|
||
* @param {object} connConfig - ssh2 连接配置(host/port/username/password/privateKey 等)
|
||
* @param {string} command - 要执行的命令
|
||
* @param {object} [options]
|
||
* @param {number} [options.connectTimeoutMs]
|
||
* @param {number} [options.execTimeoutMs]
|
||
* @param {number} [options.maxOutputBytes]
|
||
* @returns {Promise<string>} 命令输出
|
||
*/
|
||
function execSshCommand(connConfig, command, options = {}) {
|
||
const connectTimeoutMs = options.connectTimeoutMs || DEFAULT_CONNECT_TIMEOUT_MS;
|
||
const execTimeoutMs = options.execTimeoutMs || DEFAULT_EXEC_TIMEOUT_MS;
|
||
const maxOutputBytes = options.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES;
|
||
|
||
const DEBUG = process.env.INSPECTION_DEBUG === "1";
|
||
if (DEBUG) {
|
||
console.log("[InspectExec] SSH host:", connConfig.host, "| cmd:", command.slice(0, 100));
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const client = new SSHClient();
|
||
let settled = false;
|
||
let stdout = "";
|
||
let execTimer = null;
|
||
let connectTimer = null;
|
||
let sendCommandTimer = null;
|
||
let outputDoneTimer = null;
|
||
let commandSent = false;
|
||
|
||
// 匹配行尾是否是 shell prompt(同时兼容交换机和 Linux)
|
||
function hasPromptTail(line) {
|
||
// 交换机: <hostname> 结尾 或 hostname> / hostname# 结尾
|
||
if (/<[\w\-./]+>\s*$/.test(line)) return true;
|
||
if (/^[\w\-./]+[>#]\s*$/.test(line)) return true;
|
||
// Linux bash/zsh: user@host...$ / user@host...# 结尾
|
||
if (/[\w.-]+@[\w.-]+.*[$#]\s*$/.test(line)) return true;
|
||
// Linux 路径 prompt: ~$ / /tmp# 等
|
||
if (/[:~\/]\s*[$#]\s*$/.test(line)) return true;
|
||
return false;
|
||
}
|
||
|
||
if (DEBUG) {
|
||
client.on("banner", (msg) => console.log("[InspectExec] banner:", msg.slice(0, 100)));
|
||
client.on("handshake", () => console.log("[InspectExec] handshake OK"));
|
||
client.on("error", (err) => console.error("[InspectExec] client error:", err.message));
|
||
client.on("close", () => console.log("[InspectExec] client closed"));
|
||
}
|
||
|
||
const cleanup = () => {
|
||
if (execTimer) clearTimeout(execTimer);
|
||
if (connectTimer) clearTimeout(connectTimer);
|
||
if (sendCommandTimer) clearTimeout(sendCommandTimer);
|
||
if (outputDoneTimer) clearTimeout(outputDoneTimer);
|
||
try { client.end(); } catch (e) { /* ignore */ }
|
||
};
|
||
|
||
const done = (err, result) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
cleanup();
|
||
if (err) reject(err);
|
||
else resolve(result);
|
||
};
|
||
|
||
// 连接超时
|
||
connectTimer = setTimeout(() => {
|
||
console.error("[InspectExec] 连接超时,host:", connConfig.host);
|
||
done(new Error("SSH 连接超时(" + connectTimeoutMs / 1000 + "s)"));
|
||
}, connectTimeoutMs);
|
||
|
||
client.on("ready", () => {
|
||
if (DEBUG) console.log("[InspectExec] SSH ready,打开 pty shell...");
|
||
clearTimeout(connectTimer);
|
||
|
||
// 使用 pty shell(交互式)执行命令
|
||
client.shell({ term: "vt100", cols: 80, rows: 24 }, (err, stream) => {
|
||
if (err) {
|
||
console.error("[InspectExec] shell 失败:", err.message);
|
||
done(new Error("打开 shell 失败: " + err.message));
|
||
return;
|
||
}
|
||
|
||
// 执行超时
|
||
execTimer = setTimeout(() => {
|
||
console.error("[InspectExec] 命令执行超时(" + execTimeoutMs / 1000 + "s)");
|
||
done(new Error("命令执行超时"));
|
||
}, execTimeoutMs);
|
||
|
||
let outputBuffer = "";
|
||
let sendCommandTimer = null;
|
||
let outputDoneTimer = null; // 命令发送后静默期兜底(输出完成检测)
|
||
let promptStableCount = 0; // shell 稳定化:需要看到 prompt 连续出现 2 次才发命令
|
||
|
||
// shell 稳定化:先发空回车让交换机 VTY/Linux bash 进入稳定态
|
||
// shell ready 静默期 500ms(比之前的 200ms 更宽松,给 banner 时间)
|
||
const scheduleSendCommand = () => {
|
||
if (commandSent || sendCommandTimer) return;
|
||
clearTimeout(sendCommandTimer);
|
||
sendCommandTimer = setTimeout(() => {
|
||
commandSent = true;
|
||
sendCommandTimer = null;
|
||
stream.write(command + "\n");
|
||
if (DEBUG) console.log("[InspectExec] shell 稳定,发送命令:", command);
|
||
}, 500); // 500ms 给 shell 稳定时间
|
||
};
|
||
|
||
// 命令发送后,2500ms 静默期 → 兜底认为输出完成(兼容 Linux bash $ 结尾 prompt)
|
||
// 注意:top/df 等命令输出可能持续 1-2s,不能太短
|
||
const scheduleOutputDone = () => {
|
||
if (outputDoneTimer) clearTimeout(outputDoneTimer);
|
||
outputDoneTimer = setTimeout(() => {
|
||
outputDoneTimer = null;
|
||
if (stdout) return; // 已完成
|
||
// 命令回显可能被 prompt 包含了 —— 做基本判断后返回
|
||
const buf = outputBuffer.replace(/__sent_q__/g, "");
|
||
if (buf.trim().length > 0) {
|
||
stdout = buf;
|
||
clearTimeout(execTimer);
|
||
if (DEBUG) console.log("[InspectExec] 输出静默期到,兜底完成,len:", stdout.length);
|
||
done(null, stdout);
|
||
} else {
|
||
// 什么都没收到,让 execTimer 继续等
|
||
if (DEBUG) console.log("[InspectExec] 输出静默期到但缓冲区为空,继续等待");
|
||
}
|
||
}, 2500);
|
||
};
|
||
|
||
stream.on("data", (data) => {
|
||
const chunk = data.toString("utf-8");
|
||
outputBuffer += chunk;
|
||
|
||
// 未发送命令时:等待 shell prompt 出现后再给稳定时间
|
||
if (!commandSent) {
|
||
const lastLine = outputBuffer.split("\n").pop()?.trimEnd() || "";
|
||
// 精确匹配交换机/路由器 prompt
|
||
const swPrompt = /^<[\w\-./]+>\s*$/.test(lastLine)
|
||
|| /^[\w\-./]+[>#]\s*$/.test(lastLine);
|
||
if (swPrompt) {
|
||
// 看到 prompt 了,但还要给 shell 稳定时间(可能需要 enable / system-view)
|
||
// 不立即发命令 → 走 scheduleSendCommand 的 500ms 静默期逻辑
|
||
scheduleSendCommand();
|
||
} else {
|
||
scheduleSendCommand();
|
||
}
|
||
}
|
||
|
||
// 命令发送后,收集输出
|
||
if (commandSent) {
|
||
// 重置输出静默期计时器(持续有数据就不触发兜底)
|
||
scheduleOutputDone();
|
||
|
||
// 检测分页提示,发送 'q' 退出
|
||
if (outputBuffer.includes("-- More --") || outputBuffer.includes("---- More ----")) {
|
||
if (!outputBuffer.includes("__sent_q__")) {
|
||
stream.write("q");
|
||
outputBuffer += "__sent_q__"; // 避免重复发送
|
||
if (DEBUG) console.log("[InspectExec] 检测到分页,发送 q");
|
||
}
|
||
}
|
||
|
||
// 检测输出完成:匹配末尾 prompt(同时兼容交换机和 Linux)
|
||
const lines = outputBuffer.split("\n");
|
||
// 至少收到了命令本身的回显 + 一些数据才检查
|
||
if (lines.length >= 2) {
|
||
const lastContentLine = lines[lines.length - 1]?.trimEnd() || "";
|
||
const prevContentLine = lines.length >= 2 ? lines[lines.length - 2]?.trimEnd() || "" : "";
|
||
|
||
// 匹配最后一行或倒数第二行的 prompt
|
||
const promptEnd = lastContentLine.length > 0 && hasPromptTail(lastContentLine);
|
||
const prevPromptEnd = prevContentLine.length > 0 && hasPromptTail(prevContentLine);
|
||
if (promptEnd || prevPromptEnd) {
|
||
stdout = outputBuffer.replace(/__sent_q__/g, "");
|
||
clearTimeout(execTimer);
|
||
clearTimeout(outputDoneTimer);
|
||
if (DEBUG) console.log("[InspectExec] 命令完成,行数:", lines.length);
|
||
done(null, stdout);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 防止缓冲区过大
|
||
if (outputBuffer.length > maxOutputBytes * 2) {
|
||
stdout = outputBuffer.replace(/__sent_q__/g, "");
|
||
clearTimeout(execTimer);
|
||
clearTimeout(outputDoneTimer);
|
||
if (DEBUG) console.log("[InspectExec] 缓冲区满,强制返回");
|
||
done(null, stdout);
|
||
}
|
||
});
|
||
|
||
stream.stderr.on("data", (data) => {
|
||
// stderr 通常为空
|
||
});
|
||
|
||
stream.on("close", () => {
|
||
clearTimeout(execTimer);
|
||
if (!commandSent) {
|
||
// 没等到提示符就关闭了,再试一次发送命令
|
||
commandSent = true;
|
||
stream.write(command + "\n");
|
||
} else if (stdout) {
|
||
// 已有输出,正常结束
|
||
done(null, stdout);
|
||
} else {
|
||
done(new Error("连接意外关闭"));
|
||
}
|
||
});
|
||
|
||
stream.on("error", (e) => {
|
||
console.error("[InspectExec] stream error:", e.message);
|
||
done(new Error("流错误: " + e.message));
|
||
});
|
||
});
|
||
});
|
||
|
||
client.on("error", (err) => {
|
||
clearTimeout(connectTimer);
|
||
console.error("[InspectExec] SSH 连接失败:", err.message);
|
||
done(new Error("SSH 连接失败: " + err.message));
|
||
});
|
||
|
||
try {
|
||
client.connect({
|
||
host: connConfig.host,
|
||
port: connConfig.port || 22,
|
||
username: connConfig.username,
|
||
password: connConfig.password || undefined,
|
||
privateKey: connConfig.privateKey || undefined,
|
||
passphrase: connConfig.passphrase || undefined,
|
||
readyTimeout: connectTimeoutMs,
|
||
// 跳过主机密钥验证
|
||
hostVerifier: () => true,
|
||
// 使用 NetMesh 的算法构建器,启用旧算法兼容,跳过 ECDSA 主机密钥
|
||
algorithms: buildAlgorithms(true, { skipEcdsaHostKey: true }),
|
||
});
|
||
} catch (e) {
|
||
clearTimeout(connectTimer);
|
||
done(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 批量执行多条 SSH 命令(一次连接,执行多条命令)
|
||
* @param {object} connConfig - ssh2 连接配置
|
||
* @param {string[]} commands - 命令数组
|
||
* @param {object} [options]
|
||
* @returns {Promise<object>} { outputs: { command, stdout }[] }
|
||
*/
|
||
function execSshCommands(connConfig, commands, options = {}) {
|
||
const connectTimeoutMs = options.connectTimeoutMs || DEFAULT_CONNECT_TIMEOUT_MS;
|
||
const totalTimeoutMs = options.totalTimeoutMs || 180000; // 总超时 3 分钟
|
||
const maxOutputBytes = options.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES;
|
||
|
||
const DEBUG = process.env.INSPECTION_DEBUG === "1";
|
||
if (DEBUG) {
|
||
console.log("[InspectExec] 批量执行:", connConfig.host, "| 命令数:", commands.length);
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const client = new SSHClient();
|
||
let settled = false;
|
||
let connectTimer = null;
|
||
let totalTimer = null;
|
||
let cmdTimeout = null;
|
||
let outputBuffer = "";
|
||
let commandIndex = 0;
|
||
let phase = "wait-login"; // wait-login → disable-paging → idle → waiting-output → done
|
||
let disableRetries = 0;
|
||
const results = [];
|
||
|
||
const cleanup = () => {
|
||
if (connectTimer) clearTimeout(connectTimer);
|
||
if (totalTimer) clearTimeout(totalTimer);
|
||
if (cmdTimeout) clearTimeout(cmdTimeout);
|
||
try { client.end(); } catch (e) { /* ignore */ }
|
||
};
|
||
|
||
const done = (err, result) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
cleanup();
|
||
if (err) reject(err);
|
||
else resolve(result);
|
||
};
|
||
|
||
// 总超时
|
||
totalTimer = setTimeout(() => {
|
||
done(new Error("批量命令执行超时(" + totalTimeoutMs / 1000 + "s)"));
|
||
}, totalTimeoutMs);
|
||
|
||
// 匹配提示符(同时兼容交换机 <hostname> / hostname> 和 Linux bash/zsh)
|
||
function hasPrompt(text) {
|
||
const lines = (text || '').split('\n');
|
||
// 从后往前找非空行(最后一行通常是空的,我们看倒数第一和倒数第二)
|
||
const findLastLine = () => {
|
||
for (let i = lines.length - 1; i >= 0; i--) {
|
||
const trimmed = lines[i].trimEnd();
|
||
if (trimmed.length > 0) return trimmed;
|
||
}
|
||
return '';
|
||
};
|
||
const lastLine = findLastLine();
|
||
// 如果只有一行内容,再看前一行(有些 shell 会在输出末尾多一个空行)
|
||
const prevLine = lines.length >= 2 ? lines[lines.length - 2].trimEnd() : '';
|
||
|
||
if (DEBUG) {
|
||
console.log("[InspectExec] hasPrompt check lastLine:", JSON.stringify(lastLine.slice(-100)),
|
||
"| prevLine:", JSON.stringify(prevLine.slice(-100)));
|
||
}
|
||
// 华为 H3C VRP prompt(尖括号包裹,可能带 # root 标记、-D slot 标记):
|
||
// <hostname> — 用户视图
|
||
// <hostname#> — 系统视图 / root prompt
|
||
// <hostname#-D> — 指定 slot 的 root prompt
|
||
// <hostname~> — SSH 登录提示等
|
||
if (/<[\w\-./]+(?:[#~]\w*)*(?:-\w+)?>\s*$/.test(lastLine)) return true;
|
||
if (/<[\w\-./]+(?:[#~]\w*)*(?:-\w+)?>\s*$/.test(prevLine)) return true;
|
||
// Cisco IOS / 纯 hostname prompt(无尖括号): hostname> 或 hostname#
|
||
if (/^[\w\-./]+[>#]\s*$/.test(lastLine)) return true;
|
||
if (/^[\w\-./]+[>#]\s*$/.test(prevLine)) return true;
|
||
// Linux bash: user@host:path$ 或 root@host:path#(必须有 @ 符号区分 "user@host" 和命令输出里的路径)
|
||
if (/[\w.-]+@[\w.-]+[:\s].*[$#]\s*$/.test(lastLine)) return true;
|
||
// 简单 Linux prompt 兜底: 行尾是 # 或 $ 且前面有路径特征(~/ / path)
|
||
if (/[:~\/]\s*[$#]\s*$/.test(lastLine)) return true;
|
||
return false;
|
||
}
|
||
|
||
client.on("ready", () => {
|
||
if (DEBUG) console.log("[InspectExec] SSH ready,打开 shell...");
|
||
clearTimeout(connectTimer);
|
||
|
||
client.shell({ term: "vt100", cols: 200, rows: 24 }, (err, stream) => {
|
||
if (err) {
|
||
done(new Error("打开 shell 失败: " + err.message));
|
||
return;
|
||
}
|
||
|
||
let phase = "wait-login"; // wait-login → idle → waiting-output → done
|
||
let promptCheckTimer = null; // debounced prompt check
|
||
|
||
// 延迟 150ms 再检查 prompt —— 给 vt100 pty 足够时间把一整行数据吐出来
|
||
// 避免逐字节 stream 'data' 事件导致永远 split 不到 \n
|
||
const schedulePromptCheck = () => {
|
||
if (promptCheckTimer) clearTimeout(promptCheckTimer);
|
||
promptCheckTimer = setTimeout(() => {
|
||
promptCheckTimer = null;
|
||
_runPromptCheck();
|
||
}, 150);
|
||
};
|
||
|
||
const _runPromptCheck = () => {
|
||
if (phase === "wait-login") {
|
||
if (hasPrompt(outputBuffer)) {
|
||
phase = "idle";
|
||
outputBuffer = "";
|
||
if (DEBUG) console.log("[InspectExec] 登录成功,进入 idle,准备发送第一条命令(由 reportCommands 提供)");
|
||
sendNextCommand();
|
||
}
|
||
} else if (phase === "waiting-output") {
|
||
const moreIdx = outputBuffer.indexOf("-- More --");
|
||
if (moreIdx !== -1 || outputBuffer.indexOf("---- More ----") !== -1) {
|
||
if (DEBUG) console.log("[InspectExec] 检测到分页,发送空格继续");
|
||
stream.write(" ");
|
||
outputBuffer = outputBuffer.replace(/--\s*More\s*--/gi, "").replace(/----\s*More\s*----/gi, "");
|
||
schedulePromptCheck(); // 分页处理完继续等
|
||
return;
|
||
}
|
||
if (hasPrompt(outputBuffer)) {
|
||
const cmdOutput = cleanOutput(outputBuffer);
|
||
results[results.length - 1].output = cmdOutput;
|
||
outputBuffer = "";
|
||
phase = "idle";
|
||
if (DEBUG) console.log("[InspectExec] 命令输出完成,进入 idle");
|
||
sendNextCommand();
|
||
}
|
||
}
|
||
};
|
||
|
||
stream.on("data", (data) => {
|
||
const chunk = data.toString("utf-8");
|
||
outputBuffer += chunk;
|
||
|
||
if (DEBUG && outputBuffer.length % 1000 < 100) {
|
||
const lines = outputBuffer.split('\n');
|
||
console.log(`[InspectExec] phase=${phase} bufLen=${outputBuffer.length} lastLines=${JSON.stringify(lines.slice(-3))}`);
|
||
}
|
||
|
||
// 每次收到数据都重置 debounce timer —— 持续有数据就不检查,
|
||
// 等 vt100 pty 静默 150ms 后再统一跑 hasPrompt
|
||
schedulePromptCheck();
|
||
});
|
||
|
||
stream.on("close", () => {
|
||
if (phase === "done") {
|
||
// 主动 quit 关闭的,正常 resolve
|
||
if (!settled) done(null, { results });
|
||
return;
|
||
}
|
||
if (!settled) {
|
||
done(new Error("连接意外关闭"));
|
||
}
|
||
});
|
||
|
||
stream.on("error", (e) => {
|
||
done(new Error("流错误: " + e.message));
|
||
});
|
||
|
||
function sendNextCommand() {
|
||
if (commandIndex >= commands.length) {
|
||
if (DEBUG) console.log("[InspectExec] 所有命令执行完毕");
|
||
phase = "done";
|
||
stream.write("quit\n");
|
||
setTimeout(() => done(null, { results }), 500);
|
||
return;
|
||
}
|
||
|
||
const cmd = commands[commandIndex];
|
||
if (DEBUG) console.log("[InspectExec] 发送命令 " + (commandIndex + 1) + "/" + commands.length + ":", cmd.slice(0, 50));
|
||
|
||
stream.write(cmd + "\n");
|
||
results.push({ command: cmd, output: "" });
|
||
commandIndex++;
|
||
phase = "waiting-output";
|
||
|
||
// 每个命令 15 秒超时,防止卡住
|
||
if (cmdTimeout) clearTimeout(cmdTimeout);
|
||
cmdTimeout = setTimeout(() => {
|
||
if (phase === "waiting-output") {
|
||
if (DEBUG) console.log("[InspectExec] 命令超时,强制完成");
|
||
const cmdOutput = cleanOutput(outputBuffer);
|
||
results[results.length - 1].output = cmdOutput;
|
||
outputBuffer = "";
|
||
phase = "idle";
|
||
sendNextCommand();
|
||
}
|
||
}, 15000);
|
||
}
|
||
|
||
function cleanOutput(text) {
|
||
return (text || "")
|
||
// ANSI escape sequences(\x1B[... 系列 — 光标移动、颜色、进度条等)
|
||
.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "")
|
||
.replace(/\x1B\][^\x07\x1B]*(\x07|\x1B\\)/g, "")
|
||
.replace(/\x1B[PX][^\x1B]*\x1B\\/g, "")
|
||
// 其他常见控制序列
|
||
.replace(/\x1B[@-_][0-9;]*[ -\/]*[@-~]/g, "")
|
||
// 华为 CLI 的 More 提示
|
||
.replace(/--\s*More\s*--/gi, "")
|
||
.replace(/----\s*More\s*----/gi, "")
|
||
// 清理剩余可能的单控制字符
|
||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "")
|
||
.replace(/\r/g, "")
|
||
.trim();
|
||
}
|
||
});
|
||
});
|
||
|
||
client.on("error", (err) => {
|
||
clearTimeout(connectTimer);
|
||
done(new Error("SSH 连接失败: " + err.message));
|
||
});
|
||
|
||
try {
|
||
client.connect({
|
||
host: connConfig.host,
|
||
port: connConfig.port || 22,
|
||
username: connConfig.username,
|
||
password: connConfig.password || undefined,
|
||
privateKey: connConfig.privateKey || undefined,
|
||
passphrase: connConfig.passphrase || undefined,
|
||
readyTimeout: connectTimeoutMs,
|
||
hostVerifier: () => true,
|
||
algorithms: buildAlgorithms(true, { skipEcdsaHostKey: true }),
|
||
// 保活配置,防止交换机 VTY 超时断开
|
||
keepaliveInterval: 30000, // 每 30 秒发送一次心跳
|
||
keepaliveCountMax: 3, // 最多重试 3 次
|
||
});
|
||
} catch (e) {
|
||
done(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
execSshCommand,
|
||
execSshCommands,
|
||
DEFAULT_CONNECT_TIMEOUT_MS,
|
||
DEFAULT_EXEC_TIMEOUT_MS,
|
||
};
|