mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
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:
parent
7d09fb24dd
commit
ef9a67fe15
9 changed files with 156 additions and 12 deletions
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue