Reputation: 16339
I've noticed this behavior with the cli application ngrok. It's only special for this example because it pollutes the parent process terminal. Its core functionality isn't important.
Getting the executable:
wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
unzip ngrok-stable-linux-amd64.zip
# There is `ngrok` executable in this dir now
The code which creates the issue:
# ngrok_python.py
# It's in the same dir as the ngrok executable
import pty
import subprocess
import shlex
import time
import sys
pty_master, pty_slave = pty.openpty()
ngrok_cmd = "./ngrok http 80"
# This doesn't happen for others
# ngrok_cmd = "ls -la"
ngrok = subprocess.Popen(shlex.split(ngrok_cmd), stdin=pty_slave, stdout=pty_slave, stderr=pty_slave)
# It also won't pollute the current terminal when redirected to DEVNULL
# ngrok = subprocess.Popen(shlex.split(ngrok_cmd), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("The subprocess is attached to the pseudo terminal")
print("Its output should not be printed here")
print("Unless I decide to read from pty_master")
print("Checking a few times if the subprocess is done")
for i in range(3):
time.sleep(1)
if ngrok.poll is not None:
print("Subprocess finished")
sys.exit()
print("Don't want to wait any longer.")
# Running the command
python3 ngrok_python.py
Expected behavior
subprocess
has custom stdout/err/in
. It has not way to access the master terminalpty
if I wanted to read what's going on in the subprocess I would read it from pty_master
Actual behavior
./ngrok http 80
consumes the terminalThe strange part is that running the commented out parts (ngrok_cmd = "ls -la"
or subprocess with subprocess.DEVNULL
) results in expected behavior.
stdout/err/in
the child was provided with have changed?Upvotes: 0
Views: 535
Reputation: 5808
I'm not willing to run an executable downloaded from some random site, but I'm willing to bet that ngrok
explicitly opens and writes to /dev/tty
in order to present its connection information.
The /dev/tty
device refers to the process's "controlling terminal", which is one of the things that a process inherits from its parent. Reassigning the child's stdin
, stdout
and stderr
does not affect its controlling terminal. So in this case the child retains the same controlling terminal as the parent, and when the child opens and writes to /dev/tty
the output goes straight to the parent's screen, not passing through the child's stdout
or stderr
or your pseudo-terminal.
To achieve what you're looking for you need to divorce the child from the parent's controlling terminal and establish the slave end of the pseudo-terminal as the child's controlling terminal. That involves calls to setsid
and/or setpgrp
, some file-descriptor juggling and possibly some other gyrations. All of this is handled by login_tty
if you're working in C.
The good news is that there's a method in the Python pty
module that will do all of these things for you. That method is pty.spawn
. It's available in Python 2 and 3. I've linked to the Python 3 documentation because it's much, much better and includes an example program. pty.spawn
basically behaves as a combination of fork
, exec
, openpty
and login_tty
.
If you rework your program to use pty.spawn
to launch ngrok
then I'm pretty sure you'll get the behaviour you're looking for.
Upvotes: 1