Search code examples
python-3.xsubprocesspipepywin32

Detecting when a child process is waiting for stdin


I am making a terminal program that is able to run any executable (please ignore safety concerns). I need to detect when the child process is waiting for the user input (from stdin). I start the child process using:

process = subprocess.Popen(command, close_fds=False, shell=True, **file_descriptors)

I can think of 2 ways of detecting if the child process is waiting for stdin:

  • Writing a character then backspace and checking if the child has processed those 2 bytes. But here it says that "CMD does support the backspace key". So I need to find a character that when printed to the screen will delete what ever is in the stdin buffer in the command prompt.
  • The second method is to use the pywin32 library and use the WaitForInputIdle function as described here. I looked at the source code for the subprocess library and found that it uses pywin32 and it keeps a reference to the process handle. So I tried this:
win32event.WaitForInputIdle(proc._handle, 100)

But I got this error:

(1471, 'WaitForInputIdle', 'Unable to finish the requested operation because the specified process is not a GUI process.')

Also in the windows api documentation here it says: "WaitForInputIdle waits only once for a process to become idle; subsequent WaitForInputIdle calls return immediately, whether the process is idle or busy.". I think that means that I can't use the function for its purpose more than once which wouldn't solve my problem

Edit: This only needs to work on Windows but later I might try to make my program computable with Linux as well. Also I am using pipes for the stdin/stdout/stderr.

Why I need to know if the child is waiting for stdin:

Currently, when the user presses the enter key, I send all of the data, that they have written so far, to stdin and disable the user from changing it. The problem is when the child process is sleeping/calculating and the user writes some input and wants to change it before the process starts reading from stdin again.

Basically lets take this program:

sleep(10)
input("Enter value:")

and lets say that I enter in "abc\n". When using cmd it will allow me to press backspace and delete the input if the child is still sleeping. Currently my program will mark all of the text as read only when it detects the "\n" and send it to stdin.


Solution

  • class STDINHandle:
        def __init__(self, read_handle, write_handle):
            self.handled_write = False
            self.working = Lock()
            self.write_handle = write_handle
            self.read_handle = read_handle
    
        def check_child_reading(self):
            with self.working:
                # Reset the flag
                self.handled_write = True
                # Write a character that cmd will ignore
                self.write_handle.write("\r")
                thread = Thread(target=self.try_read)
                thread.start()
                sleep(0.1)
                # We need to stop the other thread by giving it data to read
                if self.handled_write:
                    # Writing only 1 "\r" fails for some reason.
                    # For good measure we write 10 "\r"s
                    self.write_handle.write("\r"*10)
                    return True
                return False
    
        def try_read(self):
            data = self.read_handle.read(1)
            self.handled_write = False
    
        def write(self, text):
            self.write_handle.write(text)
    

    I did a bit of testing and I think cmd ignores "\r" characters. I couldn't find a case where cmd will interpret it as an actual character (like what happened when I did "\b"). Sending a "\r" character and testing if it stays in the pipe. If it does stay in the pipe that means that the child hasn't processed it. If we can't read it from the pipe that means that the child has processed it. But we have a problem - we need to stop the read if we can't read from stdin otherwise it will mess with the next write to stdin. To do that we write more "\r"s to the pipe.

    Note: I might have to change the timing on the sleep(0.1) line.