Skip to content

Commit

Permalink
Use launcher pid as ppid in DAP. Fixes #42
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Feb 18, 2020
1 parent 7454f89 commit ccd9ac5
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 29 deletions.
43 changes: 31 additions & 12 deletions src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import re
import sys
from _pydev_imps._pydev_saved_modules import threading
from _pydevd_bundle.pydevd_constants import get_global_debugger, IS_WINDOWS, IS_JYTHON, get_current_thread_id
from _pydevd_bundle.pydevd_constants import get_global_debugger, IS_WINDOWS, IS_JYTHON, get_current_thread_id, \
sorted_dict_repr
from _pydev_bundle import pydev_log
from contextlib import contextmanager
from _pydevd_bundle import pydevd_constants
Expand Down Expand Up @@ -35,7 +36,7 @@ def _get_apply_arg_patching():
return getattr(_arg_patch, 'apply_arg_patching', True)


def _get_setup_updated_with_protocol(setup):
def _get_setup_updated_with_protocol_and_ppid(setup, is_exec=False):
if setup is None:
setup = {}
setup = setup.copy()
Expand All @@ -44,7 +45,11 @@ def _get_setup_updated_with_protocol(setup):
setup.pop(pydevd_constants.ARGUMENT_HTTP_JSON_PROTOCOL, None)
setup.pop(pydevd_constants.ARGUMENT_JSON_PROTOCOL, None)
setup.pop(pydevd_constants.ARGUMENT_QUOTED_LINE_PROTOCOL, None)
setup.pop(pydevd_constants.ARGUMENT_HTTP_PROTOCOL, None)

if not is_exec:
# i.e.: The ppid for the subprocess is the current pid.
# If it's an exec, keep it what it was.
setup[pydevd_constants.ARGUMENT_PPID] = os.getpid()

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


def _get_python_c_args(host, port, indC, args, setup):
setup = _get_setup_updated_with_protocol(setup)
setup = _get_setup_updated_with_protocol_and_ppid(setup)

# i.e.: We want to make the repr sorted so that it works in tests.
setup_repr = setup if setup is None else (sorted_dict_repr(setup))

return ("import sys; sys.path.insert(0, r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r; "
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, client_access_token=%r); "
"from pydevd import SetupHolder; SetupHolder.setup = %s; %s"
"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); "
"%s"
) % (
pydev_src_dir,
pydevd_constants.get_protocol(),
host,
port,
setup.get('access-token'),
setup.get('client-access-token'),
setup,
setup_repr,
args[indC + 1])


Expand Down Expand Up @@ -227,7 +236,15 @@ def get_c_option_index(args):
return ind_c


def patch_args(args):
def patch_args(args, is_exec=False):
'''
:param list args:
Arguments to patch.
:param bool is_exec:
If it's an exec, the current process will be replaced (this means we have
to keep the same ppid).
'''
try:
pydev_log.debug("Patching args: %s", args)
args = remove_quotes_from_args(args)
Expand Down Expand Up @@ -280,7 +297,9 @@ def patch_args(args):
# ['X:\\pysrc\\pydevd.py', '--multiprocess', '--print-in-debugger-startup',
# '--vm_type', 'python', '--client', '127.0.0.1', '--port', '56352', '--file', 'x:\\snippet1.py']
from _pydevd_bundle.pydevd_command_line_handling import setup_to_argv
original = setup_to_argv(_get_setup_updated_with_protocol(SetupHolder.setup)) + ['--file']
original = setup_to_argv(
_get_setup_updated_with_protocol_and_ppid(SetupHolder.setup, is_exec=is_exec)
) + ['--file']

module_name = None
m_flag = _get_str_type_compatible(args[i], '-m')
Expand Down Expand Up @@ -459,7 +478,7 @@ def new_execl(path, *args):
os.execlpe(file, arg0, arg1, ..., env)
"""
if _get_apply_arg_patching():
args = patch_args(args)
args = patch_args(args, is_exec=True)
send_process_created_message()

return getattr(os, original_name)(path, *args)
Expand All @@ -475,7 +494,7 @@ def new_execv(path, args):
os.execvp(file, args)
"""
if _get_apply_arg_patching():
args = patch_args(args)
args = patch_args(args, is_exec=True)
send_process_created_message()

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

def new_execve(path, args, env):
if _get_apply_arg_patching():
args = patch_args(args)
args = patch_args(args, is_exec=True)
send_process_created_message()

return getattr(os, original_name)(path, args, env)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os


class ArgHandlerWithParam:
'''
Handler for some arguments which needs a value
Expand Down Expand Up @@ -48,8 +51,18 @@ def handle_argv(self, argv, i, setup):
setup[self.arg_name] = True


def convert_ppid(ppid):
ret = int(ppid)
if ret != 0:
if ret == os.getpid():
raise AssertionError(
'ppid passed is the same as the current process pid (%s)!' % (ret,))
return ret


ACCEPTED_ARG_HANDLERS = [
ArgHandlerWithParam('port', int, 0),
ArgHandlerWithParam('ppid', convert_ppid, 0),
ArgHandlerWithParam('vm_type'),
ArgHandlerWithParam('client'),
ArgHandlerWithParam('access-token'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ def dict_iter_items(d):
def dict_items(d):
return d.items()


def sorted_dict_repr(d):
s = sorted(dict_iter_items(d), key=lambda x:str(x[0]))
return '{' + ', '.join(('%r: %r' % x) for x in s) + '}'


try:
xrange = xrange
except:
Expand Down Expand Up @@ -605,6 +611,8 @@ def new_func(*args, **kwargs):
HTTP_JSON_PROTOCOL = 'http_json'
ARGUMENT_HTTP_JSON_PROTOCOL = 'json-dap-http'

ARGUMENT_PPID = 'ppid'


class _GlobalSettings:
protocol = QUOTED_LINE_PROTOCOL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,12 @@ def on_pydevdsysteminfo_request(self, py_db, request):
except AttributeError:
pid = None

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

ppid = py_db.get_arg_ppid() or self.api.get_ppid()

try:
impl_desc = platform.python_implementation()
Expand Down
22 changes: 22 additions & 0 deletions src/debugpy/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,16 @@ def new_trace_dispatch(frame, event, arg):
# Stop the tracing as the last thing before the actual shutdown for a clean exit.
atexit.register(stoptrace)

def get_arg_ppid(self):
try:
setup = SetupHolder.setup
if setup:
return int(setup.get('ppid', 0))
except:
pydev_log.exception('Error getting ppid.')

return 0

def wait_for_ready_to_run(self):
while not self.ready_to_run:
# busy wait until we receive run command
Expand Down Expand Up @@ -2487,6 +2497,9 @@ def settrace(
stdout_to_server = stdout_to_server or kwargs.get('stdoutToServer', False) # Backward compatibility
stderr_to_server = stderr_to_server or kwargs.get('stderrToServer', False) # Backward compatibility

# Internal use (may be used to set the setup info directly for subprocesess).
__setup_holder__ = kwargs.get('__setup_holder__')

with _set_trace_lock:
_locked_settrace(
host,
Expand All @@ -2503,6 +2516,7 @@ def settrace(
dont_trace_end_patterns,
access_token,
client_access_token,
__setup_holder__=__setup_holder__,
)


Expand All @@ -2524,6 +2538,7 @@ def _locked_settrace(
dont_trace_end_patterns,
access_token,
client_access_token,
__setup_holder__,
):
if patch_multiprocessing:
try:
Expand All @@ -2541,6 +2556,8 @@ def _locked_settrace(
global _global_redirect_stderr_to_server

py_db = get_global_debugger()
if __setup_holder__:
SetupHolder.setup = __setup_holder__
if py_db is None:
py_db = PyDB()
pydevd_vm_type.setup_type()
Expand Down Expand Up @@ -2764,6 +2781,10 @@ def settrace_forked(setup_tracing=True):
setup = SetupHolder.setup
if setup is None:
setup = {}
else:
# i.e.: Get the ppid at this point as it just changed.
# If we later do an exec() it should remain the same ppid.
setup[pydevd_constants.ARGUMENT_PPID] = PyDevdAPI().get_ppid()
access_token = setup.get('access-token')
client_access_token = setup.get('client-access-token')

Expand Down Expand Up @@ -2898,6 +2919,7 @@ def main():

# parse the command line. --file is our last argument that is required
pydev_log.debug("Initial arguments: %s", (sys.argv,))
pydev_log.debug("Current pid: %s", os.getpid())
try:
from _pydevd_bundle.pydevd_command_line_handling import process_command_line
setup = process_command_line(sys.argv)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ def start_socket_client(self, host, port):
# 10 seconds default timeout
timeout = int(os.environ.get('PYDEVD_CONNECT_TIMEOUT', 10))
s.settimeout(timeout)
for _i in range(6):
for _i in range(20):
try:
s.connect((host, port))
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def main():
child_process = subprocess.Popen(
[sys.executable, '-u', '-c', 'import _debugger_case_pydevd_customization;_debugger_case_pydevd_customization.call()'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
elif '--posix-spawn' in sys.argv:
Expand All @@ -38,16 +39,20 @@ def main():
[sys.executable, '-u', '_debugger_case_pydevd_customization.py', '--simple-call'],
cwd=os.path.dirname(__file__),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)

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


def call():
import pydevd
from _pydevd_bundle.pydevd_api import PyDevdAPI
assert pydevd.get_global_debugger().get_arg_ppid() == PyDevdAPI().get_ppid()
print("called") # break 1 here


Expand Down
25 changes: 25 additions & 0 deletions src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2870,6 +2870,31 @@ def additional_output_checks(writer, stdout, stderr):
writer.finished_ok = True


def test_ppid(case_setup, pyfile):

@pyfile
def case_ppid():
from pydevd import get_global_debugger
assert get_global_debugger().get_arg_ppid() == 22
print('TEST SUCEEDED')

def update_command_line_args(writer, args):
ret = debugger_unittest.AbstractWriterThread.update_command_line_args(writer, args)
ret.insert(ret.index('--qt-support'), '--ppid')
ret.insert(ret.index('--qt-support'), '22')
return ret

with case_setup.test_file(
case_ppid,
update_command_line_args=update_command_line_args,
) as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch()
json_facade.write_make_initial_run()

writer.finished_ok = True


@pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython.')
def test_path_translation_and_source_reference(case_setup):

Expand Down
Loading

0 comments on commit ccd9ac5

Please sign in to comment.