Environment data
- debugpy version: 1.8.22, running from source at e220805
- OS and version: macOS 26.6.2 (arm64)
- Python version (& distribution if applicable, e.g. Anaconda): 3.10.6, CPython from python.org
- Using VS Code or Visual Studio: neither. The repro below is plain Python.
Actual behavior
JsonIOStream.read_json() never returns and never raises when the stream hits EOF part way through a header line. It burns a full core while it does that. _read_line() keeps calling reader.readline(), which returns b"" right away on a socket that is already at EOF, so the loop has nothing to block on and no bound.
Anything listening for a DAP client is exposed to this. python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.py opens a port, and any peer that writes a few bytes with no CRLF and then closes pins a core: a port scanner, a health check, an HTTPS client aimed at the wrong port. It also happens with no stranger involved. If the client or the debuggee is killed while a header is half flushed, the side that survives spins instead of ending the session. That thread is a daemon, so nothing reaps it, and every connection that does this leaves another one spinning.
This is near #1308 but it is not the same thing. A peer that closes without sending anything is handled correctly today. Only a partial line spins, which the first line of output below shows.
Expected behavior
NoMoreMessages, same as when the peer closes before sending anything. That is what JsonMessageChannel._parse_incoming_messages() is waiting for so it can fail the outstanding requests, queue the disconnect and close the channel.
Steps to reproduce:
- Check out e220805 and run this with
src on PYTHONPATH. Stdlib only. No debug session and no client.
import socket
import threading
import time
from debugpy.common.messaging import JsonIOStream
def read_after(payload):
ours, theirs = socket.socketpair()
stream = JsonIOStream.from_socket(ours)
theirs.sendall(payload)
theirs.close()
done = threading.Event()
def run():
try:
stream.read_json()
except Exception:
pass
finally:
done.set()
cpu = time.process_time()
threading.Thread(target=run, daemon=True).start()
finished = done.wait(3)
print(
"payload %-22r finished=%-5s CPU=%.2fs"
% (payload, finished, time.process_time() - cpu)
)
read_after(b"") # peer closes without sending anything
read_after(b"Content-Length: 24") # peer closes part way through a header line
- Look at the second line of output.
What I get:
payload b'' finished=True CPU=0.00s
payload b'Content-Length: 24' finished=False CPU=2.93s
The second read is still going when the script stops waiting after 3 seconds, and almost the whole wait was CPU time. That is one core pinned, and it does not stop. Across four runs the number sits between 2.93 and 3.00. I expected both lines to read finished=True CPU=0.00s.
Environment data
Actual behavior
JsonIOStream.read_json()never returns and never raises when the stream hits EOF part way through a header line. It burns a full core while it does that._read_line()keeps callingreader.readline(), which returnsb""right away on a socket that is already at EOF, so the loop has nothing to block on and no bound.Anything listening for a DAP client is exposed to this.
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.pyopens a port, and any peer that writes a few bytes with no CRLF and then closes pins a core: a port scanner, a health check, an HTTPS client aimed at the wrong port. It also happens with no stranger involved. If the client or the debuggee is killed while a header is half flushed, the side that survives spins instead of ending the session. That thread is a daemon, so nothing reaps it, and every connection that does this leaves another one spinning.This is near #1308 but it is not the same thing. A peer that closes without sending anything is handled correctly today. Only a partial line spins, which the first line of output below shows.
Expected behavior
NoMoreMessages, same as when the peer closes before sending anything. That is whatJsonMessageChannel._parse_incoming_messages()is waiting for so it can fail the outstanding requests, queue the disconnect and close the channel.Steps to reproduce:
srconPYTHONPATH. Stdlib only. No debug session and no client.What I get:
The second read is still going when the script stops waiting after 3 seconds, and almost the whole wait was CPU time. That is one core pinned, and it does not stop. Across four runs the number sits between 2.93 and 3.00. I expected both lines to read
finished=True CPU=0.00s.