Fix #1337: Get port info from debugpy

Send "debugpySockets" event with information about opened sockets when clients connect and whenever ports get opened or closed.
This commit is contained in:
Pavel Minaev 2023-10-03 00:11:24 -07:00 committed by Pavel Minaev
parent 7d09fb24dd
commit ef9a67fe15
9 changed files with 156 additions and 12 deletions

View file

@ -261,7 +261,13 @@ def get_target_filename(is_target_process_64=None, prefix=None, extension=None):
def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, show_debug_info=0):
assert '\'' not in python_code, 'Having a single quote messes with our command.'
from winappdbg.process import Process
# Suppress winappdbg warning about sql package missing.
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=ImportWarning)
from winappdbg.process import Process
if not isinstance(python_code, bytes):
python_code = python_code.encode('utf-8')

View file

@ -11,7 +11,7 @@ import sys
import debugpy
from debugpy import adapter, common, launcher
from debugpy.common import json, log, messaging, sockets
from debugpy.adapter import components, servers, sessions
from debugpy.adapter import clients, components, launchers, servers, sessions
class Client(components.Component):
@ -110,6 +110,7 @@ class Client(components.Component):
"data": {"packageVersion": debugpy.__version__},
},
)
sessions.report_sockets()
def propagate_after_start(self, event):
# pydevd starts sending events as soon as we connect, but the client doesn't
@ -701,6 +702,24 @@ class Client(components.Component):
def disconnect(self):
super().disconnect()
def report_sockets(self):
sockets = [
{
"host": host,
"port": port,
"internal": listener is not clients.listener,
}
for listener in [clients.listener, launchers.listener, servers.listener]
if listener is not None
for (host, port) in [listener.getsockname()]
]
self.channel.send_event(
"debugpySockets",
{
"sockets": sockets
},
)
def notify_of_subprocess(self, conn):
log.info("{1} is a subprocess of {0}.", self, conn)
with self.session:
@ -752,11 +771,16 @@ class Client(components.Component):
def serve(host, port):
global listener
listener = sockets.serve("Client", Client, host, port)
sessions.report_sockets()
return listener.getsockname()
def stop_serving():
try:
listener.close()
except Exception:
log.swallow_exception(level="warning")
global listener
if listener is not None:
try:
listener.close()
except Exception:
log.swallow_exception(level="warning")
listener = None
sessions.report_sockets()

View file

@ -8,7 +8,9 @@ import sys
from debugpy import adapter, common
from debugpy.common import log, messaging, sockets
from debugpy.adapter import components, servers
from debugpy.adapter import components, servers, sessions
listener = None
class Launcher(components.Component):
@ -76,6 +78,8 @@ def spawn_debuggee(
console_title,
sudo,
):
global listener
# -E tells sudo to propagate environment variables to the target process - this
# is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR.
cmdline = ["sudo", "-E"] if sudo else []
@ -101,6 +105,7 @@ def spawn_debuggee(
raise start_request.cant_handle(
"{0} couldn't create listener socket for launcher: {1}", session, exc
)
sessions.report_sockets()
try:
launcher_host, launcher_port = listener.getsockname()
@ -189,3 +194,5 @@ def spawn_debuggee(
finally:
listener.close()
listener = None
sessions.report_sockets()

View file

@ -13,7 +13,7 @@ import time
import debugpy
from debugpy import adapter
from debugpy.common import json, log, messaging, sockets
from debugpy.adapter import components
from debugpy.adapter import components, sessions
import traceback
import io
@ -394,6 +394,7 @@ class Server(components.Component):
def serve(host="127.0.0.1", port=0):
global listener
listener = sockets.serve("Server", Connection, host, port)
sessions.report_sockets()
return listener.getsockname()
@ -409,6 +410,7 @@ def stop_serving():
listener = None
except Exception:
log.swallow_exception(level="warning")
sessions.report_sockets()
def connections():

View file

@ -282,3 +282,12 @@ def wait_until_ended():
return
_sessions_changed.clear()
_sessions_changed.wait()
def report_sockets():
if not _sessions:
return
session = sorted(_sessions, key=lambda session: session.id)[0]
client = session.client
if client is not None:
client.report_sockets()

View file

@ -125,6 +125,9 @@ class DebugConfig(MutableMapping):
assert key in self.PROPERTIES
self._dict[key] = value
def __repr__(self):
return repr(dict(self))
def __getstate__(self):
return dict(self)

View file

@ -199,6 +199,7 @@ while "_attach_pid" not in scratchpad:
config["processId"] = session.debuggee.pid
session.spawn_adapter()
session.expect_server_socket()
with session.request_attach():
yield
@ -260,6 +261,10 @@ if {wait!r}:
except KeyError:
pass
# If adapter is connecting to the client, the server is already started,
# so it should be reported in the initial event.
session.expect_server_socket()
session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
session.wait_for_adapter_socket()
session.connect_to_adapter((host, port))

View file

@ -102,6 +102,11 @@ class Session(object):
self.adapter = None
"""psutil.Popen instance for the adapter process."""
self.expected_adapter_sockets = {
"client": {"host": some.str, "port": some.int, "internal": False},
}
"""The sockets which the adapter is expected to report."""
self.adapter_endpoints = None
"""Name of the file that contains the adapter endpoints information.
@ -128,6 +133,10 @@ class Session(object):
self.scratchpad = comms.ScratchPad(self)
"""The ScratchPad object to talk to the debuggee."""
self.start_command = None
"""Set to either "launch" or "attach" just before the corresponding request is sent.
"""
self.start_request = None
"""The "launch" or "attach" request that started executing code in this session.
"""
@ -183,6 +192,7 @@ class Session(object):
timeline.Event("module"),
timeline.Event("continued"),
timeline.Event("debugpyWaitingForServer"),
timeline.Event("debugpySockets"),
timeline.Event("thread", some.dict.containing({"reason": "started"})),
timeline.Event("thread", some.dict.containing({"reason": "exited"})),
timeline.Event("output", some.dict.containing({"category": "stdout"})),
@ -296,6 +306,10 @@ class Session(object):
@property
def ignore_unobserved(self):
return self.timeline.ignore_unobserved
@property
def is_subprocess(self):
return "subProcessId" in self.config
def open_backchannel(self):
assert self.backchannel is None
@ -352,7 +366,9 @@ class Session(object):
return env
def _make_python_cmdline(self, exe, *args):
return [str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]]
return [
str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]
]
def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None):
assert self.debuggee is None
@ -406,7 +422,9 @@ class Session(object):
assert self.adapter is None
assert self.channel is None
args = self._make_python_cmdline(sys.executable, os.path.dirname(debugpy.adapter.__file__), *args)
args = self._make_python_cmdline(
sys.executable, os.path.dirname(debugpy.adapter.__file__), *args
)
env = self._make_env(self.spawn_adapter.env)
log.info(
@ -430,12 +448,22 @@ class Session(object):
stream = messaging.JsonIOStream.from_process(self.adapter, name=self.adapter_id)
self._start_channel(stream)
def expect_server_socket(self, port=some.int):
self.expected_adapter_sockets["server"] = {
"host": some.str,
"port": port,
"internal": True,
}
def connect_to_adapter(self, address):
assert self.channel is None
self.before_connect(address)
host, port = address
log.info("Connecting to {0} at {1}:{2}", self.adapter_id, host, port)
self.expected_adapter_sockets["client"]["port"] = port
sock = sockets.create_client()
sock.connect(address)
@ -470,8 +498,12 @@ class Session(object):
if self.timeline.is_frozen and proceed:
self.proceed()
if command in ("launch", "attach"):
self.start_command = command
message = self.channel.send_request(command, arguments)
request = self.timeline.record_request(message)
if command in ("launch", "attach"):
self.start_request = request
@ -483,16 +515,52 @@ class Session(object):
def _process_event(self, event):
occ = self.timeline.record_event(event, block=False)
if event.event == "exited":
self.observe(occ)
self.exit_code = event("exitCode", int)
self.exit_reason = event("reason", str, optional=True)
assert self.exit_code == self.expected_exit_code
elif event.event == "terminated":
# Server socket should be closed next.
self.expected_adapter_sockets.pop("server", None)
elif event.event == "debugpyAttach":
self.observe(occ)
pid = event("subProcessId", int)
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
elif event.event == "debugpySockets":
assert not self.is_subprocess
sockets = list(event("sockets", json.array(json.object())))
for purpose, expected_socket in self.expected_adapter_sockets.items():
if expected_socket is None:
continue
socket = None
for socket in sockets:
if socket == expected_socket:
break
assert (
socket is not None
), f"Expected {purpose} socket {expected_socket} not reported by adapter"
sockets.remove(socket)
assert not sockets, f"Unexpected sockets reported by adapter: {sockets}"
if self.start_command == "launch":
if "launcher" in self.expected_adapter_sockets:
# If adapter has just reported the launcher socket, it shouldn't be
# reported thereafter.
self.expected_adapter_sockets["launcher"] = None
elif "server" in self.expected_adapter_sockets:
# If adapter just reported the server socket, the next event should
# report the launcher socket.
self.expected_adapter_sockets["launcher"] = {
"host": some.str,
"port": some.int,
"internal": False,
}
def run_in_terminal(self, args, cwd, env):
exe = args.pop(0)
self.spawn_debuggee.env.update(env)
@ -514,10 +582,12 @@ class Session(object):
except Exception as exc:
log.swallow_exception('"runInTerminal" failed:')
raise request.cant_handle(str(exc))
elif request.command == "startDebugging":
pid = request("configuration", dict)("subProcessId", int)
watchdog.register_spawn(pid, f"{self.debuggee_id}-subprocess-{pid}")
return {}
else:
raise request.isnt_valid("not supported")
@ -567,6 +637,9 @@ class Session(object):
)
)
if not self.is_subprocess:
self.wait_for_next(timeline.Event("debugpySockets"))
self.request("initialize", self.capabilities)
def all_events(self, event, body=some.object):
@ -632,9 +705,20 @@ class Session(object):
# If specified, launcher will use it in lieu of PYTHONPATH it inherited
# from the adapter when spawning debuggee, so we need to adjust again.
self.config.env.prepend_to("PYTHONPATH", DEBUGGEE_PYTHONPATH.strpath)
# Adapter is going to start listening for server and spawn the launcher at
# this point. Server socket gets reported first.
self.expect_server_socket()
return self._request_start("launch")
def request_attach(self):
# In attach(listen) scenario, adapter only starts listening for server
# after receiving the "attach" request.
listen = self.config.get("listen", None)
if listen is not None:
assert "server" not in self.expected_adapter_sockets
self.expect_server_socket(listen["port"])
return self._request_start("attach")
def request_continue(self):
@ -787,7 +871,9 @@ class Session(object):
return StopInfo(stopped, frames, tid, fid)
def wait_for_next_subprocess(self):
message = self.timeline.wait_for_next(timeline.Event("debugpyAttach") | timeline.Request("startDebugging"))
message = self.timeline.wait_for_next(
timeline.Event("debugpyAttach") | timeline.Request("startDebugging")
)
if isinstance(message, timeline.EventOccurrence):
config = message.body
assert "request" in config

View file

@ -72,6 +72,7 @@ def test_attach_api(pyfile, wait_for_client, is_client_connected, stop_method):
)
session.wait_for_adapter_socket()
session.expect_server_socket()
session.connect_to_adapter((host, port))
with session.request_attach():
pass
@ -124,13 +125,14 @@ def test_reattach(pyfile, target, run):
session1.expected_exit_code = None # not expected to exit on disconnect
with run(session1, target(code_to_debug)):
pass
expected_adapter_sockets = session1.expected_adapter_sockets.copy()
session1.wait_for_stop(expected_frames=[some.dap.frame(code_to_debug, "first")])
session1.disconnect()
with debug.Session() as session2:
session2.config.update(session1.config)
session2.expected_adapter_sockets = expected_adapter_sockets
if "connect" in session2.config:
session2.connect_to_adapter(
(session2.config["connect"]["host"], session2.config["connect"]["port"])