Skip to content

Commit 5993e06

Browse files
committed
Use a thread to manage interrupts on Windows
1 parent 4c0b431 commit 5993e06

File tree

2 files changed

+58
-23
lines changed

2 files changed

+58
-23
lines changed

Lib/pdb.py

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import json
7878
import token
7979
import types
80+
import atexit
8081
import codeop
8182
import pprint
8283
import signal
@@ -93,6 +94,7 @@
9394
import traceback
9495
import linecache
9596
import selectors
97+
import threading
9698
import _colorize
9799

98100
from contextlib import ExitStack
@@ -2906,15 +2908,15 @@ def default(self, line):
29062908

29072909

29082910
class _PdbClient:
2909-
def __init__(self, pid, server_socket, interrupt_script):
2911+
def __init__(self, pid, server_socket, interrupt_sock):
29102912
self.pid = pid
29112913
self.read_buf = b""
29122914
self.signal_read = None
29132915
self.signal_write = None
29142916
self.sigint_received = False
29152917
self.raise_on_sigint = False
29162918
self.server_socket = server_socket
2917-
self.interrupt_script = interrupt_script
2919+
self.interrupt_sock = interrupt_sock
29182920
self.pdb_instance = Pdb()
29192921
self.pdb_commands = set()
29202922
self.completion_matches = []
@@ -3136,14 +3138,11 @@ def send_interrupt(self):
31363138
# PyErr_CheckSignals is called or the eval loop regains control.
31373139
os.kill(self.pid, signal.SIGINT)
31383140
else:
3139-
# On Windows, inject a remote script that calls Pdb.set_trace()
3140-
# when the eval loop regains control. This cannot interrupt IO, and
3141-
# also cannot interrupt statements executed at a PDB prompt.
3142-
print(
3143-
"\n*** Program will stop at the next bytecode instruction."
3144-
" (Use 'cont' to resume)."
3145-
)
3146-
sys.remote_exec(self.pid, self.interrupt_script)
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())
31473146

31483147
def process_payload(self, payload):
31493148
match payload:
@@ -3226,6 +3225,41 @@ def complete(self, text, state):
32263225
return None
32273226

32283227

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+
32293263
def _connect(host, port, frame, commands, version):
32303264
with closing(socket.create_connection((host, port))) as conn:
32313265
sockfile = conn.makefile("rwb")
@@ -3255,9 +3289,14 @@ def attach(pid, commands=()):
32553289
server = stack.enter_context(
32563290
closing(socket.create_server(("localhost", 0)))
32573291
)
3258-
32593292
port = server.getsockname()[1]
32603293

3294+
if sys.platform == "win32":
3295+
commands = [
3296+
f"__import__('pdb')._start_interrupt_listener('localhost', {port})",
3297+
*commands,
3298+
]
3299+
32613300
connect_script = stack.enter_context(
32623301
tempfile.NamedTemporaryFile("w", delete_on_close=False)
32633302
)
@@ -3281,20 +3320,16 @@ def attach(pid, commands=()):
32813320

32823321
# TODO Add a timeout? Or don't bother since the user can ^C?
32833322
client_sock, _ = server.accept()
3284-
32853323
stack.enter_context(closing(client_sock))
32863324

3287-
interrupt_script = stack.enter_context(
3288-
tempfile.NamedTemporaryFile("w", delete_on_close=False)
3289-
)
3290-
interrupt_script.write(
3291-
'import pdb, sys\n'
3292-
'if inst := pdb.Pdb._last_pdb_instance:\n'
3293-
' inst.set_trace(sys._getframe(1))\n'
3294-
)
3295-
interrupt_script.close()
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
32963331

3297-
_PdbClient(pid, client_sock, interrupt_script.name).cmdloop()
3332+
_PdbClient(pid, client_sock, interrupt_sock).cmdloop()
32983333

32993334

33003335
# Post-Mortem interface

Lib/test/test_remote_pdb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def mock_input(prompt):
163163
client = _PdbClient(
164164
pid=0,
165165
server_socket=server_sock,
166-
interrupt_script="/a/b.py",
166+
interrupt_sock=unittest.mock.Mock(spec=socket.socket),
167167
)
168168

169169
if expected_exception is not None:

0 commit comments

Comments
 (0)