简体   繁体   中英

Ignore SIGINT in Python multiprocessing Subprocess

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.

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