When I run the following code on OSX or Linux and then press ctrl+c
a "graceful shutdown" is initiated. Which looks something like this:
$ python subprocess_test.py
Subprocess: <MyProcess(MyProcess-1, started)>
^CMain: Graceful shutdown
Subprocess: shutdown
However, when I run the some code on a Windows10 machine a KeyboardInterrupt
is raised in line self.event.wait()
preventing a graceful shutdown. I have tried different approaches as described here to prevent that the subprocess is receiving the signal.
What is the correct way to to get the same behavior on the different OS using Python 2.7?
import multiprocessing
import signal
class MyProcess(multiprocessing.Process):
def __init__(self):
super(MyProcess, self).__init__()
self.event = multiprocessing.Event()
def run(self):
print "Subprocess: ", multiprocessing.current_process()
self.event.wait()
print "Subprocess: shutdown"
def sighandler(a,b,):
print "Main: Graceful shutdown"
p1.event.set()
def run():
signal.signal(signal.SIGINT, signal.SIG_IGN)
global p1
p1 = MyProcess()
p1.start()
signal.signal(signal.SIGINT, sighandler)
p1.join()
if __name__ == '__main__':
run()
On Windows SIGINT
is implemented using a console control event handler for CTRL_C_EVENT
. It's the console state that gets inherited by a child process, not the CRT's signal handling state. Thus you need to first call SetConsoleCtrlHandler
to ignore Ctrl+C in the parent process before creating a child process if you want the child to ignore Ctrl+C.
There's a catch. Python doesn't use alertable waits on Windows, such as the wait in the process join
method. Since it dispatches signal handlers on the main thread, the fact that the main thread is blocked in join()
means your signal handler will never be called. You have to replace the join with a loop on time.sleep()
, which is interruptible by Ctrl+C because internally it waits on a Windows Event and sets its own control handler that sets this Event. Or you can instead use your own asynchronous control handler set via ctypes. The following example implements both approaches and works in both Python 2 and 3.
import sys
import signal
import multiprocessing
if sys.platform == "win32":
# Handle Ctrl+C in the Windows Console
import time
import errno
import ctypes
import threading
from ctypes import wintypes
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(
wintypes.BOOL,
wintypes.DWORD) # _In_ dwCtrlType
win_ignore_ctrl_c = PHANDLER_ROUTINE() # alias for NULL handler
def _errcheck_bool(result, func, args):
if not result:
raise ctypes.WinError(ctypes.get_last_error())
return args
kernel32.SetConsoleCtrlHandler.errcheck = _errcheck_bool
kernel32.SetConsoleCtrlHandler.argtypes = (
PHANDLER_ROUTINE, # _In_opt_ HandlerRoutine
wintypes.BOOL) # _In_ Add
class MyProcess(multiprocessing.Process):
def __init__(self):
super(MyProcess, self).__init__()
self.event = multiprocessing.Event()
def run(self):
print("Subprocess: %r" % multiprocessing.current_process())
self.event.wait()
print("Subprocess: shutdown")
if sys.platform == "win32":
def join(self, timeout=None):
if threading.current_thread().name != "MainThread":
super(MyProcess, self).join(timeout)
else:
# use time.sleep to allow the main thread to
# interruptible by Ctrl+C
interval = 1
remaining = timeout
while self.is_alive():
if timeout is not None:
if remaining <= 0:
break
if remaining < interval:
interval = remaining
remaining = 0
else:
remaining -= interval
try:
time.sleep(interval)
except IOError as e:
if e.errno != errno.EINTR:
raise
break
def run():
p1 = MyProcess()
# Ignore Ctrl+C, which is inherited by the child process.
if sys.platform == "win32":
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
signal.signal(signal.SIGINT, signal.SIG_IGN)
p1.start()
# Set a Ctrl+C handler to signal graceful shutdown.
if sys.platform == "win32":
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, False)
# comment out the following to rely on sig_handler
# instead. Note that using the normal sig_handler requires
# joining using a loop on time.sleep() instead of the
# normal process join method. See the join() method
# defined above.
@PHANDLER_ROUTINE
def win_ctrl_handler(dwCtrlType):
if (dwCtrlType == signal.CTRL_C_EVENT and
not p1.event.is_set()):
print("Main <win_ctrl_handler>: Graceful shutdown")
p1.event.set()
return False
kernel32.SetConsoleCtrlHandler(win_ctrl_handler, True)
def sig_handler(signum, frame):
if not p1.event.is_set():
print("Main <sig_handler>: Graceful shutdown")
p1.event.set()
signal.signal(signal.SIGINT, sig_handler)
p1.join()
if __name__ == "__main__":
run()
Using win32api.SetConsoleCtrlHandler
from pywin32 one can control how Windows the interrupts. Using SetConsoleCtrlHandler(None, True)
causes the calling process to ignore CTRL+C input. With SetConsoleCtrlHandler(sighandler, True)
a specific handler can be registered.
Putting it all together the issue is addressed like this:
import multiprocessing
import signal
import sys
class MyProcess(multiprocessing.Process):
def __init__(self):
super(MyProcess, self).__init__()
self.event = multiprocessing.Event()
def run(self):
if sys.platform == "win32":
import win32api # ignoring the signal
win32api.SetConsoleCtrlHandler(None, True)
print "Subprocess: ", multiprocessing.current_process()
self.event.wait()
print "Subprocess: shutdown"
def sighandler(a,b=None):
print "Main: Graceful shutdown"
p1.event.set()
def run():
signal.signal(signal.SIGINT, signal.SIG_IGN)
global p1
p1 = MyProcess()
p1.start()
if sys.platform == "win32":
import win32api
win32api.SetConsoleCtrlHandler(sighandler, True)
else:
signal.signal(signal.SIGINT, sighandler)
p1.join()
if __name__ == '__main__':
run()
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.