Skip to content

Commit 94c9999

Browse files
author
Pavel Minaev
committed
Fix microsoft#1337: Get port info from debugpy
Send "debugpySockets" event with information about opened sockets when clients connect and whenever ports get opened or closed.
1 parent 7d09fb2 commit 94c9999

File tree

7 files changed

+140
-11
lines changed

7 files changed

+140
-11
lines changed

src/debugpy/adapter/clients.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import debugpy
1212
from debugpy import adapter, common, launcher
1313
from debugpy.common import json, log, messaging, sockets
14-
from debugpy.adapter import components, servers, sessions
14+
from debugpy.adapter import clients, components, launchers, servers, sessions
1515

1616

1717
class Client(components.Component):
@@ -110,6 +110,7 @@ def __init__(self, sock):
110110
"data": {"packageVersion": debugpy.__version__},
111111
},
112112
)
113+
sessions.report_sockets()
113114

114115
def propagate_after_start(self, event):
115116
# pydevd starts sending events as soon as we connect, but the client doesn't
@@ -701,6 +702,24 @@ def disconnect_request(self, request):
701702
def disconnect(self):
702703
super().disconnect()
703704

705+
def report_sockets(self):
706+
sockets = [
707+
{
708+
"host": host,
709+
"port": port,
710+
"internal": listener is not clients.listener,
711+
}
712+
for listener in [clients.listener, launchers.listener, servers.listener]
713+
if listener is not None
714+
for (host, port) in [listener.getsockname()]
715+
]
716+
self.channel.send_event(
717+
"debugpySockets",
718+
{
719+
"sockets": sockets
720+
},
721+
)
722+
704723
def notify_of_subprocess(self, conn):
705724
log.info("{1} is a subprocess of {0}.", self, conn)
706725
with self.session:
@@ -752,11 +771,16 @@ def notify_of_subprocess(self, conn):
752771
def serve(host, port):
753772
global listener
754773
listener = sockets.serve("Client", Client, host, port)
774+
sessions.report_sockets()
755775
return listener.getsockname()
756776

757777

758778
def stop_serving():
759-
try:
760-
listener.close()
761-
except Exception:
762-
log.swallow_exception(level="warning")
779+
global listener
780+
if listener is not None:
781+
try:
782+
listener.close()
783+
except Exception:
784+
log.swallow_exception(level="warning")
785+
listener = None
786+
sessions.report_sockets()

src/debugpy/adapter/launchers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from debugpy import adapter, common
1010
from debugpy.common import log, messaging, sockets
11-
from debugpy.adapter import components, servers
11+
from debugpy.adapter import components, servers, sessions
12+
13+
listener = None
1214

1315

1416
class Launcher(components.Component):
@@ -76,6 +78,8 @@ def spawn_debuggee(
7678
console_title,
7779
sudo,
7880
):
81+
global listener
82+
7983
# -E tells sudo to propagate environment variables to the target process - this
8084
# is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR.
8185
cmdline = ["sudo", "-E"] if sudo else []
@@ -101,6 +105,7 @@ def on_launcher_connected(sock):
101105
raise start_request.cant_handle(
102106
"{0} couldn't create listener socket for launcher: {1}", session, exc
103107
)
108+
sessions.report_sockets()
104109

105110
try:
106111
launcher_host, launcher_port = listener.getsockname()
@@ -189,3 +194,5 @@ def on_launcher_connected(sock):
189194

190195
finally:
191196
listener.close()
197+
listener = None
198+
sessions.report_sockets()

src/debugpy/adapter/servers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import debugpy
1414
from debugpy import adapter
1515
from debugpy.common import json, log, messaging, sockets
16-
from debugpy.adapter import components
16+
from debugpy.adapter import components, sessions
1717
import traceback
1818
import io
1919

@@ -394,6 +394,7 @@ def disconnect(self):
394394
def serve(host="127.0.0.1", port=0):
395395
global listener
396396
listener = sockets.serve("Server", Connection, host, port)
397+
sessions.report_sockets()
397398
return listener.getsockname()
398399

399400

@@ -409,6 +410,7 @@ def stop_serving():
409410
listener = None
410411
except Exception:
411412
log.swallow_exception(level="warning")
413+
sessions.report_sockets()
412414

413415

414416
def connections():

src/debugpy/adapter/sessions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,12 @@ def wait_until_ended():
282282
return
283283
_sessions_changed.clear()
284284
_sessions_changed.wait()
285+
286+
287+
def report_sockets():
288+
if not _sessions:
289+
return
290+
session = sorted(_sessions, key=lambda session: session.id)[0]
291+
client = session.client
292+
if client is not None:
293+
client.report_sockets()

tests/debug/runners.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None):
260260
except KeyError:
261261
pass
262262

263+
# If adapter is connecting to the client, the server is already started,
264+
# so it should be reported in the initial event.
265+
session.expect_server_socket()
266+
263267
session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
264268
session.wait_for_adapter_socket()
265269
session.connect_to_adapter((host, port))

tests/debug/session.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ def __init__(self, debug_config=None):
102102
self.adapter = None
103103
"""psutil.Popen instance for the adapter process."""
104104

105+
self.expected_adapter_sockets = {
106+
"client": {"host": some.str, "port": some.int, "internal": False},
107+
}
108+
"""The sockets which the adapter is expected to report."""
109+
105110
self.adapter_endpoints = None
106111
"""Name of the file that contains the adapter endpoints information.
107112
@@ -183,6 +188,7 @@ def __init__(self, debug_config=None):
183188
timeline.Event("module"),
184189
timeline.Event("continued"),
185190
timeline.Event("debugpyWaitingForServer"),
191+
timeline.Event("debugpySockets"),
186192
timeline.Event("thread", some.dict.containing({"reason": "started"})),
187193
timeline.Event("thread", some.dict.containing({"reason": "exited"})),
188194
timeline.Event("output", some.dict.containing({"category": "stdout"})),
@@ -296,6 +302,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
296302
@property
297303
def ignore_unobserved(self):
298304
return self.timeline.ignore_unobserved
305+
306+
@property
307+
def is_subprocess(self):
308+
return "subProcessId" in self.config
299309

300310
def open_backchannel(self):
301311
assert self.backchannel is None
@@ -352,7 +362,9 @@ def _make_env(self, base_env, codecov=True):
352362
return env
353363

354364
def _make_python_cmdline(self, exe, *args):
355-
return [str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]]
365+
return [
366+
str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]
367+
]
356368

357369
def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None):
358370
assert self.debuggee is None
@@ -406,7 +418,9 @@ def spawn_adapter(self, args=()):
406418
assert self.adapter is None
407419
assert self.channel is None
408420

409-
args = self._make_python_cmdline(sys.executable, os.path.dirname(debugpy.adapter.__file__), *args)
421+
args = self._make_python_cmdline(
422+
sys.executable, os.path.dirname(debugpy.adapter.__file__), *args
423+
)
410424
env = self._make_env(self.spawn_adapter.env)
411425

412426
log.info(
@@ -430,12 +444,22 @@ def spawn_adapter(self, args=()):
430444
stream = messaging.JsonIOStream.from_process(self.adapter, name=self.adapter_id)
431445
self._start_channel(stream)
432446

447+
def expect_server_socket(self, port=some.int):
448+
self.expected_adapter_sockets["server"] = {
449+
"host": some.str,
450+
"port": port,
451+
"internal": True,
452+
}
453+
433454
def connect_to_adapter(self, address):
434455
assert self.channel is None
435456

436457
self.before_connect(address)
437458
host, port = address
438459
log.info("Connecting to {0} at {1}:{2}", self.adapter_id, host, port)
460+
461+
self.expected_adapter_sockets["client"]["port"] = port
462+
439463
sock = sockets.create_client()
440464
sock.connect(address)
441465

@@ -483,16 +507,55 @@ def send_request(self, command, arguments=None, proceed=True):
483507

484508
def _process_event(self, event):
485509
occ = self.timeline.record_event(event, block=False)
510+
486511
if event.event == "exited":
487512
self.observe(occ)
488513
self.exit_code = event("exitCode", int)
489514
self.exit_reason = event("reason", str, optional=True)
490515
assert self.exit_code == self.expected_exit_code
516+
517+
elif event.event == "terminated":
518+
# Server socket should be closed next.
519+
self.expected_adapter_sockets.pop("server", None)
520+
491521
elif event.event == "debugpyAttach":
492522
self.observe(occ)
493523
pid = event("subProcessId", int)
494524
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
495525

526+
elif event.event == "debugpySockets":
527+
assert not self.is_subprocess
528+
sockets = list(event("sockets", json.array(json.object())))
529+
for purpose, expected_socket in self.expected_adapter_sockets.items():
530+
if expected_socket is None:
531+
continue
532+
socket = None
533+
for socket in sockets:
534+
if socket == expected_socket:
535+
break
536+
assert (
537+
socket is not None
538+
), f"Expected {purpose} socket {expected_socket} not reported by adapter"
539+
sockets.remove(socket)
540+
assert not sockets, f"Unexpected sockets reported by adapter: {sockets}"
541+
542+
if (
543+
self.start_request is not None
544+
and self.start_request.command == "launch"
545+
):
546+
if "launcher" in self.expected_adapter_sockets:
547+
# If adapter has just reported the launcher socket, it shouldn't be
548+
# reported thereafter.
549+
self.expected_adapter_sockets["launcher"] = None
550+
elif "server" in self.expected_adapter_sockets:
551+
# If adapter just reported the server socket, the next event should
552+
# report the launcher socket.
553+
self.expected_adapter_sockets["launcher"] = {
554+
"host": some.str,
555+
"port": some.int,
556+
"internal": False,
557+
}
558+
496559
def run_in_terminal(self, args, cwd, env):
497560
exe = args.pop(0)
498561
self.spawn_debuggee.env.update(env)
@@ -514,10 +577,12 @@ def _process_request(self, request):
514577
except Exception as exc:
515578
log.swallow_exception('"runInTerminal" failed:')
516579
raise request.cant_handle(str(exc))
580+
517581
elif request.command == "startDebugging":
518582
pid = request("configuration", dict)("subProcessId", int)
519583
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
520584
return {}
585+
521586
else:
522587
raise request.isnt_valid("not supported")
523588

@@ -567,6 +632,9 @@ def _start_channel(self, stream):
567632
)
568633
)
569634

635+
if not self.is_subprocess:
636+
self.wait_for_next(timeline.Event("debugpySockets"))
637+
570638
self.request("initialize", self.capabilities)
571639

572640
def all_events(self, event, body=some.object):
@@ -632,9 +700,20 @@ def request_launch(self):
632700
# If specified, launcher will use it in lieu of PYTHONPATH it inherited
633701
# from the adapter when spawning debuggee, so we need to adjust again.
634702
self.config.env.prepend_to("PYTHONPATH", DEBUGGEE_PYTHONPATH.strpath)
703+
704+
# Adapter is going to start listening for server and spawn the launcher at
705+
# this point. Server socket gets reported first.
706+
self.expect_server_socket()
707+
635708
return self._request_start("launch")
636709

637710
def request_attach(self):
711+
# In attach(listen) scenario, adapter only starts listening for server
712+
# after receiving the "attach" request.
713+
listen = self.config.get("listen", None)
714+
if listen is not None:
715+
assert "server" not in self.expected_adapter_sockets
716+
self.expect_server_socket(listen["port"])
638717
return self._request_start("attach")
639718

640719
def request_continue(self):
@@ -787,7 +866,9 @@ def wait_for_stop(
787866
return StopInfo(stopped, frames, tid, fid)
788867

789868
def wait_for_next_subprocess(self):
790-
message = self.timeline.wait_for_next(timeline.Event("debugpyAttach") | timeline.Request("startDebugging"))
869+
message = self.timeline.wait_for_next(
870+
timeline.Event("debugpyAttach") | timeline.Request("startDebugging")
871+
)
791872
if isinstance(message, timeline.EventOccurrence):
792873
config = message.body
793874
assert "request" in config

tests/debugpy/test_attach.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def code_to_debug():
7272
)
7373
session.wait_for_adapter_socket()
7474

75+
session.expect_server_socket()
7576
session.connect_to_adapter((host, port))
7677
with session.request_attach():
7778
pass
@@ -124,13 +125,14 @@ def code_to_debug():
124125
session1.expected_exit_code = None # not expected to exit on disconnect
125126

126127
with run(session1, target(code_to_debug)):
127-
pass
128+
expected_adapter_sockets = session1.expected_adapter_sockets.copy()
128129

129130
session1.wait_for_stop(expected_frames=[some.dap.frame(code_to_debug, "first")])
130131
session1.disconnect()
131132

132133
with debug.Session() as session2:
133134
session2.config.update(session1.config)
135+
session2.expected_adapter_sockets = expected_adapter_sockets
134136
if "connect" in session2.config:
135137
session2.connect_to_adapter(
136138
(session2.config["connect"]["host"], session2.config["connect"]["port"])

0 commit comments

Comments
 (0)