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()