【问题标题】:Handle Success/Error Responses from ssh2 using Promises使用 Promises 处理来自 ssh2 的成功/错误响应
【发布时间】:2018-06-20 07:31:04
【问题描述】:

我正在构建一个 node.js 应用程序,该应用程序在生产中将充当许多服务器的 SSH 客户端,其中一些服务器可能在任何给定时间都无法访问。我正在尝试编写一个函数,该函数尝试在启动时对其配置中的每个客户端运行 SSH 命令,但我无法同时处理成功的会话和以错误结束的会话。我在一个承诺中包装了一个 ssh2 客户端。如果我删除了第三个(垃圾)服务器并且只有成功的结果,这很好!查看输出:

STDOUT: Hello World

STDOUT: Hello World

Session closed
Session closed
Successful session: Hello World,Hello World

但是,如果其中一个连接超时,即使我处理了错误,我也无法保留我的任何数据。看起来错误消息覆盖了所有已解决的承诺

Successful session: Error: Timed out while waiting for handshake,Error: 
Timed out while waiting for handshake,Error: Timed out while waiting 
for handshake

这是我的代码,如果这有点分散,请原谅我,因为为了这个问题,我已经组合了一些模块。我的目标是保留成功会话中的数据并优雅地处理失败。

var Client = require('ssh2').Client;

 const labs = {
    "ny1": "192.168.1.2",
    "ny2": "192.168.1.3",
    "ny3": "1.1.1.1"
};

function checkLabs() {
    let numLabs = Object.keys(labs).length;
    let promises = [];

    for(i=0;i<numLabs;i++){
        let labName = Object.keys(labs)[i];
        promises.push(asyncSSH("echo 'Hello World'", labs[labName]));
    }

    Promise.all(promises.map(p => p.catch(e => e)))
        .then(results => console.log("Successful session: " + results))
        .catch(e => console.log("Error! " + e));
}

var sendSSH = function (command, dest, callback) {
var conn = new Client();

        conn.on('ready', function() {
            return conn.exec(command, function(err, stream) {
                if (err) throw err;
                stream.on('data', function(data) {
                    callback(null, data);
                    console.log('STDOUT: ' + data);
                }).stderr.on('data', function(data){
                    callback(err);
                    console.log('STDERR: ' + data);
                }).on('close', function(err) {
                    if(err) {
                        console.log('Session closed due to error');
                    } else {
                        console.log('Session closed');
                    }
                });
                stream.end('ls -l\nexit\n');
            });
        }).on('error', function(err){
            callback(err);
        }).connect({
            host: dest,
            port: 22,
            username: 'root',
            readyTimeout: 10000,
            privateKey: require('fs').readFileSync('link-to-my-key')
        });
};

function asyncSSH(command, dest) {
    return new Promise(function(resolve, reject){
        sendSSH(command, dest, function(err,data) {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    });
}

checklabs();

如何更好地使用这个 promise 包装器来处理来自 ssh2 客户端的任何错误?任何提示都表示赞赏。

【问题讨论】:

  • 变量 conn 似乎不知从何而来,神奇地存在。不知道为什么你定义labs 而不是const labs = {...
  • 您只能解决或拒绝一次承诺,但您可以解决多次,因为数据发生了多次。这不会导致错误,但在第一个之后的任何调用 resolve 都将被忽略。相反,您应该在关闭时解析并将数据保存在数组中的任何数据,以便您可以在关闭时使用该数组解析。不过,这看起来不像您可以实际运行并演示您的问题的任何代码。
  • @HMR 这很有帮助,谢谢。我在一个单独的文件中使用了define方法来充当配置,也许我应该称它为const。如果我删除了 ssh2 代码并在伪代码中展示了成功/错误,那么实现这一点作为演示会更有用。我想处理 ssh2 行为对我的问题非常重要,我需要花更多时间处理这部分。
  • 为了让问题更上一层楼,我的问题是我需要使用同一段 ssh2 代码进行多个事务。每个都关闭,在这种情况下.on('data' API 会成功关闭,在连接失败的情况下会导致close.on('error'。每个事务可能需要长达 10 秒的超时时间。我不在乎它们是同步发生还是异步发生,我只需要保存每个数据,并优雅地处理发生的任何错误,恢复任何剩余的 ssh 会话。
  • 似乎有点奇怪,您应该使用Client() 的单个实例来达到“Hello World,Hello World”。我原以为您需要(a)每个主机一个实例,或(b)执行串联连接。如果回显的消息包含独特的内容,例如 ny1/ny2 键,会发生什么情况。

标签: javascript node.js ssh promise


【解决方案1】:

为了充分利用每个连接,您可以(并且可以说应该)分别承诺:

  • 每个 Client() 实例的实例化
  • 每个实例的conn.exec() 方法(以及任何其他需要的异步方法)

这将允许Client() 的每个实例与不同的命令一起使用(尽管在此示例中不需要)。

您还应该确保在其工作完成后断开每个套接字的连接,方法是调用client_.end()。为此,建议使用“disposer pattern”。

考虑到这些观点和一些假设,这就是我最终得出的结论:

var Client = require('ssh2').Client;

const labs = {
    "ny1": "192.168.1.2",
    "ny2": "192.168.1.3",
    "ny3": "1.1.1.1"
};

function checkLabs() {
    let promises = Object.keys(labs).map((key) => {
        return withConn(labs[key], (conn) => { 
            return conn.execAsync("echo 'Hello World'")
            .catch((e) => "Error: " + e.message); // catch in order to immunise the whole process against any single failure.
                                                  // and inject an error message into the success path.
        });
    });
    Promise.all(promises)
    .then(results => console.log("Successful session: " + results))
    .catch(e => console.log("Error! " + e.message)); // with individual errors caught above, you should not end up here.
}

// disposer pattern, based on https://stackoverflow.com/a/28915678/3478010
function withConn(dest, work) {
    var conn_;
    return getConnection(dest).then((conn) => {
        conn_ = conn;
        return work(conn);
    }).then(() => {
        if(conn_) {
            conn_.end(); // on success, disconnect the socket (ie dispose of conn_).
        }
    }, () => {
        if(conn_) {
            conn_.end(); // on error, disconnect the socket (ie dispose of conn_).
        }
    });
    // Note: with Bluebird promises, simplify .then(fn,fn) to .finally(fn).
}

function getConnection(dest) {
    return new Promise((resolve, reject) => {
        let conn = promisifyConnection(new Client());
        conn.on('ready', () => {
            resolve(conn);
        })
        .on('error', reject)
        .connect({
            host: dest,
            port: 22,
            username: 'root',
            readyTimeout: 10000,
            privateKey: require('fs').readFileSync('link-to-my-key')
        });
    });
}

function promisifyConnection(conn) {
    conn.execAsync = (command) => { // promisify conn.exec()
        return new Promise((resolve, reject) => {
            conn.exec(command, (err, stream) => {
                if(err) {
                    reject(err);
                } else {
                    let streamSegments = []; // array in which to accumulate streamed data
                    stream.on('close', (err) => {
                        if(err) {
                            reject(err);
                        } else {
                            resolve(streamSegments.join('')); // or whatever is necessary to combine the accumulated stream segments
                        }
                    }).on('data', (data) => {
                        streamSegments.push(data);
                    }).stderr.on('data', function(data) {
                        reject(new Error(data)); // assuming `data` to be String
                    });
                    stream.end('ls -l\nexit\n'); // not sure what this does?
                }
            });
        });
    };
    // ... promisify any further Client methods here ...
    return conn;
}

注意事项:

  • conn.exec() 的承诺包括可以在一系列段(例如数据包)中接收数据的假设。如果这个假设不成立,那么对 streamSegments 数组的需求就消失了。
  • getConnection()promisifyConnection() 可以写成一个函数,但使用单独的函数更容易看到发生了什么。
  • getConnection()promisifyConnection() 让所有乱七八糟的东西远离应用程序代码。

【讨论】:

  • 这很棒,非常感谢您的设计。这也无法返回 Promise 中的流式数据,仍在尝试找出那部分。
  • 再次阅读文档后刚刚编辑。我仍然不确定流的行为方式。该部分可能需要修复才能通过 Promise 传递流数据。
  • @user9201592,有什么快乐了吗?
  • 我能够通过承诺返回我的数据的唯一方法是.on('data',但这对我来说在测试中工作得很好。再次感谢您的帮助!
  • 嗯,这表明流的close 事件不会触发,至少对于“回声”命令不会触发..
猜你喜欢
  • 2020-09-10
  • 2018-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-10
  • 2012-10-12
  • 2017-01-11
相关资源
最近更新 更多