From b850ab1ff4314634294cffc7523339035cbdfaf8 2007-04-05 06:00:13 From: fperez Date: 2007-04-05 06:00:13 Subject: [PATCH] - I possibly cross-thread KeyboardInterrupt support for the multithreaded shells. This is still pretty hackish, so testing is needed. --- diff --git a/IPython/Shell.py b/IPython/Shell.py index 11b29fa..007c57f 100644 --- a/IPython/Shell.py +++ b/IPython/Shell.py @@ -4,7 +4,7 @@ All the matplotlib support code was co-developed with John Hunter, matplotlib's author. -$Id: Shell.py 2164 2007-03-20 00:15:03Z fperez $""" +$Id: Shell.py 2216 2007-04-05 06:00:13Z fperez $""" #***************************************************************************** # Copyright (C) 2001-2006 Fernando Perez @@ -18,15 +18,26 @@ __author__ = '%s <%s>' % Release.authors['Fernando'] __license__ = Release.license # Code begins +# Stdlib imports import __builtin__ import __main__ import Queue +import inspect import os -import signal import sys import threading import time +from signal import signal, SIGINT + +try: + import ctypes + HAS_CTYPES = True +except ImportError: + HAS_CTYPES = False + + +# IPython imports import IPython from IPython import ultraTB from IPython.genutils import Term,warn,error,flag_calls @@ -35,12 +46,19 @@ from IPython.ipmaker import make_IPython from IPython.Magic import Magic from IPython.ipstruct import Struct +# Globals # global flag to pass around information about Ctrl-C without exceptions KBINT = False # global flag to turn on/off Tk support. USE_TK = False +# ID for the main thread, used for cross-thread exceptions +MAIN_THREAD_ID = None + +# Tag when runcode() is active, for exception handling +CODE_RUN = None + #----------------------------------------------------------------------------- # This class is trivial now, but I want to have it in to publish a clean # interface. Later when the internals are reorganized, code that uses this @@ -241,20 +259,70 @@ class IPShellEmbed: self.exit_msg = exit_msg #----------------------------------------------------------------------------- -def sigint_handler (signum,stack_frame): - """Sigint handler for threaded apps. +if HAS_CTYPES: + # Add async exception support. Trick taken from: + # http://sebulba.wikispaces.com/recipe+thread2 + def _async_raise(tid, exctype): + """raises the exception, performs cleanup if needed""" + if not inspect.isclass(exctype): + raise TypeError("Only types can be raised (not instances)") + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, + ctypes.py_object(exctype)) + if res == 0: + raise ValueError("invalid thread id") + elif res != 1: + # """if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect""" + ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0) + raise SystemError("PyThreadState_SetAsyncExc failed") + + def sigint_handler (signum,stack_frame): + """Sigint handler for threaded apps. + + This is a horrible hack to pass information about SIGINT _without_ + using exceptions, since I haven't been able to properly manage + cross-thread exceptions in GTK/WX. In fact, I don't think it can be + done (or at least that's my understanding from a c.l.py thread where + this was discussed).""" - This is a horrible hack to pass information about SIGINT _without_ using - exceptions, since I haven't been able to properly manage cross-thread - exceptions in GTK/WX. In fact, I don't think it can be done (or at least - that's my understanding from a c.l.py thread where this was discussed).""" + global KBINT + + if CODE_RUN: + _async_raise(MAIN_THREAD_ID,KeyboardInterrupt) + else: + KBINT = True + print '\nKeyboardInterrupt - Press to continue.', + Term.cout.flush() + +else: + def sigint_handler (signum,stack_frame): + """Sigint handler for threaded apps. + + This is a horrible hack to pass information about SIGINT _without_ + using exceptions, since I haven't been able to properly manage + cross-thread exceptions in GTK/WX. In fact, I don't think it can be + done (or at least that's my understanding from a c.l.py thread where + this was discussed).""" + + global KBINT + + print '\nKeyboardInterrupt - Press to continue.', + Term.cout.flush() + # Set global flag so that runsource can know that Ctrl-C was hit + KBINT = True - global KBINT - - print '\nKeyboardInterrupt - Press to continue.', - Term.cout.flush() - # Set global flag so that runsource can know that Ctrl-C was hit - KBINT = True + +def _set_main_thread_id(): + """Ugly hack to find the main thread's ID. + """ + global MAIN_THREAD_ID + for tid, tobj in threading._active.items(): + # There must be a better way to do this than looking at the str() for + # each thread object... + if 'MainThread' in str(tobj): + #print 'main tid:',tid # dbg + MAIN_THREAD_ID = tid + break class MTInteractiveShell(InteractiveShell): """Simple multi-threaded shell.""" @@ -338,17 +406,16 @@ class MTInteractiveShell(InteractiveShell): Multithreaded wrapper around IPython's runcode().""" + + global CODE_RUN + + # Exceptions need to be raised differently depending on which thread is + # active + CODE_RUN = True + # lock thread-protected stuff got_lock = self.thread_ready.acquire(False) - # Install sigint handler - try: - signal.signal(signal.SIGINT, sigint_handler) - except SystemError: - # This happens under Windows, which seems to have all sorts - # of problems with signal handling. Oh well... - pass - if self._kill: print >>Term.cout, 'Closing threads...', Term.cout.flush() @@ -356,6 +423,15 @@ class MTInteractiveShell(InteractiveShell): tokill() print >>Term.cout, 'Done.' + # Install sigint handler. It feels stupid to do this on every single + # pass + try: + signal(SIGINT,sigint_handler) + except SystemError: + # This happens under Windows, which seems to have all sorts + # of problems with signal handling. Oh well... + pass + # Flush queue of pending code by calling the run methood of the parent # class with all items which may be in the queue. while 1: @@ -372,6 +448,9 @@ class MTInteractiveShell(InteractiveShell): # We're done with thread-protected variables if got_lock: self.thread_ready.release() + + # We're done... + CODE_RUN = False # This MUST return true for gtk threading to work return True @@ -597,7 +676,16 @@ def hijack_gtk(): # desired, the factory function start() below should be used instead (it # selects the proper threaded class). -class IPShellGTK(threading.Thread): +class IPThread(threading.Thread): + def run(self): + self.IP.mainloop(self._banner) + self.IP.kill() + + def start(self): + threading.Thread.start(self) + _set_main_thread_id() + +class IPShellGTK(IPThread): """Run a gtk mainloop() in a separate thread. Python commands can be passed to the thread where they will be executed. @@ -633,10 +721,6 @@ class IPShellGTK(threading.Thread): threading.Thread.__init__(self) - def run(self): - self.IP.mainloop(self._banner) - self.IP.kill() - def mainloop(self,sys_exit=0,banner=None): self._banner = banner @@ -680,7 +764,8 @@ class IPShellGTK(threading.Thread): time.sleep(0.01) return True -class IPShellWX(threading.Thread): + +class IPShellWX(IPThread): """Run a wx mainloop() in a separate thread. Python commands can be passed to the thread where they will be executed. @@ -721,7 +806,6 @@ class IPShellWX(threading.Thread): # Allows us to use both Tk and GTK. self.tk = get_tk() - # HACK: slot for banner in self; it will be passed to the mainloop # method only and .run() needs it. The actual value will be set by # .mainloop(). @@ -734,10 +818,6 @@ class IPShellWX(threading.Thread): self.app.agent.timer.Stop() self.app.ExitMainLoop() - def run(self): - self.IP.mainloop(self._banner) - self.IP.kill() - def mainloop(self,sys_exit=0,banner=None): self._banner = banner @@ -774,13 +854,14 @@ class IPShellWX(threading.Thread): self.agent.Show(False) self.agent.StartWork() return True - + + _set_main_thread_id() self.app = App(redirect=False) self.wx_mainloop(self.app) self.join() -class IPShellQt(threading.Thread): +class IPShellQt(IPThread): """Run a Qt event loop in a separate thread. Python commands can be passed to the thread where they will be executed. @@ -825,10 +906,6 @@ class IPShellQt(threading.Thread): threading.Thread.__init__(self) - def run(self): - self.IP.mainloop(self._banner) - self.IP.kill() - def mainloop(self,sys_exit=0,banner=None): import qt @@ -854,7 +931,7 @@ class IPShellQt(threading.Thread): return result -class IPShellQt4(threading.Thread): +class IPShellQt4(IPThread): """Run a Qt event loop in a separate thread. Python commands can be passed to the thread where they will be executed. @@ -899,10 +976,6 @@ class IPShellQt4(threading.Thread): threading.Thread.__init__(self) - def run(self): - self.IP.mainloop(self._banner) - self.IP.kill() - def mainloop(self,sys_exit=0,banner=None): from PyQt4 import QtCore, QtGui diff --git a/doc/ChangeLog b/doc/ChangeLog index 8162678..344028e 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,24 @@ +2007-04-04 Fernando Perez + + * IPython/Shell.py (sigint_handler): I *THINK* I finally got + asynchronous exceptions working, i.e., Ctrl-C can actually + interrupt long-running code in the multithreaded shells. + + This is using Tomer Filiba's great ctypes-based trick: + http://sebulba.wikispaces.com/recipe+thread2. I'd already tried + this in the past, but hadn't been able to make it work before. So + far it looks like it's actually running, but this needs more + testing. If it really works, I'll be *very* happy, and we'll owe + a huge thank you to Tomer. My current implementation is ugly, + hackish and uses nasty globals, but I don't want to try and clean + anything up until we know if it actually works. + + NOTE: this feature needs ctypes to work. ctypes is included in + Python2.5, but 2.4 users will need to manually install it. This + feature makes multi-threaded shells so much more usable that it's + a minor price to pay (ctypes is very easy to install, already a + requirement for win32 and available in major linux distros). + 2007-04-04 Ville Vainio * Extensions/ipy_completers.py, ipy_stock_completers.py: