mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
* Add more logging for 313 failure * Auto retry failures * Add retry for socket.write * Put watchdog timeout back and drop errors * Fix linter * Add message on error so at least it's logged * Try a different combination * Fix linter * Go back to longer watchdog * Try a different tactic * Don't log failure and wait a bit in the watchdog * Go back to the NoMoreMessages * Just disable watchdog in 3.13 for now * Change watchdog timeout * Try different algorithm for ports in use * Add retries option * Fix usages of get_test_server_port * Retries don't seem to work at global level with xdist, use retries on individual tests
348 lines
10 KiB
Python
348 lines
10 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License. See LICENSE in the project root
|
|
# for license information.
|
|
|
|
"""Runners are recipes for executing Targets in a debug.Session.
|
|
|
|
Every function in this module that is decorated with @_runner must have at least two
|
|
positional arguments: (session, target) - and can have additional arguments. For every
|
|
such function, two artifacts are produced.
|
|
|
|
The function is exposed directly as a method on Session, with the session argument
|
|
becoming self.
|
|
|
|
The function is also exposed as a Runner object from this module. Runner objects are
|
|
callable, and invoke the wrapped function when called, but in addition, they can also
|
|
be bound to specific arguments, by using either [] or with_options(), which can be
|
|
chained arbitrarily::
|
|
|
|
# Direct invocation:
|
|
session.attach_connect("cli", log_dir="...")
|
|
|
|
# Indirect invocation:
|
|
run = runners.attach_connect
|
|
run = run["cli"]
|
|
run = run.with_options(log_dir="...")
|
|
run(session, target)
|
|
|
|
runner[x][y][z] is just a convenient shorthand for binding positional arguments, same
|
|
as runner.with_options(x, y, z).
|
|
|
|
Runners are immutable, so every use of [] or with_options() creates a new runner with
|
|
the specified arguments bound. The runner must have all its required arguments bound
|
|
before it can be invoked.
|
|
|
|
Regardless of whether the runner is invoked directly on the Session, or via a Runner
|
|
object, if the start DAP sequence involves a configuration phase (the "initialized"
|
|
event and the "configurationDone" request), the runner must be used in a with-statement.
|
|
The statements inside the with-statement are executed after receiving the "initialized"
|
|
event, and before sending the "configurationDone" request::
|
|
|
|
with run(session, target):
|
|
# DAP requests can be made to session, but target is not running yet.
|
|
session.set_breakpoints(...)
|
|
# target is running now!
|
|
|
|
If there is no configuration phase, the runner returns directly::
|
|
|
|
session.config["noDebug"] = True
|
|
run(session, target)
|
|
# target is running now!
|
|
"""
|
|
|
|
import contextlib
|
|
import os
|
|
import pytest
|
|
import sys
|
|
|
|
import debugpy
|
|
from debugpy.common import json, log
|
|
from tests import net, timeline
|
|
from tests.debug import session
|
|
from tests.patterns import some
|
|
|
|
|
|
def _runner(f):
|
|
# assert f.__name__.startswith("launch") or f.__name__.startswith("attach")
|
|
setattr(session.Session, f.__name__, f)
|
|
|
|
class Runner(object):
|
|
request = "launch" if f.__name__.startswith("launch") else "attach"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._args = tuple(args)
|
|
self._kwargs = dict(kwargs)
|
|
|
|
def __getattr__(self, name):
|
|
return self._kwargs[name]
|
|
|
|
def __call__(self, session, target, *args, **kwargs):
|
|
if len(args) or len(kwargs):
|
|
return self.with_options(*args, **kwargs)(session, target)
|
|
return f(session, target, *self._args, **self._kwargs)
|
|
|
|
def __iter__(self):
|
|
# Since we implement __getitem__, iter() will assume that runners are
|
|
# iterable, and will iterate over them by calling __getitem__ until it
|
|
# raises IndexError - i.e. indefinitely. To prevent that, explicitly
|
|
# implement __iter__ as unsupported.
|
|
raise NotImplementedError
|
|
|
|
def __getitem__(self, arg):
|
|
return self.with_options(arg)
|
|
|
|
def with_options(self, *args, **kwargs):
|
|
new_args = self._args + args
|
|
new_kwargs = dict(self._kwargs)
|
|
new_kwargs.update(kwargs)
|
|
return Runner(*new_args, **new_kwargs)
|
|
|
|
def __repr__(self):
|
|
result = type(self).__name__
|
|
args = [str(x) for x in self._args] + [
|
|
f"{k}={v}" for k, v in self._kwargs.items()
|
|
]
|
|
if len(args):
|
|
result += "(" + ", ".join(args) + ")"
|
|
return result
|
|
|
|
@property
|
|
def pytest_id(self):
|
|
return repr(self)
|
|
|
|
Runner.__name__ = f.__name__
|
|
return Runner()
|
|
|
|
|
|
@_runner
|
|
def launch(session, target, console=None, cwd=None):
|
|
assert console in (
|
|
None,
|
|
"internalConsole",
|
|
"integratedTerminal",
|
|
"externalTerminal",
|
|
)
|
|
|
|
log.info("Launching {0} in {1} using {2}.", target, session, json.repr(console))
|
|
|
|
target.configure(session)
|
|
config = session.config
|
|
config.setdefaults(
|
|
{"console": "externalTerminal", "internalConsoleOptions": "neverOpen"}
|
|
)
|
|
if console is not None:
|
|
config["console"] = console
|
|
if cwd is not None:
|
|
config["cwd"] = cwd
|
|
if "python" not in config and "pythonPath" not in config:
|
|
config["python"] = sys.executable
|
|
|
|
env = (
|
|
session.spawn_adapter.env
|
|
if config["console"] == "internalConsole"
|
|
else config.env
|
|
)
|
|
target.cli(env)
|
|
|
|
session.spawn_adapter()
|
|
return session.request_launch()
|
|
|
|
|
|
def _attach_common_config(session, target, cwd):
|
|
assert (
|
|
target.code is None or "debuggee.setup()" in target.code
|
|
), f"{target.filename} must invoke debuggee.setup()."
|
|
|
|
target.configure(session)
|
|
config = session.config
|
|
if cwd is not None:
|
|
config.setdefault("pathMappings", [{"localRoot": cwd, "remoteRoot": "."}])
|
|
return config
|
|
|
|
|
|
@_runner
|
|
@contextlib.contextmanager
|
|
def attach_pid(session, target, cwd=None, wait=True):
|
|
if wait and not sys.platform.startswith("linux"):
|
|
pytest.skip("https://github.com/microsoft/ptvsd/issues/1926")
|
|
|
|
log.info("Attaching {0} to {1} by PID.", session, target)
|
|
|
|
config = session.config
|
|
try:
|
|
config["processId"] = int(target)
|
|
except TypeError:
|
|
pass
|
|
|
|
if "processId" not in config:
|
|
_attach_common_config(session, target, cwd)
|
|
args = target.cli(session.spawn_debuggee.env)
|
|
|
|
if wait:
|
|
debuggee_setup = """
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
while "debugpy" not in sys.modules:
|
|
time.sleep(0.1)
|
|
|
|
from debuggee import scratchpad
|
|
|
|
while "_attach_pid" not in scratchpad:
|
|
time.sleep(0.1)
|
|
"""
|
|
else:
|
|
debuggee_setup = None
|
|
|
|
session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
|
|
config["processId"] = session.debuggee.pid
|
|
|
|
session.spawn_adapter()
|
|
session.expect_server_socket()
|
|
with session.request_attach():
|
|
yield
|
|
|
|
if wait:
|
|
session.scratchpad["_attach_pid"] = True
|
|
|
|
|
|
@_runner
|
|
def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None):
|
|
log.info(
|
|
"Attaching {0} to {1} by socket using {2}.", session, target, method.upper()
|
|
)
|
|
|
|
assert method in ("api", "cli")
|
|
|
|
config = _attach_common_config(session, target, cwd)
|
|
config["connect"] = {}
|
|
config["connect"]["host"] = host = attach_connect.host
|
|
config["connect"]["port"] = port = attach_connect.port
|
|
|
|
if method == "cli":
|
|
args = [
|
|
os.path.dirname(debugpy.__file__),
|
|
"--listen",
|
|
f"{host}:{port}",
|
|
]
|
|
if wait:
|
|
args += ["--wait-for-client"]
|
|
if log_dir is not None:
|
|
args += ["--log-to", log_dir]
|
|
if "subProcess" in config:
|
|
args += ["--configure-subProcess", str(config["subProcess"])]
|
|
debuggee_setup = None
|
|
elif method == "api":
|
|
args = []
|
|
api_config = {k: v for k, v in config.items() if k in {"subProcess"}}
|
|
debuggee_setup = """
|
|
import debugpy
|
|
if {log_dir!r}:
|
|
debugpy.log_to({log_dir!r})
|
|
debugpy.configure({api_config!r})
|
|
debugpy.listen(({host!r}, {port!r}))
|
|
if {wait!r}:
|
|
debugpy.wait_for_client()
|
|
"""
|
|
debuggee_setup = debuggee_setup.format(
|
|
host=host,
|
|
port=port,
|
|
wait=wait,
|
|
log_dir=log_dir,
|
|
api_config=api_config,
|
|
)
|
|
else:
|
|
raise ValueError
|
|
args += target.cli(session.spawn_debuggee.env)
|
|
|
|
try:
|
|
del config["subProcess"]
|
|
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))
|
|
return session.request_attach()
|
|
|
|
|
|
attach_connect.host = "127.0.0.1"
|
|
attach_connect.port = net.get_test_server_port()
|
|
|
|
|
|
@_runner
|
|
def attach_listen(session, target, method, cwd=None, log_dir=None):
|
|
log.info(
|
|
"Attaching {0} to {1} by socket using {2}.", session, target, method.upper()
|
|
)
|
|
|
|
assert method in ("api", "cli")
|
|
|
|
config = _attach_common_config(session, target, cwd)
|
|
config["listen"] = {}
|
|
config["listen"]["host"] = host = attach_listen.host
|
|
config["listen"]["port"] = port = attach_listen.port
|
|
|
|
if method == "cli":
|
|
args = [
|
|
os.path.dirname(debugpy.__file__),
|
|
"--connect",
|
|
f"{host}:{port}",
|
|
]
|
|
if log_dir is not None:
|
|
args += ["--log-to", log_dir]
|
|
if "subProcess" in config:
|
|
args += ["--configure-subProcess", str(config["subProcess"])]
|
|
debuggee_setup = None
|
|
elif method == "api":
|
|
args = []
|
|
api_config = {k: v for k, v in config.items() if k in {"subProcess"}}
|
|
debuggee_setup = f"""
|
|
import debugpy
|
|
if {log_dir!r}:
|
|
debugpy.log_to({log_dir!r})
|
|
debugpy.configure({api_config!r})
|
|
debugpy.connect({(host, port)!r})
|
|
"""
|
|
else:
|
|
raise ValueError
|
|
args += target.cli(session.spawn_debuggee.env)
|
|
|
|
try:
|
|
del config["subProcess"]
|
|
except KeyError:
|
|
pass
|
|
|
|
def spawn_debuggee(occ):
|
|
assert occ.body == some.dict.containing({"host": host, "port": port})
|
|
session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
|
|
|
|
session.timeline.when(timeline.Event("debugpyWaitingForServer"), spawn_debuggee)
|
|
session.spawn_adapter(args=[] if log_dir is None else ["--log-dir", log_dir])
|
|
return session.request_attach()
|
|
|
|
|
|
attach_listen.host = "127.0.0.1"
|
|
attach_listen.port = net.get_test_server_port()
|
|
|
|
all_launch_terminal = [
|
|
launch.with_options(console="integratedTerminal"),
|
|
launch.with_options(console="externalTerminal"),
|
|
]
|
|
|
|
all_launch = [launch.with_options(console="internalConsole")] + all_launch_terminal
|
|
|
|
all_attach_listen = [attach_listen["api"], attach_listen["cli"]]
|
|
|
|
all_attach_connect = [attach_connect["api"], attach_connect["cli"]]
|
|
|
|
all_attach_socket = all_attach_listen + all_attach_connect
|
|
|
|
all_attach = all_attach_socket + [attach_pid]
|
|
|
|
all = all_launch + all_attach
|