Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 442
Florian Margaine Avatar asked Nov 09 '22 14:11

Florian Margaine


1 Answers

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)
like image 144
Florian Margaine Avatar answered Nov 14 '22 22:11

Florian Margaine