From 963100cfd63cc27f92d8a1e8be9c4dc2e3c1113b 2008-08-07 15:10:52
From: gvaroquaux <gvaroquaux@gvaroquaux-desktop>
Date: 2008-08-07 15:10:52
Subject: [PATCH] Bind Ctrl-C to kill process, when in process execution.

---

diff --git a/IPython/frontend/killable_process.py b/IPython/frontend/killable_process.py
new file mode 100644
index 0000000..9ca38ef
--- /dev/null
+++ b/IPython/frontend/killable_process.py
@@ -0,0 +1,168 @@
+# Addapted from killableprocess.py.  
+#______________________________________________________________________________
+#
+# killableprocess - subprocesses which can be reliably killed
+#
+# Parts of this module are copied from the subprocess.py file contained
+# in the Python distribution.
+#
+# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
+#
+# Additions and modifications written by Benjamin Smedberg
+# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
+# <http://www.mozilla.org/>
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of the
+# author not be used in advertising or publicity pertaining to
+# distribution of the software without specific, written prior
+# permission.
+#
+# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+r"""killableprocess - Subprocesses which can be reliably killed
+
+This module is a subclass of the builtin "subprocess" module. It allows
+processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method.
+
+It also adds a timeout argument to Wait() for a limited period of time before
+forcefully killing the process.
+
+Note: On Windows, this module requires Windows 2000 or higher (no support for
+Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with
+Python 2.5+ or available from http://python.net/crew/theller/ctypes/
+"""
+
+import subprocess
+from subprocess import PIPE
+import sys
+import os
+import time
+import types
+
+try:
+    from subprocess import CalledProcessError
+except ImportError:
+    # Python 2.4 doesn't implement CalledProcessError
+    class CalledProcessError(Exception):
+        """This exception is raised when a process run by check_call() returns
+        a non-zero exit status. The exit status will be stored in the
+        returncode attribute."""
+        def __init__(self, returncode, cmd):
+            self.returncode = returncode
+            self.cmd = cmd
+        def __str__(self):
+            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
+mswindows = (sys.platform == "win32")
+
+if mswindows:
+    import winprocess
+else:
+    import signal
+
+if not mswindows:
+    def DoNothing(*args):
+        pass
+
+class Popen(subprocess.Popen):
+    if not mswindows:
+        # Override __init__ to set a preexec_fn
+        def __init__(self, *args, **kwargs):
+            if len(args) >= 7:
+                raise Exception("Arguments preexec_fn and after must be passed by keyword.")
+
+            real_preexec_fn = kwargs.pop("preexec_fn", None)
+            def setpgid_preexec_fn():
+                os.setpgid(0, 0)
+                if real_preexec_fn:
+                    apply(real_preexec_fn)
+
+            kwargs['preexec_fn'] = setpgid_preexec_fn
+
+            subprocess.Popen.__init__(self, *args, **kwargs)
+
+    if mswindows:
+        def _execute_child(self, args, executable, preexec_fn, close_fds,
+                           cwd, env, universal_newlines, startupinfo,
+                           creationflags, shell,
+                           p2cread, p2cwrite,
+                           c2pread, c2pwrite,
+                           errread, errwrite):
+            if not isinstance(args, types.StringTypes):
+                args = subprocess.list2cmdline(args)
+
+            if startupinfo is None:
+                startupinfo = winprocess.STARTUPINFO()
+
+            if None not in (p2cread, c2pwrite, errwrite):
+                startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES
+                
+                startupinfo.hStdInput = int(p2cread)
+                startupinfo.hStdOutput = int(c2pwrite)
+                startupinfo.hStdError = int(errwrite)
+            if shell:
+                startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW
+                startupinfo.wShowWindow = winprocess.SW_HIDE
+                comspec = os.environ.get("COMSPEC", "cmd.exe")
+                args = comspec + " /c " + args
+
+            # We create a new job for this process, so that we can kill
+            # the process and any sub-processes 
+            self._job = winprocess.CreateJobObject()
+
+            creationflags |= winprocess.CREATE_SUSPENDED
+            creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
+
+            hp, ht, pid, tid = winprocess.CreateProcess(
+                executable, args,
+                None, None, # No special security
+                1, # Must inherit handles!
+                creationflags,
+                winprocess.EnvironmentBlock(env),
+                cwd, startupinfo)
+            
+            self._child_created = True
+            self._handle = hp
+            self._thread = ht
+            self.pid = pid
+
+            winprocess.AssignProcessToJobObject(self._job, hp)
+            winprocess.ResumeThread(ht)
+
+            if p2cread is not None:
+                p2cread.Close()
+            if c2pwrite is not None:
+                c2pwrite.Close()
+            if errwrite is not None:
+                errwrite.Close()
+
+    def kill(self, group=True):
+        """Kill the process. If group=True, all sub-processes will also be killed."""
+        if mswindows:
+            if group:
+                winprocess.TerminateJobObject(self._job, 127)
+            else:
+                winprocess.TerminateProcess(self._handle, 127)
+            self.returncode = 127    
+        else:
+            if group:
+                os.killpg(self.pid, signal.SIGKILL)
+            else:
+                os.kill(self.pid, signal.SIGKILL)
+            self.returncode = -9
+
+
diff --git a/IPython/frontend/piped_process.py b/IPython/frontend/piped_process.py
index 94b8ed4..6e51a7d 100644
--- a/IPython/frontend/piped_process.py
+++ b/IPython/frontend/piped_process.py
@@ -15,7 +15,7 @@ __docformat__ = "restructuredtext en"
 #-------------------------------------------------------------------------------
 # Imports
 #-------------------------------------------------------------------------------
-from subprocess import Popen, PIPE
+from killable_process import Popen, PIPE
 from threading import Thread
 from time import sleep
 
diff --git a/IPython/frontend/wx/wx_frontend.py b/IPython/frontend/wx/wx_frontend.py
index 3fd52d1..44ac3db 100644
--- a/IPython/frontend/wx/wx_frontend.py
+++ b/IPython/frontend/wx/wx_frontend.py
@@ -28,6 +28,7 @@ from console_widget import ConsoleWidget
 import __builtin__
 from time import sleep
 import sys
+import signal
 
 from threading import Lock
 
@@ -49,19 +50,45 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
     output_prompt = \
     '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02%i\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02'
 
+    # Print debug info on what is happening to the console.
     debug = True
 
+    # The title of the terminal, as captured through the ANSI escape
+    # sequences.
+
+    def _set_title(self, title):
+            return self.Parent.SetTitle(title)
+
+    def _get_title(self):
+            return self.Parent.GetTitle()
+
+    title = property(_get_title, _set_title)
+
+    #--------------------------------------------------------------------------
+    # Private Attributes
+    #--------------------------------------------------------------------------
+
+    # A flag governing the behavior of the input. Can be:
+    #
+    #       'readline' for readline-like behavior with a prompt 
+    #            and an edit buffer.
+    #       'subprocess' for sending the raw input directly to a
+    #           subprocess.
+    #       'buffering' for buffering of the input, that will be used
+    #           when the input state switches back to another state.
+    _input_state = 'readline'
+
     # Attribute to store reference to the pipes of a subprocess, if we
     # are running any.
-    running_process = False
+    _running_process = False
 
     # A queue for writing fast streams to the screen without flooding the
     # event loop
-    write_buffer = []
+    _out_buffer = []
 
-    # A lock to lock the write_buffer to make sure we don't empty it
+    # A lock to lock the _out_buffer to make sure we don't empty it
     # while it is being swapped
-    write_buffer_lock = Lock()
+    _out_buffer_lock = Lock()
 
     #--------------------------------------------------------------------------
     # Public API
@@ -147,6 +174,11 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
                     print >>sys.__stdout__, completions 
 
 
+    def new_prompt(self, prompt):
+        self._input_state = 'readline'
+        ConsoleWidget.new_prompt(self, prompt)
+
+
     def raw_input(self, prompt):
         """ A replacement from python's raw_input.
         """
@@ -161,10 +193,12 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
             wx.Yield()
             sleep(0.1)
         self._on_enter = self.__old_on_enter
+        self._input_state = 'buffering'
         return self.get_current_edit_buffer().rstrip('\n')
         
  
     def execute(self, python_string, raw_string=None):
+        self._input_state = 'buffering'
         self.CallTipCancel()
         self._cursor = wx.BusyCursor()
         if raw_string is None:
@@ -197,15 +231,15 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
 
     
     def system_call(self, command_string):
-        self.running_process = True
-        self.running_process = PipedProcess(command_string, 
+        self._input_state = 'subprocess'
+        self._running_process = PipedProcess(command_string, 
                     out_callback=self.buffered_write,
                     end_callback = self._end_system_call)
-        self.running_process.start()
+        self._running_process.start()
         # XXX: another one of these polling loops to have a blocking
         # call
         wx.Yield()
-        while self.running_process:
+        while self._running_process:
             wx.Yield()
             sleep(0.1)
         # Be sure to flush the buffer.
@@ -219,32 +253,13 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
             This can be called outside of the main loop, in separate
             threads.
         """
-        self.write_buffer_lock.acquire()
-        self.write_buffer.append(text)
-        self.write_buffer_lock.release()
+        self._out_buffer_lock.acquire()
+        self._out_buffer.append(text)
+        self._out_buffer_lock.release()
         if not self._buffer_flush_timer.IsRunning():
             self._buffer_flush_timer.Start(100)  # milliseconds
 
 
-    def _end_system_call(self):
-        """ Called at the end of a system call.
-        """
-        self.running_process = False
-
-
-    def _buffer_flush(self, event):
-        """ Called by the timer to flush the write buffer.
-            
-            This is always called in the mainloop, by the wx timer.
-        """
-        self.write_buffer_lock.acquire()
-        write_buffer = self.write_buffer
-        self.write_buffer = []
-        self.write_buffer_lock.release()
-        self.write(''.join(write_buffer))
-        self._buffer_flush_timer.Stop()
-
-
     def show_traceback(self):
         start_line = self.GetCurrentLine()
         PrefilterFrontEnd.show_traceback(self)
@@ -261,14 +276,27 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
         """ Capture the character events, let the parent
             widget handle them, and put our logic afterward.
         """
+        print >>sys.__stderr__, event.KeyCode
         current_line_number = self.GetCurrentLine()
-        if self.running_process and event.KeyCode<256 \
+        if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
+            # Capture Control-C
+            if self._input_state == 'subprocess':
+                if self.debug:
+                    print >>sys.__stderr__, 'Killing running process'
+                self._running_process.process.kill()
+            elif self._input_state == 'buffering':
+                if self.debug:
+                    print >>sys.__stderr__, 'Raising KeyboardException'
+                raise KeyboardException
+                # XXX: We need to make really sure we
+                # get back to a prompt.
+        elif self._input_state == 'subprocess' and event.KeyCode<256 \
                         and event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
-            #  We are running a process, let us not be too clever.
+            #  We are running a process, we redirect keys.
             ConsoleWidget._on_key_down(self, event, skip=skip)
             if self.debug:
                 print >>sys.__stderr__, chr(event.KeyCode)
-            self.running_process.process.stdin.write(chr(event.KeyCode))
+            self._running_process.process.stdin.write(chr(event.KeyCode))
         elif event.KeyCode in (ord('('), 57):
             # Calltips
             event.Skip()
@@ -320,18 +348,32 @@ class WxController(PrefilterFrontEnd, ConsoleWidget):
         else:
             ConsoleWidget._on_key_up(self, event, skip=skip)
 
+
     def _on_enter(self):
         if self.debug:
             print >>sys.__stdout__, repr(self.get_current_edit_buffer())
         PrefilterFrontEnd._on_enter(self)
 
-    def _set_title(self, title):
-            return self.Parent.SetTitle(title)
 
-    def _get_title(self):
-            return self.Parent.GetTitle()
+    def _end_system_call(self):
+        """ Called at the end of a system call.
+        """
+        print >>sys.__stderr__, 'End of system call'
+        self._input_state = 'buffering'
+        self._running_process = False
 
-    title = property(_get_title, _set_title)
+
+    def _buffer_flush(self, event):
+        """ Called by the timer to flush the write buffer.
+            
+            This is always called in the mainloop, by the wx timer.
+        """
+        self._out_buffer_lock.acquire()
+        _out_buffer = self._out_buffer
+        self._out_buffer = []
+        self._out_buffer_lock.release()
+        self.write(''.join(_out_buffer))
+        self._buffer_flush_timer.Stop()
 
 
 if __name__ == '__main__':