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 +# +# Additions and modifications written by Benjamin Smedberg +# are Copyright (c) 2006 by the Mozilla Foundation +# +# +# 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__':