Python subprocess (incorrectly?) merging stderr and stdout for ssh

Viewed 396

I'm trying to run various linux commands via python's subprocess and ssh. I'd like to be able to run the command and check the stderr and stdout lengths to determine if the command was successful or not. For example if there's no error it was successful. The problem is that after the initial connection all the output is going to stdout.

I did try using paramiko but kept getting unreliable authentication behaviour. This approach seems more robust, if I could just get it to work.

import subprocess
import os
import pty
from select import select

class open_ssh_helper(object):
    def __init__(self, host="127.0.0.1", user="root"):
        self.host = host
        self.user = user
        self.cmd = ['ssh',
                    user +'@'+host, 
                    "-o", "StrictHostKeyChecking=no", 
                    "-o", "UserKnownHostsFile=/dev/null"]

        self.prompt = '~#'
        self.error = ''
        self.output = ''

        self.mtty_stdin, self.stty_stdin = pty.openpty()
        self.mtty_stdout, self.stty_stdout = pty.openpty()
        self.mtty_stderr, self.stty_stderr = pty.openpty()

        self.ssh = subprocess.Popen(self.cmd,
                                    shell=False,
                                    stdin=self.stty_stdin,
                                    stdout=self.stty_stdout,
                                    stderr=self.stty_stderr)
        self._read_stderr()
        self._read_stdout()

    def _reader(self, read_pty):
        char = ""
        buf = ""
        while True:
            rs, ws, es = select([read_pty], [], [], 0.5)
            if read_pty in rs:
                char = os.read(rs[0], 1)
                buf += char 
            else:
                break

        return buf

    def _read_stderr(self):
        self.error = self._reader(self.mtty_stderr)

    def _read_stdout(self):
        self.output = self._reader(self.mtty_stdout)

    def execute(self, command):
        os.write(self.mtty_stdin, command)

        self._read_stderr()
        self._read_stdout()

if __name__=='__main__':
    ss = open_ssh_helper('10.201.202.236', 'root')

    print "Error: \t\t: " + ss.error
    print "Output: \t: " + ss.output

    ss.execute("cat /not/a/file\n")

    print "Error: \t\t: " + ss.error
    print "Output: \t: " + ss.output

Which outputs something like:

Error:      : Warning: Permanently added '10.201.202.236' (ECDSA) to the list of known hosts.
Debian GNU/Linux 8
BeagleBoard.org Debian Image xxxx-xx-xx
Support/FAQ: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian

Output:     : Last login: Thu Oct  5 07:32:06 2017 from 10.201.203.29
root@beaglebone:~# 
Error:      : 
Output:     : cat /not/a/file
cat: /not/a/file: No such file or directory
root@beaglebone:~# 

My hope was that the line cat: /not/a/file: No such file or directory would be printed as the error line above it. However it seems that for some reason the stdout is printing both the output and the error.

1 Answers

ssh pulls back both stdout and stderr on the remote system as a single stream so they both come through on the ssh stdout.

If you want to separate the two streams on the remote system you will have to do that by redirecting one or both of them to a remote file and then reading the files. Or less reliably but possibly easier, you could redirect stderr through a filter that puts something like 'STDERR:' on the start of each line and split the streams again that way.

Related