diff options
author | Gregory P. Smith <greg@mad-scientist.com> | 2010-05-08 23:38:49 (GMT) |
---|---|---|
committer | Gregory P. Smith <greg@mad-scientist.com> | 2010-05-08 23:38:49 (GMT) |
commit | 56fe6569fd5916c371c9075fec47973c74c34573 (patch) | |
tree | 92915b3915e4187c2eab290d8312a375aae1a1bc | |
parent | e8150e8441509306242a9d9def27a5ee78e95d8a (diff) | |
download | cpython-56fe6569fd5916c371c9075fec47973c74c34573.zip cpython-56fe6569fd5916c371c9075fec47973c74c34573.tar.gz cpython-56fe6569fd5916c371c9075fec47973c74c34573.tar.bz2 |
Fixes [issue7245] Better Ctrl-C support in pdb.
-rwxr-xr-x | Lib/pdb.py | 44 | ||||
-rw-r--r-- | Lib/test/test_pdb2.py | 197 | ||||
-rw-r--r-- | Misc/NEWS | 2 |
3 files changed, 238 insertions, 5 deletions
@@ -13,6 +13,7 @@ import os import re import pprint import traceback +import signal class Restart(Exception): @@ -72,6 +73,8 @@ class Pdb(bdb.Bdb, cmd.Cmd): import readline except ImportError: pass + self.allow_kbdint = False + signal.signal(signal.SIGINT, self.sigint_handler) # Read $HOME/.pdbrc and ./.pdbrc self.rcLines = [] @@ -104,6 +107,13 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.commands_bnum = None # The breakpoint number for which we are # defining a list + def sigint_handler(self, signum, frame): + if self.allow_kbdint: + raise KeyboardInterrupt() + print >>self.stdout, "\nProgram interrupted. (Use 'cont' to resume)." + self.set_step() + self.set_trace(frame) + def reset(self): bdb.Bdb.reset(self) self.forget() @@ -176,7 +186,7 @@ class Pdb(bdb.Bdb, cmd.Cmd): if not self.commands_silent[currentbp]: self.print_stack_entry(self.stack[self.curindex]) if self.commands_doprompt[currentbp]: - self.cmdloop() + self._cmdloop() self.forget() return return 1 @@ -199,11 +209,22 @@ class Pdb(bdb.Bdb, cmd.Cmd): self.interaction(frame, exc_traceback) # General interaction function + def _cmdloop(self): + while 1: + try: + # keyboard interrupts allow for an easy way to interrupt + # the current command + self.allow_kbdint = True + self.cmdloop() + self.allow_kbdint = False + break + except KeyboardInterrupt: + print >>self.stdout, '--KeyboardInterrupt--' def interaction(self, frame, traceback): self.setup(frame, traceback) self.print_stack_entry(self.stack[self.curindex]) - self.cmdloop() + self._cmdloop() self.forget() def displayhook(self, obj): @@ -329,9 +350,22 @@ class Pdb(bdb.Bdb, cmd.Cmd): prompt_back = self.prompt self.prompt = '(com) ' self.commands_defining = True - self.cmdloop() - self.commands_defining = False - self.prompt = prompt_back + try: + self.cmdloop() + except (KeyboardInterrupt, IOError): + # It appears that that when pdb is reading input from a pipe + # we may get IOErrors, rather than KeyboardInterrupt. + # Now discard all the commands entered so far (essentially undo + # any effect of this "commands" cmd) + self.commands.pop(bnum) + self.commands_doprompt.pop(bnum) + self.commands_silent.pop(bnum) + # this will get caught by the _cmdloop and pdb will reenter + # the main command loop + raise KeyboardInterrupt() + finally: + self.commands_defining = False + self.prompt = prompt_back def do_break(self, arg, temporary = 0): # break [ ([filename:]lineno | function) [, "condition"] ] diff --git a/Lib/test/test_pdb2.py b/Lib/test/test_pdb2.py new file mode 100644 index 0000000..71f6e3f --- /dev/null +++ b/Lib/test/test_pdb2.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python +# pdb tests in the Lib/test style +import os +import sys +import time +import re +import subprocess +import signal +from test.test_support import TESTFN +import test.test_support +import unittest + +# allow alt pdb locations, if environment variable is specified then +# the test files will be stored in t/ directory and will not be deleted +# after pdb run +DEBUG_PDB = os.environ.get("_DEBUG_PDB", None) +TMP_DIR = "./t" # dir for tmp files if DEBUG_PDB is set + +if DEBUG_PDB: + if not os.path.exists(TMP_DIR): + os.mkdir(TMP_DIR) + +def _write_test_file(testname, text): + filename = TESTFN + if DEBUG_PDB: + filename = os.path.join(TMP_DIR, testname) + with open(filename, "wt") as f: + f.write(text+"\n") + return filename + + +class PdbProcess(object): + def __init__(self, testname, testprg): + self.testname = testname + self.filename = _write_test_file(testname, testprg) + # unbuffer pdb.py output (if it gets any ideas to buffer it) + # make sure that we use the same interpreter to run tests wrapper and + # pdb itself + cmd = [sys.executable, '-u'] + if DEBUG_PDB: + cmd.append(DEBUG_PDB) + else: + cmd.extend(['-m', 'pdb']) + cmd.append(self.filename) + self.pdbhandle = subprocess.Popen(cmd, bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + self.startup_msg = self.wait_for_prompt() + self.finished=0 + + + def wait_for_normal_exit(self, timeout=2): + """wait for pdb subprocess to exit, timeout is in seconds""" + step = 0.1 + for i in range(int(timeout/step)): + status=self.pdbhandle.poll() + if status is not None: + break + time.sleep(step) + if status is -1: + describe = "pdb has not exited" + elif status> 0: + describe = "pdb exited abnormally with status=%d" % status + assert status == 0, describe + + def wait_for_line(self, stopline, alt_stopline=None): + output='' + line='' + while 1 : + ch=self.pdbhandle.stdout.read(1) + # sys.stdout.write(ch) + line += ch + if line == stopline or line == alt_stopline: + return output + if ch == '\n': + output += line + line='' + if ch == '':#eof + output += line + return output + + # note: this can block if issued at the wrong time + def wait_for_prompt(self): + """collect any output from pdb session til the prompt is encountered. + Return this output (exlcuding prompt)""" + return self.wait_for_line("(Pdb) ", "(com) ") + + def send_cmd(self, cmd): + """send a command but do not wait for response""" + #print "sending:", cmd + self.pdbhandle.stdin.write(cmd+"\n") + + + def cmd(self, cmd, response_text=""): + """send a single command to pdb, collect pdb response (by waiting + for the next prompt). Verify that response contains specified + response_text""" + + self.pdbhandle.stdin.write(cmd+"\n") + response = self.wait_for_prompt() + if not response_text: + return response + + if DEBUG_PDB: + print "%s: testing response for '%s':" % (self.testname,cmd), + assert response.find(response_text) >= 0, ( + "response:\n%s\n does not contain expected substring '%s'" % + (response, response_text)) + if DEBUG_PDB: + print "Ok" + return response + + def send_kbdint(self): + # os.kill is Posix-specific. We could have used a X-platform + # send_signal method of Popen objects, but it still cann't send + # SIGINT on win32 and it's not present on python2.5 + # self.pdbhandle.send_signal(signal.SIGINT) + os.kill(self.pdbhandle.pid, signal.SIGINT) + + def __del__(self): + # if pdb is still running, kill it, leaving it running does not serve + # any useful purpose + if self.pdbhandle.poll() is None: + self.pdbhandle.send_signal(signal.SIGTERM) + if not DEBUG_PDB: + os.unlink(self.filename) + return self.pdbhandle.wait() + + +class PdbTest(unittest.TestCase): + + def test_00startup(self): + pdb = PdbProcess("pdb_t_startup", "print 'Hello, world'") + pdb.cmd("r", "Hello, world") + pdb.cmd("q") + pdb.wait_for_normal_exit() + + @unittest.skipIf(sys.platform.startswith("win"), + "test_sigint requires a posix system.") + def test_sigint(self): + pdb = PdbProcess("pdb_t_loop", """\ +for i in xrange(100000000): + print 'i=%d' %i +""" ) + # first, test Ctrl-C/kbdint handling while the program is running + # kbdint should interrupt the program and return to pdb prompt, + # the program must be resumable + pdb.send_cmd("c") + # we could use time.sleep() delays but they are not reliable so you + # end up with making them much longer than necessary (and still failing + # from time to time) + pdb.wait_for_line("i=19") + pdb.send_kbdint() + pdb.wait_for_prompt() + response = pdb.cmd('p "i=%d" % i') + m = re.search('i=(\d+)', response) + assert m, "unexpected response %s" % response + i0 = int(m.group(1)) + pdb.send_cmd("c") + pdb.wait_for_line("i=%d" % (i0+99)) + pdb.send_kbdint() + pdb.wait_for_prompt() + response = pdb.cmd('p "i=%d" % i') + m = re.search('i=(\d+)', response) + assert m, "unexpected response %s" % response + i1 = int(m.group(1)) + assert i1 > i0 + # now test kbd interrupts in interactive mode, they should interrupt + # the current cmd + # simple case: just generate kdbint + pdb.send_kbdint() + pdb.wait_for_prompt() + pdb.cmd("p 'hello'", "hello") # check that we are at prompt + # more complicated case: Ctrl-C while defining bp commands + # interrupted commands should have no effect + pdb.cmd("b 2") + pdb.cmd("commands 1") + pdb.cmd("p 'marker'") + pdb.send_kbdint() + pdb.wait_for_prompt() + pdb.cmd("p 'hello'", "hello") # check that we are back at normal prompt + pdb.send_cmd("c") + response = pdb.wait_for_prompt() + assert not re.search("marker", response, re.I), ( + "unexpected response '%s'" % response) + pdb.cmd("p 'hello'", "hello") #check that we are back at prompt + pdb.send_cmd("q") + pdb.wait_for_normal_exit() + + +def test_main(): + test.test_support.run_unittest(PdbTest) + + +if __name__ == "__main__": + test_main() @@ -15,6 +15,8 @@ Core and Builtins Library ------- +- [issue7245] Better Ctrl-C support in pdb. + What's New in Python 2.7 beta 2? ================================ |