diff -r 402a227564f5 Lib/pdb.py --- a/Lib/pdb.py Sun Dec 11 14:48:44 2016 -0800 +++ b/Lib/pdb.py Sat Dec 17 02:46:27 2016 +0800 @@ -158,6 +158,7 @@ pass self.allow_kbdint = False self.nosigint = nosigint + self.sigint_hook_type = None # Read $HOME/.pdbrc and ./.pdbrc self.rcLines = [] @@ -188,9 +189,31 @@ def sigint_handler(self, signum, frame): if self.allow_kbdint: raise KeyboardInterrupt - self.message("\nProgram interrupted. (Use 'cont' to resume).") - self.set_step() - self.set_trace(frame) + try: + self.message("\nProgram interrupted (please wait).") + + if self.sigint_hook_type == 'continue': + self.set_step() + else: + self.set_continue() + + self.set_trace(frame) + except Exception as e: + # e.g. 'print' is not safe in signal handlers, check issue24283 + raise bdb.BdbQuit('Fail to resume Pdb when receiving SIGINT (^C).') + + def register_sigint_handler(self, hook_type=''): + if not self.nosigint: + self.sigint_hook_type = hook_type + try: + Pdb._previous_sigint_handler = \ + signal.signal(signal.SIGINT, self.sigint_handler) + except ValueError: + # ValueError happens when any do_xxx() is invoked from + # a non-main thread in which case we just continue without + # SIGINT set. Would printing a message here (once) make + # sense? + pass def reset(self): bdb.Bdb.reset(self) @@ -977,6 +1000,7 @@ or equal to that is reached. In both cases, also stop when the current frame returns. """ + self.register_sigint_handler() if arg: try: lineno = int(arg) @@ -1008,6 +1032,7 @@ Continue execution until the next line in the current function is reached or it returns. """ + self.register_sigint_handler() self.set_next(self.curframe) return 1 do_n = do_next @@ -1033,6 +1058,7 @@ """r(eturn) Continue execution until the current function returns. """ + self.register_sigint_handler() self.set_return(self.curframe) return 1 do_r = do_return @@ -1041,16 +1067,7 @@ """c(ont(inue)) Continue execution, only stop when a breakpoint is encountered. """ - if not self.nosigint: - try: - Pdb._previous_sigint_handler = \ - signal.signal(signal.SIGINT, self.sigint_handler) - except ValueError: - # ValueError happens when do_continue() is invoked from - # a non-main thread in which case we just continue without - # SIGINT set. Would printing a message here (once) make - # sense? - pass + self.register_sigint_handler(hook_type='continue') self.set_continue() return 1 do_c = do_cont = do_continue diff -r 402a227564f5 Lib/test/test_pdb.py --- a/Lib/test/test_pdb.py Sun Dec 11 14:48:44 2016 -0800 +++ b/Lib/test/test_pdb.py Sat Dec 17 02:46:27 2016 +0800 @@ -8,6 +8,7 @@ import unittest import subprocess import textwrap +import time from test import support # This little helper class is essential for testing pdb under doctest. @@ -1110,6 +1111,72 @@ if save_home is not None: os.environ['HOME'] = save_home + @unittest.skipIf(sys.platform == 'win32', 'Not valid on Windows') + @unittest.skipIf(not support.multiprocessing, + 'Require multiprocessing module') + def test_break_during_interactive_input(self): + # Spawn a process to send Ctrl-C event (by os.kill()) to Pdb + script = """ + import time + import pdb + import signal + import os + + from multiprocessing import Process + def ctrl_c_sender(pid): + time.sleep(1) + os.kill(pid, signal.SIGINT) + p = Process(target=ctrl_c_sender, args=(os.getpid(),)) + p.daemon = True + p.start() + + pdb.Pdb(readrc=False).set_trace() + """ + commands = """ + continue + [time.sleep(0.01) for i in range(200)] + p 'Resume Pdb' + quit + """ + start = time.time() + stdout, stderr = self.run_pdb(script, commands) + self.assertIn('KeyboardInterrupt', stdout) + self.assertIn('Resume Pdb', stdout) + # Make sure that Ctrl-C did break the long-wait loop + end = time.time() + self.assertTrue(2 > (end-start)) + + @unittest.skipIf(sys.platform == 'win32', 'Not valid on Windows') + def test_resume_from_sigint(self): + # Use os.kill() to send Ctrl-C event + script = """ + import pdb + import signal + import os + + def f(): + loc = 'in f()' + os.kill(os.getpid(), signal.SIGINT) + print(' '.join(['Should', 'be', 'skipped'])) + + def main(): + pdb.Pdb(readrc=False).set_trace() + f() + + main() + """ + tested_cmds = ['continue', 'return', 'until 99', 'next'] + for cmd in tested_cmds: + pdb_input = """ + continue + {} + p 'Resume Pdb ' + loc + quit + """.format(cmd) + stdout, stderr = self.run_pdb(script, pdb_input) + self.assertIn('Resume Pdb in f()', stdout) + self.assertNotIn('Should be skipped', stdout) + def tearDown(self): support.unlink(support.TESTFN)