Repository: ipython/ipython

Issue:
Hang after running pdb in a cell, Kernel/Interrupt doesn't help
If you debug a cell via pdb, then don't quit it before rerunning the cell, you can't get back to your IPython environment.  Reproduce it via [Test case for pdb hang in Jupyter notebook](https://gist.github.com/nealmcb/1f0a14d63d539940e8dfccc29d6d69ae). Here's a screenshot:

![pdb_resists_kernel_interrupt ipynb](https://cloud.githubusercontent.com/assets/119472/25805924/bddd3162-33be-11e7-8a89-d54245abfd87.png)

For the record and search engines, to hang your notebook:
* Run the following cell
* Do not "quit" via the pdb prompt
* Then run it again.

```
def test():
    import pdb; pdb.set_trace()  # XXX BREAKPOINT
    return 0

test()
```

Note that Kernel/Interupt doesn't help. You can save the notebook, but to run cells again, you need to restart the kernel, at which point "All variables will be lost"

This happens to me not infrequently if I am searching thru the rest of the notebook for hints on how to debug the cell, then forget to quit before changing the cell and running it again.  Yes, I can be more careful, but others have the same experience.  Thanks to @wmayner for a convenient test case described at #3400.

I'm running Jupyter notebook 4.3.1 on Python 3.4.3 (default, Nov 17 2016, 01:08:31) [GCC 4.8.4]
Kernel Information: Python 3.4.3 (default, Nov 17 2016, 01:08:31)

Input from @takluyver at #10499 
> pdb runs in the same process, and we do know when it's expecting input. I think it needs someone to do the work so that when the HTML input box is removed in the frontend, a message is sent back to the kernel which will raise EOFError.
> That may need a new message type, or we could do it as a metadata field on the existing stdin reply message.

Discussion:

Patch:
diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py
index 0d0b71e88..48b24f015 100644
--- a/IPython/core/debugger.py
+++ b/IPython/core/debugger.py
@@ -628,6 +628,34 @@ def do_where(self, arg):
     do_w = do_where
 
 
+class InterruptiblePdb(Pdb):
+    """Version of debugger where KeyboardInterrupt exits the debugger altogether."""
+
+    def cmdloop(self):
+        """Wrap cmdloop() such that KeyboardInterrupt stops the debugger."""
+        try:
+            return OldPdb.cmdloop(self)
+        except KeyboardInterrupt:
+            self.stop_here = lambda frame: False
+            self.do_quit("")
+            sys.settrace(None)
+            self.quitting = False
+            raise
+
+    def _cmdloop(self):
+        while True:
+            try:
+                # keyboard interrupts allow for an easy way to cancel
+                # the current command, so allow them during interactive input
+                self.allow_kbdint = True
+                self.cmdloop()
+                self.allow_kbdint = False
+                break
+            except KeyboardInterrupt:
+                self.message('--KeyboardInterrupt--')
+                raise
+
+
 def set_trace(frame=None):
     """
     Start debugging from `frame`.
diff --git a/docs/source/whatsnew/pr/interruptible-debugger.rst b/docs/source/whatsnew/pr/interruptible-debugger.rst
new file mode 100644
index 000000000..f501b3476
--- /dev/null
+++ b/docs/source/whatsnew/pr/interruptible-debugger.rst
@@ -0,0 +1,4 @@
+IPython.core.debugger.Pdb is now interruptible
+==============================================
+
+A ``KeyboardInterrupt`` will now interrupt IPython's extended debugger, in order to make Jupyter able to interrupt it.


Test patch:
diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py
index 665d8f84f..69506800b 100644
--- a/IPython/core/tests/test_debugger.py
+++ b/IPython/core/tests/test_debugger.py
@@ -4,8 +4,16 @@
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+import signal
 import sys
+import time
 import warnings
+from tempfile import NamedTemporaryFile
+from subprocess import check_output, CalledProcessError, PIPE
+import subprocess
+from unittest.mock import patch
+import builtins
+import bdb
 
 import nose.tools as nt
 
@@ -220,3 +228,26 @@ def can_exit():
 
     >>> sys.settrace(old_trace)
     '''
+
+
+def test_interruptible_core_debugger():
+    """The debugger can be interrupted.
+
+    The presumption is there is some mechanism that causes a KeyboardInterrupt
+    (this is implemented in ipykernel).  We want to ensure the
+    KeyboardInterrupt cause debugging to cease.
+    """
+    def raising_input(msg="", called=[0]):
+        called[0] += 1
+        if called[0] == 1:
+            raise KeyboardInterrupt()
+        else:
+            raise AssertionError("input() should only be called once!")
+
+    with patch.object(builtins, "input", raising_input):
+        debugger.InterruptiblePdb().set_trace()
+        # The way this test will fail is by set_trace() never exiting,
+        # resulting in a timeout by the test runner. The alternative
+        # implementation would involve a subprocess, but that adds issues with
+        # interrupting subprocesses that are rather complex, so it's simpler
+        # just to do it this way.