Skip to content
Open
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
13 changes: 12 additions & 1 deletion src/debugpy/server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,18 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
block_until_connected=False,
**settrace_kwargs
)
return
# pydevd binds the listening socket on a background reader thread, so
# the OS-assigned port (when port=0 is passed) isn't visible from
# here directly. Wait for the socket to be ready and read the actual
# bound endpoint back from the global debugger.
pydb = get_global_debugger()
if pydb is not None:
pydb.wait_for_server_socket_ready()
actual_host, actual_port = pydb._server_socket_name
else:
actual_host, actual_port = host, port

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

If _settrace() succeeds without installing a global debugger, this fallback returns the requested endpoint and can silently return port 0, violating the documented actual-endpoint contract. Treat the missing debugger as an invariant failure rather than returning a success-shaped value.

[verified]

listen.called = True
return actual_host, actual_port

import subprocess

Expand Down
57 changes: 57 additions & 0 deletions tests/debugpy/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pytest

import debugpy.server.api as _api


@pytest.fixture
def no_settrace(monkeypatch):
# Avoid actually starting pydevd; we only care about listen()'s return value.
monkeypatch.setattr(_api, "_settrace", lambda **kwargs: None)
monkeypatch.setattr(_api.listen, "called", False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

The fixture stubs _settrace but not ensure_logging(), which the @_starts_debugging wrapper still calls and can leave process-global logging state behind for other tests. Stub or restore that state so this unit test remains isolated.

[verified]



def test_listen_in_process_returns_endpoint(no_settrace):
# Regression test for #1656: listen(..., in_process_debug_adapter=True) used
# to return None, unlike the out-of-process path which returns (host, port).
endpoint = _api.listen(("127.0.0.1", 5678), in_process_debug_adapter=True)
assert endpoint == ("127.0.0.1", 5678)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

The fixed-port, no-op _settrace test only verifies that the input endpoint is echoed back; it cannot catch the port=0 contract gap. Add coverage that establishes whether the in-process path returns the resolved bound port for port 0.

[verified]

assert _api.listen.called is True


class _FakePyDB:
"""Minimal stand-in for the global pydevd debugger.

Mirrors the bits of the real PyDB that listen()'s in-process path reads
after _settrace(): ``wait_for_server_socket_ready`` and
``_server_socket_name`` (set by the reader thread once the listening
socket is bound).
"""

def __init__(self, host, port):
self._server_socket_name = (host, port)

def wait_for_server_socket_ready(self):
return None


@pytest.fixture
def in_process_debugger(monkeypatch):
# Stub out _settrace (don't actually start pydevd) and inject a fake
# global debugger so that listen()'s in-process path can read the
# actual bound endpoint back from it.
monkeypatch.setattr(_api, "_settrace", lambda **kwargs: None)
monkeypatch.setattr(_api.listen, "called", False)
fake = _FakePyDB("127.0.0.1", 54321)
monkeypatch.setattr(_api, "get_global_debugger", lambda: fake)
return fake


def test_listen_in_process_port_zero_resolves_bound_port(in_process_debugger):
# Regression test for #1656: when port=0 is passed, listen() must return
# the OS-assigned bound port rather than the literal 0.
endpoint = _api.listen(("127.0.0.1", 0), in_process_debug_adapter=True)
host, port = endpoint
assert host == "127.0.0.1"
assert isinstance(port, int)
assert port > 0
assert port == in_process_debugger._server_socket_name[1]
Loading