1

This question is similar to Run interactive Bash with popen and a dedicated TTY Python, except that I want to run Bash in a "dumb" terminal (TERM=dumb), and without putting the tty into raw mode.

The code below is my attempt. The code is similar to the solution given in the linked question, with the major difference that it does not put the tty into raw mode, and sets TERM=dumb.

import os
import pty
import select
import subprocess
import sys

master_fd, slave_fd = pty.openpty()

p = subprocess.Popen(['bash'],
                     stdin=slave_fd,
                     stdout=slave_fd,
                     stderr=slave_fd,
                     # Run in a new process group to enable bash's job control.
                     preexec_fn=os.setsid,
                     # Run bash in "dumb" terminal.
                     env=dict(os.environ, TERM='dumb'))

while p.poll() is None:
    r, w, e = select.select([sys.stdin, master_fd], [], [])
    if sys.stdin in r:
        user_input = os.read(sys.stdin.fileno(), 10240)
        os.write(master_fd, user_input)
    elif master_fd in r:
        output = os.read(master_fd, 10240)
        os.write(sys.stdout.fileno(), output)

There are two problems with the code above:

  • The code will re-echo whatever the user inputs. For example, if the user inputs printf '', the code above will print printf '' on the next line before printing the next bash prompt.
  • Ctrlc and Ctrld do not behave as one would expect in bash.

How should I fix these problems?

Flux
  • 9,805
  • 5
  • 46
  • 92

1 Answers1

0

That's exactly the side effects of not putting the tty in raw mode. Usually a program (like ) which handles pty would put the outer tty in raw mode.

  • Your Python script's tty (or pty) echos what you input and the new pty echos for the 2nd time. You can disable ECHO on the new pty. For example:

    $ python3 using-pty.py
    bash-5.1$ echo hello
    echo hello
    hello
    bash-5.1$ stty -echo
    stty -echo
    bash-5.1$ echo hello    # <-- no double echo any more
    hello
    bash-5.1$ exit
    exit
    
  • Your Python script's tty is not in raw mode so when you press ctrl-d Python would not get the literal ctrl-d ('\004'). Instead, Python would reach EOF and read() returns an empty string. So to make the spawned shell exit you can

    user_input = os.read(sys.stdin.fileno(), 10240)
    if not user_input:
         # explicitly send ctrl-d to the spawned process
         os.write(master_fd, b'\04')
    else:
         os.write(master_fd, user_input)
    
  • Similarly, the Python's tty is not in raw mode so when you press ctrl-c, it'll not get the literal ctrl-c ('\003'). Instead it's killed. As a workaround you can catch SIGINT.

    def handle_sigint(signum, stack):
        global master_fd
        # send ctrl-c
        os.write(master_fd, b'\03')
    signal.signal(signal.SIGINT, handle_sigint)
    
pynexj
  • 19,215
  • 5
  • 38
  • 56
  • 1
    I found that I can use termios to disable ECHO like this: `import termios`; `slave_attr = termios.tcgetattr(slave_fd)`; `slave_attr[3] = slave_attr[3] & ~termios.ECHO`; `termios.tcsetattr(slave_fd, termios.TCSADRAIN, slave_attr)`. – Flux Nov 13 '21 at 10:25
  • With your SIGINT handler, why does Ctrl-c cause `^C^C` to be displayed instead of only `^C` (as would be expected from `TERM=dumb bash`)? – Flux Nov 13 '21 at 10:28
  • each pty outputs a ^C – pynexj Nov 13 '21 at 11:00
  • Okay, I found that I must do `stty -echoctl` to prevent the echo of a second `^C`. – Flux Nov 13 '21 at 11:07
  • Suppose I replace `['bash']` with `['ssh', 'user@example.com']`. Why does Ctrl-c cause the script to exit instead of displaying the next prompt in the SSH session? – Flux Nov 13 '21 at 11:49
  • with or without the SIGINT handler? – pynexj Nov 13 '21 at 12:00
  • Oh, never mind. There is no problem. I was looking at the wrong code. – Flux Nov 13 '21 at 12:31