Florian Margaine Florian Margaine - 6 months ago 27
Linux Question

How do I reproduce `stdin=sys.stdin` with `stdin=PIPE`?

I have the following code that works exactly as intended:

from subprocess import Popen

process = Popen(
["/bin/bash"],
stdin=sys.stdin,
stdout=sys.stdout,
stderr=sys.stderr,
)
process.wait()


I can interactively use bash, tab works, etc.

However, I want to control what I send to stdin, so I'd like the following to work:

import os
import sys
from subprocess import Popen, PIPE
from select import select

process = Popen(
["/bin/bash"],
stdin=PIPE,
stdout=sys.stdout,
stderr=sys.stderr,
)

while True:
if process.poll() is not None:
break

r, _, _ = select([sys.stdin], [], [])

if sys.stdin in r:
stdin = os.read(sys.stdin.fileno(), 1024)
# Do w/e I want with stdin
os.write(process.stdin.fileno(), stdin)

process.wait()


But the behavior just isn't the same. I've tried another approach (going through a pty):

import os
import sys
import tty
from subprocess import Popen
from select import select

master, slave = os.openpty()
stdin = sys.stdin.fileno()

try:
tty.setraw(master)
ttyname = os.ttyname(slave)

def _preexec():
os.setsid()
open(ttyname, "r+")

process = Popen(
args=["/bin/bash"],
preexec_fn=_preexec,
stdin=slave,
stdout=sys.stdout,
stderr=sys.stderr,
close_fds=True,
)

while True:
if process.poll() is not None:
break

r, _, _ = select([sys.stdin], [], [])

if sys.stdin in r:
os.write(master, os.read(stdin, 1024))
finally:
os.close(master)
os.close(slave)


And the behavior is pretty close, except tab still doesn't work. Well, tab is properly sent, but my terminal doesn't show the completion, even though it was done by bash. Arrows also show
^[[A
instead of going through history.

Any idea?

Answer

All I needed was setting my sys.stdout to raw. I also found out 3 things:

  • I need to restore the terminal settings on sys.stdout
  • subprocess.Popen has a start_new_session argument that does what my _preexec function is doing.
  • select.select accepts a 4th argument, which is a timeout before giving up. It lets me avoid being stuck in the select loop after exiting.

Final code:

import os
import sys
import tty
import termios
import select
import subprocess

master, slave = os.openpty()
stdin = sys.stdin.fileno()

try:
    old_settings = termios.tcgetattr(sys.stdout)
    tty.setraw(sys.stdout)

    process = subprocess.Popen(
        args=["/bin/bash"],
        stdin=slave,
        stdout=sys.stdout,
        stderr=sys.stderr,
        close_fds=True,
        start_new_session=True,
    )

    while True:
        if process.poll() is not None:
            break

        r, _, _ = select.select([sys.stdin], [], [], 0.2)

        if sys.stdin in r:
            os.write(master, os.read(stdin, 1024))
finally:
    termios.tcsetattr(sys.stdout, termios.TCSADRAIN, old_settings)
    os.close(master)
    os.close(slave)