From aa846e31eebed4c9af3aa4eb1599bc55a56fb9ec 2011-10-16 20:28:27 From: Fernando Perez <fperez.net@gmail.com> Date: 2011-10-16 20:28:27 Subject: [PATCH] Merge pull request #876 from minrk/customtb Protect IPython from bad custom exception handlers. Also ensures that ipdb can be used as a tracing debugger in a manner similar to `pdb.set_trace`, via: from IPython.core.debugger import Tracer tracer = Tracer() # then, call tracer() anywhere in the code to start it up. --- diff --git a/IPython/core/application.py b/IPython/core/application.py index 7d6e4bc..5679aa7 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -27,6 +27,7 @@ Authors: # Imports #----------------------------------------------------------------------------- +import atexit import glob import logging import os @@ -156,6 +157,9 @@ class BaseIPythonApplication(Application): """Create a crash handler, typically setting sys.excepthook to it.""" self.crash_handler = self.crash_handler_class(self) sys.excepthook = self.crash_handler + def unset_crashhandler(): + sys.excepthook = sys.__excepthook__ + atexit.register(unset_crashhandler) def _ipython_dir_changed(self, name, old, new): if old in sys.path: diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index d7b22a3..b7f2165 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -38,7 +38,7 @@ from IPython.core.excolors import exception_colors has_pydb = False prompt = 'ipdb> ' #We have to check this directly from sys.argv, config struct not yet available -if '-pydb' in sys.argv: +if '--pydb' in sys.argv: try: import pydb if hasattr(pydb.pydb, "runl") and pydb.version>'1.17': @@ -64,7 +64,7 @@ def BdbQuit_excepthook(et,ev,tb): else: BdbQuit_excepthook.excepthook_ori(et,ev,tb) -def BdbQuit_IPython_excepthook(self,et,ev,tb): +def BdbQuit_IPython_excepthook(self,et,ev,tb,tb_offset=None): print 'Exiting Debugger.' @@ -104,8 +104,8 @@ class Tracer(object): """ try: - ip = ipapi.get() - except: + ip = get_ipython() + except NameError: # Outside of ipython, we set our own exception hook manually BdbQuit_excepthook.excepthook_ori = sys.excepthook sys.excepthook = BdbQuit_excepthook diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index c621b58..a96dd42 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1447,29 +1447,37 @@ class InteractiveShell(SingletonConfigurable, Magic): Set a custom exception handler, which will be called if any of the exceptions in exc_tuple occur in the mainloop (specifically, in the - run_code() method. + run_code() method). - Inputs: + Parameters + ---------- + + exc_tuple : tuple of exception classes + A *tuple* of exception classes, for which to call the defined + handler. It is very important that you use a tuple, and NOT A + LIST here, because of the way Python's except statement works. If + you only want to trap a single exception, use a singleton tuple:: - - exc_tuple: a *tuple* of valid exceptions to call the defined - handler for. It is very important that you use a tuple, and NOT A - LIST here, because of the way Python's except statement works. If - you only want to trap a single exception, use a singleton tuple: + exc_tuple == (MyCustomException,) - exc_tuple == (MyCustomException,) + handler : callable + handler must have the following signature:: - - handler: this must be defined as a function with the following - basic interface:: + def my_handler(self, etype, value, tb, tb_offset=None): + ... + return structured_traceback - def my_handler(self, etype, value, tb, tb_offset=None) - ... - # The return value must be - return structured_traceback + Your handler must return a structured traceback (a list of strings), + or None. - This will be made into an instance method (via types.MethodType) - of IPython itself, and it will be called if any of the exceptions - listed in the exc_tuple are caught. If the handler is None, an - internal basic one is used, which just prints basic info. + This will be made into an instance method (via types.MethodType) + of IPython itself, and it will be called if any of the exceptions + listed in the exc_tuple are caught. If the handler is None, an + internal basic one is used, which just prints basic info. + + To protect IPython from crashes, if your handler ever raises an + exception or returns an invalid result, it will be immediately + disabled. WARNING: by putting in your own exception handler into IPython's main execution loop, you run a very good chance of nasty crashes. This @@ -1478,16 +1486,62 @@ class InteractiveShell(SingletonConfigurable, Magic): assert type(exc_tuple)==type(()) , \ "The custom exceptions must be given AS A TUPLE." - def dummy_handler(self,etype,value,tb): + def dummy_handler(self,etype,value,tb,tb_offset=None): print '*** Simple custom exception handler ***' print 'Exception type :',etype print 'Exception value:',value print 'Traceback :',tb #print 'Source code :','\n'.join(self.buffer) - - if handler is None: handler = dummy_handler - - self.CustomTB = types.MethodType(handler,self) + + def validate_stb(stb): + """validate structured traceback return type + + return type of CustomTB *should* be a list of strings, but allow + single strings or None, which are harmless. + + This function will *always* return a list of strings, + and will raise a TypeError if stb is inappropriate. + """ + msg = "CustomTB must return list of strings, not %r" % stb + if stb is None: + return [] + elif isinstance(stb, basestring): + return [stb] + elif not isinstance(stb, list): + raise TypeError(msg) + # it's a list + for line in stb: + # check every element + if not isinstance(line, basestring): + raise TypeError(msg) + return stb + + if handler is None: + wrapped = dummy_handler + else: + def wrapped(self,etype,value,tb,tb_offset=None): + """wrap CustomTB handler, to protect IPython from user code + + This makes it harder (but not impossible) for custom exception + handlers to crash IPython. + """ + try: + stb = handler(self,etype,value,tb,tb_offset=tb_offset) + return validate_stb(stb) + except: + # clear custom handler immediately + self.set_custom_exc((), None) + print >> io.stderr, "Custom TB Handler failed, unregistering" + # show the exception in handler first + stb = self.InteractiveTB.structured_traceback(*sys.exc_info()) + print >> io.stdout, self.InteractiveTB.stb2text(stb) + print >> io.stdout, "The original exception:" + stb = self.InteractiveTB.structured_traceback( + (etype,value,tb), tb_offset=tb_offset + ) + return stb + + self.CustomTB = types.MethodType(wrapped,self) self.custom_exceptions = exc_tuple def excepthook(self, etype, value, tb): @@ -1556,11 +1610,7 @@ class InteractiveShell(SingletonConfigurable, Magic): sys.last_value = value sys.last_traceback = tb if etype in self.custom_exceptions: - # FIXME: Old custom traceback objects may just return a - # string, in that case we just put it into a list stb = self.CustomTB(etype, value, tb, tb_offset) - if isinstance(ctb, basestring): - stb = [stb] else: if exception_only: stb = ['An exception has occurred, use %tb to see ' @@ -1571,9 +1621,11 @@ class InteractiveShell(SingletonConfigurable, Magic): stb = self.InteractiveTB.structured_traceback(etype, value, tb, tb_offset=tb_offset) + self._showtraceback(etype, value, stb) if self.call_pdb: # drop into debugger self.debugger(force=True) + return # Actually show the traceback self._showtraceback(etype, value, stb) diff --git a/IPython/core/shellapp.py b/IPython/core/shellapp.py index 99ebbd2..414e3a3 100644 --- a/IPython/core/shellapp.py +++ b/IPython/core/shellapp.py @@ -50,6 +50,14 @@ addflag('pdb', 'InteractiveShell.pdb', "Enable auto calling the pdb debugger after every exception.", "Disable auto calling the pdb debugger after every exception." ) +# pydb flag doesn't do any config, as core.debugger switches on import, +# which is before parsing. This just allows the flag to be passed. +shell_flags.update(dict( + pydb = ({}, + """"Use the third party 'pydb' package as debugger, instead of pdb. + Requires that pydb is installed.""" + ) +)) addflag('pprint', 'PlainTextFormatter.pprint', "Enable auto pretty printing of results.", "Disable auto auto pretty printing of results." diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 350ad0a..2621be9 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -146,3 +146,37 @@ class InteractiveShellTestCase(unittest.TestCase): finally: # Reset compiler flags so we don't mess up other tests. ip.compile.reset_compiler_flags() + + def test_bad_custom_tb(self): + """Check that InteractiveShell is protected from bad custom exception handlers""" + ip = get_ipython() + from IPython.utils import io + save_stderr = io.stderr + try: + # capture stderr + io.stderr = StringIO() + ip.set_custom_exc((IOError,), lambda etype,value,tb: 1/0) + self.assertEquals(ip.custom_exceptions, (IOError,)) + ip.run_cell(u'raise IOError("foo")') + self.assertEquals(ip.custom_exceptions, ()) + self.assertTrue("Custom TB Handler failed" in io.stderr.getvalue()) + finally: + io.stderr = save_stderr + + def test_bad_custom_tb_return(self): + """Check that InteractiveShell is protected from bad return types in custom exception handlers""" + ip = get_ipython() + from IPython.utils import io + save_stderr = io.stderr + try: + # capture stderr + io.stderr = StringIO() + ip.set_custom_exc((NameError,),lambda etype,value,tb, tb_offset=None: 1) + self.assertEquals(ip.custom_exceptions, (NameError,)) + ip.run_cell(u'a=abracadabra') + self.assertEquals(ip.custom_exceptions, ()) + self.assertTrue("Custom TB Handler failed" in io.stderr.getvalue()) + finally: + io.stderr = save_stderr + +