Search code examples
javashellsshsudojsch

Separating standard and error output of a command (sudo) executed with PTY with JSch Java


I am using Jsch and my task is to login to server and run command as following

sudo "command"

Using following code I am successfully able to connect and execute commands (without the error "sudo: sorry, you must have a tty")

public String runSudo(RemoteHost remoteHost, String[] commands, OperationData operationData, String known_hosts, String id_rsa) {
    String result = "";
    Properties config = new Properties();
    config.put("StrictHostKeyChecking", "no");
    JSch jsch = new JSch();
    // Create a JSch session to connect to the server
    Session session = null;

    try {
        session = jsch.getSession(remoteHost.getUsername(), remoteHost.getHost(), remoteHost.getPort());

        if (remoteHost.getPassword() != null)
            session.setPassword(remoteHost.getPassword());
        else{
            session.setConfig("PreferredAuthentications", "publickey");
            jsch.setKnownHosts(known_hosts);
            jsch.addIdentity(id_rsa);
        }
        session.setConfig(config);
        // Establish the connection
        session.connect();
        logger.debug("Connected...");

        for (int k = 0; k < commands.length; k++) {
            ChannelExec channel = (ChannelExec) session.openChannel("exec");
            channel.setCommand(commands[k]);
            OutputStream outputStreamStdErr = new ByteArrayOutputStream();
            channel.setErrStream(outputStreamStdErr, true);
            channel.setPty(true);

            InputStream in = channel.getInputStream();
            channel.connect();
            byte[] tmp = new byte[1024];
            while (true) {
                while (in.available() > 0) {
                    int i = in.read(tmp, 0, 1024);
                    if (i < 0) {
                        break;
                    }
                    result = result + new String(tmp, 0, i);
                }
                if (channel.isClosed()) {
                    this.exitStatus += channel.getExitStatus();
                    break;
                }
                Thread.sleep(1000);
            }
            String errors = readInputStream(outputstream2inputstream(outputStreamStdErr));
            if (!errors.equals("")) {
                operationData.setErrorMessage(errors);
            }
            channel.getErrStream().close();
            channel.disconnect();
        }
        session.disconnect();
    } catch (JSchException e) {
        operationData.setErrorMessage(e.getMessage());
        logger.error(e.getMessage());
        return result;
    } catch (IOException e) {
        operationData.setErrorMessage(e.getMessage());
        logger.error(e.getMessage());
        return result;
    } catch (InterruptedException e) {
        operationData.setErrorMessage(e.getMessage());
        logger.error(e.getMessage());
        return result;
    } finally {
        return result;
    }
}

But using the property channel.setPty(true); I can't read the error stream because it's null and the errors are displayed in the output of the command. Instead, if I don't use the property channel.setPty(true); I can read the error stream, but the method return the error "sudo: sorry, you must have a tty" that i want to avoid.

Can someone give me help to use the error stream and the property channel.setPty(true); in the same code?


Solution

  • It's the standard SSH behaviour (at least on Linux) that when terminal emulation is enabled, all output goes to one stream in the SSH connection. That has nothing do you with your Java/JSch code.

    Do not use PTY for command automation. PTY is intended to implement an interactive terminal for a human use.

    You can configure sudo not to require PTY.

    There are also other alternatives.
    See Allowing automatic command execution as root on Linux using SSH.


    Another workaround is to redirect the stdout and stderr to temporary files on the server. And then print the files separately to the terminal/output.

    A naive implementation can be like:

    sudo ... > /tmp/my_stdout 2> /tmp/my_stderr ; echo "stdout:" ; cat /tmp/my_stdout ; echo "stderr:" ; cat /tmp/my_stderr