8000 gh-131591: Allow pdb to attach to a running process by godlygeek · Pull Request #132451 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-131591: Allow pdb to attach to a running process #132451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Apr 25, 2025
Merged
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5225333
Allow pdb to attach to a running process
godlygeek Apr 12, 2025
78a3085
Remove 2 unused _RemotePdb instance attributes
godlygeek Apr 15, 2025
90e0a81
Reduce duplication for 'debug' command
godlygeek Apr 15, 2025
e44a670
End commands entry on 'end' and ^C and ^D
godlygeek Apr 15, 2025
e837246
Set the frame for remote pdb to stop in explicitly
godlygeek Apr 15, 2025
27efa97
Fix an unbound local in an error message
godlygeek Apr 15, 2025
557a725
Clean up remote PDB detaching
godlygeek Apr 15, 2025
7f7584a
Allow ctrl-c to interrupt a running process
godlygeek Apr 17, 2025
5666ffb
Automatically detach if the client dies unexpectedly
godlygeek Apr 17, 2025
325f166
Clear _last_pdb_instance on detach
godlygeek Apr 17, 2025
72830e2
Refuse to attach if another PDB instance is installed
godlygeek Apr 17, 2025
27c6780
Handle the confirmation prompt issued by 'clear'
godlygeek Apr 18, 2025
baaf28a
Make message and error handle non-string args
godlygeek Apr 18, 2025
e61cc31
Add some basic tests
pablogsal Apr 18, 2025
5d59ce1
Don't use deprecated method
pablogsal Apr 18, 2025
09adb2b
Try to prevent a PermissionError on Windows
godlygeek Apr 18, 2025
600aa05
Address review comments
godlygeek Apr 20, 2025
0601f10
Add protocol versioning and support -c commands
godlygeek Apr 20, 2025
5a1755b
Fix tests to match new _connect signature for protocol versioning/com…
godlygeek Apr 21, 2025
f184e4e
Add some comments describing our protocol
godlygeek Apr 21, 2025
82b71f8
Use the 'commands' state for '(com)' prompts
godlygeek Apr 22, 2025
3986c17
Remove choices parameter from _prompt_for_confirmation
8000 godlygeek Apr 22, 2025
2e69667
Rename _RemotePdb to _PdbServer
godlygeek Apr 22, 2025
55adbcc
Avoid fallthrough in signal handling
godlygeek Apr 22, 2025
0bda5c2
Fix handling of a SystemExit raised in normal pdb mode
godlygeek Apr 22, 2025
5e93247
Address nit
godlygeek Apr 22, 2025
f799e83
Use textwrap.dedent for test readability
godlygeek Apr 22, 2025
46fb219
Drop dataclasses dependency
godlygeek Apr 22, 2025
1ec9475
Combine the two blocks for handling -p PID into one
godlygeek Apr 22, 2025
662c7eb
Add a news entry
godlygeek Apr 22, 2025
ac36d7d
Skip remote PDB integration test on WASI
godlygeek Apr 22, 2025
f06d9c2
Two small things missed in the previous fixes
godlygeek Apr 22, 2025
715af27
Remove call to set_step in interrupt handler
godlygeek Apr 22, 2025
c654fdf
More tests
pablogsal Apr 23, 2025
205bc55
More tests
pablogsal Apr 23, 2025
bbe784b
More tests
pablogsal Apr 23, 2025
30cb537
Add what's new entry
pablogsal Apr 23, 2025
659556f
use dedent
pablogsal Apr 23, 2025
6c2d970
Add synchronization to test_keyboard_interrupt
godlygeek Apr 24, 2025
100be44
Stop sending a "signal" message in test_keyboard_interrupt"
godlygeek Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More tests
  • Loading branch information
pablogsal committed Apr 23, 2025
commit 205bc552b68f145a26e0263b38051a399afdba59
168 changes: 168 additions & 0 deletions Lib/test/test_remote_pdb.py
8000
Original file line number Diff line number Diff line change
Expand Up @@ -501,5 +501,173 @@ def bar():
self.assertIn("Function returned: 42", stdout)
self.assertEqual(process.returncode, 0)

def test_handle_eof(self):
"""Test that EOF signal properly exits the debugger."""
self._create_script()
process, client_file = self._connect_and_get_client_file()

with process:
# Skip initial messages until we get to the prompt
self._read_until_prompt(client_file)

# Send EOF signal to exit the debugger
client_file.write(json.dumps({"signal": "EOF"}).encode() + b"\n")
client_file.flush()

# The process should complete normally after receiving EOF
stdout, stderr = process.communicate(timeout=5)

# Verify process completed correctly
self.assertIn("Function returned: 42", stdout)
self.assertEqual(process.returncode, 0)
self.assertEqual(stderr, "")

def test_protocol_version(self):
"""Test that incompatible protocol versions are properly detected."""
# Create a script using an incompatible protocol version
script = f"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you do textwrap.dedent() for all the scripts? So it's visually indented properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import sys
import pdb

def run_test():
frame = sys._getframe()

# Use a fake version number that's definitely incompatible
fake_version = 0x01010101 # A fake version that doesn't match any real Python version

# Connect with the wrong version
pdb._connect(
host='127.0.0.1',
port={self.port},
frame=frame,
commands="",
version=fake_version,
)

# This should print if the debugger detaches correctly
print("Debugger properly detected version mismatch")
return True

if __name__ == "__main__":
print("Test result:", run_test())
"""
self._create_script(script=script)
process, client_file = self._connect_and_get_client_file()

with process:
# First message should be an error about protocol version mismatch
data = client_file.readline()
message = json.loads(data.decode())

self.assertIn('message', message)
self.assertEqual(message['type'], 'error')
self.assertIn('incompatible', message['message'])
self.assertIn('protocol version', message['message'])

# The process should complete normally
stdout, stderr = process.communicate(timeout=5)

# Verify the process completed successfully
self.assertIn("Test result: True", stdout)
self.assertIn("Debugger properly detected version mismatch", stdout)
self.assertEqual(process.returncode, 0)

def test_help_system(self):
"""Test that the help system properly sends help text to the client."""
self._create_script()
process, client_file = self._connect_and_get_client_file()

with process:
# Skip initial messages until we get to the prompt
self._read_until_prompt(client_file)

# Request help for different commands
help_commands = ["help", "help break", "help continue", "help pdb"]

for cmd in help_commands:
self._send_command(client_file, cmd)

# Look for help message
data = client_file.readline()
message = json.loads(data.decode())

self.assertIn('help', message)

if cmd == "help":
# Should just contain the command itself
self.assertEqual(message['help'], "")
else:
# Should contain the specific command we asked for help with
command = cmd.split()[1]
self.assertEqual(message['help'], command)

# Skip to the next prompt
self._read_until_prompt(client_file)

# Continue execution to finish the program
self._send_command(client_file, "c")

stdout, stderr = process.communicate(timeout=5)
self.assertIn("Function returned: 42", stdout)
self.assertEqual(process.returncode, 0)

def test_multi_line_commands(self):
"""Test that multi-line commands work properly over remote connection."""
self._create_script()
process, client_file = self._connect_and_get_client_file()

with process:
# Skip initial messages until we get to the prompt
self._read_until_prompt(client_file)

# Send a multi-line command
multi_line_commands = [
# Define a function
"def test_func():\n return 42",

# For loop
"for i in range(3):\n print(i)",

# If statement
"if True:\n x = 42\nelse:\n x = 0",

# Try/except
"try:\n result = 10/2\n print(result)\nexcept ZeroDivisionError:\n print('Error')",

# Class definition
"class TestClass:\n def __init__(self):\n self.value = 100\n def get_value(self):\n return self.value"
]

for cmd in multi_line_commands:
self._send_command(client_file, cmd)
self._read_until_prompt(client_file)

# Test executing the defined function
self._send_command(client_file, "test_func()")
messages = self._read_until_prompt(client_file)

# Find the result message
result_msg = next(msg['message'] for msg in messages if 'message' in msg)
self.assertIn("42", result_msg)

# Test creating an instance of the defined class
self._send_command(client_file, "obj = TestClass()")
self._read_until_prompt(client_file)

# Test calling a method on the instance
self._send_command(client_file, "obj.get_value()")
messages = self._read_until_prompt(client_file)

# Find the result message
result_msg = next(msg['message'] for msg in messages if 'message' in msg)
self.assertIn("100", result_msg)

# Continue execution to finish
self._send_command(client_file, "c")

stdout, stderr = process.communicate(timeout=5)
self.assertIn("Function returned: 42", stdout)
self.assertEqual(process.returncode, 0)

if __name__ == "__main__":
unittest.main()
Loading
0