remoteshell

This commit is contained in:
cheney
2026-06-17 10:52:30 +08:00
parent 37fa4cf020
commit 97614a9079
3 changed files with 224 additions and 1 deletions
+145 -1
View File
@@ -335,12 +335,155 @@ function scp(from, to, params = {}) {
}));
}
function loadSsh2Client() {
try {
return require("ssh2").Client;
} catch (e) {
throw new Error("remoteShell requires pure JS dependency ssh2. Please run npm install ssh2 in kit project.");
}
}
function normalizeRemoteShellConfig(config = {}) {
const host = config.host || config.hostname;
if (!host) {
throw new Error("remoteShell host is required");
}
const username = config.username || config.user;
if (!username) {
throw new Error("remoteShell username is required");
}
const password = resolveScpPassword(config);
const privateKey = config.privateKey || config.keyText || (config.keyFile ? fs.readFileSync(path.resolve(config.keyFile), "utf-8") : undefined);
const connectConfig = Object.assign({}, config, {
host,
username,
port: config.port || 22,
password,
privateKey,
passphrase: resolveValue(config.passphrase)
});
delete connectConfig.user;
delete connectConfig.hostname;
delete connectConfig.pwd;
delete connectConfig.plainPwd;
delete connectConfig.plainPassword;
delete connectConfig.plaintextPwd;
delete connectConfig.plaintextPassword;
delete connectConfig.keyText;
delete connectConfig.keyFile;
return connectConfig;
}
function remoteShell(config = {}) {
const Client = loadSsh2Client();
const connectConfig = normalizeRemoteShellConfig(config);
let client = null;
let connectPromise = null;
function connect() {
if (client) {
return Promise.resolve(client);
}
if (connectPromise) {
return connectPromise;
}
connectPromise = new Promise((resolve, reject) => {
const conn = new Client();
conn.once("ready", () => {
client = conn;
resolve(conn);
});
conn.once("error", (e) => {
connectPromise = null;
reject(e);
});
conn.once("close", () => {
client = null;
connectPromise = null;
});
conn.connect(connectConfig);
});
return connectPromise;
}
async function exec(cmd, options = {}) {
if (!cmd) {
throw new Error("remoteShell exec cmd is required");
}
const conn = await connect();
return new Promise((resolve, reject) => {
const execOptions = Object.assign({}, options);
delete execOptions.stdio;
delete execOptions.rejectOnError;
conn.exec(cmd, execOptions, (err, stream) => {
if (err) {
reject(err);
return;
}
let stdout = "";
let stderr = "";
stream.on("error", reject);
stream.on("close", (code, signal) => {
const result = { code, signal, stdout, stderr };
if (options.rejectOnError === false || code === 0) {
resolve(result);
} else {
const error = new Error(`Remote command failed with code ${code}: ${cmd}`);
error.result = result;
reject(error);
}
});
stream.on("data", (data) => {
const text = data.toString();
stdout += text;
if (options.stdio === "inherit") {
process.stdout.write(text);
}
});
stream.stderr.on("data", (data) => {
const text = data.toString();
stderr += text;
if (options.stdio === "inherit") {
process.stderr.write(text);
}
});
});
});
}
async function execText(cmd, options = {}) {
const result = await exec(cmd, options);
return result.stdout;
}
async function call(cmd, options = {}) {
return exec(cmd, Object.assign({ stdio: "inherit" }, options));
}
function close() {
if (client) {
client.end();
}
client = null;
connectPromise = null;
}
return { connect, exec, execText, call, close };
}
function exit(code = 0) {
process.exit(code);
}
function isSecretLogKey(key) {
return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword)$/i.test(key);
return /^(pwd|password|plainPwd|plainPassword|plaintextPwd|plaintextPassword|privateKey|passphrase|keyText)$/i.test(key);
}
function redactLogValue(value) {
@@ -403,6 +546,7 @@ module.exports = {
readJson: withCallLog("readJson", readJson),
writeJson: withCallLog("writeJson", writeJson),
scp: withCallLog("scp", scp),
remoteShell: withCallLog("remoteShell", remoteShell),
exit: withCallLog("exit", exit),
setVar: withCallLog("setVar", setVar),
getVar: withCallLog("getVar", getVar),