Skip to content

Commit

Permalink
Fix #1337: Get port info from debugpy
Browse files Browse the repository at this point in the history
Send "debugpySockets" event with information about opened sockets when clients connect and whenever ports get opened or closed.
  • Loading branch information
int19h committed Oct 6, 2023
1 parent 7d09fb2 commit ef9a67f
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 12 deletions.
Expand Up @@ -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')

Expand Down
34 changes: 29 additions & 5 deletions src/debugpy/adapter/clients.py
Expand Up @@ -11,7 +11,7 @@
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):
Expand Down Expand Up @@ -110,6 +110,7 @@ def __init__(self, sock):
"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
Expand Down Expand Up @@ -701,6 +702,24 @@ def disconnect_request(self, request):
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:
Expand Down Expand Up @@ -752,11 +771,16 @@ def notify_of_subprocess(self, conn):
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()
9 changes: 8 additions & 1 deletion src/debugpy/adapter/launchers.py
Expand Up @@ -8,7 +8,9 @@

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):
Expand Down Expand Up @@ -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 []
Expand All @@ -101,6 +105,7 @@ def on_launcher_connected(sock):
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()
Expand Down Expand Up @@ -189,3 +194,5 @@ def on_launcher_connected(sock):

finally:
listener.close()
listener = None
sessions.report_sockets()
4 changes: 3 additions & 1 deletion src/debugpy/adapter/servers.py
Expand Up @@ -13,7 +13,7 @@
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

Expand Down Expand Up @@ -394,6 +394,7 @@ def disconnect(self):
def serve(host="127.0.0.1", port=0):
global listener
listener = sockets.serve("Server", Connection, host, port)
sessions.report_sockets()
return listener.getsockname()


Expand All @@ -409,6 +410,7 @@ def stop_serving():
listener = None
except Exception:
log.swallow_exception(level="warning")
sessions.report_sockets()


def connections():
Expand Down
9 changes: 9 additions & 0 deletions src/debugpy/adapter/sessions.py
Expand Up @@ -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()
3 changes: 3 additions & 0 deletions tests/debug/config.py
Expand Up @@ -125,6 +125,9 @@ def __setitem__(self, key, value):
assert key in self.PROPERTIES
self._dict[key] = value

def __repr__(self):
return repr(dict(self))

def __getstate__(self):
return dict(self)

Expand Down
5 changes: 5 additions & 0 deletions tests/debug/runners.py
Expand Up @@ -199,6 +199,7 @@ def attach_pid(session, target, cwd=None, wait=True):
config["processId"] = session.debuggee.pid

session.spawn_adapter()
session.expect_server_socket()
with session.request_attach():
yield

Expand Down Expand Up @@ -260,6 +261,10 @@ def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None):
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))
Expand Down
92 changes: 89 additions & 3 deletions tests/debug/session.py
Expand Up @@ -102,6 +102,11 @@ def __init__(self, debug_config=None):
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.
Expand All @@ -128,6 +133,10 @@ def __init__(self, debug_config=None):
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.
"""
Expand Down Expand Up @@ -183,6 +192,7 @@ def __init__(self, debug_config=None):
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"})),
Expand Down Expand Up @@ -296,6 +306,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
@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
Expand Down Expand Up @@ -352,7 +366,9 @@ def _make_env(self, base_env, codecov=True):
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
Expand Down Expand Up @@ -406,7 +422,9 @@ def spawn_adapter(self, args=()):
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(
Expand All @@ -430,12 +448,22 @@ def spawn_adapter(self, args=()):
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)

Expand Down Expand Up @@ -470,8 +498,12 @@ def send_request(self, command, arguments=None, proceed=True):
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

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

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)
Expand All @@ -514,10 +582,12 @@ def _process_request(self, request):
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")

Expand Down Expand Up @@ -567,6 +637,9 @@ def _start_channel(self, stream):
)
)

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):
Expand Down Expand Up @@ -632,9 +705,20 @@ def request_launch(self):
# 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):
Expand Down Expand Up @@ -787,7 +871,9 @@ def wait_for_stop(
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
Expand Down

0 comments on commit ef9a67f

Please sign in to comment.