Skip to content

Commit 1eb4468

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 27fff9e commit 1eb4468

File tree

9 files changed

+156
-12
lines changed

9 files changed

+156
-12
lines changed

src/debugpy/_vendored/pydevd/pydevd_attach_to_process/add_code_to_python_process.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,13 @@ def get_target_filename(is_target_process_64=None, prefix=None, extension=None):
261261

262262
def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, show_debug_info=0):
263263
assert '\'' not in python_code, 'Having a single quote messes with our command.'
264-
from winappdbg.process import Process
264+
265+
# Suppress winappdbg warning about sql package missing.
266+
import warnings
267+
with warnings.catch_warnings():
268+
warnings.simplefilter("ignore", category=ImportWarning)
269+
from winappdbg.process import Process
270+
265271
if not isinstance(python_code, bytes):
266272
python_code = python_code.encode('utf-8')
267273

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/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ def __setitem__(self, key, value):
125125
assert key in self.PROPERTIES
126126
self._dict[key] = value
127127

128+
def __repr__(self):
129+
return repr(dict(self))
130+
128131
def __getstate__(self):
129132
return dict(self)
130133

tests/debug/runners.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ def attach_pid(session, target, cwd=None, wait=True):
199199
config["processId"] = session.debuggee.pid
200200

201201
session.spawn_adapter()
202+
session.expect_server_socket()
202203
with session.request_attach():
203204
yield
204205

@@ -260,6 +261,10 @@ def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None):
260261
except KeyError:
261262
pass
262263

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

tests/debug/session.py

Lines changed: 89 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
@@ -128,6 +133,10 @@ def __init__(self, debug_config=None):
128133
self.scratchpad = comms.ScratchPad(self)
129134
"""The ScratchPad object to talk to the debuggee."""
130135

136+
self.start_command = None
137+
"""Set to either "launch" or "attach" just before the corresponding request is sent.
138+
"""
139+
131140
self.start_request = None
132141
"""The "launch" or "attach" request that started executing code in this session.
133142
"""
@@ -183,6 +192,7 @@ def __init__(self, debug_config=None):
183192
timeline.Event("module"),
184193
timeline.Event("continued"),
185194
timeline.Event("debugpyWaitingForServer"),
195+
timeline.Event("debugpySockets"),
186196
timeline.Event("thread", some.dict.containing({"reason": "started"})),
187197
timeline.Event("thread", some.dict.containing({"reason": "exited"})),
188198
timeline.Event("output", some.dict.containing({"category": "stdout"})),
@@ -296,6 +306,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
296306
@property
297307
def ignore_unobserved(self):
298308
return self.timeline.ignore_unobserved
309+
310+
@property
311+
def is_subprocess(self):
312+
return "subProcessId" in self.config
299313

300314
def open_backchannel(self):
301315
assert self.backchannel is None
@@ -352,7 +366,9 @@ def _make_env(self, base_env, codecov=True):
352366
return env
353367

354368
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]]
369+
return [
370+
str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]
371+
]
356372

357373
def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None):
358374
assert self.debuggee is None
@@ -406,7 +422,9 @@ def spawn_adapter(self, args=()):
406422
assert self.adapter is None
407423
assert self.channel is None
408424

409-
args = self._make_python_cmdline(sys.executable, os.path.dirname(debugpy.adapter.__file__), *args)
425+
args = self._make_python_cmdline(
426+
sys.executable, os.path.dirname(debugpy.adapter.__file__), *args
427+
)
410428
env = self._make_env(self.spawn_adapter.env)
411429

412430
log.info(
@@ -430,12 +448,22 @@ def spawn_adapter(self, args=()):
430448
stream = messaging.JsonIOStream.from_process(self.adapter, name=self.adapter_id)
431449
self._start_channel(stream)
432450

451+
def expect_server_socket(self, port=some.int):
452+
self.expected_adapter_sockets["server"] = {
453+
"host": some.str,
454+
"port": port,
455+
"internal": True,
456+
}
457+
433458
def connect_to_adapter(self, address):
434459
assert self.channel is None
435460

436461
self.before_connect(address)
437462
host, port = address
438463
log.info("Connecting to {0} at {1}:{2}", self.adapter_id, host, port)
464+
465+
self.expected_adapter_sockets["client"]["port"] = port
466+
439467
sock = sockets.create_client()
440468
sock.connect(address)
441469

@@ -470,8 +498,12 @@ def send_request(self, command, arguments=None, proceed=True):
470498
if self.timeline.is_frozen and proceed:
471499
self.proceed()
472500

501+
if command in ("launch", "attach"):
502+
self.start_command = command
503+
473504
message = self.channel.send_request(command, arguments)
474505
request = self.timeline.record_request(message)
506+
475507
if command in ("launch", "attach"):
476508
self.start_request = request
477509

@@ -483,16 +515,52 @@ def send_request(self, command, arguments=None, proceed=True):
483515

484516
def _process_event(self, event):
485517
occ = self.timeline.record_event(event, block=False)
518+
486519
if event.event == "exited":
487520
self.observe(occ)
488521
self.exit_code = event("exitCode", int)
489522
self.exit_reason = event("reason", str, optional=True)
490523
assert self.exit_code == self.expected_exit_code
524+
525+
elif event.event == "terminated":
526+
# Server socket should be closed next.
527+
self.expected_adapter_sockets.pop("server", None)
528+
491529
elif event.event == "debugpyAttach":
492530
self.observe(occ)
493531
pid = event("subProcessId", int)
494532
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
495533

534+
elif event.event == "debugpySockets":
535+
assert not self.is_subprocess
536+
sockets = list(event("sockets", json.array(json.object())))
537+
for purpose, expected_socket in self.expected_adapter_sockets.items():
538+
if expected_socket is None:
539+
continue
540+
socket = None
541+
for socket in sockets:
542+
if socket == expected_socket:
543+
break
544+
assert (
545+
socket is not None
546+
), f"Expected {purpose} socket {expected_socket} not reported by adapter"
547+
sockets.remove(socket)
548+
assert not sockets, f"Unexpected sockets reported by adapter: {sockets}"
549+
550+
if self.start_command == "launch":
551+
if "launcher" in self.expected_adapter_sockets:
552+
# If adapter has just reported the launcher socket, it shouldn't be
553+
# reported thereafter.
554+
self.expected_adapter_sockets["launcher"] = None
555+
elif "server" in self.expected_adapter_sockets:
556+
# If adapter just reported the server socket, the next event should
557+
# report the launcher socket.
558+
self.expected_adapter_sockets["launcher"] = {
559+
"host": some.str,
560+
"port": some.int,
561+
"internal": False,
562+
}
563+
496564
def run_in_terminal(self, args, cwd, env):
497565
exe = args.pop(0)
498566
self.spawn_debuggee.env.update(env)
@@ -514,10 +582,12 @@ def _process_request(self, request):
514582
except Exception as exc:
515583
log.swallow_exception('"runInTerminal" failed:')
516584
raise request.cant_handle(str(exc))
585+
517586
elif request.command == "startDebugging":
518587
pid = request("configuration", dict)("subProcessId", int)
519588
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
520589
return {}
590+
521591
else:
522592
raise request.isnt_valid("not supported")
523593

@@ -567,6 +637,9 @@ def _start_channel(self, stream):
567637
)
568638
)
569639

640+
if not self.is_subprocess:
641+
self.wait_for_next(timeline.Event("debugpySockets"))
642+
570643
self.request("initialize", self.capabilities)
571644

572645
def all_events(self, event, body=some.object):
@@ -632,9 +705,20 @@ def request_launch(self):
632705
# If specified, launcher will use it in lieu of PYTHONPATH it inherited
633706
# from the adapter when spawning debuggee, so we need to adjust again.
634707
self.config.env.prepend_to("PYTHONPATH", DEBUGGEE_PYTHONPATH.strpath)
708+
709+
# Adapter is going to start listening for server and spawn the launcher at
710+
# this point. Server socket gets reported first.
711+
self.expect_server_socket()
712+
635713
return self._request_start("launch")
636714

637715
def request_attach(self):
716+
# In attach(listen) scenario, adapter only starts listening for server
717+
# after receiving the "attach" request.
718+
listen = self.config.get("listen", None)
719+
if listen is not None:
720+
assert "server" not in self.expected_adapter_sockets
721+
self.expect_server_socket(listen["port"])
638722
return self._request_start("attach")
639723

640724
def request_continue(self):
@@ -787,7 +871,9 @@ def wait_for_stop(
787871
return StopInfo(stopped, frames, tid, fid)
788872

789873
def wait_for_next_subprocess(self):
790-
message = self.timeline.wait_for_next(timeline.Event("debugpyAttach") | timeline.Request("startDebugging"))
874+
message = self.timeline.wait_for_next(
875+
timeline.Event("debugpyAttach") | timeline.Request("startDebugging")
876+
)
791877
if isinstance(message, timeline.EventOccurrence):
792878
config = message.body
793879
assert "request" in config

0 commit comments

Comments
 (0)