Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions src/debugpy/common/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,16 @@ def _read_line(self, reader: Union[io.RawIOBase, BinaryIO]) -> bytes:
line: bytes = b""
while True:
try:
line += reader.readline()
chunk = reader.readline()
except Exception as exc:
raise NoMoreMessages(str(exc), stream=self)
if not line:
if not chunk:
# EOF. The check has to be on the chunk that was just read, not on
# the accumulated line - once any bytes have arrived, the line is
# never empty again, and readline() on a stream that is already at
# EOF keeps returning b"" without blocking, so retrying spins.
raise NoMoreMessages(stream=self)
line += chunk
if line.endswith(b"\r\n"):
line = line[0:-2]
return line
Expand Down
44 changes: 44 additions & 0 deletions tests/debugpy/common/test_messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ def write_json(self, value, encoder=None):
self.output.append(value)


class ReaderSpinning(BaseException):
"""Raised by TruncatedReader when it is read past EOF too many times.

Derived from BaseException rather than Exception on purpose: JsonIOStream turns
any Exception raised by the reader into NoMoreMessages, and that is the very
thing the test needs to tell apart from a real EOF.
"""


class TruncatedReader(io.RawIOBase):
"""Yields data once, and then EOF forever, like a socket whose peer wrote part
of a message and disconnected.
"""

EOF_READS_ALLOWED = 10

def __init__(self, data):
super().__init__()
self.data = data
self.eof_reads = 0

def readline(self, size: int | None = -1) -> bytes:
if self.data:
data, self.data = self.data, b""
return data
self.eof_reads += 1
if self.eof_reads > self.EOF_READS_ALLOWED:
raise ReaderSpinning(
"readline() was called %d times at EOF" % self.eof_reads
)
return b""


class TestJsonIOStream(object):
MESSAGE_BODY_TEMPLATE = '{"arguments": {"threadId": 3}, "command": "next", "seq": %d, "type": "request"}'
MESSAGES = []
Expand Down Expand Up @@ -92,6 +125,17 @@ def test_read(self):
stream.read_json()
assert exc_info.value.stream is stream

def test_read_truncated_header(self):
# A peer that writes part of a header line and then disconnects. readline()
# returns the partial line once, and b"" from then on, same as a socket that
# is at EOF. The stream must report NoMoreMessages rather than keep reading.
reader = TruncatedReader(b"Content-Length: 24")
stream = messaging.JsonIOStream(reader, io.BytesIO(), "data")
with pytest.raises(messaging.NoMoreMessages) as exc_info:
stream.read_json()
assert exc_info.value.stream is stream
assert reader.eof_reads == 1

def test_write(self):
data = io.BytesIO()
stream = messaging.JsonIOStream(data, data, "data")
Expand Down
Loading