SSH and telnet library
authorFaiyaz Ahmed <faiyaza@cs.princeton.edu>
Tue, 14 Nov 2006 19:36:09 +0000 (19:36 +0000)
committerFaiyaz Ahmed <faiyaza@cs.princeton.edu>
Tue, 14 Nov 2006 19:36:09 +0000 (19:36 +0000)
pyssh/__init__.py [new file with mode: 0644]
pyssh/fssa.py [new file with mode: 0644]
pyssh/nbpipe.py [new file with mode: 0644]
pyssh/ptyext.py [new file with mode: 0644]

diff --git a/pyssh/__init__.py b/pyssh/__init__.py
new file mode 100644 (file)
index 0000000..bc57610
--- /dev/null
@@ -0,0 +1,333 @@
+"""A SSH Interface class.\r
+\r
+An interface to ssh on posix systems, and plink (part of the Putty\r
+suite) on Win32 systems.\r
+\r
+By Rasjid Wilcox.\r
+Copyright (c) 2002.\r
+\r
+Version: 0.2\r
+Last modified 4 September 2002.\r
+\r
+Drawing on ideas from work by Julian Schaefer-Jasinski, Guido's telnetlib and\r
+version 0.1 of pyssh (http://pyssh.sourceforge.net) by Chuck Esterbrook.\r
+\r
+Licenced under a Python 2.2 style license.  See License.txt.\r
+"""\r
+\r
+DEBUG_LEVEL = 0\r
+\r
+import os, getpass\r
+import signal    # should cause all KeyboardInterrupts to go to the main thread\r
+                 # try for Linux, does not seem to be try under Cygwin\r
+import nbpipe\r
+import time\r
+\r
+# Constants\r
+SSH_PORT=806\r
+SSH_PATH=''\r
+\r
+CTRL_C=chr(3)\r
+\r
+READ_LAZY=0\r
+READ_SOME=1\r
+READ_ALL=2\r
+READ_UNTIL=3\r
+\r
+# set the path to ssh / plink, and chose the popen2 funciton to use\r
+if os.name=='posix':\r
+    import fssa    # we can look for ssh-agent on posix\r
+                   # XXX Can we on Win32/others?\r
+    import ptyext  # if my patch gets accepted, change this to check for a\r
+                   # sufficiently high version of python, and assign ptyext=pty\r
+                   # if sufficient.\r
+    sshpopen2=ptyext.popen2\r
+    CLOSE_STR='~.'\r
+    tp=os.popen('/usr/bin/which ssh')\r
+    SSH_PATH=tp.read().strip()\r
+    try:\r
+        tp.close()\r
+    except IOError:\r
+        # probably no child process\r
+        pass\r
+    if SSH_PATH == '':\r
+        tp=os.popen('command -v ssh')  # works in bash, ash etc, not csh etc.\r
+        SSH_PATH=tp.read().strip()\r
+        tp.close()\r
+    if SSH_PATH == '':\r
+        check = ['/usr/bin/ssh', '/usr/local/bin/ssh', '/bin/ssh']\r
+        for item in check:\r
+            if os.path.isfile(item):\r
+                SSH_PATH=item\r
+                break\r
+    PORT_STR='-p '\r
+else:\r
+    sshpopen2=os.popen2\r
+    CLOSE_STR=CTRL_C        # FIX-ME: This does not work.\r
+                            # I think I need to implement a 'kill' component\r
+                            # to the close function using win32api.\r
+    SSH_PATH=''\r
+    PORT_STR='-P '\r
+\r
+class mysshError(Exception):\r
+    """Error class for myssh."""\r
+    pass\r
+\r
+# Helper functions\r
+def _prompt(prompt):\r
+    """Print the message as the prompt for input.\r
+    Return the text entered."""\r
+    noecho = (prompt.lower().find('password:') >= 0) or \\r
+        (prompt.lower().find('passphrase:') >=0)\r
+    print """User input required for ssh connection.\r
+    (Type Ctrl-C to abort connection.)"""\r
+    abort = 0\r
+    try:\r
+        if noecho:\r
+            response = getpass.getpass(prompt)\r
+        else:\r
+            response = raw_input(prompt)\r
+    except KeyboardInterrupt:\r
+        response = ''\r
+        abort = 1\r
+    return response, abort\r
+\r
+class Ssh:\r
+    """A SSH connection class."""\r
+    def __init__(self, username=None, host='localhost', port=None):\r
+        """Constructor.  This does not try to connect."""\r
+        self.debuglevel = DEBUG_LEVEL\r
+        self.sshpath = SSH_PATH\r
+        self.username = username\r
+        self.host = host\r
+        self.port = port\r
+        self.isopen = 0\r
+        self.sshpid = 0  # perhaps merge this with isopen\r
+        self.old_handler = signal.getsignal(signal.SIGCHLD)\r
+        sig_handler = signal.signal(signal.SIGCHLD, self.sig_handler)\r
+        \r
+    def __del__(self):\r
+        """Destructor -- close the connection."""\r
+        if self.isopen:\r
+            self.close()\r
+    \r
+    def sig_handler(self, signum, stack):\r
+        """ Handle SIGCHLD signal """\r
+        if signum == signal.SIGCHLD:\r
+            try:\r
+                os.waitpid(self.sshpid, 0)\r
+            except:\r
+                pass\r
+        if self.old_handler != signal.SIG_DFL:\r
+            self.old_handler(signum, stack)\r
+\r
+    def attach_agent(self, key=None):\r
+        if os.name != 'posix':\r
+            # only posix support at this time\r
+            return\r
+        if 'SSH_AUTH_SOCK' not in os.environ.keys():\r
+            fssa.fssa(key)\r
+\r
+    def set_debuglevel(self, debuglevel):\r
+        """Set the debug level."""\r
+        self.debuglevel = debuglevel\r
+        \r
+    def set_sshpath(self, sshpath):\r
+        """Set the ssh path."""\r
+        self.sshpath=sshpath\r
+    \r
+    # Low level functions\r
+    def open(self, cmd=None):\r
+        """Opens a ssh connection.\r
+        \r
+        Raises an mysshError if myssh.sshpath is not a file.\r
+        Raises an error if attempting to open an already open connection.\r
+        """\r
+        self.attach_agent()\r
+        if not os.path.isfile(self.sshpath):\r
+            raise mysshError, \\r
+            "Path to ssh or plink is not defined or invalid.\nsshpath='%s'" \\r
+             % self.sshpath\r
+        if self.isopen:\r
+            raise mysshError, "Connection already open."\r
+        sshargs = ''\r
+        if self.sshpath.lower().find('plink') != -1:\r
+            sshargs = '-ssh '\r
+        if self.port and self.port != '':\r
+            sshargs += PORT_STR + self.port + ' '\r
+        if self.username and self.username !='':\r
+            sshargs += self.username + '@'\r
+        sshargs += self.host\r
+        if cmd:\r
+            sshargs += ' ' + cmd\r
+        if self.debuglevel:\r
+            print ">> Running %s %s." % (self.sshpath, sshargs)\r
+        # temporary workaround until I get pid's working under win32\r
+        if os.name == 'posix':\r
+            self.sshin, self.sshoutblocking, self.sshpid = \\r
+                                sshpopen2(self.sshpath + ' ' + sshargs)\r
+        else:\r
+            self.sshin, self.sshoutblocking = \\r
+                                sshpopen2(self.sshpath + ' ' + sshargs)\r
+        self.sshout = nbpipe.nbpipe(self.sshoutblocking)\r
+        self.isopen = 1\r
+        if self.debuglevel:\r
+            print ">> ssh pid is %s." % self.sshpid\r
+        \r
+    def close(self, addnewline=1):\r
+        """Close the ssh connection by closing the input and output pipes.\r
+        Returns the closing messages.\r
+        \r
+        On Posix systems, by default it adds a newline before sending the\r
+        disconnect escape sequence.   Turn this off by setting addnewline=0.\r
+        """\r
+        if os.name == 'posix':\r
+            try:\r
+                if addnewline:\r
+                    self.write('\n')\r
+                self.write(CLOSE_STR)\r
+            except (OSError, IOError, mysshError):\r
+                pass\r
+        output = self.read_lazy()\r
+        try:\r
+            self.sshin.close()\r
+            self.sshoutblocking.close()\r
+        except:\r
+            pass\r
+        if os.name == 'posix':\r
+            try:\r
+                os.kill(self.sshpid, signal.SIGHUP)\r
+                os.waitpid(self.sshpid, 0)\r
+            except:\r
+                pass\r
+        self.isopen = 0\r
+        if self.debuglevel:\r
+            print ">> Connection closed."\r
+        return output\r
+        \r
+    def write(self, text):\r
+        """Send text to the ssh process."""\r
+        # May block?? Probably not in practice, as ssh has a large input buffer.\r
+        if self.debuglevel:\r
+            print ">> Sending %s" % text\r
+        if self.isopen:\r
+            while len(text):\r
+                numtaken = os.write(self.sshin.fileno(),text)\r
+                if self.debuglevel:\r
+                    print ">> %s characters taken" % numtaken\r
+                text = text[numtaken:]\r
+        else:\r
+            raise mysshError, "Attempted to write to closed connection."\r
+    \r
+    # There is a question about what to do with connections closed by the other\r
+    # end.  Should write and read check for this, and force proper close?\r
+    def read_lazy(self):\r
+        """Lazy read from sshout.  Waits a little, but does not block."""\r
+        return self.sshout.read_lazy()\r
+    \r
+    def read_some(self):\r
+        """Always read at least one block, unless the connection is closed.\r
+        My block."""\r
+        if self.isopen:\r
+            return self.sshout.read_some()\r
+        else:\r
+            return self.sshout.read_lazy()\r
+        \r
+    def read_until(self, match, timeout=None):\r
+        """Read until a given string is encountered or until timeout.\r
+\r
+        When no match is found, return whatever is available instead,\r
+        possibly the empty string.  Raise EOFError if the connection\r
+        is closed and no cooked data is available.\r
+\r
+        """\r
+        if self.isopen:\r
+            return self.sshout.read_until(match, timeout)\r
+        else:\r
+            return self.sshout.read_lazy()    \r
+\r
+    def read_all(self):\r
+        """Reads until end of file hit.  May block."""\r
+        if self.isopen:\r
+            return self.sshout.read_all()\r
+        else:\r
+            return self.sshout.read_lazy()    \r
+        \r
+    # High level funcitons\r
+    def login(self, logintext='Last login:', prompt_callback=_prompt):\r
+        """Logs in to the ssh host.  Checks for standard prompts, and calls\r
+        the function passed as promptcb to process them.\r
+        Returns the login banner, or 'None' if login process aborted.\r
+        """\r
+        self.open()\r
+        banner = self.read_some()\r
+        if self.debuglevel:\r
+            print ">> 1st banner read is: %s" % banner\r
+        while banner.find(logintext) == -1:\r
+            response, abort = prompt_callback(banner)\r
+            if abort:\r
+                return self.close()\r
+            self.write(response + '\n')\r
+            banner = self.read_some()\r
+        return banner\r
+    \r
+    def logout(self):\r
+        """Logs out the session."""\r
+        self.close()\r
+        \r
+    def sendcmd(self, cmd, readtype=READ_SOME, expected=None, timeout=None):\r
+        """Sends the command 'cmd' over the ssh connection, and returns the\r
+        result.  By default it uses read_some, which may block.\r
+        """\r
+        if cmd[-1] != '\n':\r
+            cmd += '\n'\r
+        self.write(cmd)\r
+        if readtype == READ_ALL:\r
+            return self.read_all()\r
+        elif readtype == READ_LAZY:\r
+            return self.read_lazy()\r
+        elif readtype == READ_UNTIL and expected is not None:\r
+            return self.read_until(expected, timeout)\r
+        else:\r
+            return self.read_some()\r
+    \r
+def test():\r
+    """Test routine for myssh.\r
+    \r
+    Usage: python myssh.py [-d] [-sshp path-to-ssh] [username@host | host] [port]\r
+    \r
+    Default host is localhost, default port is 22.\r
+    """\r
+    import sys\r
+    debug = 0\r
+    if sys.argv[1:] and sys.argv[1] == '-d':\r
+        debug = 1\r
+        del sys.argv[1]\r
+    testsshpath = SSH_PATH\r
+    if sys.argv[1:] and sys.argv[1] == '-sshp':\r
+        testsshpath = sys.argv[2]\r
+        del sys.argv[1]\r
+        del sys.argv[1]\r
+    testusername = None\r
+    testhost = 'localhost'\r
+    testport = '22'\r
+    if sys.argv[1:]:\r
+        testhost = sys.argv[1]\r
+        if testhost.find('@') != -1:\r
+            testusername, testhost = testhost.split('@')\r
+    if sys.argv[2:]:\r
+        testport = sys.argv[2]\r
+        \r
+    testcon = Ssh(testusername, testhost, testport)\r
+    testcon.set_debuglevel(debug)\r
+    testcon.set_sshpath(testsshpath)\r
+    testcon.login()\r
+    \r
+    cmd = None\r
+    while (cmd != 'exit') and testcon.isopen:\r
+        cmd = raw_input("Enter command to send: ")\r
+        print testcon.sendcmd(cmd)\r
+    testcon.close()\r
+\r
+if __name__ == '__main__':\r
+    test()\r
diff --git a/pyssh/fssa.py b/pyssh/fssa.py
new file mode 100644 (file)
index 0000000..6ca17be
--- /dev/null
@@ -0,0 +1,75 @@
+# vi:et:ts=4:tw=0
+""" fssa.py
+
+    Search for an ssh-agent for the calling user and attach to it
+    if found.
+
+    Tested on poxix only
+"""
+# This is a Python port of Steve Allen's fsa.sh script found
+# at http://www.ucolick.org/~sla/ssh/sshcron.html
+# Ported by Mark W. Alexander <slash@dotnetslash.net>
+
+import os
+if os.name == 'posix':
+    import pwd, stat, sys
+    from commands import getoutput
+    def fssa(key=None):
+        """ fssa(key=None)
+
+        Searches /tmp/ssh-* owned by the calling user for ssh-agent
+        sockets. If key is provided, only sockets matching the key will be
+        considered. If key is not provided, the calling users username
+        will be used instead.
+        """
+        user, pw, uid, gid, gecos, home, shell = pwd.getpwuid(os.getuid())
+        if key is None:
+            key = user
+
+        # Find /tmp/ssh-* dirs owned by this user
+        candidate_dirs=[]
+        for filename in os.listdir('/tmp'):
+            file_stat = os.stat("/tmp/%s" % (filename))
+            if file_stat[stat.ST_UID] == os.getuid() \
+            and stat.S_ISDIR(file_stat[stat.ST_MODE]) \
+            and filename.find('ssh-') == 0:
+                candidate_dirs.append("/tmp/%s" % filename)
+
+        candidate_sockets=[]
+        for d in candidate_dirs:
+            for f in os.listdir(d):
+                file_stat = os.stat("%s/%s" % (d, f))
+                if file_stat[stat.ST_UID] == os.getuid() \
+                and stat.S_ISSOCK(file_stat[stat.ST_MODE]) \
+                and f.find('agent.') == 0:
+                    candidate_sockets.append("%s/%s" % (d, f))
+        alive = None
+        # order by pid, prefering sockets where the parent pid
+        # is gone. This gives preference to agents running in the
+        # background and reaped by init (maybe). Only works on
+        # systems with a /proc filesystem
+        if stat.S_ISDIR(os.stat("/proc")[stat.ST_MODE]):
+            reorder=[]
+            for s in candidate_sockets:
+                pid = s[s.find('.')+1:]
+                try:
+                    stat.S_ISDIR(os.stat("/proc/%s" % pid)[stat.ST_MODE])
+                    reorder.append(s)
+                except:
+                    reorder.insert(0,s)
+            candidate_sockets = reorder
+
+        for s in candidate_sockets:
+            os.environ['SSH_AUTH_SOCK'] = s
+            try:
+                pubkey = getoutput("ssh-add -l 2>/dev/null")
+            except:
+                continue
+            if pubkey.find(key):
+                alive = 1
+                break
+            os.environ.pop('SSH_AUTH_SOCK')
+        if alive:
+            return pubkey
+        else:
+            return None
diff --git a/pyssh/nbpipe.py b/pyssh/nbpipe.py
new file mode 100644 (file)
index 0000000..08b4f97
--- /dev/null
@@ -0,0 +1,111 @@
+"""Implements a non-blocking pipe class."""\r
+\r
+# Since it uses thread rather than select, it is portable to at least\r
+# posix and windows environments.\r
+\r
+# Author: Rasjid Wilcox, copyright (c) 2002\r
+# Ideas taken from the Python 2.2 telnetlib.py library.\r
+#\r
+# Last modified: 3 August 2002\r
+# Licence: Python 2.2 Style License.  See license.txt.\r
+\r
+# TO DO:\r
+#     * Handle excpetions better, particularly Keyboard Interupts.\r
+#     * Possibly do a threadless version for posix environments\r
+#       where we can use select (is probably more efficient).\r
+#     * A test function.\r
+\r
+import Queue\r
+import thread\r
+import os\r
+import time\r
+import types\r
+\r
+#INT_TYPE = type(1)\r
+MIN_TIMEOUT = 0.01\r
+\r
+class nbpipe:\r
+    def __init__(self, readfile, pipesize=0, blocksize=1024):\r
+        """Initialise a non-blocking pipe object, given a real file or file-descriptor.\r
+        pipesize = the size (in blocks) of the queue used to buffer the blocks read\r
+        blocksize = the maximum block size for a raw read."""\r
+        if type(readfile) == types.IntType:\r
+            self.fd = readfile\r
+        else:\r
+            self.fd = readfile.fileno()\r
+        self.pipesize = pipesize\r
+        self.blocksize = blocksize\r
+        self.eof = 0\r
+        self._q = Queue.Queue(self.pipesize)\r
+        self.data = ''\r
+        thread.start_new_thread(self._readtoq, ())\r
+    def _readtoq(self):\r
+        finish = 0\r
+        while (1):\r
+            try:\r
+                item = os.read(self.fd, self.blocksize)\r
+            except (IOError, OSError):\r
+                finish = 1\r
+            if (item == '') or finish:\r
+                # Wait until everything has been read from the queue before\r
+                # setting eof = 1 and exiting.\r
+                while not self._q.empty():\r
+                    time.sleep(MIN_TIMEOUT)\r
+                self.eof = 1\r
+                thread.exit()\r
+            else:\r
+                self._q.put(item)\r
+    def has_data(self):\r
+        return self.data\r
+    def eof(self):\r
+        return self.eof\r
+    def read_lazy(self):\r
+        """Process and return data that's already in the queues (lazy).\r
+\r
+        Return '' if no data available. Don't block.\r
+\r
+        """\r
+        while not self._q.empty():\r
+            self.data += self._q.get()\r
+        data = self.data\r
+        self.data = ''\r
+        return data\r
+    def read_some(self, until_eof=False):\r
+        """Read at least one byte of cooked data unless EOF is hit.\r
+\r
+        Return '' if EOF is hit.  Block if no data is immediately\r
+        available.\r
+\r
+        """\r
+        data = ''\r
+        while (until_eof or not data) and not self.eof:\r
+            data += self.read_lazy()\r
+            time.sleep(MIN_TIMEOUT)\r
+        return data\r
+    def read_until(self, match, timeout=None):\r
+        """Read until a given string is encountered or until timeout.\r
+\r
+        If no match is found or EOF is hit, return whatever is\r
+        available instead, possibly the empty string.\r
+        """\r
+        if timeout is not None:\r
+            timeout = timeout / MIN_TIMEOUT\r
+        else:\r
+            timeout = 1\r
+        n = len(match)\r
+        data = self.read_lazy()\r
+        i = 0\r
+        while timeout >= 0 and not self.eof:\r
+            i = data.find(match, i)\r
+            if i >= 0:\r
+                i += n\r
+                self.data = data[i:]\r
+                return data[:i]\r
+            time.sleep(MIN_TIMEOUT)\r
+            timeout -= 1\r
+            i = max(0, len(data) - n)\r
+            data += self.read_lazy()\r
+        return data\r
+    def read_all(self):\r
+        """Read until the EOF. May block."""\r
+        return read_some(until_eof=True)\r
diff --git a/pyssh/ptyext.py b/pyssh/ptyext.py
new file mode 100644 (file)
index 0000000..b22835b
--- /dev/null
@@ -0,0 +1,209 @@
+"""Pseudo terminal utilities."""\r
+\r
+# Bugs: No signal handling.  Doesn't set slave termios and window size.\r
+#       Only tested on Linux.\r
+# See:  W. Richard Stevens. 1992.  Advanced Programming in the\r
+#       UNIX Environment.  Chapter 19.\r
+# Author: Steen Lumholt -- with additions by Guido.\r
+\r
+from select import select, error\r
+import os\r
+\r
+# Absurd:  import termios and then delete it.  This is to force an attempt\r
+# to import pty to raise an ImportError on platforms that lack termios.\r
+# Without this explicit import of termios here, some other module may\r
+# import tty first, which in turn imports termios and dies with an\r
+# ImportError then.  But since tty *does* exist across platforms, that\r
+# leaves a damaged module object for tty in sys.modules, and the import\r
+# of tty here then appears to work despite that the tty imported is junk.\r
+import termios\r
+del termios\r
+\r
+import tty\r
+\r
+__all__ = ["openpty","fork","spawn","th_spawn","popen2"]\r
+\r
+STDIN_FILENO = 0\r
+STDOUT_FILENO = 1\r
+STDERR_FILENO = 2\r
+\r
+CHILD = 0\r
+\r
+def openpty():\r
+    """openpty() -> (master_fd, slave_fd)\r
+    Open a pty master/slave pair, using os.openpty() if possible."""\r
+\r
+    try:\r
+        return os.openpty()\r
+    except (AttributeError, OSError):\r
+        pass\r
+    master_fd, slave_name = _open_terminal()\r
+    slave_fd = slave_open(slave_name)\r
+    return master_fd, slave_fd\r
+\r
+def master_open():\r
+    """master_open() -> (master_fd, slave_name)\r
+    Open a pty master and return the fd, and the filename of the slave end.\r
+    Deprecated, use openpty() instead."""\r
+\r
+    try:\r
+        master_fd, slave_fd = os.openpty()\r
+    except (AttributeError, OSError):\r
+        pass\r
+    else:\r
+        slave_name = os.ttyname(slave_fd)\r
+        os.close(slave_fd)\r
+        return master_fd, slave_name\r
+\r
+    return _open_terminal()\r
+\r
+def _open_terminal():\r
+    """Open pty master and return (master_fd, tty_name).\r
+    SGI and generic BSD version, for when openpty() fails."""\r
+    try:\r
+        import sgi\r
+    except ImportError:\r
+        pass\r
+    else:\r
+        try:\r
+            tty_name, master_fd = sgi._getpty(os.O_RDWR, 0666, 0)\r
+        except IOError, msg:\r
+            raise os.error, msg\r
+        return master_fd, tty_name\r
+    for x in 'pqrstuvwxyzPQRST':\r
+        for y in '0123456789abcdef':\r
+            pty_name = '/dev/pty' + x + y\r
+            try:\r
+                fd = os.open(pty_name, os.O_RDWR)\r
+            except os.error:\r
+                continue\r
+            return (fd, '/dev/tty' + x + y)\r
+    raise os.error, 'out of pty devices'\r
+\r
+def slave_open(tty_name):\r
+    """slave_open(tty_name) -> slave_fd\r
+    Open the pty slave and acquire the controlling terminal, returning\r
+    opened filedescriptor.\r
+    Deprecated, use openpty() instead."""\r
+\r
+    return os.open(tty_name, os.O_RDWR)\r
+\r
+def fork():\r
+    """fork() -> (pid, master_fd)\r
+    Fork and make the child a session leader with a controlling terminal."""\r
+\r
+    try:\r
+        pid, fd = os.forkpty()\r
+    except (AttributeError, OSError):\r
+        pass\r
+    else:\r
+        if pid == CHILD:\r
+            try:\r
+                os.setsid()\r
+            except OSError:\r
+                # os.forkpty() already set us session leader\r
+                pass\r
+        return pid, fd\r
+\r
+    master_fd, slave_fd = openpty()\r
+    pid = os.fork()\r
+    if pid == CHILD:\r
+        # Establish a new session.\r
+        os.setsid()\r
+        os.close(master_fd)\r
+\r
+        # Slave becomes stdin/stdout/stderr of child.\r
+        os.dup2(slave_fd, STDIN_FILENO)\r
+        os.dup2(slave_fd, STDOUT_FILENO)\r
+        os.dup2(slave_fd, STDERR_FILENO)\r
+        if (slave_fd > STDERR_FILENO):\r
+            os.close (slave_fd)\r
+\r
+    # Parent and child process.\r
+    return pid, master_fd\r
+\r
+def _writen(fd, data):\r
+    """Write all the data to a descriptor."""\r
+    while data != '':\r
+        n = os.write(fd, data)\r
+        data = data[n:]\r
+\r
+def _read(fd):\r
+    """Default read function."""\r
+    return os.read(fd, 1024)\r
+\r
+def _copy(master_fd, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO,\r
+             stdout_fd=STDOUT_FILENO):\r
+    """Parent copy loop.\r
+    Copies\r
+            pty master -> stdout_fd     (master_read)\r
+            stdin_fd   -> pty master    (stdin_read)"""\r
+    try:\r
+        mode = tty.tcgetattr(stdin_fd)\r
+        tty.setraw(stdin_fd)\r
+        restore = 1\r
+    except tty.error:    # This is the same as termios.error\r
+        restore = 0\r
+    try:\r
+        while 1:\r
+            rfds, wfds, xfds = select(\r
+                    [master_fd, stdin_fd], [], [])\r
+            if master_fd in rfds:\r
+                data = master_read(master_fd)\r
+                os.write(stdout_fd, data)\r
+            if stdin_fd in rfds:\r
+                data = stdin_read(stdin_fd)\r
+                _writen(master_fd, data)\r
+    except (IOError, OSError, error):  # The last entry is select.error\r
+        if restore:\r
+            tty.tcsetattr(stdin_fd, tty.TCSAFLUSH, mode)\r
+        if stdin_fd > STDERR_FILENO:\r
+            os.close(stdin_fd)\r
+        if stdout_fd > STDERR_FILENO:\r
+            os.close(stdout_fd)\r
+\r
+def spawn(argv, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO,\r
+            stdout_fd=STDOUT_FILENO):\r
+    """Create a spawned process. The controlling terminal reads and\r
+    writes its data to stdin_fd and stdout_fd respectively.\r
+    \r
+    NOTE: This function does not return until one of the input or output file\r
+    descriptors are closed, or the child process exits."""\r
+    if type(argv) == type(''):\r
+        argv = (argv,)\r
+    pid, master_fd = fork()\r
+    if pid == CHILD:\r
+        apply(os.execlp, (argv[0],) + argv)\r
+    _copy(master_fd, master_read, stdin_read, stdin_fd, stdout_fd)\r
+\r
+def th_spawn(argv, master_read=_read, stdin_read=_read, stdin_fd=STDIN_FILENO,\r
+                stdout_fd=STDOUT_FILENO):\r
+    """Create a spawned process. The controlling terminal reads and\r
+    writes its data to stdin_fd and stdout_fd respectively. The function\r
+    returns the pid of the spawned process.  (It returns immediately.)"""\r
+    import thread\r
+    if type(argv) == type(''):\r
+        argv = (argv,)\r
+    pid, master_fd = fork()\r
+    if pid == CHILD:\r
+        apply(os.execlp, (argv[0],) + argv)\r
+    thread.start_new_thread(_copy, (master_fd, master_read, stdin_read, \\r
+                                            stdin_fd, stdout_fd))\r
+    return pid\r
+\r
+def popen2(cmd, bufsize=1024, master_read=_read, stdin_read=_read):\r
+    """Execute the shell command 'cmd' in a sub-process.\r
+    \r
+    If 'bufsize' is specified, it sets the buffer size for the I/O pipes.\r
+    The function returns (child_stdin, child_stdout, child_pid), where the\r
+    file objects are pipes connected to the spawned process's controling\r
+    terminal, and the child_pid is the pid of the child process.\r
+    """\r
+    argv = ('/bin/sh', '-c', cmd)\r
+    child_stdin_rfd, child_stdin_wfd = os.pipe()\r
+    child_stdout_rfd, child_stdout_wfd = os.pipe()\r
+    child_pid = th_spawn(argv, master_read, stdin_read, child_stdin_rfd, \\r
+                            child_stdout_wfd)\r
+    child_stdin = os.fdopen(child_stdin_wfd, 'w', bufsize)\r
+    child_stdout = os.fdopen(child_stdout_rfd, 'r', bufsize)\r
+    return child_stdin, child_stdout, child_pid\r