Skip to content

Commit dd7c9b1

Browse files
committed
Use the signal handling thread approach on all platforms
Let the caller of `_connect` tell us whether to start the thread.
1 parent 5993e06 commit dd7c9b1

File tree

2 files changed

+72
-79
lines changed

2 files changed

+72
-79
lines changed

Lib/pdb.py

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,12 +2614,21 @@ async def set_trace_async(*, header=None, commands=None):
26142614
# Remote PDB
26152615

26162616
class _PdbServer(Pdb):
2617-
def __init__(self, sockfile, owns_sockfile=True, **kwargs):
2617+
def __init__(
2618+
self,
2619+
sockfile,
2620+
signal_server=None,
2621+
owns_sockfile=True,
2622+
**kwargs,
2623+
):
26182624
self._owns_sockfile = owns_sockfile
26192625
self._interact_state = None
26202626
self._sockfile = sockfile
26212627
self._command_name_cache = []
26222628
self._write_failed = False
2629+
if signal_server:
2630+
# Only started by the top level _PdbServer, not recursive ones.
2631+
self._start_signal_listener(signal_server)
26232632
super().__init__(**kwargs)
26242633

26252634
@staticmethod
@@ -2675,6 +2684,39 @@ def _ensure_valid_message(self, msg):
26752684
f"PDB message doesn't follow the schema! {msg}"
26762685
)
26772686

2687+
@classmethod
2688+
def _start_signal_listener(cls, address):
2689+
def listener(sock):
2690+
with closing(sock):
2691+
# Check if the interpreter is finalizing every quarter of a second.
2692+
# Clean up and exit if so.
2693+
sock.settimeout(0.25)
2694+
sock.shutdown(socket.SHUT_WR)
2695+
while not shut_down.is_set():
2696+
try:
2697+
data = sock.recv(1024)
2698+
except socket.timeout:
2699+
continue
2700+
if data == b"":
2701+
return # EOF
2702+
signal.raise_signal(signal.SIGINT)
2703+
2704+
def stop_thread():
2705+
shut_down.set()
2706+
thread.join()
2707+
2708+
# Use a daemon thread so that we don't detach until after all non-daemon
2709+
# threads are done. Use an atexit handler to stop gracefully at that point,
2710+
# so that our thread is stopped before the interpreter is torn down.
2711+
shut_down = threading.Event()
2712+
thread = threading.Thread(
2713+
target=listener,
2714+
args=[socket.create_connection(address, timeout=5)],
2715+
daemon=True,
2716+
)
2717+
atexit.register(stop_thread)
2718+
thread.start()
2719+
26782720
def _send(self, **kwargs):
26792721
self._ensure_valid_message(kwargs)
26802722
json_payload = json.dumps(kwargs)
@@ -3132,17 +3174,13 @@ def cmdloop(self):
31323174
self.process_payload(payload)
31333175

31343176
def send_interrupt(self):
3135-
if sys.platform != "win32":
3136-
# On Unix, send a SIGINT to the remote process, which interrupts IO
3137-
# and makes it raise a KeyboardInterrupt on the main thread when
3138-
# PyErr_CheckSignals is called or the eval loop regains control.
3139-
os.kill(self.pid, signal.SIGINT)
3140-
else:
3141-
# On Windows, write to a socket that the PDB server listens on.
3142-
# This triggers the remote to raise a SIGINT for itself. We do this
3143-
# because Windows doesn't allow triggering SIGINT remotely.
3144-
# See https://stackoverflow.com/a/35792192 for many more details.
3145-
self.interrupt_sock.sendall(signal.SIGINT.to_bytes())
3177+
# Write to a socket that the PDB server listens on. This triggers
3178+
# the remote to raise a SIGINT for itself. We do this because
3179+
# Windows doesn't allow triggering SIGINT remotely.
3180+
# See https://stackoverflow.com/a/35792192 for many more details.
3181+
# We could directly send SIGINT to the remote process on Unix, but
3182+
# doing the same thing on all platforms simplifies maintenance.
3183+
self.interrupt_sock.sendall(signal.SIGINT.to_bytes())
31463184

31473185
def process_payload(self, payload):
31483186
match payload:
@@ -3225,46 +3263,19 @@ def complete(self, text, state):
32253263
return None
32263264

32273265

3228-
def _start_interrupt_listener(host, port):
3229-
def sigint_listener(host, port):
3230-
with closing(
3231-
socket.create_connection((host, port), timeout=5)
3232-
) as sock:
3233-
# Check if the interpreter is finalizing every quarter of a second.
3234-
# Clean up and exit if so.
3235-
sock.settimeout(0.25)
3236-
sock.shutdown(socket.SHUT_WR)
3237-
while not shut_down.is_set():
3238-
try:
3239-
data = sock.recv(1024)
3240-
except socket.timeout:
3241-
continue
3242-
if data == b"":
3243-
return # EOF
3244-
signal.raise_signal(signal.SIGINT)
3245-
3246-
def stop_thread():
3247-
shut_down.set()
3248-
thread.join()
3249-
3250-
# Use a daemon thread so that we don't detach until after all non-daemon
3251-
# threads are done. Use an atexit handler to stop gracefully at that point,
3252-
# so that our thread is stopped before the interpreter is torn down.
3253-
shut_down = threading.Event()
3254-
thread = threading.Thread(
3255-
target=sigint_listener,
3256-
args=(host, port),
3257-
daemon=True,
3258-
)
3259-
atexit.register(stop_thread)
3260-
thread.start()
3261-
3262-
3263-
def _connect(host, port, frame, commands, version):
3266+
def _connect(host, port, frame, commands, version, signal_raising_thread):
32643267
with closing(socket.create_connection((host, port))) as conn:
32653268
sockfile = conn.makefile("rwb")
32663269

3267-
remote_pdb = _PdbServer(sockfile)
3270+
# Starting a signal raising thread is optional to allow us the flexibility
3271+
# to switch to sending signals directly on Unix platforms in the future
3272+
# without breaking backwards compatibility. This also makes tests simpler.
3273+
if signal_raising_thread:
3274+
signal_server = (host, port)
3275+
else:
3276+
signal_server = None
3277+
3278+
remote_pdb = _PdbServer(sockfile, signal_server=signal_server)
32683279
weakref.finalize(remote_pdb, sockfile.close)
32693280

32703281
if Pdb._last_pdb_instance is not None:
@@ -3291,12 +3302,6 @@ def attach(pid, commands=()):
32913302
)
32923303
port = server.getsockname()[1]
32933304

3294-
if sys.platform == "win32":
3295-
commands = [
3296-
f"__import__('pdb')._start_interrupt_listener('localhost', {port})",
3297-
*commands,
3298-
]
3299-
33003305
connect_script = stack.enter_context(
33013306
tempfile.NamedTemporaryFile("w", delete_on_close=False)
33023307
)
@@ -3311,6 +3316,7 @@ def attach(pid, commands=()):
33113316
frame=sys._getframe(1),
33123317
commands={json.dumps("\n".join(commands))},
33133318
version={_PdbServer.protocol_version()},
3319+
signal_raising_thread=True,
33143320
)
33153321
"""
33163322
)
@@ -3322,12 +3328,9 @@ def attach(pid, commands=()):
33223328
client_sock, _ = server.accept()
33233329
stack.enter_context(closing(client_sock))
33243330

3325-
if sys.platform == "win32":
3326-
interrupt_sock, _ = server.accept()
3327-
stack.enter_context(closing(interrupt_sock))
3328-
interrupt_sock.setblocking(False)
3329-
else:
3330-
interrupt_sock = None
3331+
interrupt_sock, _ = server.accept()
3332+
stack.enter_context(closing(interrupt_sock))
3333+
interrupt_sock.setblocking(False)
33313334

33323335
_PdbClient(pid, client_sock, interrupt_sock).cmdloop()
33333336

Lib/test/test_remote_pdb.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,7 @@ def dummy_function():
919919
frame=frame,
920920
commands="",
921921
version=pdb._PdbServer.protocol_version(),
922+
signal_raising_thread=False,
922923
)
923924
return x # This line won't be reached in debugging
924925
@@ -976,23 +977,6 @@ def _send_command(self, client_file, command):
976977
client_file.write(json.dumps({"reply": command}).encode() + b"\n")
977978
client_file.flush()
978979

979-
def _send_interrupt(self, pid):
980-
"""Helper to send an interrupt signal to the debugger."""
981-
# with tempfile.NamedTemporaryFile("w", delete_on_close=False) as interrupt_script:
982-
interrupt_script = TESTFN + "_interrupt_script.py"
983-
with open(interrupt_script, 'w') as f:
984-
f.write(
985-
'import pdb, sys\n'
986-
'print("Hello, world!")\n'
987-
'if inst := pdb.Pdb._last_pdb_instance:\n'
988-
' inst.set_trace(sys._getframe(1))\n'
989-
)
990-
self.addCleanup(unlink, interrupt_script)
991-
try:
992-
sys.remote_exec(pid, interrupt_script)
993-
except PermissionError:
994-
self.skipTest("Insufficient permissions to execute code in remote process")
995-
996980
def test_connect_and_basic_commands(self):
997981
"""Test connecting to a remote debugger and sending basic commands."""
998982
self._create_script()
@@ -1105,6 +1089,7 @@ def bar():
11051089
frame=frame,
11061090
commands="",
11071091
version=pdb._PdbServer.protocol_version(),
1092+
signal_raising_thread=True,
11081093
)
11091094
print("Connected to debugger")
11101095
iterations = 50
@@ -1120,6 +1105,10 @@ def bar():
11201105
self._create_script(script=script)
11211106
process, client_file = self._connect_and_get_client_file()
11221107

1108+
# Accept a 2nd connection from the subprocess to tell it about signals
1109+
signal_sock, _ = self.server_sock.accept()
1110+
self.addCleanup(signal_sock.close)
1111+
11231112
with kill_on_error(process):
11241113
# Skip initial messages until we get to the prompt
11251114
self._read_until_prompt(client_file)
@@ -1135,7 +1124,7 @@ def bar():
11351124
break
11361125

11371126
# Inject a script to interrupt the running process
1138-
self._send_interrupt(process.pid)
1127+
signal_sock.sendall(signal.SIGINT.to_bytes())
11391128
messages = self._read_until_prompt(client_file)
11401129

11411130
# Verify we got the keyboard interrupt message.
@@ -1191,6 +1180,7 @@ def run_test():
11911180
frame=frame,
11921181
commands="",
11931182
version=fake_version,
1183+
signal_raising_thread=False,
11941184
)
11951185
11961186
# This should print if the debugger detaches correctly

0 commit comments

Comments
 (0)