1

I am making a terminal command line interface program as part of a bigger project. I want the user to be able to run arbitrary commands (like in cmd). The problem is that when I start a python process using subprocess, python doesn't write anything to stdout. I am not even sure if it reads what I wrote in stdin. This is my code:

from os import pipe, read, write
from subprocess import Popen
from time import sleep

# Create the stdin/stdout pipes
out_read_pipe_fd, out_write_pipe_fd = pipe()
in_read_pipe_fd, in_write_pipe_fd = pipe()

# Start the process
proc = Popen("python", stdin=in_read_pipe_fd, stdout=out_write_pipe_fd,
             close_fds=True, shell=True)

# Make sure the process started
sleep(2)

# Write stuff to stdin
write(in_write_pipe_fd, b"print(\"hello world\")\n")

# Read all of the data written to stdout 1 byte at a time
print("Reading:")
while True:
    print(repr(read(out_read_pipe_fd, 1)))

The code above works when I change "python" to "myexe.exe" where myexe.exe is my hello world program written in C++ compiled by MinGW. Why does this happen? This is the full code but the above example shows my problem. It also works correctly when I change "python" to "cmd".

PS: when I run python from the command prompt it gives me:

Python 3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 18:58:18) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

That means that there should be stuff written to stdout.

kmaork
  • 5,722
  • 2
  • 23
  • 40
TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • Are you sure the REPL output is stdout? – OneCricketeer Mar 23 '21 at 13:21
  • @OneCricketeer Well I assumed that the python process should be writing to stdout because it writes stuff to the screen when I run it from cmd. Is that what you mean? – TheLizzard Mar 23 '21 at 13:23
  • stderr also prints to the screen, not only stdout – OneCricketeer Mar 23 '21 at 13:25
  • 1
    @OneCricketeer I checked stderr it's also empty – TheLizzard Mar 23 '21 at 13:25
  • 1
    So, you're never sending an "enter key" / line break for the REPL to actually do anything, so why not just use `eval()` instead of a subshell? – OneCricketeer Mar 23 '21 at 13:29
  • @OneCricketeer good point with me not sending `"\n"`. Still doesn't work. I even tried `"\r\n"`. Also the user might run any arbitrary command not just python commands. Mostly I am using it to run ".exe" files but I want the user to be able to run any command even commands like: cmd (which works with the code above) – TheLizzard Mar 23 '21 at 13:34
  • You could try `python -c` to evaluate a block of code, too https://stackoverflow.com/a/27157703/2308683 – OneCricketeer Mar 23 '21 at 13:36
  • @OneCricketeer I want to redirect the new process' stdout. The first argument of `Popen` is controlled by the end user. I want to get the REPL interface in a pipe that I can read and display using tkinter. Changing the argument isn't going to work for me. Also I want to know **why** it happens because my program is able to run any other process. – TheLizzard Mar 23 '21 at 13:38
  • I assume the CMD explicitly handles a key event for Enter, otherwise explicitly writing a literal `\n` is allowed, but doesn't do anything – OneCricketeer Mar 23 '21 at 13:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/230266/discussion-between-thelizzard-and-onecricketeer). – TheLizzard Mar 23 '21 at 13:39
  • Any particular reason that you're creating the pipes yourself, rather than passing `subprocess.PIPE` to `Popen()`? – jasonharper Mar 23 '21 at 14:17
  • @jasonharper I don't really get the difference between me creating my own pipes and using `subprocess.PIPE`. Would it make a difference when calling `python`? – TheLizzard Mar 23 '21 at 16:37
  • If you call, for example `python -h` it works. And, in Linux, if you execute `y`, it fails as with python. The problem with the code is not python itself, but the fact the executed command does not exit. `ping google.com -n 2` vs `ping google.com` (-n for windows, -c for Linux) is a good minimal test case. – dataista Mar 27 '21 at 11:49
  • 1
    On the other hand, sending exit()\n to the stdin -instead of hello world - didn't close the execution, so that probably isn't working either. – dataista Mar 27 '21 at 11:49

2 Answers2

2

The Problem

Note that you are connecting python to a non-tty standard input, and so it behaves differently from when you just run the command python in your terminal. It instead behaves as if you used the command cat script | python, which means it waits until stdin is closed, and then executes everything as a single script. This behavior is described in the docs:

The interpreter operates somewhat like the Unix shell: when called with standard input connected to a tty device, it reads and executes commands interactively; when called with a file name argument or with a file as standard input, it reads and executes a script from that file.

Try adding close(in_write_pipe_fd) before reading, and you'll see that it succeeds.

Solution 1: force python to run interactively

To solve your problem, we're gonna need python to ignore the fact it is not run interactively. When running python --help you might notice the flag -i:

-i     : inspect interactively after running script; forces a prompt even
         if stdin does not appear to be a terminal; also PYTHONINSPECT=x

Sounds good :) Just change your Popen call to:

Popen("python -i", stdin=in_read_pipe_fd, stdout=out_write_pipe_fd,
      close_fds=True, shell=True)

And stuff should start working as expected.

Solution 2: pretend to be a terminal

You might have heard of pty, a pseudo-terminal device. It is a feature in a few operating systems that allows you to connect a pipe to a tty driver instead of a terminal emulator, thus, in your case, allowing you write a terminal emulator yourself. You can use the python module pty to open one and connect it to the subprocess instead of a normal pipe. That will trick python to think it is connected to an actual tty device, and will also allow you to emulate Ctrl-C presses, arrow-up/arrow-down, and more.

But that comes with a price - some programs, when connected to a tty, will also alter their output accordingly. For example, in many linux distributions, the grep command colors the matched pattern in the output. If you don't make sure you can handle colors correctly in your program, or configure the tty to declare it doesn't support colors (and other tty features), you'll start getting garbage in some command's outputs.

Small note

I do feel like this might not be the best method to achieve your goal. If you describe it more in detail I might be able to help you think of an alternative :)

kmaork
  • 5,722
  • 2
  • 23
  • 40
  • I confirmed that this is the problem. Adding `close(in_write_pipe_fd)` works. Is there a way to make the `stdin` tty? What does it mean for a pipe to be tty? – TheLizzard Mar 28 '21 at 09:59
  • Great. Why not just use the `-i` flag when running python? – kmaork Mar 28 '21 at 10:01
  • In my terminal the user can run any arbitrary command so my terminal is basically cmd in tkinter. It will be better if I don't have to force the user to use the `-i` flag. I still think that this is the answer I was looking for and in 1h I will give you the +100 reputation. – TheLizzard Mar 28 '21 at 10:06
  • You just have to run the subprocess with that flag, like so: `Popen("python -i", stdin=in_read_pipe_fd, stdout=out_write_pipe_fd, close_fds=True, shell=True)` – kmaork Mar 28 '21 at 10:08
  • In my terminal I run `"cmd"` and the user can interact with it. It works when I camplie/run my C++ files but when I run `python` it didn't put anything on my screen. I will just force the user to use the `-i` flag when calling python. – TheLizzard Mar 28 '21 at 10:11
  • I understand now. I edited my answer and added an additional solution. – kmaork Mar 28 '21 at 10:27
  • Thanks so much. I can handle the special characters that come with tty. By the way you also solved my [other question](https://stackoverflow.com/questions/66829864/sending-ctrl-c-to-a-child-process). If you want to type an answer there I will upvote it and accept it. – TheLizzard Mar 28 '21 at 10:31
  • With pleasure :) it does sound like a pty would solve your problem there, but sadly windows doesn't have real support for it yet (jesus microsoft it's 2021). – kmaork Mar 28 '21 at 10:49
  • Well I found [this](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/). I think that it means that Windows supports pty pipes. If that doesn't work I will have to just stick to forcing the user to pass in `-i` each time they call `python`. Also I was wandering why `g++` (MinGW) was colouring the text when run from `cmd` but not from my program but now I know. I will give you the +100 reputation in 32 min (stackoverflow cooldown) – TheLizzard Mar 28 '21 at 10:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/230471/discussion-between-kmaork-and-thelizzard). – kmaork Mar 28 '21 at 12:50
0

The python interpreter is more often used to run scripts from the command line than in interactive mode, therefore its interactive elements are not written to stdout else they would interfere with script output. Nobody wants to have to remove the introductory text from the script output.

To facilitate this, when interacting with the user, the interpreter uses the sys.displayhook method to deliberately send output to stdout otherwise nothing goes to stdout. The rest (e.g. the intro text, and >>> prompt) are written to stderr according to the docs:

  • stdin is used for all interactive input (including calls to input());
  • stdout is used for the output of print() and expression statements and for the prompts of input();
  • The interpreter’s own prompts and its error messages go to stderr.
scign
  • 812
  • 5
  • 15
  • That looks like a plausible explanation but why doesn't python respond to `print("Hello world")\n` being written to `stdin`? Also `stderr` is also empty. – TheLizzard Mar 27 '21 at 20:57
  • I wonder if the interactive interface is using different streams than the underlying interpreter? `subprocess.run("python", input=b"print('hello')")` works as expected. – scign Mar 28 '21 at 06:00
  • That is possible but wouldn't that mean that I can't run `python` from `cmd`. I think `cmd` only listens on `stdout`/`stderr` so if python uses its own streams `cmd` shouldn't display anything on the screen. Also IDLE catches the processes `stdout` and `stderr`. I will try to read IDLE's source code. – TheLizzard Mar 28 '21 at 09:27