1

Short Version: Is it possible to log the stdout and stderr on the local side of a command executed remotely via ssh in the same order as it was output on the remote host? If so, how?

Long Version:

I am attempting to log the standard and error output from a remotely exec'd SSH command (using Jsch) in the same order as it was output by the remote command. In other words, if the remote command writes "a" to stdout, then "b" to stderr, then "c" to stdout, I want the log on the client (local) side to read:

a
b
c

Below is what I have so far. It comes relatively close to what I want, but I think it will be apparent that it does not guarantee the correct output order on the client side.

public int exec(String strCommand) throws ExceptionUnableToExecCommand {
    JSch jsch = new JSch();
    Session session = null;
    ChannelExec channel = null;
    try {
          session = jsch.getSession(user, host, 22);
          UserInfo ui = new cyclOps.jsch.UserInfo(password);
          session.setUserInfo(ui);
          session.connect();
          channel = (ChannelExec) session.openChannel("exec");
          channel.setCommand(strCommand);
          channel.setInputStream(null);
          InputStream in = channel.getInputStream();
          InputStream err = channel.getErrStream();
          channel.connect();
          /* getOutput() defined below. */
          return this.getOutput(channel, in, err);
    } catch (JSchException | IOException e) {
        throw new ExceptionUnableToExecCommand("Unable to execute " + strCommand + " " + this.toString(), e);
    } finally {
        if (channel != null) channel.disconnect();
        if (session != null) session.disconnect();
    }
}

private int getOutput(ChannelExec channel, InputStream in, InputStream err) throws IOException { 
    byte[] tmp = new byte[1024];
    while(true){
        while(in.available() > 0){
            int i=in.read(tmp, 0, 1024);
            if(i<0)break;
            this.sshLogger.logOutputFromSSH(new String(tmp, 0, i));
        }
        while(err.available() > 0){
            int i=err.read(tmp, 0, 1024);
            if(i<0)break;
            this.sshLogger.logOutputFromSSH(new String(tmp, 0, i));
        }
        if(channel.isClosed()){
            return channel.getExitStatus();
        }
        try{Thread.sleep(1000);}catch(Exception ee){}
    }
}

I think I should point out that this is a modified version of Exec.java from the Jsch web site examples.

John Fitzpatrick
  • 4,207
  • 7
  • 48
  • 71

2 Answers2

3

How about the following chunk of code?

channel.setCommand(command);

PipedOutputStream pos=new PipedOutputStream();
PipedInputStream pis=new PipedInputStream(pos);
channel.setOutputStream(pos);
channel.setExtOutputStream(pos);
InputStream in=pis;

channel.connect();
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
ymnk
  • 1,145
  • 7
  • 7
1

You are working with separate streams, so no, there is no way you can possibly guarantee to get the exact same output.

that said, your code has a number of potential problems, among them:

  • InputStream.available is a generally useless method and should be avoided
  • you are reading bytes and converting arbitrary byte arrays to chars/Strings, which could result in broken output (if you are getting multi-byte chars you could end up splitting them).
  • reading 2 different blocking streams with a single thread can result in deadlock
jtahlborn
  • 52,909
  • 5
  • 76
  • 118
  • What would be the best approach to the third point? Two streamgobblers, one for stdout and one for stderr? – John Fitzpatrick Feb 12 '14 at 18:31
  • Sorry had to take back the "correct answer" status. Your guidance was valuable but ymnk's answer actually seems to be giving me the output in the correct order. – John Fitzpatrick Feb 13 '14 at 13:59