From f5b6f943110955544a35c1ab35e677609e79f7e4 2010-09-14 22:58:20 From: epatters Date: 2010-09-14 22:58:20 Subject: [PATCH] Implemented kernel interrupts for Windows. --- diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 4ea3439..e7885b8 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -2,7 +2,6 @@ from __future__ import print_function # Standard library imports from collections import namedtuple -import signal import sys # System library imports @@ -391,7 +390,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): if self.custom_interrupt: self.custom_interrupt_requested.emit() elif self.kernel_manager.has_kernel: - self.kernel_manager.signal_kernel(signal.SIGINT) + self.kernel_manager.interrupt_kernel() else: self._append_plain_text('Kernel process is either remote or ' 'unspecified. Cannot interrupt.\n') diff --git a/IPython/zmq/entry_point.py b/IPython/zmq/entry_point.py index b20c6b1..31ca70b 100644 --- a/IPython/zmq/entry_point.py +++ b/IPython/zmq/entry_point.py @@ -15,11 +15,11 @@ import zmq from IPython.core.ultratb import FormattedTB from IPython.external.argparse import ArgumentParser from IPython.utils import io -from exitpoller import ExitPollerUnix, ExitPollerWindows from displayhook import DisplayHook +from heartbeat import Heartbeat from iostream import OutStream +from parentpoller import ParentPollerUnix, ParentPollerWindows from session import Session -from heartbeat import Heartbeat def bind_port(socket, ip, port): """ Binds the specified ZMQ socket. If the port is zero, a random port is @@ -51,6 +51,9 @@ def make_argument_parser(): help='set the heartbeat port [default: random]') if sys.platform == 'win32': + parser.add_argument('--interrupt', type=int, metavar='HANDLE', + default=0, help='interrupt this process when ' + 'HANDLE is signaled') parser.add_argument('--parent', type=int, metavar='HANDLE', default=0, help='kill this process if the process ' 'with HANDLE dies') @@ -118,12 +121,13 @@ def make_kernel(namespace, kernel_factory, def start_kernel(namespace, kernel): """ Starts a kernel. """ - # Configure this kernel/process to die on parent termination, if necessary. - if namespace.parent: - if sys.platform == 'win32': - poller = ExitPollerWindows(namespace.parent) - else: - poller = ExitPollerUnix() + # Configure this kernel process to poll the parent process, if necessary. + if sys.platform == 'win32': + if namespace.interrupt or namespace.parent: + poller = ParentPollerWindows(namespace.interrupt, namespace.parent) + poller.start() + elif namespace.parent: + poller = ParentPollerUnix() poller.start() # Start the kernel mainloop. @@ -205,6 +209,9 @@ def base_launch_kernel(code, xrep_port=0, pub_port=0, req_port=0, hb_port=0, # Spawn a kernel. if sys.platform == 'win32': + # Create a Win32 event for interrupting the kernel. + interrupt_event = ParentPollerWindows.create_interrupt_event() + arguments += [ '--interrupt', str(int(interrupt_event)) ] # If using pythonw, stdin, stdout, and stderr are invalid. Popen will # fail unless they are suitably redirected. We don't read from the @@ -212,7 +219,8 @@ def base_launch_kernel(code, xrep_port=0, pub_port=0, req_port=0, hb_port=0, redirect = PIPE if sys.executable.endswith('pythonw.exe') else None if independent: - proc = Popen(['start', '/b'] + arguments, shell=True, + proc = Popen(arguments, + creationflags=512, # CREATE_NEW_PROCESS_GROUP stdout=redirect, stderr=redirect, stdin=redirect) else: from _subprocess import DuplicateHandle, GetCurrentProcess, \ @@ -224,6 +232,9 @@ def base_launch_kernel(code, xrep_port=0, pub_port=0, req_port=0, hb_port=0, proc = Popen(arguments + ['--parent', str(int(handle))], stdout=redirect, stderr=redirect, stdin=redirect) + # Attach the interrupt event to the Popen objet so it can be used later. + proc.win32_interrupt_event = interrupt_event + # Clean up pipes created to work around Popen bug. if redirect is not None: proc.stdout.close() diff --git a/IPython/zmq/exitpoller.py b/IPython/zmq/exitpoller.py deleted file mode 100644 index 6f55703..0000000 --- a/IPython/zmq/exitpoller.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import time -from threading import Thread - - -class ExitPollerUnix(Thread): - """ A Unix-specific daemon thread that terminates the program immediately - when the parent process no longer exists. - """ - - def __init__(self): - super(ExitPollerUnix, self).__init__() - self.daemon = True - - def run(self): - # We cannot use os.waitpid because it works only for child processes. - from errno import EINTR - while True: - try: - if os.getppid() == 1: - os._exit(1) - time.sleep(1.0) - except OSError, e: - if e.errno == EINTR: - continue - raise - -class ExitPollerWindows(Thread): - """ A Windows-specific daemon thread that terminates the program immediately - when a Win32 handle is signaled. - """ - - def __init__(self, handle): - super(ExitPollerWindows, self).__init__() - self.daemon = True - self.handle = handle - - def run(self): - from _subprocess import WaitForSingleObject, WAIT_OBJECT_0, INFINITE - result = WaitForSingleObject(self.handle, INFINITE) - if result == WAIT_OBJECT_0: - os._exit(1) \ No newline at end of file diff --git a/IPython/zmq/kernelmanager.py b/IPython/zmq/kernelmanager.py index aac5ed1..d1d6b75 100644 --- a/IPython/zmq/kernelmanager.py +++ b/IPython/zmq/kernelmanager.py @@ -1,8 +1,6 @@ """Base classes to manage the interaction with a running kernel. -Todo -==== - +TODO * Create logger to handle debugging and console messages. """ @@ -20,6 +18,7 @@ Todo # Standard library imports. from Queue import Queue, Empty from subprocess import Popen +import signal import sys from threading import Thread import time @@ -804,8 +803,23 @@ class KernelManager(HasTraits): else: raise RuntimeError("Cannot kill kernel. No kernel is running!") + def interrupt_kernel(self): + """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is + well supported on all platforms. + """ + if self.has_kernel: + if sys.platform == 'win32': + from parentpoller import ParentPollerWindows as Poller + Poller.send_interrupt(self.kernel.win32_interrupt_event) + else: + self.kernel.send_signal(signal.SIGINT) + else: + raise RuntimeError("Cannot interrupt kernel. No kernel is running!") + def signal_kernel(self, signum): - """ Sends a signal to the kernel. """ + """ Sends a signal to the kernel. Note that since only SIGTERM is + supported on Windows, this function is only useful on Unix systems. + """ if self.has_kernel: self.kernel.send_signal(signum) else: diff --git a/IPython/zmq/parentpoller.py b/IPython/zmq/parentpoller.py new file mode 100644 index 0000000..5e0e009 --- /dev/null +++ b/IPython/zmq/parentpoller.py @@ -0,0 +1,125 @@ +# Standard library imports. +import ctypes +import os +import time +from thread import interrupt_main +from threading import Thread + +# Local imports. +from IPython.utils.io import raw_print + + +class ParentPollerUnix(Thread): + """ A Unix-specific daemon thread that terminates the program immediately + when the parent process no longer exists. + """ + + def __init__(self): + super(ParentPollerUnix, self).__init__() + self.daemon = True + + def run(self): + # We cannot use os.waitpid because it works only for child processes. + from errno import EINTR + while True: + try: + if os.getppid() == 1: + raw_print('Killed by parent poller!') + os._exit(1) + time.sleep(1.0) + except OSError, e: + if e.errno == EINTR: + continue + raise + + +class ParentPollerWindows(Thread): + """ A Windows-specific daemon thread that listens for a special event that + signals an interrupt and, optionally, terminates the program immediately + when the parent process no longer exists. + """ + + def __init__(self, interrupt_handle=None, parent_handle=None): + """ Create the poller. At least one of the optional parameters must be + provided. + + Parameters: + ----------- + interrupt_handle : HANDLE (int), optional + If provided, the program will generate a Ctrl+C event when this + handle is signaled. + + parent_handle : HANDLE (int), optional + If provided, the program will terminate immediately when this + handle is signaled. + """ + assert(interrupt_handle or parent_handle) + super(ParentPollerWindows, self).__init__() + self.daemon = True + self.interrupt_handle = interrupt_handle + self.parent_handle = parent_handle + + @staticmethod + def create_interrupt_event(): + """ Create an interrupt event handle. + + The parent process should use this static method for creating the + interrupt event that is passed to the child process. It should store + this handle and use it with ``send_interrupt`` to interrupt the child + process. + """ + # Create a security attributes struct that permits inheritance of the + # handle by new processes. + # FIXME: We can clean up this mess by requiring pywin32 for IPython. + class SECURITY_ATTRIBUTES(ctypes.Structure): + _fields_ = [ ("nLength", ctypes.c_int), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", ctypes.c_int) ] + sa = SECURITY_ATTRIBUTES() + sa_p = ctypes.pointer(sa) + sa.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + sa.lpSecurityDescriptor = 0 + sa.bInheritHandle = 1 + + return ctypes.windll.kernel32.CreateEventA( + sa_p, # lpEventAttributes + False, # bManualReset + False, # bInitialState + '') # lpName + + @staticmethod + def send_interrupt(interrupt_handle): + """ Sends an interrupt event using the specified handle. + """ + ctypes.windll.kernel32.SetEvent(interrupt_handle) + + def run(self): + """ Run the poll loop. This method never returns. + """ + from _subprocess import WAIT_OBJECT_0, INFINITE + + # Build the list of handle to listen on. + handles = [] + if self.interrupt_handle: + handles.append(self.interrupt_handle) + if self.parent_handle: + handles.append(self.parent_handle) + + # Listen forever. + while True: + result = ctypes.windll.kernel32.WaitForMultipleObjects( + len(handles), # nCount + (ctypes.c_int * len(handles))(*handles), # lpHandles + False, # bWaitAll + INFINITE) # dwMilliseconds + + if WAIT_OBJECT_0 <= result < len(handles): + handle = handles[result - WAIT_OBJECT_0] + + if handle == self.interrupt_handle: + raw_print('Interrupted by parent poller!') + interrupt_main() + + elif handle == self.parent_handle: + raw_print('Killed by parent poller!') + os._exit(1)