6

I'm using Python to call a C++ program using the subprocess module. Since the program takes some time to run, I'd like to be able to terminate it using Ctrl+C. I've seen a few questions regarding this on StackOverflow but none of the solutions seem to work for me.

What I would like is for the subprocess to be terminated on KeyboardInterrupt. This is the code that I have (similar to suggestions in other questions):

import subprocess

binary_path = '/path/to/binary'
args = 'arguments' # arbitrary

call_str = '{} {}'.format(binary_path, args)

proc = subprocess.Popen(call_str)

try:
    proc.wait()
except KeyboardInterrupt:
    proc.terminate()

However, if I run this, the code is hung up waiting for the process to end and never registers the KeyboardInterrupt. I have tried the following as well:

import subprocess
import time

binary_path = '/path/to/binary'
args = 'arguments' # arbitrary

call_str = '{} {}'.format(binary_path, args)

proc = subprocess.Popen(call_str)
time.sleep(5)
proc.terminate()

This code snippet works fine at terminating the program, so it's not the actual signal that's being sent to terminate that is the problem.

How can I change the code so that the subprocess can be terminated on KeyboardInterrupt?

I'm running Python 2.7 and Windows 7 64-bit. Thanks in advance!

Some related questions that I tried:

Python sub process Ctrl+C

Kill subprocess.call after KeyboardInterrupt

kill subprocess when python process is killed?

Community
  • 1
  • 1
limi44
  • 288
  • 3
  • 10
  • Why not catch the ctrl-c signal in your main program and use that to call `proc.terminate()`? – CoconutBandit Sep 14 '16 at 21:40
  • In Python 3 you could use `_winapi.WaitForMultipleObjects([proc._handle], False, -1)`. For the main thread and given the wait-all flag is false, this wait automatically includes Python's `SIGINT` event. In 2.x you'd have to implement this from scratch using ctypes or PyWin32. – Eryk Sun Sep 15 '16 at 02:17
  • @eryksun Thanks for the suggestion. There is a module called win32event that has a similar function and can be imported for Python 2, but I tried `win32event.WaitForMultipleObjects([proc._handle], False, -1)` and it didn't make a difference. – limi44 Sep 15 '16 at 05:27
  • @limi44, 3.x use a Windows Event object that gets set by the signal handler for Ctrl+C. This event gets automatically added to the list when `_winapi.WaitForMultipleObjects` is called. Implementing this in 2.x requires PyWin32 or ctypes to define a console `CTRL_C_EVENT` handler (e.g. passing a ctypes callback function to `SetConsoleCtrlHandler`) that sets an Event (created via `CreateEvent`). You'd then wrap `WaitForMultipleObjects` to append the Ctrl+C event handle to the list and reset the event (`ResetEvent`) before waiting, but only when called from the main thread. – Eryk Sun Sep 15 '16 at 05:41

2 Answers2

8

I figured out a way to do this, similar to Jean-Francois's answer with the loop but without the multiple threads. The key is to use Popen.poll() to determine if the subprocess has finished (will return None if still running).

import subprocess
import time

binary_path = '/path/to/binary'
args = 'arguments' # arbitrary

call_str = '{} {}'.format(binary_path, args)

proc = subprocess.Popen(call_str)

try:
    while proc.poll() is None:
        time.sleep(0.1)

except KeyboardInterrupt:
    proc.terminate()
    raise

I added an additional raise after KeyboardInterrupt so the Python program is also interrupted in addition to the subprocess.

EDIT: Changed pass to time.sleep(0.1) as per eryksun's comment to reduce CPU consumption.

limi44
  • 288
  • 3
  • 10
2

My ugly but successfull attempt on Windows:

import subprocess
import threading

import time

binary_path = 'notepad'
args = 'foo.txt' # arbitrary

proc = None
done = False

def s():
    call_str = '{} {}'.format(binary_path, args)
    global done
    global proc
    proc = subprocess.Popen(call_str,stdout=subprocess.PIPE)
    proc.wait()
    done = True


t = threading.Thread(target=s)
t.start()


try:
    while not done:
        time.sleep(0.1)

except KeyboardInterrupt:
    print("terminated")
    proc.terminate()

Create a thread where the subprocess runs. Export the proc variable.

Then wait forever in a non-active loop. When CTRL+C is pressed, the exception is triggered. Inter-process communication (ex: proc.wait()) is conflicting with CTRL+C handling otherwise. When running in a thread, no such issue.

note: I have tried to avoid this time loop using threading.lock() but stumbled on the same CTRL+C ignore.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219