3

There's a console program I want to run from a python script. Let me call it the child. Once in a while, to continue processing, the child expects to read 0 bytes of data from stdin. For simplicity, let's assume the child is the following python script:

child.py
import os
import sys
import time

stdin = sys.stdin.fileno()

def speak(message):
    print(message, flush=True, end="")

while True:
    speak("Please say nothing!")

    data = os.read(stdin, 1024)

    if data == b"":
        speak("Thank you for nothing.")
        time.sleep(5)
    else:
        speak("I won't continue unless you keep silent.")

To work successfully (i.e. seeing "Thank you for nothing." printed), you must typically hit Ctrl+D when running it in a UNIX terminal, while in cmd or PowerShell under Windows hitting Ctrl+Z followed by Enter will do the trick.

Here's an attempt to run the child from inside a python script, which I shall call the parent:

parent.py
import os
import subprocess

def speak_to_child(child_stdin_r, child_stdin_w, message):
    child = subprocess.Popen(
        ["python", "child.py"],
        stdin=child_stdin_r,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )

    child_stdout_r = child.stdout.fileno()

    while True:
        data = os.read(child_stdout_r, 1024)
        print(f"child said: {data}")

        if data == b"Please say nothing!":
            os.write(child_stdin_w, message)


child_stdin_r, child_stdin_w = os.pipe()
speak_to_child(child_stdin_r, child_stdin_w, b"Not sure how to say nothing.")

This is of course an unsuccessful attempt as the child will clearly answer with "I won't continue unless you keep silent." after reading "Not sure how to say nothing." from its stdin.

Naively changing the message b"Not sure how to say nothing." in the parent to the empty message b"" doesn't get rid of the problem, since writing 0 bytes to a pipe won't cause a read of 0 bytes on the receiving end.

Now on UNIX we could easily solve the problem by replacing the pipe with a pseudoterminal and the empty message b"" with an EOT character b"\x04" like so:

import pty

child_stdin_w, child_stdin_r = pty.openpty()
speak_to_child(child_stdin_r, child_stdin_w, b"\x04")

This works ... but evidently not on Windows. So now my questions:

  1. On UNIX, is using a pseudoterminal the best way to force the read of 0 bytes or is there a better way?
  2. On Windows, given that pseudoterminals aren't available, how can I solve the problem?

A platform agnostic solution would of course be ideal.

  • 1
    I assume you have no control over the console program, so you don't have the option of just... not relying on zero byte input as a signal? – Nick Bailey May 23 '22 at 17:49
  • Yes, that's exactly the problem. I already tried sending signals to the console program in question, but unfortunately none of them had the same effect as the zero-byte write. – Printenmonster May 23 '22 at 19:28
  • 1
    I don't think there is a simple solution to this. When you type ctrl-D in the terminal, the terminal driver intercepts that and sends a signal; but when there is no terminal, there is no terminal driver. Perhaps instead implement some sort of timeout mechanism? – tripleee May 24 '22 at 09:56
  • By signal you mean one in the sense of [Signal (IPC)](https://en.wikipedia.org/wiki/Signal_(IPC))? As I mentioned in the previous comment, analyzing the behavior of the console program I got the impression that it is not checking for signals but instead for a zero-byte read. – Printenmonster May 24 '22 at 10:10
  • Regular files can produce reads of 0 bytes and then a non-zero number (if more was written to them), but of course it’s impossible to time that usefully. As for Windows, you have to [get a pty first](https://stackoverflow.com/q/11516258/8586227). – Davis Herring Jul 07 '23 at 20:12

0 Answers0