aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/pdb.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/pdb.py')
-rw-r--r--Lib/pdb.py439
1 files changed, 349 insertions, 90 deletions
diff --git a/Lib/pdb.py b/Lib/pdb.py
index e38621d4533..fc83728fb6d 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -75,8 +75,10 @@ import dis
import code
import glob
import json
+import stat
import token
import types
+import atexit
import codeop
import pprint
import signal
@@ -92,10 +94,12 @@ import tokenize
import itertools
import traceback
import linecache
+import selectors
+import threading
import _colorize
+import _pyrepl.utils
-from contextlib import closing
-from contextlib import contextmanager
+from contextlib import ExitStack, closing, contextmanager
from rlcompleter import Completer
from types import CodeType
from warnings import deprecated
@@ -339,7 +343,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
_last_pdb_instance = None
def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
- nosigint=False, readrc=True, mode=None, backend=None):
+ nosigint=False, readrc=True, mode=None, backend=None, colorize=False):
bdb.Bdb.__init__(self, skip=skip, backend=backend if backend else get_default_backend())
cmd.Cmd.__init__(self, completekey, stdin, stdout)
sys.audit("pdb.Pdb")
@@ -352,6 +356,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
self._wait_for_mainpyfile = False
self.tb_lineno = {}
self.mode = mode
+ self.colorize = colorize and _colorize.can_colorize(file=stdout or sys.stdout)
# Try to load readline if it exists
try:
import readline
@@ -743,12 +748,34 @@ class Pdb(bdb.Bdb, cmd.Cmd):
self.message(repr(obj))
@contextmanager
- def _enable_multiline_completion(self):
+ def _enable_multiline_input(self):
+ try:
+ import readline
+ except ImportError:
+ yield
+ return
+
+ def input_auto_indent():
+ last_index = readline.get_current_history_length()
+ last_line = readline.get_history_item(last_index)
+ if last_line:
+ if last_line.isspace():
+ # If the last line is empty, we don't need to indent
+ return
+
+ last_line = last_line.rstrip('\r\n')
+ indent = len(last_line) - len(last_line.lstrip())
+ if last_line.endswith(":"):
+ indent += 4
+ readline.insert_text(' ' * indent)
+
completenames = self.completenames
try:
self.completenames = self.complete_multiline_names
+ readline.set_startup_hook(input_auto_indent)
yield
finally:
+ readline.set_startup_hook()
self.completenames = completenames
return
@@ -857,7 +884,7 @@ class Pdb(bdb.Bdb, cmd.Cmd):
try:
if (code := codeop.compile_command(line + '\n', '<stdin>', 'single')) is None:
# Multi-line mode
- with self._enable_multiline_completion():
+ with self._enable_multiline_input():
buffer = line
continue_prompt = "... "
while (code := codeop.compile_command(buffer, '<stdin>', 'single')) is None:
@@ -879,7 +906,11 @@ class Pdb(bdb.Bdb, cmd.Cmd):
return None, None, False
else:
line = line.rstrip('\r\n')
- buffer += '\n' + line
+ if line.isspace():
+ # empty line, just continue
+ buffer += '\n'
+ else:
+ buffer += '\n' + line
self.lastcmd = buffer
except SyntaxError as e:
# Maybe it's an await expression/statement
@@ -1036,6 +1067,13 @@ class Pdb(bdb.Bdb, cmd.Cmd):
return True
return False
+ def _colorize_code(self, code):
+ if self.colorize:
+ colors = list(_pyrepl.utils.gen_colors(code))
+ chars, _ = _pyrepl.utils.disp_str(code, colors=colors, force_color=True)
+ code = "".join(chars)
+ return code
+
# interface abstraction functions
def message(self, msg, end='\n'):
@@ -1157,6 +1195,22 @@ class Pdb(bdb.Bdb, cmd.Cmd):
state += 1
return matches
+ @contextmanager
+ def _enable_rlcompleter(self, ns):
+ try:
+ import readline
+ except ImportError:
+ yield
+ return
+
+ try:
+ old_completer = readline.get_completer()
+ completer = Completer(ns)
+ readline.set_completer(completer.complete)
+ yield
+ finally:
+ readline.set_completer(old_completer)
+
# Pdb meta commands, only intended to be used internally by pdb
def _pdbcmd_print_frame_status(self, arg):
@@ -2150,6 +2204,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
s += '->'
elif lineno == exc_lineno:
s += '>>'
+ if self.colorize:
+ line = self._colorize_code(line)
self.message(s + '\t' + line.rstrip())
def do_whatis(self, arg):
@@ -2242,9 +2298,10 @@ class Pdb(bdb.Bdb, cmd.Cmd):
contains all the (global and local) names found in the current scope.
"""
ns = {**self.curframe.f_globals, **self.curframe.f_locals}
- console = _PdbInteractiveConsole(ns, message=self.message)
- console.interact(banner="*pdb interact start*",
- exitmsg="*exit from pdb interact command*")
+ with self._enable_rlcompleter(ns):
+ console = _PdbInteractiveConsole(ns, message=self.message)
+ console.interact(banner="*pdb interact start*",
+ exitmsg="*exit from pdb interact command*")
def do_alias(self, arg):
"""alias [name [command]]
@@ -2348,8 +2405,14 @@ class Pdb(bdb.Bdb, cmd.Cmd):
prefix = '> '
else:
prefix = ' '
- self.message(prefix +
- self.format_stack_entry(frame_lineno, prompt_prefix))
+ stack_entry = self.format_stack_entry(frame_lineno, prompt_prefix)
+ if self.colorize:
+ lines = stack_entry.split(prompt_prefix, 1)
+ if len(lines) > 1:
+ # We have some code to display
+ lines[1] = self._colorize_code(lines[1])
+ stack_entry = prompt_prefix.join(lines)
+ self.message(prefix + stack_entry)
# Provide help
@@ -2587,7 +2650,7 @@ def set_trace(*, header=None, commands=None):
if Pdb._last_pdb_instance is not None:
pdb = Pdb._last_pdb_instance
else:
- pdb = Pdb(mode='inline', backend='monitoring')
+ pdb = Pdb(mode='inline', backend='monitoring', colorize=True)
if header is not None:
pdb.message(header)
pdb.set_trace(sys._getframe().f_back, commands=commands)
@@ -2602,7 +2665,7 @@ async def set_trace_async(*, header=None, commands=None):
if Pdb._last_pdb_instance is not None:
pdb = Pdb._last_pdb_instance
else:
- pdb = Pdb(mode='inline', backend='monitoring')
+ pdb = Pdb(mode='inline', backend='monitoring', colorize=True)
if header is not None:
pdb.message(header)
await pdb.set_trace_async(sys._getframe().f_back, commands=commands)
@@ -2610,13 +2673,26 @@ async def set_trace_async(*, header=None, commands=None):
# Remote PDB
class _PdbServer(Pdb):
- def __init__(self, sockfile, owns_sockfile=True, **kwargs):
+ def __init__(
+ self,
+ sockfile,
+ signal_server=None,
+ owns_sockfile=True,
+ colorize=False,
+ **kwargs,
+ ):
self._owns_sockfile = owns_sockfile
self._interact_state = None
self._sockfile = sockfile
self._command_name_cache = []
self._write_failed = False
- super().__init__(**kwargs)
+ if signal_server:
+ # Only started by the top level _PdbServer, not recursive ones.
+ self._start_signal_listener(signal_server)
+ # Override the `colorize` attribute set by the parent constructor,
+ # because it checks the server's stdout, rather than the client's.
+ super().__init__(colorize=False, **kwargs)
+ self.colorize = colorize
@staticmethod
def protocol_version():
@@ -2671,15 +2747,49 @@ class _PdbServer(Pdb):
f"PDB message doesn't follow the schema! {msg}"
)
+ @classmethod
+ def _start_signal_listener(cls, address):
+ def listener(sock):
+ with closing(sock):
+ # Check if the interpreter is finalizing every quarter of a second.
+ # Clean up and exit if so.
+ sock.settimeout(0.25)
+ sock.shutdown(socket.SHUT_WR)
+ while not shut_down.is_set():
+ try:
+ data = sock.recv(1024)
+ except socket.timeout:
+ continue
+ if data == b"":
+ return # EOF
+ signal.raise_signal(signal.SIGINT)
+
+ def stop_thread():
+ shut_down.set()
+ thread.join()
+
+ # Use a daemon thread so that we don't detach until after all non-daemon
+ # threads are done. Use an atexit handler to stop gracefully at that point,
+ # so that our thread is stopped before the interpreter is torn down.
+ shut_down = threading.Event()
+ thread = threading.Thread(
+ target=listener,
+ args=[socket.create_connection(address, timeout=5)],
+ daemon=True,
+ )
+ atexit.register(stop_thread)
+ thread.start()
+
def _send(self, **kwargs):
self._ensure_valid_message(kwargs)
json_payload = json.dumps(kwargs)
try:
self._sockfile.write(json_payload.encode() + b"\n")
self._sockfile.flush()
- except OSError:
- # This means that the client has abruptly disconnected, but we'll
- # handle that the next time we try to read from the client instead
+ except (OSError, ValueError):
+ # We get an OSError if the network connection has dropped, and a
+ # ValueError if detach() if the sockfile has been closed. We'll
+ # handle this the next time we try to read from the client instead
# of trying to handle it from everywhere _send() may be called.
# Track this with a flag rather than assuming readline() will ever
# return an empty string because the socket may be half-closed.
@@ -2716,7 +2826,7 @@ class _PdbServer(Pdb):
try:
payload = json.loads(msg)
except json.JSONDecodeError:
- self.error(f"Disconnecting: client sent invalid JSON {msg}")
+ self.error(f"Disconnecting: client sent invalid JSON {msg!r}")
raise EOFError
match payload:
@@ -2749,14 +2859,18 @@ class _PdbServer(Pdb):
self.error(f"Ignoring invalid message from client: {msg}")
def _complete_any(self, text, line, begidx, endidx):
- if begidx == 0:
- return self.completenames(text, line, begidx, endidx)
-
- cmd = self.parseline(line)[0]
- if cmd:
- compfunc = getattr(self, "complete_" + cmd, self.completedefault)
- else:
+ # If we're in 'interact' mode, we need to use the default completer
+ if self._interact_state:
compfunc = self.completedefault
+ else:
+ if begidx == 0:
+ return self.completenames(text, line, begidx, endidx)
+
+ cmd = self.parseline(line)[0]
+ if cmd:
+ compfunc = getattr(self, "complete_" + cmd, self.completedefault)
+ else:
+ compfunc = self.completedefault
return compfunc(text, line, begidx, endidx)
def cmdloop(self, intro=None):
@@ -2866,7 +2980,11 @@ class _PdbServer(Pdb):
@typing.override
def _create_recursive_debugger(self):
- return _PdbServer(self._sockfile, owns_sockfile=False)
+ return _PdbServer(
+ self._sockfile,
+ owns_sockfile=False,
+ colorize=self.colorize,
+ )
@typing.override
def _prompt_for_confirmation(self, prompt, default):
@@ -2903,15 +3021,21 @@ class _PdbServer(Pdb):
class _PdbClient:
- def __init__(self, pid, sockfile, interrupt_script):
+ def __init__(self, pid, server_socket, interrupt_sock):
self.pid = pid
- self.sockfile = sockfile
- self.interrupt_script = interrupt_script
+ self.read_buf = b""
+ self.signal_read = None
+ self.signal_write = None
+ self.sigint_received = False
+ self.raise_on_sigint = False
+ self.server_socket = server_socket
+ self.interrupt_sock = interrupt_sock
self.pdb_instance = Pdb()
self.pdb_commands = set()
self.completion_matches = []
self.state = "dumb"
self.write_failed = False
+ self.multiline_block = False
def _ensure_valid_message(self, msg):
# Ensure the message conforms to our protocol.
@@ -2947,8 +3071,7 @@ class _PdbClient:
self._ensure_valid_message(kwargs)
json_payload = json.dumps(kwargs)
try:
- self.sockfile.write(json_payload.encode() + b"\n")
- self.sockfile.flush()
+ self.server_socket.sendall(json_payload.encode() + b"\n")
except OSError:
# This means that the client has abruptly disconnected, but we'll
# handle that the next time we try to read from the client instead
@@ -2957,9 +3080,44 @@ class _PdbClient:
# return an empty string because the socket may be half-closed.
self.write_failed = True
- def read_command(self, prompt):
- reply = input(prompt)
+ def _readline(self):
+ if self.sigint_received:
+ # There's a pending unhandled SIGINT. Handle it now.
+ self.sigint_received = False
+ raise KeyboardInterrupt
+ # Wait for either a SIGINT or a line or EOF from the PDB server.
+ selector = selectors.DefaultSelector()
+ selector.register(self.signal_read, selectors.EVENT_READ)
+ selector.register(self.server_socket, selectors.EVENT_READ)
+
+ while b"\n" not in self.read_buf:
+ for key, _ in selector.select():
+ if key.fileobj == self.signal_read:
+ self.signal_read.recv(1024)
+ if self.sigint_received:
+ # If not, we're reading wakeup events for sigints that
+ # we've previously handled, and can ignore them.
+ self.sigint_received = False
+ raise KeyboardInterrupt
+ elif key.fileobj == self.server_socket:
+ data = self.server_socket.recv(16 * 1024)
+ self.read_buf += data
+ if not data and b"\n" not in self.read_buf:
+ # EOF without a full final line. Drop the partial line.
+ self.read_buf = b""
+ return b""
+
+ ret, sep, self.read_buf = self.read_buf.partition(b"\n")
+ return ret + sep
+
+ def read_input(self, prompt, multiline_block):
+ self.multiline_block = multiline_block
+ with self._sigint_raises_keyboard_interrupt():
+ return input(prompt)
+
+ def read_command(self, prompt):
+ reply = self.read_input(prompt, multiline_block=False)
if self.state == "dumb":
# No logic applied whatsoever, just pass the raw reply back.
return reply
@@ -2982,9 +3140,9 @@ class _PdbClient:
return prefix + reply
# Otherwise, valid first line of a multi-line statement
- continue_prompt = "...".ljust(len(prompt))
+ more_prompt = "...".ljust(len(prompt))
while codeop.compile_command(reply, "<stdin>", "single") is None:
- reply += "\n" + input(continue_prompt)
+ reply += "\n" + self.read_input(more_prompt, multiline_block=True)
return prefix + reply
@@ -3009,11 +3167,70 @@ class _PdbClient:
finally:
readline.set_completer(old_completer)
+ @contextmanager
+ def _sigint_handler(self):
+ # Signal handling strategy:
+ # - When we call input() we want a SIGINT to raise KeyboardInterrupt
+ # - Otherwise we want to write to the wakeup FD and set a flag.
+ # We'll break out of select() when the wakeup FD is written to,
+ # and we'll check the flag whenever we're about to accept input.
+ def handler(signum, frame):
+ self.sigint_received = True
+ if self.raise_on_sigint:
+ # One-shot; don't raise again until the flag is set again.
+ self.raise_on_sigint = False
+ self.sigint_received = False
+ raise KeyboardInterrupt
+
+ sentinel = object()
+ old_handler = sentinel
+ old_wakeup_fd = sentinel
+
+ self.signal_read, self.signal_write = socket.socketpair()
+ with (closing(self.signal_read), closing(self.signal_write)):
+ self.signal_read.setblocking(False)
+ self.signal_write.setblocking(False)
+
+ try:
+ old_handler = signal.signal(signal.SIGINT, handler)
+
+ try:
+ old_wakeup_fd = signal.set_wakeup_fd(
+ self.signal_write.fileno(),
+ warn_on_full_buffer=False,
+ )
+ yield
+ finally:
+ # Restore the old wakeup fd if we installed a new one
+ if old_wakeup_fd is not sentinel:
+ signal.set_wakeup_fd(old_wakeup_fd)
+ finally:
+ self.signal_read = self.signal_write = None
+ if old_handler is not sentinel:
+ # Restore the old handler if we installed a new one
+ signal.signal(signal.SIGINT, old_handler)
+
+ @contextmanager
+ def _sigint_raises_keyboard_interrupt(self):
+ if self.sigint_received:
+ # There's a pending unhandled SIGINT. Handle it now.
+ self.sigint_received = False
+ raise KeyboardInterrupt
+
+ try:
+ self.raise_on_sigint = True
+ yield
+ finally:
+ self.raise_on_sigint = False
+
def cmdloop(self):
- with self.readline_completion(self.complete):
+ with (
+ self._sigint_handler(),
+ self.readline_completion(self.complete),
+ ):
while not self.write_failed:
try:
- if not (payload_bytes := self.sockfile.readline()):
+ if not (payload_bytes := self._readline()):
break
except KeyboardInterrupt:
self.send_interrupt()
@@ -3023,7 +3240,7 @@ class _PdbClient:
payload = json.loads(payload_bytes)
except json.JSONDecodeError:
print(
- f"*** Invalid JSON from remote: {payload_bytes}",
+ f"*** Invalid JSON from remote: {payload_bytes!r}",
flush=True,
)
continue
@@ -3031,11 +3248,17 @@ class _PdbClient:
self.process_payload(payload)
def send_interrupt(self):
- print(
- "\n*** Program will stop at the next bytecode instruction."
- " (Use 'cont' to resume)."
- )
- sys.remote_exec(self.pid, self.interrupt_script)
+ if self.interrupt_sock is not None:
+ # Write to a socket that the PDB server listens on. This triggers
+ # the remote to raise a SIGINT for itself. We do this because
+ # Windows doesn't allow triggering SIGINT remotely.
+ # See https://stackoverflow.com/a/35792192 for many more details.
+ self.interrupt_sock.sendall(signal.SIGINT.to_bytes())
+ else:
+ # On Unix we can just send a SIGINT to the remote process.
+ # This is preferable to using the signal thread approach that we
+ # use on Windows because it can interrupt IO in the main thread.
+ os.kill(self.pid, signal.SIGINT)
def process_payload(self, payload):
match payload:
@@ -3084,9 +3307,13 @@ class _PdbClient:
origline = readline.get_line_buffer()
line = origline.lstrip()
- stripped = len(origline) - len(line)
- begidx = readline.get_begidx() - stripped
- endidx = readline.get_endidx() - stripped
+ if self.multiline_block:
+ # We're completing a line contained in a multi-line block.
+ # Force the remote to treat it as a Python expression.
+ line = "! " + line
+ offset = len(origline) - len(line)
+ begidx = readline.get_begidx() - offset
+ endidx = readline.get_endidx() - offset
msg = {
"complete": {
@@ -3101,7 +3328,7 @@ class _PdbClient:
if self.write_failed:
return None
- payload = self.sockfile.readline()
+ payload = self._readline()
if not payload:
return None
@@ -3118,11 +3345,31 @@ class _PdbClient:
return None
-def _connect(host, port, frame, commands, version):
+def _connect(
+ *,
+ host,
+ port,
+ frame,
+ commands,
+ version,
+ signal_raising_thread,
+ colorize,
+):
with closing(socket.create_connection((host, port))) as conn:
sockfile = conn.makefile("rwb")
- remote_pdb = _PdbServer(sockfile)
+ # The client requests this thread on Windows but not on Unix.
+ # Most tests don't request this thread, to keep them simpler.
+ if signal_raising_thread:
+ signal_server = (host, port)
+ else:
+ signal_server = None
+
+ remote_pdb = _PdbServer(
+ sockfile,
+ signal_server=signal_server,
+ colorize=colorize,
+ )
weakref.finalize(remote_pdb, sockfile.close)
if Pdb._last_pdb_instance is not None:
@@ -3137,49 +3384,57 @@ def _connect(host, port, frame, commands, version):
f"\nLocal pdb module's protocol version: {attach_ver}"
)
else:
- remote_pdb.rcLines.extend(commands.splitlines())
- remote_pdb.set_trace(frame=frame)
+ remote_pdb.set_trace(frame=frame, commands=commands.splitlines())
def attach(pid, commands=()):
"""Attach to a running process with the given PID."""
- with closing(socket.create_server(("localhost", 0))) as server:
+ with ExitStack() as stack:
+ server = stack.enter_context(
+ closing(socket.create_server(("localhost", 0)))
+ )
port = server.getsockname()[1]
- with tempfile.NamedTemporaryFile("w", delete_on_close=False) as connect_script:
- connect_script.write(
- textwrap.dedent(
- f"""
- import pdb, sys
- pdb._connect(
- host="localhost",
- port={port},
- frame=sys._getframe(1),
- commands={json.dumps("\n".join(commands))},
- version={_PdbServer.protocol_version()},
- )
- """
+ connect_script = stack.enter_context(
+ tempfile.NamedTemporaryFile("w", delete_on_close=False)
+ )
+
+ use_signal_thread = sys.platform == "win32"
+ colorize = _colorize.can_colorize()
+
+ connect_script.write(
+ textwrap.dedent(
+ f"""
+ import pdb, sys
+ pdb._connect(
+ host="localhost",
+ port={port},
+ frame=sys._getframe(1),
+ commands={json.dumps("\n".join(commands))},
+ version={_PdbServer.protocol_version()},
+ signal_raising_thread={use_signal_thread!r},
+ colorize={colorize!r},
)
+ """
)
- connect_script.close()
- sys.remote_exec(pid, connect_script.name)
-
- # TODO Add a timeout? Or don't bother since the user can ^C?
- client_sock, _ = server.accept()
-
- with closing(client_sock):
- sockfile = client_sock.makefile("rwb")
-
- with closing(sockfile):
- with tempfile.NamedTemporaryFile("w", delete_on_close=False) as interrupt_script:
- interrupt_script.write(
- 'import pdb, sys\n'
- 'if inst := pdb.Pdb._last_pdb_instance:\n'
- ' inst.set_trace(sys._getframe(1))\n'
- )
- interrupt_script.close()
+ )
+ connect_script.close()
+ orig_mode = os.stat(connect_script.name).st_mode
+ os.chmod(connect_script.name, orig_mode | stat.S_IROTH | stat.S_IRGRP)
+ sys.remote_exec(pid, connect_script.name)
+
+ # TODO Add a timeout? Or don't bother since the user can ^C?
+ client_sock, _ = server.accept()
+ stack.enter_context(closing(client_sock))
+
+ if use_signal_thread:
+ interrupt_sock, _ = server.accept()
+ stack.enter_context(closing(interrupt_sock))
+ interrupt_sock.setblocking(False)
+ else:
+ interrupt_sock = None
- _PdbClient(pid, sockfile, interrupt_script.name).cmdloop()
+ _PdbClient(pid, client_sock, interrupt_sock).cmdloop()
# Post-Mortem interface
@@ -3237,7 +3492,8 @@ def help():
_usage = """\
Debug the Python program given by pyfile. Alternatively,
an executable module or package to debug can be specified using
-the -m switch.
+the -m switch. You can also attach to a running Python process
+using the -p option with its PID.
Initial commands are read from .pdbrc files in your home directory
and in the current directory, if they exist. Commands supplied with
@@ -3251,10 +3507,13 @@ To let the script run up to a given line X in the debugged file, use
def main():
import argparse
- parser = argparse.ArgumentParser(usage="%(prog)s [-h] [-c command] (-m module | -p pid | pyfile) [args ...]",
- description=_usage,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- allow_abbrev=False)
+ parser = argparse.ArgumentParser(
+ usage="%(prog)s [-h] [-c command] (-m module | -p pid | pyfile) [args ...]",
+ description=_usage,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ allow_abbrev=False,
+ color=True,
+ )
# We need to maunally get the script from args, because the first positional
# arguments could be either the script we need to debug, or the argument
@@ -3317,7 +3576,7 @@ def main():
# modified by the script being debugged. It's a bad idea when it was
# changed by the user from the command line. There is a "restart" command
# which allows explicit specification of command line arguments.
- pdb = Pdb(mode='cli', backend='monitoring')
+ pdb = Pdb(mode='cli', backend='monitoring', colorize=True)
pdb.rcLines.extend(opts.commands)
while True:
try: