简体   繁体   中英

How to kill a Windows subprocess in Python when it expects a key but simply doesn't react to it through stdin?

I am trying to kill a subprocess that expects a key press 'q' in the terminal in order to stop gracefully.

This executable has two modes of running (tried both):

  • takes over the terminal with probably some sort of ncurses (considering it is Windows it is probably something else)
  • just runs in the terminal as a regular command and waits for a key press

I have tried spawning the subprocess with subprocess.Popen(command_parts) where command_parts is a list with the executable and it's various flags.

I have added the following arguments to the Popen constructor in multiple combinations:

  • no special flags
  • with creationflags=subprocess.DETACHED_PROCESS
  • with stdin=PIPE

I have tried sending to the stdin of the executable the following strings:

  • b"q"
  • b"q\n"
  • b"q\r\n"

I have tried communicating with the executable in the following ways:

  • subprocess_instance.communicate(input_stuff)
  • subprocess_instance.stdin.write(input_stuff); subprocess_instance.stdin.flush()

None of these attempts results in the executable gracefully shutting down, and just lingers forever as if nothing happened on the stdin.

Observations:

  • the q keystroke works if simply running the executable from power shell
  • the executable has to close gracefully otherwise it results in some undesired behaviour
  • Python versions used: 3.8.*, 3.9.*

UPDATE:

I tried using a sample C program that waits for 'q':

#include <stdio.h>
#include <conio.h>


int main(int argc, char const *argv[]) {
  printf("Hello world\n");

  printf("Waiting for char ...\n");

  while (1) {
    unsigned int x = getch();
    printf("PRESSED: %c\n", x);
    if (x == 'q') {
      printf("Quitting ...\r\n");
      break;
    };
  }

  printf("\n----\n");
  printf("DONE\n\n");
  return 0;
}

And then the script I tried to use to run it is:

import time
import subprocess as sp
import pathlib as plib


def main() -> None:
    print("\nSTARTING")

    print("Create subproces ...");
    exe_path = plib.Path('guinea_pig_executable/bla.exe').absolute()
    exe_path_str = str(exe_path)
    exe = sp.Popen(
        [exe_path_str],
        stdin=sp.PIPE,
        stdout=sp.PIPE,
        stderr=sp.PIPE,
    )

    print("Wait ...")
    time.sleep(5)

    print("Try quitting ...")
    try:
        exe.communicate(b'q\r\n', timeout=2)
        # exe.stdin.write(b'q')
        # exe.stdin.flush()
    except Exception as err:
        print(repr(err))

    print("Wait for sub proc to die ...")
    try:
        exe.wait(timeout=5)
    except sp.TimeoutExpired as terr:
        print("Timeout error", repr(terr))
        exe.kill()  # forcefully killing

    print("\nEND\n---\n")


if __name__ == '__main__':
    main()

The output I get is:

PS E:\workspace\try-kill-exe> python .\main.py

STARTING
Create subproces ...
Wait ...
Try quitting ...
TimeoutExpired(['E:\\workspace\\try-kill-exe\\guinea_pig_executable\\bla.exe'], 2)
Wait for sub proc to die ...
Timeout error TimeoutExpired(['E:\\workspace\\try-kill-exe\\guinea_pig_executable\\bla.exe'], 5)

END
---

What could be the cause for this? Is this something windows specific? Is it something that Python can't handle properly? What other things could I try?

There are multiple ways in which a python script can communicate with a subprocess when it comes to keypresses.

  • pywin32
  • pynput
  • pyautogui
  • ctypes + user32.dll

Examples

PyWin32

(credits to @john-hen -> inspired from https://stackoverflow.com/a/8117562/858565 )

Package: https://pypi.org/project/pywin32/

import win32console


def make_keydown_input(c) -> None:
    input_record = win32console.PyINPUT_RECORDType(win32console.KEY_EVENT)
    input_record.KeyDown = 1
    input_record.RepeatCount = 1
    input_record.Char = c
    return input_record

console_handler = win32console.GetStdHandle(win32console.STD_INPUT_HANDLE)

q_keydown = make_keydown_input('q')
console_handler.WriteConsoleInput([q_keydown])

pynput

Package: https://pypi.org/project/pynput/

import pynput

keyb_controller = pynput.keyboard.Controller()

keyb_controller.press('q')

pyautogui

Package: https://pypi.org/project/PyAutoGUI/

Examples: https://pyautogui.readthedocs.io/en/latest/keyboard.html

This is probably the simplest method.

import pyautogui

pyautogui.press('q')

ctypes + user32.dll

Please refer to this answer: https://stackoverflow.com/a/13615802/858565

In the end you just have to call:

PressKey(0x51)

where 0x51 is the hex keycode for q according to ( https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes )

Observations

Notice that all of these methods will apply the keypress in the context of the program that calls it. I have not tried it with multiprocessing.Process, perhaps it will react differently.

Probably to have a good isolated context one would have to run a specific subprocess in an isolated python script that simply receives messages from outside like via a socket.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM