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
+
+