Python select() not waiting for terminal input after forkpty()
Question: I am trying to write a python script that will automatically log in to a
remote host via ssh and update a users password. Since ssh demands that it
take its input from a terminal, I am using os.forkpty(), running ssh in the
child process and using the parent process to send command input through the
pseudo terminal. Here is what I have so far:

    
    
    import os, sys, time, getpass, select, termios
    
    # Time in seconds between commands sent to tty
    SLEEP_TIME = 1
    NETWORK_TIMEOUT = 15
    
    #------------------------------Get Passwords------------------------------------
    
    # get username
    login = getpass.getuser()
    
    # get current password
    current_pass = getpass.getpass("Enter current password: ")
    
    # get new password, retry if same as current password
    new_pass = current_pass
    first_try = True
    
    while new_pass == current_pass:
        if first_try:
            first_try = False
        else:
            # New password equal to old password
            print("New password must differ from current password.")
    
        # Get new password
        new_pass = getpass.getpass("Enter new password: ")
        new_pass_confirm = getpass.getpass("Confirm new password: ")
        while new_pass != new_pass_confirm:
            # Passwords do not match
            print("Passwords do not match")
            new_pass = getpass.getpass("Enter new password: ")
            new_pass_confirm = getpass.getpass("Confirm new password: ")
    
    #------------------------------End Get Passwords--------------------------------
    
    ssh = "/usr/bin/ssh" # ssh bin location
    args = ["ssh", login + "@localhost", "-o StrictHostKeyChecking=no"]
    
    #fork
    pid, master = os.forkpty()
    if pid == 0:
        # Turn off echo so master does not need to read back its own input
        attrs = termios.tcgetattr(sys.stdin.fileno())
        attrs[3] = attrs[3] & ~termios.ECHO
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attrs)
        os.execv(ssh, args)
    else:
        select.select([master], [], [])
        os.write(master, current_pass + "\n")
        select.select([master], [], [])
        os.write(master, "passwd\n")
        select.select([master], [], [])
        os.write(master, current_pass + "\n")
        select.select([master], [], [])
        os.write(master, new_pass + "\n")
        select.select([master], [], [])
        os.write(master, new_pass + "\n")
        select.select([master], [], [])
        os.write(master, "id\n")
        select.select([master], [], [])
        sys.stdout.write(os.read(master, 2048))
        os.wait()
    

The script prompts the user for his/her current and new passwords, then forks
and sends appropriate responses to ssh login prompt and then passwd prompts.

The problem I am having is that the select syscalls are not behaving as I
would expect. They don't appear to be blocking at all. I'm thinking that I am
misunderstanding something about the way select works with the master end of a
pty.

If I replace them all with time.sleep(1), the script works fine, but I don't
want to have to rely on that solution because I can't always guarantee the
network will respond in a short time, and I don't want to make it something
rediculous that will take forever (I intend to use this to programatically log
into several servers to update passwords)

Is there a way to reliably poll the master side of a pty to wait for the
slave's output?

Note: I realize there are better solutions to this problem with things like
sshpass and chpasswd, but I am in an environment where this cannot be run as
root and very few utilities are available. Thankfully python is.


Answer: `select` doesn't read any data; it simply blocks until data is available to be
read.

Since you don't read any data after the first `select`, there will still be
data left in the buffer for you to read, so any subsequent `select` will not
block.

You need to read the data in the buffer before calling `select` again. Doing
this without blocking means that you will likely have to set the file to non-
blocking mode (I don't know how to do that in Python).

* * *

A better way of providing the password over SSH would be to [use the `--stdin`
flag if your `passwd` supports
it](http://stackoverflow.com/questions/714915/using-the-passwd-command-from-
within-a-shell-script), and to run the command directly over SSH instead of
through the created shell.

    
    
    handle = subprocess.Popen(["ssh", login + "@localhost", "-o StrictHostKeyChecking=no", "passwd --stdin"])
    handle.communicate("\n".join([oldpass, newpass, newpass, ""]))
    

