Skip to content

Commit b387710

Browse files
authored
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
1 parent 0d65353 commit b387710

6 files changed

Lines changed: 106 additions & 13 deletions

File tree

src/debugpy/_vendored/pydevd/pydevd.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2947,6 +2947,7 @@ def settrace(
29472947
client_access_token=None,
29482948
notify_stdin=True,
29492949
protocol=None,
2950+
ppid=0,
29502951
**kwargs,
29512952
):
29522953
"""Sets the tracing function with the pydev debug function and initializes needed facilities.
@@ -3006,6 +3007,11 @@ def settrace(
30063007
When using in Eclipse the protocol should not be passed, but when used in VSCode
30073008
or some other IDE/editor that accepts the Debug Adapter Protocol then 'dap' should
30083009
be passed.
3010+
3011+
:param ppid:
3012+
Override the parent process id (PPID) for the current debugging session. This PPID is
3013+
reported to the debug client (IDE) and can be used to act like a child process of an
3014+
existing debugged process without being a child process.
30093015
"""
30103016
if protocol and protocol.lower() == "dap":
30113017
pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL
@@ -3034,6 +3040,7 @@ def settrace(
30343040
client_access_token,
30353041
__setup_holder__=__setup_holder__,
30363042
notify_stdin=notify_stdin,
3043+
ppid=ppid,
30373044
)
30383045

30393046

@@ -3057,6 +3064,7 @@ def _locked_settrace(
30573064
client_access_token,
30583065
__setup_holder__,
30593066
notify_stdin,
3067+
ppid,
30603068
):
30613069
if patch_multiprocessing:
30623070
try:
@@ -3088,6 +3096,7 @@ def _locked_settrace(
30883096
"port": int(port),
30893097
"multiprocess": patch_multiprocessing,
30903098
"skip-notify-stdin": not notify_stdin,
3099+
pydevd_constants.ARGUMENT_PPID: ppid,
30913100
}
30923101
SetupHolder.setup = setup
30933102

src/debugpy/public_api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def listen(
120120
...
121121

122122
@_api()
123-
def connect(__endpoint: Endpoint | int, *, access_token: str | None = None) -> Endpoint:
123+
def connect(__endpoint: Endpoint | int, *, access_token: str | None = None, parent_session_pid: int | None = None) -> Endpoint:
124124
"""Tells an existing debug adapter instance that is listening on the
125125
specified address to debug this process.
126126
@@ -131,6 +131,10 @@ def connect(__endpoint: Endpoint | int, *, access_token: str | None = None) -> E
131131
`access_token` must be the same value that was passed to the adapter
132132
via the `--server-access-token` command-line switch.
133133
134+
`parent_session_pid` is the PID of the parent session to associate
135+
with. This is useful if running in a process that is not an immediate
136+
child of the parent process being debugged.
137+
134138
This function does't wait for a client to connect to the debug
135139
adapter that it connects to. Use `wait_for_client` to block
136140
execution until the client connects.

src/debugpy/server/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,9 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
293293

294294

295295
@_starts_debugging
296-
def connect(address, settrace_kwargs, access_token=None):
296+
def connect(address, settrace_kwargs, access_token=None, parent_session_pid=None):
297297
host, port = address
298-
_settrace(host=host, port=port, client_access_token=access_token, **settrace_kwargs)
298+
_settrace(host=host, port=port, client_access_token=access_token, ppid=parent_session_pid or 0, **settrace_kwargs)
299299

300300

301301
class wait_for_client:

src/debugpy/server/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
[--wait-for-client]
3535
[--configure-<name> <value>]...
3636
[--log-to <path>] [--log-to-stderr]
37+
[--parent-session-pid <pid>]]
3738
{1}
3839
[<arg>]...
3940
""".format(
@@ -51,6 +52,7 @@ class Options(object):
5152
wait_for_client = False
5253
adapter_access_token = None
5354
config: Dict[str, Any] = {}
55+
parent_session_pid: Union[int, None] = None
5456

5557

5658
options = Options()
@@ -179,6 +181,7 @@ def do(arg, it):
179181
("--connect", "<address>", set_address("connect")),
180182
("--wait-for-client", None, set_const("wait_for_client", True)),
181183
("--configure-.+", "<value>", set_config),
184+
("--parent-session-pid", "<pid>", set_arg("parent_session_pid", lambda x: int(x) if x else None)),
182185

183186
# Switches that are used internally by the client or debugpy itself.
184187
("--adapter-access-token", "<token>", set_arg("adapter_access_token")),
@@ -230,6 +233,8 @@ def parse_args():
230233
raise ValueError("either --listen or --connect is required")
231234
if options.adapter_access_token is not None and options.mode != "connect":
232235
raise ValueError("--adapter-access-token requires --connect")
236+
if options.parent_session_pid is not None and options.mode != "connect":
237+
raise ValueError("--parent-session-pid requires --connect")
233238
if options.target_kind == "pid" and options.wait_for_client:
234239
raise ValueError("--pid does not support --wait-for-client")
235240

@@ -321,7 +326,7 @@ def start_debugging(argv_0):
321326
if options.mode == "listen" and options.address is not None:
322327
debugpy.listen(options.address)
323328
elif options.mode == "connect" and options.address is not None:
324-
debugpy.connect(options.address, access_token=options.adapter_access_token)
329+
debugpy.connect(options.address, access_token=options.adapter_access_token, parent_session_pid=options.parent_session_pid)
325330
else:
326331
raise AssertionError(repr(options.mode))
327332

tests/debugpy/server/test_cli.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def cli_parser():
4545
"target",
4646
"target_kind",
4747
"wait_for_client",
48+
"parent_session_pid",
4849
]
4950
}
5051

@@ -71,7 +72,7 @@ def parse(args):
7172
log.debug("Failed to deserialize output: {0}, Output was: {1!r}", e, output)
7273
raise
7374
except subprocess.CalledProcessError as exc:
74-
log.debug("Process exited with code {0}. Output: {1!r}, Error: {2!r}",
75+
log.debug("Process exited with code {0}. Output: {1!r}, Error: {2!r}",
7576
exc.returncode, exc.output, exc.stderr)
7677
raise pickle.loads(exc.output)
7778
except EOFError:
@@ -163,20 +164,20 @@ def test_configure_subProcess_from_environment(cli, value):
163164
def test_unsupported_switch(cli):
164165
with pytest.raises(ValueError) as ex:
165166
cli(["--listen", "8888", "--xyz", "123", "spam.py"])
166-
167+
167168
assert "unrecognized switch --xyz" in str(ex.value)
168169

169170
def test_unsupported_switch_from_environment(cli):
170171
with pytest.raises(ValueError) as ex:
171172
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--xyz 123"}):
172173
cli(["--listen", "8888", "spam.py"])
173-
174+
174175
assert "unrecognized switch --xyz" in str(ex.value)
175176

176177
def test_unsupported_configure(cli):
177178
with pytest.raises(ValueError) as ex:
178179
cli(["--connect", "127.0.0.1:8888", "--configure-xyz", "123", "spam.py"])
179-
180+
180181
assert "unknown property 'xyz'" in str(ex.value)
181182

182183
def test_unsupported_configure_from_environment(cli):
@@ -189,26 +190,26 @@ def test_unsupported_configure_from_environment(cli):
189190
def test_address_required(cli):
190191
with pytest.raises(ValueError) as ex:
191192
cli(["-m", "spam"])
192-
193+
193194
assert "either --listen or --connect is required" in str(ex.value)
194195

195196
def test_missing_target(cli):
196197
with pytest.raises(ValueError) as ex:
197198
cli(["--listen", "8888"])
198-
199+
199200
assert "missing target" in str(ex.value)
200201

201202
def test_duplicate_switch(cli):
202203
with pytest.raises(ValueError) as ex:
203204
cli(["--listen", "8888", "--listen", "9999", "spam.py"])
204-
205+
205206
assert "duplicate switch on command line: --listen" in str(ex.value)
206207

207208
def test_duplicate_switch_from_environment(cli):
208209
with pytest.raises(ValueError) as ex:
209210
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--listen 8888 --listen 9999"}):
210211
cli(["spam.py"])
211-
212+
212213
assert "duplicate switch from environment: --listen" in str(ex.value)
213214

214215
# Test that switches can be read from the environment
@@ -240,3 +241,10 @@ def test_script_args(cli):
240241

241242
assert argv == ["arg1", "arg2"]
242243
assert options["target"] == "spam.py"
244+
245+
# Tests that --parent-session-pid fails with --listen
246+
def test_script_parent_pid_with_listen_failure(cli):
247+
with pytest.raises(ValueError) as ex:
248+
cli(["--listen", "8888", "--parent-session-pid", "1234", "spam.py"])
249+
250+
assert "--parent-session-pid requires --connect" in str(ex.value)

tests/debugpy/test_multiproc.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def parent():
203203
return
204204

205205
expected_child_config = expected_subprocess_config(parent_session)
206-
206+
207207
if method == "startDebugging":
208208
subprocess_request = parent_session.timeline.wait_for_next(timeline.Request("startDebugging"))
209209
child_config = subprocess_request.arguments("configuration", dict)
@@ -596,3 +596,70 @@ def parent():
596596
child_pid = backchannel.receive()
597597
assert child_pid == child_config["subProcessId"]
598598
assert str(child_pid) in child_config["name"]
599+
600+
601+
@pytest.mark.parametrize("run", runners.all_launch)
602+
def test_subprocess_with_parent_pid(pyfile, target, run):
603+
@pyfile
604+
def child():
605+
import sys
606+
607+
assert "debugpy" in sys.modules
608+
609+
import debugpy
610+
611+
assert debugpy # @bp
612+
613+
@pyfile
614+
def parent():
615+
import debuggee
616+
import os
617+
import subprocess
618+
import sys
619+
620+
from debugpy.server import cli as debugpy_cli
621+
622+
debuggee.setup()
623+
624+
# Running it through a shell is necessary to ensure the
625+
# --parent-session-pid option is tested and the underlying
626+
# Python subprocess can associate with this one's debug session.
627+
if sys.platform == "win32":
628+
argv = ["cmd.exe", "/c"]
629+
else:
630+
argv = ["/bin/sh", "-c"]
631+
632+
host, port = debugpy_cli.options.address
633+
access_token = debugpy_cli.options.adapter_access_token
634+
635+
shell_args = [
636+
sys.executable,
637+
"-m",
638+
"debugpy",
639+
"--connect", f"{host}:{port}",
640+
"--parent-session-pid", str(os.getpid()),
641+
"--adapter-access-token", access_token,
642+
sys.argv[1],
643+
]
644+
argv.append(" ".join(shell_args))
645+
646+
subprocess.check_call(argv, env=os.environ | {"DEBUGPY_RUNNING": "false"})
647+
648+
with debug.Session() as parent_session:
649+
with run(parent_session, target(parent, args=[child])):
650+
parent_session.set_breakpoints(child, all)
651+
652+
with parent_session.wait_for_next_subprocess() as child_session:
653+
expected_child_config = expected_subprocess_config(parent_session)
654+
child_config = child_session.config
655+
child_config.pop("isOutputRedirected", None)
656+
assert child_config == expected_child_config
657+
658+
with child_session.start():
659+
child_session.set_breakpoints(child, all)
660+
661+
child_session.wait_for_stop(
662+
"breakpoint",
663+
expected_frames=[some.dap.frame(child, line="bp")],
664+
)
665+
child_session.request_continue()

0 commit comments

Comments
 (0)