TheMeaningfulEngineer
TheMeaningfulEngineer

Reputation: 16339

Subprocess polluting the parent terminal when using pty

Example

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

Actual behavior

The strange part is that running the commented out parts (ngrok_cmd = "ls -la" or subprocess with subprocess.DEVNULL) results in expected behavior.

Questions

  1. Why does a child process know how to get access to the parents terminal if stdout/err/in the child was provided with have changed?
  2. How to overcome this in python?

Upvotes: 0

Views: 535

Answers (1)

ottomeister
ottomeister

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

Related Questions