打印多个并行运行的SSH命令的输出

时间:2018-07-22 07:13:57

标签: java ssh concurrency stream jsch

我正在尝试编写日志工具,该工具将通过ssh连接到少数服务器,打开指定的日志文件并将结果打印到System.out.print。目前,我已经实现了从一个来源获取日志的功能。从SSHManager类开始,仅使用Jsch来实现。

public void tailLogFile() {
     System.out.println("Starting to monitor logs for " + server.getIp());
     String command = "tail -f " + server.getLogFilePath();
     try {
         Channel channel = getSession().openChannel("exec");
         ((ChannelExec)channel).setCommand(command);
         InputStream commandOutput = channel.getInputStream();
         channel.connect();
         int readByte = commandOutput.read();

         while(readByte != 0xffffffff) {
             readByte = commandOutput.read();
             System.out.print(server.getFontColor().toString() + (char)readByte);
         }
         channel.disconnect();

     } catch (Exception e) {
         e.printStackTrace();
     }
 }

我猜想其余的内容都没有意义,它将彩色的日志从SSH打印到我的System.out。但是,此程序的主要目的是将多个文件记录到一个位置。所以我试着跟随

for(SSHManager sshManager : getSshManagers()) {
       sshManager.tailLogFile();
}

它现在不起作用,它从print的第一次迭代开始for-loop个日志,并且由于while中的SSHManager.tailLogFile()没有终止,它会继续打印日志从第一个来源。可以想象,我希望SSHManager的n个实例共享System.out,并同时提供所有来源的输出。我想知道最简单的方法是什么?我需要参与并发吗?

2 个答案:

答案 0 :(得分:1)

您必须以非阻塞方式连续读取所有输出流。

您可以使用InputStream.available(),如下所示:

ArrayList<ChannelExec> channels = new ArrayList<ChannelExec>();

ChannelExec channel;
channel = (ChannelExec)session1.openChannel("exec");
channel.setCommand(
    "echo one && sleep 2 && echo two && sleep 2 && echo three");
channel.connect();
channels.add(channel);

channel = (ChannelExec)session2.openChannel("exec");
channel.setCommand(
    "sleep 1 && echo eins && sleep 2 && echo zwei && sleep 2 && echo drei");
channel.connect();
channels.add(channel);

ArrayList<InputStream> outputs = new ArrayList<InputStream>();
for (int i = 0; i < channels.size(); i++)
{
    outputs.add(channels.get(i).getInputStream());
}

Boolean anyOpened = true;
while (anyOpened)
{
    anyOpened = false;
    for (int i = 0; i < channels.size(); i++)
    {
        channel = channels.get(i);
        if (!channel.isClosed())
        {
            anyOpened = true;
            InputStream output = outputs.get(i);
            while (output.available() > 0)
            {
                int readByte = output.read();
                System.out.print((char)readByte);
            }
        }
    }
}

将帮助您(假设使用Linux服务器):

one
eins
two
zwei
three
drei

请注意,答案按字节/字符读取输出。它不能保证在切换到另一个会话之前获得完整的发言。因此,您可能最终混合了来自不同会话的部分线路。在将缓冲区打印到输出之前,您应该在缓冲区中累积字节/字符,寻找新的一行。

答案 1 :(得分:1)

对于我来说,我更喜欢为要写入的通道提供一个OutputStream,而不是从它提供给我的InputStream中读取。

我将定义如下内容:

protected class MyOutputStream extends OutputStream {

    private StringBuilder stringBuilder = new StringBuilder();
    private Object lock;

    public MyOutputStream(Object lock) {
        this.lock = lock;
    }

    @Override
    public void write(int b) throws IOException {
        this.stringBuilder.append(b);

        if (b == '\n') {
            this.parseOutput();
        }
    }

    @Override
    public void write(byte[] b) throws IOException {
        String str = new String(b);
        this.stringBuilder.append(str);

        if (str.contains("\n")) {
            this.parseOutput();
        }
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        String str = new String(b, off, len);
        this.stringBuilder.append(str);

        if (str.contains("\n")) {
            this.parseOutput();
        }
    }

    @Override
    public void flush() throws IOException {
    }

    @Override
    public void close() throws IOException {
        LOGGER.info("My output stream has closed");
    }

    private void parseOutput() throws IOException {
        // we split the text but we make sure not to drop the empty strings or the trailing char
        String[] lines = this.stringBuilder.toString().split("\n", -1);

        int num = 0;
        int last = lines.length - 1;
        String trunkated = null;

        // synchronize the writing
        synchronized (this.lock) {
            for (String line : lines) {
                // Dont treat the trunkated last line
                if (num == last && line.length() > 0) {
                    trunkated = line;
                    break;
                }
                // write a full line    
                System.out.print(line);     

                num++;
            }
        }

        // flush the buffer and keep the last trunkated line
        this.stringBuilder.setLength(0);
        if (trunkated != null) {
            this.stringBuilder.append(trunkated);
        }
    }
}

因此用法如下:

ArrayList<ChannelExec> channels = new ArrayList<ChannelExec>();
Object lock = new Object();

ChannelExec channel;
channel = (ChannelExec)session1.openChannel("exec");
channel.setCommand("echo one && sleep 2 && echo two && sleep 2 && echo three");
channel.setOutputStream(new MyOutputStream(lock));
channel.connect();
channels.add(channel);

channel = (ChannelExec)session2.openChannel("exec");
channel.setCommand("sleep 1 && echo eins && sleep 2 && echo zwei && sleep 2 && echo drei");
channel.setOutputStream(new MyOutputStream(lock));
channel.connect();
channels.add(channel);

for (ChannelExec channel : channels) {
    while (!channel.isClosed()) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    } 
}

好处是,您可以受益于Jsch通道中已经存在的多线程,然后避免了泛滥日志的问题,该问题不会让其他日志被打印。 使用不同的流类处理每个日志也更加容易和清晰。 StringBuilder是累积字符的好方法,直到获得完整的行为止。

还请注意,一次编写整行避免了每个char调用一个函数,并且避免了将已写char的数量乘以系统     server.getFontColor()。toString()

请确保正确锁定,我编写的代码未经测试。