Skip to content

Commit ccd9ac5

Browse files
committed
Use launcher pid as ppid in DAP. Fixes #42
1 parent 7454f89 commit ccd9ac5

9 files changed

Lines changed: 133 additions & 29 deletions

File tree

src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import re
44
import sys
55
from _pydev_imps._pydev_saved_modules import threading
6-
from _pydevd_bundle.pydevd_constants import get_global_debugger, IS_WINDOWS, IS_JYTHON, get_current_thread_id
6+
from _pydevd_bundle.pydevd_constants import get_global_debugger, IS_WINDOWS, IS_JYTHON, get_current_thread_id, \
7+
sorted_dict_repr
78
from _pydev_bundle import pydev_log
89
from contextlib import contextmanager
910
from _pydevd_bundle import pydevd_constants
@@ -35,7 +36,7 @@ def _get_apply_arg_patching():
3536
return getattr(_arg_patch, 'apply_arg_patching', True)
3637

3738

38-
def _get_setup_updated_with_protocol(setup):
39+
def _get_setup_updated_with_protocol_and_ppid(setup, is_exec=False):
3940
if setup is None:
4041
setup = {}
4142
setup = setup.copy()
@@ -44,7 +45,11 @@ def _get_setup_updated_with_protocol(setup):
4445
setup.pop(pydevd_constants.ARGUMENT_HTTP_JSON_PROTOCOL, None)
4546
setup.pop(pydevd_constants.ARGUMENT_JSON_PROTOCOL, None)
4647
setup.pop(pydevd_constants.ARGUMENT_QUOTED_LINE_PROTOCOL, None)
47-
setup.pop(pydevd_constants.ARGUMENT_HTTP_PROTOCOL, None)
48+
49+
if not is_exec:
50+
# i.e.: The ppid for the subprocess is the current pid.
51+
# If it's an exec, keep it what it was.
52+
setup[pydevd_constants.ARGUMENT_PPID] = os.getpid()
4853

4954
protocol = pydevd_constants.get_protocol()
5055
if protocol == pydevd_constants.HTTP_JSON_PROTOCOL:
@@ -65,18 +70,22 @@ def _get_setup_updated_with_protocol(setup):
6570

6671

6772
def _get_python_c_args(host, port, indC, args, setup):
68-
setup = _get_setup_updated_with_protocol(setup)
73+
setup = _get_setup_updated_with_protocol_and_ppid(setup)
74+
75+
# i.e.: We want to make the repr sorted so that it works in tests.
76+
setup_repr = setup if setup is None else (sorted_dict_repr(setup))
77+
6978
return ("import sys; sys.path.insert(0, r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r; "
70-
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, client_access_token=%r); "
71-
"from pydevd import SetupHolder; SetupHolder.setup = %s; %s"
79+
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, client_access_token=%r, __setup_holder__=%s); "
80+
"%s"
7281
) % (
7382
pydev_src_dir,
7483
pydevd_constants.get_protocol(),
7584
host,
7685
port,
7786
setup.get('access-token'),
7887
setup.get('client-access-token'),
79-
setup,
88+
setup_repr,
8089
args[indC + 1])
8190

8291

@@ -227,7 +236,15 @@ def get_c_option_index(args):
227236
return ind_c
228237

229238

230-
def patch_args(args):
239+
def patch_args(args, is_exec=False):
240+
'''
241+
:param list args:
242+
Arguments to patch.
243+
244+
:param bool is_exec:
245+
If it's an exec, the current process will be replaced (this means we have
246+
to keep the same ppid).
247+
'''
231248
try:
232249
pydev_log.debug("Patching args: %s", args)
233250
args = remove_quotes_from_args(args)
@@ -280,7 +297,9 @@ def patch_args(args):
280297
# ['X:\\pysrc\\pydevd.py', '--multiprocess', '--print-in-debugger-startup',
281298
# '--vm_type', 'python', '--client', '127.0.0.1', '--port', '56352', '--file', 'x:\\snippet1.py']
282299
from _pydevd_bundle.pydevd_command_line_handling import setup_to_argv
283-
original = setup_to_argv(_get_setup_updated_with_protocol(SetupHolder.setup)) + ['--file']
300+
original = setup_to_argv(
301+
_get_setup_updated_with_protocol_and_ppid(SetupHolder.setup, is_exec=is_exec)
302+
) + ['--file']
284303

285304
module_name = None
286305
m_flag = _get_str_type_compatible(args[i], '-m')
@@ -459,7 +478,7 @@ def new_execl(path, *args):
459478
os.execlpe(file, arg0, arg1, ..., env)
460479
"""
461480
if _get_apply_arg_patching():
462-
args = patch_args(args)
481+
args = patch_args(args, is_exec=True)
463482
send_process_created_message()
464483

465484
return getattr(os, original_name)(path, *args)
@@ -475,7 +494,7 @@ def new_execv(path, args):
475494
os.execvp(file, args)
476495
"""
477496
if _get_apply_arg_patching():
478-
args = patch_args(args)
497+
args = patch_args(args, is_exec=True)
479498
send_process_created_message()
480499

481500
return getattr(os, original_name)(path, args)
@@ -491,7 +510,7 @@ def create_execve(original_name):
491510

492511
def new_execve(path, args, env):
493512
if _get_apply_arg_patching():
494-
args = patch_args(args)
513+
args = patch_args(args, is_exec=True)
495514
send_process_created_message()
496515

497516
return getattr(os, original_name)(path, args, env)

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_command_line_handling.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
3+
14
class ArgHandlerWithParam:
25
'''
36
Handler for some arguments which needs a value
@@ -48,8 +51,18 @@ def handle_argv(self, argv, i, setup):
4851
setup[self.arg_name] = True
4952

5053

54+
def convert_ppid(ppid):
55+
ret = int(ppid)
56+
if ret != 0:
57+
if ret == os.getpid():
58+
raise AssertionError(
59+
'ppid passed is the same as the current process pid (%s)!' % (ret,))
60+
return ret
61+
62+
5163
ACCEPTED_ARG_HANDLERS = [
5264
ArgHandlerWithParam('port', int, 0),
65+
ArgHandlerWithParam('ppid', convert_ppid, 0),
5366
ArgHandlerWithParam('vm_type'),
5467
ArgHandlerWithParam('client'),
5568
ArgHandlerWithParam('access-token'),

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,12 @@ def dict_iter_items(d):
347347
def dict_items(d):
348348
return d.items()
349349

350+
351+
def sorted_dict_repr(d):
352+
s = sorted(dict_iter_items(d), key=lambda x:str(x[0]))
353+
return '{' + ', '.join(('%r: %r' % x) for x in s) + '}'
354+
355+
350356
try:
351357
xrange = xrange
352358
except:
@@ -605,6 +611,8 @@ def new_func(*args, **kwargs):
605611
HTTP_JSON_PROTOCOL = 'http_json'
606612
ARGUMENT_HTTP_JSON_PROTOCOL = 'json-dap-http'
607613

614+
ARGUMENT_PPID = 'ppid'
615+
608616

609617
class _GlobalSettings:
610618
protocol = QUOTED_LINE_PROTOCOL

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,12 @@ def on_pydevdsysteminfo_request(self, py_db, request):
983983
except AttributeError:
984984
pid = None
985985

986-
ppid = self.api.get_ppid()
986+
# It's possible to have the ppid reported from args. In this case, use that instead of the
987+
# real ppid (athough we're using `ppid`, what we want in meaning is the `launcher_pid` --
988+
# so, if a python process is launched from another python process, consider that process the
989+
# parent and not any intermediary stubs).
990+
991+
ppid = py_db.get_arg_ppid() or self.api.get_ppid()
987992

988993
try:
989994
impl_desc = platform.python_implementation()

src/debugpy/_vendored/pydevd/pydevd.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,16 @@ def new_trace_dispatch(frame, event, arg):
623623
# Stop the tracing as the last thing before the actual shutdown for a clean exit.
624624
atexit.register(stoptrace)
625625

626+
def get_arg_ppid(self):
627+
try:
628+
setup = SetupHolder.setup
629+
if setup:
630+
return int(setup.get('ppid', 0))
631+
except:
632+
pydev_log.exception('Error getting ppid.')
633+
634+
return 0
635+
626636
def wait_for_ready_to_run(self):
627637
while not self.ready_to_run:
628638
# busy wait until we receive run command
@@ -2487,6 +2497,9 @@ def settrace(
24872497
stdout_to_server = stdout_to_server or kwargs.get('stdoutToServer', False) # Backward compatibility
24882498
stderr_to_server = stderr_to_server or kwargs.get('stderrToServer', False) # Backward compatibility
24892499

2500+
# Internal use (may be used to set the setup info directly for subprocesess).
2501+
__setup_holder__ = kwargs.get('__setup_holder__')
2502+
24902503
with _set_trace_lock:
24912504
_locked_settrace(
24922505
host,
@@ -2503,6 +2516,7 @@ def settrace(
25032516
dont_trace_end_patterns,
25042517
access_token,
25052518
client_access_token,
2519+
__setup_holder__=__setup_holder__,
25062520
)
25072521

25082522

@@ -2524,6 +2538,7 @@ def _locked_settrace(
25242538
dont_trace_end_patterns,
25252539
access_token,
25262540
client_access_token,
2541+
__setup_holder__,
25272542
):
25282543
if patch_multiprocessing:
25292544
try:
@@ -2541,6 +2556,8 @@ def _locked_settrace(
25412556
global _global_redirect_stderr_to_server
25422557

25432558
py_db = get_global_debugger()
2559+
if __setup_holder__:
2560+
SetupHolder.setup = __setup_holder__
25442561
if py_db is None:
25452562
py_db = PyDB()
25462563
pydevd_vm_type.setup_type()
@@ -2764,6 +2781,10 @@ def settrace_forked(setup_tracing=True):
27642781
setup = SetupHolder.setup
27652782
if setup is None:
27662783
setup = {}
2784+
else:
2785+
# i.e.: Get the ppid at this point as it just changed.
2786+
# If we later do an exec() it should remain the same ppid.
2787+
setup[pydevd_constants.ARGUMENT_PPID] = PyDevdAPI().get_ppid()
27672788
access_token = setup.get('access-token')
27682789
client_access_token = setup.get('client-access-token')
27692790

@@ -2898,6 +2919,7 @@ def main():
28982919

28992920
# parse the command line. --file is our last argument that is required
29002921
pydev_log.debug("Initial arguments: %s", (sys.argv,))
2922+
pydev_log.debug("Current pid: %s", os.getpid())
29012923
try:
29022924
from _pydevd_bundle.pydevd_command_line_handling import process_command_line
29032925
setup = process_command_line(sys.argv)

src/debugpy/_vendored/pydevd/tests_python/debugger_unittest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ def start_socket_client(self, host, port):
893893
# 10 seconds default timeout
894894
timeout = int(os.environ.get('PYDEVD_CONNECT_TIMEOUT', 10))
895895
s.settimeout(timeout)
896-
for _i in range(6):
896+
for _i in range(20):
897897
try:
898898
s.connect((host, port))
899899
break

src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_pydevd_customization.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def main():
2525
child_process = subprocess.Popen(
2626
[sys.executable, '-u', '-c', 'import _debugger_case_pydevd_customization;_debugger_case_pydevd_customization.call()'],
2727
stdout=subprocess.PIPE,
28+
stderr=subprocess.PIPE,
2829
env=env,
2930
)
3031
elif '--posix-spawn' in sys.argv:
@@ -38,16 +39,20 @@ def main():
3839
[sys.executable, '-u', '_debugger_case_pydevd_customization.py', '--simple-call'],
3940
cwd=os.path.dirname(__file__),
4041
stdout=subprocess.PIPE,
42+
stderr=subprocess.PIPE,
4143
env=env,
4244
)
4345

4446
if child_process:
4547
stdout, stderr = child_process.communicate()
46-
assert b'called' in stdout, 'Did not find b"called" in: %s' % (stdout,)
48+
assert b'called' in stdout, 'Did not find b"called" in stdout:\n>>%s<<\nstderr:\n>>%s<<\n' % (stdout, stderr)
4749
print('TEST SUCEEDED!') # break 2 here
4850

4951

5052
def call():
53+
import pydevd
54+
from _pydevd_bundle.pydevd_api import PyDevdAPI
55+
assert pydevd.get_global_debugger().get_arg_ppid() == PyDevdAPI().get_ppid()
5156
print("called") # break 1 here
5257

5358

src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,31 @@ def additional_output_checks(writer, stdout, stderr):
28702870
writer.finished_ok = True
28712871

28722872

2873+
def test_ppid(case_setup, pyfile):
2874+
2875+
@pyfile
2876+
def case_ppid():
2877+
from pydevd import get_global_debugger
2878+
assert get_global_debugger().get_arg_ppid() == 22
2879+
print('TEST SUCEEDED')
2880+
2881+
def update_command_line_args(writer, args):
2882+
ret = debugger_unittest.AbstractWriterThread.update_command_line_args(writer, args)
2883+
ret.insert(ret.index('--qt-support'), '--ppid')
2884+
ret.insert(ret.index('--qt-support'), '22')
2885+
return ret
2886+
2887+
with case_setup.test_file(
2888+
case_ppid,
2889+
update_command_line_args=update_command_line_args,
2890+
) as writer:
2891+
json_facade = JsonFacade(writer)
2892+
json_facade.write_launch()
2893+
json_facade.write_make_initial_run()
2894+
2895+
writer.finished_ok = True
2896+
2897+
28732898
@pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython.')
28742899
def test_path_translation_and_source_reference(case_setup):
28752900

0 commit comments

Comments
 (0)