8000 Have _PdbClient work with the socket directly · python/cpython@4428f9d · GitHub
[go: up one dir, main page]

Skip to content

Commit 4428f9d

Browse files
committed
Have _PdbClient work with the socket directly
Previously we worked with a file object created with `socket.makefile`, but on Windows the `readline()` method of a socket file can't be interrupted with Ctrl-C, and neither can `recv()` on a socket. This refactor lays the ground work for a solution this this problem.
1 parent 8d89bf4 commit 4428f9d

File tree

2 files changed

+45
-117
lines changed

2 files changed

+45
-117
lines changed

Lib/pdb.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2903,9 +2903,10 @@ def default(self, line):
29032903

29042904

29052905
class _PdbClient:
2906-
def __init__(self, pid, sockfile, interrupt_script):
2906+
def __init__(self, pid, server_socket, interrupt_script):
29072907
self.pid = pid
2908-
self.sockfile = sockfile
2908+
self.read_buf = b""
2909+
self.server_socket = server_socket
29092910
self.interrupt_script = interrupt_script
29102911
self.pdb_instance = Pdb()
29112912
self.pdb_commands = set()
@@ -2947,8 +2948,7 @@ def _send(self, **kwargs):
29472948
self._ensure_valid_message(kwargs)
29482949
json_payload = json.dumps(kwargs)
29492950
try:
2950-
self.sockfile.write(json_payload.encode() + b"\n")
2951-
self.sockfile.flush()
2951+
self.server_socket.sendall(json_payload.encode() + b"\n")
29522952
except OSError:
29532953
# This means that the client has abruptly disconnected, but we'll
29542954
# handle that the next time we try to read from the client instead
@@ -2957,6 +2957,15 @@ def _send(self, **kwargs):
29572957
# return an empty string because the socket may be half-closed.
29582958
self.write_failed = True
29592959

2960+
def _readline(self):
2961+
while b"\n" not in self.read_buf:
2962+
self.read_buf += self.server_socket.recv(16 * 1024)
2963+
if not self.read_buf:
2964+
return b""
2965+
2966+
ret, sep, self.read_buf = self.read_buf.partition(b"\n")
2967+
return ret + sep
2968+
29602969
def read_command(self, prompt):
29612970
reply = input(prompt)
29622971

@@ -3054,7 +3063,7 @@ def cmdloop(self):
30543063
while not self.write_failed:
30553064
try:
30563065
with self._handle_sigint(signal.default_int_handler):
3057-
if not (payload_bytes := self.sockfile.readline()):
3066+
if not (payload_bytes := self._readline()):
30583067
break
30593068
except KeyboardInterrupt:
30603069
self.send_interrupt()
@@ -3144,7 +3153,7 @@ def complete(self, text, state):
31443153
return None
31453154

31463155
with self._handle_sigint(signal.default_int_handler):
3147-
payload = self.sockfile.readline()
3156+
payload = self._readline()
31483157
if not payload:
31493158
return None
31503159

@@ -3211,9 +3220,6 @@ def attach(pid, commands=()):
32113220
client_sock, _ = server.accept()
32123221

32133222
with closing(client_sock):
3214-
sockfile = client_sock.makefile("rwb")
3215-
3216-
with closing(sockfile):
32173223
with tempfile.NamedTemporaryFile("w", delete_on_close=False) as interrupt_script:
32183224
interrupt_script.write(
32193225
'import pdb, sys\n'
@@ -3222,7 +3228,7 @@ def attach(pid, commands=()):
32223228
)
32233229
interrupt_script.close()
32243230

3225-
_PdbClient(pid, sockfile, interrupt_script.name).cmdloop()
3231+
_PdbClient(pid, client_sock, interrupt_script.name).cmdloop()
32263232

32273233

32283234
# Post-Mortem interface

Lib/test/test_remote_pdb.py

Lines changed: 29 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import threading
1313
import unittest
1414
import unittest.mock
15-
from contextlib import contextmanager, redirect_stdout, ExitStack
15+
from contextlib import closing, contextmanager, redirect_stdout, ExitStack
1616
from pathlib import Path
1717
from test.support import is_wasi, os_helper, requires_subprocess, SHORT_TIMEOUT
1818
from test.support.os_helper import temp_dir, TESTFN, unlink
@@ -79,52 +79,14 @@ def get_output(self) -> List[dict]:
7979
return results
8080

8181

82-
class MockDebuggerSocket:
83-
"""Mock file-like simulating a connection to a _RemotePdb instance"""
84-
85-
def __init__(self, incoming):
86-
self.incoming = iter(incoming)
87-
self.outgoing = []
88-
self.buffered = bytearray()
89-
90-
def write(self, data: bytes) -> None:
91-
"""Simulate write to socket."""
92-
self.buffered += data
93-
94-
def flush(self) -> None:
95-
"""Ensure each line is valid JSON."""
96-
lines = self.buffered.splitlines(keepends=True)
97-
self.buffered.clear()
98-
for line in lines:
99-
assert line.endswith(b"\n")
100-
self.outgoing.append(json.loads(line))
101-
102-
def readline(self) -> bytes:
103-
"""Read a line from the prepared input queue."""
104-
# Anything written must be flushed before trying to read,
105-
# since the read will be dependent upon the last write.
106-
assert not self.buffered
107-
try:
108-
item = next(self.incoming)
109-
if not isinstance(item, bytes):
110-
item = json.dumps(item).encode()
111-
return item + b"\n"
112-
except StopIteration:
113-
return b""
114-
115-
def close(self) -> None:
116-
"""No-op close implementation."""
117-
pass
118-
119-
12082
class PdbClientTestCase(unittest.TestCase):
12183
"""Tests for the _PdbClient class."""
12284

12385
def do_test(
12486
self,
12587
*,
12688
incoming,
127-
simulate_failure=None,
89+
simulate_send_failure=False,
12890
expected_outgoing=None,
12991
expected_completions=None,
13092
expected_exception=None,
@@ -142,16 +104,6 @@ def do_test(
142104
expected_state.setdefault("write_failed", False)
143105
messages = [m for source, m in incoming if source == "server"]
144106
prompts = [m["prompt"] for source, m in incoming if source == "user"]
145-
sockfile = MockDebuggerSocket(messages)
146-
stdout = io.StringIO()
147-
148-
if simulate_failure:
149-
sockfile.write = unittest.mock.Mock()
150-
sockfile.flush = unittest.mock.Mock()
151-
if simulate_failure == "write":
152-
sockfile.write.side_effect = OSError("write failed")
153-
elif simulate_failure == "flush":
154-
sockfile.flush.side_effect = OSError("flush failed")
155107

156108
input_iter = (m for source, m in incoming if source == "user")
157109
completions = []
@@ -181,14 +133,33 @@ def mock_input(prompt):
181133
return reply
182134

183135
with ExitStack() as stack:
136+
client_sock, server_sock = socket.socketpair()
137+
stack.enter_context(closing(client_sock))
138+
stack.enter_context(closing(server_sock))
139+
140+
server_sock = unittest.mock.Mock(wraps=server_sock)
141+
142+
client_sock.sendall(
143+
b"".join(
144+
(m if isinstance(m, bytes) else json.dumps(m).encode()) + b"\n"
145+
for m in messages
146+
)
147+
)
148+
client_sock.shutdown(socket.SHUT_WR)
149+
150+
if simulate_send_failure:
151+
client_sock.shutdown(socket.SHUT_RD)
152+
153+
stdout = io.StringIO()
154+
184155
input_mock = stack.enter_context(
185156
unittest.mock.patch("pdb.input", side_effect=mock_input)
186157
)
187158
stack.enter_context(redirect_stdout(stdout))
188159

189160
client = _PdbClient(
190161
pid=0,
191-
sockfile=sockfile,
162+
server_socket=server_sock,
192163
interrupt_script="/a/b.py",
193164
)
194165

@@ -199,13 +170,12 @@ def mock_input(prompt):
199170

200171
client.cmdloop()
201172

202-
actual_outgoing = sockfile.outgoing
203-
if simulate_failure:
204-
actual_outgoing += [
205-
json.loads(msg.args[0]) for msg in sockfile.write.mock_calls
206-
]
173+
sent_msgs = [msg.args[0] for msg in server_sock.sendall.mock_calls]
174+
for msg in sent_msgs:
175+
assert msg.endswith(b"\n")
176+
actual_outgoing = [json.loads(msg) for msg in sent_msgs]
207177

208-
self.assertEqual(sockfile.outgoing, expected_outgoing)
178+
self.assertEqual(actual_outgoing, expected_outgoing)
209179
self.assertEqual(completions, expected_completions)
210180
if expected_stdout_substring and not expected_stdout:
211181
self.assertIn(expected_stdout_substring, stdout.getvalue())
@@ -478,20 +448,7 @@ def test_write_failing(self):
478448
self.do_test(
479449
incoming=incoming,
480450
expected_outgoing=[{"signal": "INT"}],
481-
simulate_failure="write",
482-
expected_state={"write_failed": True},
483-
)
484-
485-
def test_flush_failing(self):
486-
"""Test terminating if flush fails due to a half closed socket."""
487-
incoming = [
488-
("server", {"prompt": "(Pdb) ", "state": "pdb"}),
489-
("user", {"prompt": "(Pdb) ", "input": KeyboardInterrupt()}),
490-
]
491-
self.do_test(
492-
incoming=incoming,
493-
expected_outgoing=[{"signal": "INT"}],
494-
simulate_failure="flush",
451+
simulate_send_failure=True,
495452
expected_state={"write_failed": True},
496453
)
497454

@@ -622,42 +579,7 @@ def test_write_failure_during_completion(self):
622579
},
623580
{"reply": "xyz"},
624581
],
625-
simulate_failure="write",
626-
expected_completions=[],
627-
expected_state={"state": "interact", "write_failed": True},
628-
)
629-
630-
def test_flush_failure_during_completion(self):
631-
"""Test failing to flush to the socket to request tab completions."""
632-
incoming = [
633-
("server", {"prompt": ">>> ", "state": "interact"}),
634-
(
635-
"user",
636-
{
637-
"prompt": ">>> ",
638-
"completion_request": {
639-
"line": "xy",
640-
"begidx": 0,
641-
"endidx": 2,
642-
},
643-
"input": "xyz",
644-
},
645-
),
646-
]
647-
self.do_test(
648-
incoming=incoming,
649-
expected_outgoing=[
650-
{
651-
"complete": {
652-
"text": "xy",
653-
"line": "xy",
654-
"begidx": 0,
655-
"endidx": 2,
656-
}
657-
},
658-
{"reply": "xyz"},
659-
],
660-
simulate_failure="flush",
582+
simulate_send_failure=True,
661583
expected_completions=[],
662584
expected_state={"state": "interact", "write_failed": True},
663585
)

0 commit comments

Comments
 (0)
0