mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Add parent-session-pid argument (#1920)
* Add parent-session-pid argument Add the ability to specify the parent process id when connecting a new DAP server to the client. This value is used instead of the actual process' parent id so that it can be associated with a specific debug session even if it hasn't been spawned directly by that parent process. * Add tests for new option
This commit is contained in:
parent
0d65353cc6
commit
b387710b7f
6 changed files with 106 additions and 13 deletions
|
|
@ -2947,6 +2947,7 @@ def settrace(
|
|||
client_access_token=None,
|
||||
notify_stdin=True,
|
||||
protocol=None,
|
||||
ppid=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""Sets the tracing function with the pydev debug function and initializes needed facilities.
|
||||
|
|
@ -3006,6 +3007,11 @@ def settrace(
|
|||
When using in Eclipse the protocol should not be passed, but when used in VSCode
|
||||
or some other IDE/editor that accepts the Debug Adapter Protocol then 'dap' should
|
||||
be passed.
|
||||
|
||||
:param ppid:
|
||||
Override the parent process id (PPID) for the current debugging session. This PPID is
|
||||
reported to the debug client (IDE) and can be used to act like a child process of an
|
||||
existing debugged process without being a child process.
|
||||
"""
|
||||
if protocol and protocol.lower() == "dap":
|
||||
pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL
|
||||
|
|
@ -3034,6 +3040,7 @@ def settrace(
|
|||
client_access_token,
|
||||
__setup_holder__=__setup_holder__,
|
||||
notify_stdin=notify_stdin,
|
||||
ppid=ppid,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3057,6 +3064,7 @@ def _locked_settrace(
|
|||
client_access_token,
|
||||
__setup_holder__,
|
||||
notify_stdin,
|
||||
ppid,
|
||||
):
|
||||
if patch_multiprocessing:
|
||||
try:
|
||||
|
|
@ -3088,6 +3096,7 @@ def _locked_settrace(
|
|||
"port": int(port),
|
||||
"multiprocess": patch_multiprocessing,
|
||||
"skip-notify-stdin": not notify_stdin,
|
||||
pydevd_constants.ARGUMENT_PPID: ppid,
|
||||
}
|
||||
SetupHolder.setup = setup
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def listen(
|
|||
...
|
||||
|
||||
@_api()
|
||||
def connect(__endpoint: Endpoint | int, *, access_token: str | None = None) -> Endpoint:
|
||||
def connect(__endpoint: Endpoint | int, *, access_token: str | None = None, parent_session_pid: int | None = None) -> Endpoint:
|
||||
"""Tells an existing debug adapter instance that is listening on the
|
||||
specified address to debug this process.
|
||||
|
||||
|
|
@ -131,6 +131,10 @@ def connect(__endpoint: Endpoint | int, *, access_token: str | None = None) -> E
|
|||
`access_token` must be the same value that was passed to the adapter
|
||||
via the `--server-access-token` command-line switch.
|
||||
|
||||
`parent_session_pid` is the PID of the parent session to associate
|
||||
with. This is useful if running in a process that is not an immediate
|
||||
child of the parent process being debugged.
|
||||
|
||||
This function does't wait for a client to connect to the debug
|
||||
adapter that it connects to. Use `wait_for_client` to block
|
||||
execution until the client connects.
|
||||
|
|
|
|||
|
|
@ -293,9 +293,9 @@ listen.called = False
|
|||
|
||||
|
||||
@_starts_debugging
|
||||
def connect(address, settrace_kwargs, access_token=None):
|
||||
def connect(address, settrace_kwargs, access_token=None, parent_session_pid=None):
|
||||
host, port = address
|
||||
_settrace(host=host, port=port, client_access_token=access_token, **settrace_kwargs)
|
||||
_settrace(host=host, port=port, client_access_token=access_token, ppid=parent_session_pid or 0, **settrace_kwargs)
|
||||
|
||||
|
||||
class wait_for_client:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ Usage: debugpy --listen | --connect
|
|||
[--wait-for-client]
|
||||
[--configure-<name> <value>]...
|
||||
[--log-to <path>] [--log-to-stderr]
|
||||
[--parent-session-pid <pid>]]
|
||||
{1}
|
||||
[<arg>]...
|
||||
""".format(
|
||||
|
|
@ -51,6 +52,7 @@ class Options(object):
|
|||
wait_for_client = False
|
||||
adapter_access_token = None
|
||||
config: Dict[str, Any] = {}
|
||||
parent_session_pid: Union[int, None] = None
|
||||
|
||||
|
||||
options = Options()
|
||||
|
|
@ -179,6 +181,7 @@ switches = [
|
|||
("--connect", "<address>", set_address("connect")),
|
||||
("--wait-for-client", None, set_const("wait_for_client", True)),
|
||||
("--configure-.+", "<value>", set_config),
|
||||
("--parent-session-pid", "<pid>", set_arg("parent_session_pid", lambda x: int(x) if x else None)),
|
||||
|
||||
# Switches that are used internally by the client or debugpy itself.
|
||||
("--adapter-access-token", "<token>", set_arg("adapter_access_token")),
|
||||
|
|
@ -230,6 +233,8 @@ def parse_args():
|
|||
raise ValueError("either --listen or --connect is required")
|
||||
if options.adapter_access_token is not None and options.mode != "connect":
|
||||
raise ValueError("--adapter-access-token requires --connect")
|
||||
if options.parent_session_pid is not None and options.mode != "connect":
|
||||
raise ValueError("--parent-session-pid requires --connect")
|
||||
if options.target_kind == "pid" and options.wait_for_client:
|
||||
raise ValueError("--pid does not support --wait-for-client")
|
||||
|
||||
|
|
@ -321,7 +326,7 @@ def start_debugging(argv_0):
|
|||
if options.mode == "listen" and options.address is not None:
|
||||
debugpy.listen(options.address)
|
||||
elif options.mode == "connect" and options.address is not None:
|
||||
debugpy.connect(options.address, access_token=options.adapter_access_token)
|
||||
debugpy.connect(options.address, access_token=options.adapter_access_token, parent_session_pid=options.parent_session_pid)
|
||||
else:
|
||||
raise AssertionError(repr(options.mode))
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ def cli(pyfile):
|
|||
"target",
|
||||
"target_kind",
|
||||
"wait_for_client",
|
||||
"parent_session_pid",
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ def cli(pyfile):
|
|||
log.debug("Failed to deserialize output: {0}, Output was: {1!r}", e, output)
|
||||
raise
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log.debug("Process exited with code {0}. Output: {1!r}, Error: {2!r}",
|
||||
log.debug("Process exited with code {0}. Output: {1!r}, Error: {2!r}",
|
||||
exc.returncode, exc.output, exc.stderr)
|
||||
raise pickle.loads(exc.output)
|
||||
except EOFError:
|
||||
|
|
@ -163,20 +164,20 @@ def test_configure_subProcess_from_environment(cli, value):
|
|||
def test_unsupported_switch(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["--listen", "8888", "--xyz", "123", "spam.py"])
|
||||
|
||||
|
||||
assert "unrecognized switch --xyz" in str(ex.value)
|
||||
|
||||
def test_unsupported_switch_from_environment(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--xyz 123"}):
|
||||
cli(["--listen", "8888", "spam.py"])
|
||||
|
||||
|
||||
assert "unrecognized switch --xyz" in str(ex.value)
|
||||
|
||||
def test_unsupported_configure(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["--connect", "127.0.0.1:8888", "--configure-xyz", "123", "spam.py"])
|
||||
|
||||
|
||||
assert "unknown property 'xyz'" in str(ex.value)
|
||||
|
||||
def test_unsupported_configure_from_environment(cli):
|
||||
|
|
@ -189,26 +190,26 @@ def test_unsupported_configure_from_environment(cli):
|
|||
def test_address_required(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["-m", "spam"])
|
||||
|
||||
|
||||
assert "either --listen or --connect is required" in str(ex.value)
|
||||
|
||||
def test_missing_target(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["--listen", "8888"])
|
||||
|
||||
|
||||
assert "missing target" in str(ex.value)
|
||||
|
||||
def test_duplicate_switch(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["--listen", "8888", "--listen", "9999", "spam.py"])
|
||||
|
||||
|
||||
assert "duplicate switch on command line: --listen" in str(ex.value)
|
||||
|
||||
def test_duplicate_switch_from_environment(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--listen 8888 --listen 9999"}):
|
||||
cli(["spam.py"])
|
||||
|
||||
|
||||
assert "duplicate switch from environment: --listen" in str(ex.value)
|
||||
|
||||
# Test that switches can be read from the environment
|
||||
|
|
@ -240,3 +241,10 @@ def test_script_args(cli):
|
|||
|
||||
assert argv == ["arg1", "arg2"]
|
||||
assert options["target"] == "spam.py"
|
||||
|
||||
# Tests that --parent-session-pid fails with --listen
|
||||
def test_script_parent_pid_with_listen_failure(cli):
|
||||
with pytest.raises(ValueError) as ex:
|
||||
cli(["--listen", "8888", "--parent-session-pid", "1234", "spam.py"])
|
||||
|
||||
assert "--parent-session-pid requires --connect" in str(ex.value)
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ def test_subprocess(pyfile, target, run, subProcess, method):
|
|||
return
|
||||
|
||||
expected_child_config = expected_subprocess_config(parent_session)
|
||||
|
||||
|
||||
if method == "startDebugging":
|
||||
subprocess_request = parent_session.timeline.wait_for_next(timeline.Request("startDebugging"))
|
||||
child_config = subprocess_request.arguments("configuration", dict)
|
||||
|
|
@ -596,3 +596,70 @@ def test_subprocess_replace(pyfile, target, run):
|
|||
child_pid = backchannel.receive()
|
||||
assert child_pid == child_config["subProcessId"]
|
||||
assert str(child_pid) in child_config["name"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("run", runners.all_launch)
|
||||
def test_subprocess_with_parent_pid(pyfile, target, run):
|
||||
@pyfile
|
||||
def child():
|
||||
import sys
|
||||
|
||||
assert "debugpy" in sys.modules
|
||||
|
||||
import debugpy
|
||||
|
||||
assert debugpy # @bp
|
||||
|
||||
@pyfile
|
||||
def parent():
|
||||
import debuggee
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from debugpy.server import cli as debugpy_cli
|
||||
|
||||
debuggee.setup()
|
||||
|
||||
# Running it through a shell is necessary to ensure the
|
||||
# --parent-session-pid option is tested and the underlying
|
||||
# Python subprocess can associate with this one's debug session.
|
||||
if sys.platform == "win32":
|
||||
argv = ["cmd.exe", "/c"]
|
||||
else:
|
||||
argv = ["/bin/sh", "-c"]
|
||||
|
||||
host, port = debugpy_cli.options.address
|
||||
access_token = debugpy_cli.options.adapter_access_token
|
||||
|
||||
shell_args = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"debugpy",
|
||||
"--connect", f"{host}:{port}",
|
||||
"--parent-session-pid", str(os.getpid()),
|
||||
"--adapter-access-token", access_token,
|
||||
sys.argv[1],
|
||||
]
|
||||
argv.append(" ".join(shell_args))
|
||||
|
||||
subprocess.check_call(argv, env=os.environ | {"DEBUGPY_RUNNING": "false"})
|
||||
|
||||
with debug.Session() as parent_session:
|
||||
with run(parent_session, target(parent, args=[child])):
|
||||
parent_session.set_breakpoints(child, all)
|
||||
|
||||
with parent_session.wait_for_next_subprocess() as child_session:
|
||||
expected_child_config = expected_subprocess_config(parent_session)
|
||||
child_config = child_session.config
|
||||
child_config.pop("isOutputRedirected", None)
|
||||
assert child_config == expected_child_config
|
||||
|
||||
with child_session.start():
|
||||
child_session.set_breakpoints(child, all)
|
||||
|
||||
child_session.wait_for_stop(
|
||||
"breakpoint",
|
||||
expected_frames=[some.dap.frame(child, line="bp")],
|
||||
)
|
||||
child_session.request_continue()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue