Skip to content

Commit

Permalink
Enable pydevd to be used in DAP mode directly. WIP: microsoft#532
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Sep 29, 2022
1 parent 01b1c7b commit 0892603
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 65 deletions.
11 changes: 11 additions & 0 deletions src/debugpy/_vendored/force_pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,14 @@ def debugpy_breakpointhook():
from _pydevd_bundle import pydevd_constants
from _pydevd_bundle import pydevd_defaults
pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL

# Enable some defaults related to debugpy such as sending a single notification when
# threads pause and stopping on any exception.
pydevd_defaults.PydevdCustomization.DEBUG_MODE = 'debugpy-dap'

# This is important when pydevd attaches automatically to a subprocess. In this case, we have to
# make sure that debugpy is properly put back in the game for users to be able to use it.
pydevd_defaults.PydevdCustomization.PREIMPORT = '%r;%s' % (
os.path.dirname(os.path.dirname(debugpy.__file__)),
'debugpy._vendored.force_pydevd'
)
10 changes: 9 additions & 1 deletion src/debugpy/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
sorted_dict_repr, set_global_debugger, DebugInfoHolder
from _pydev_bundle import pydev_log
from contextlib import contextmanager
from _pydevd_bundle import pydevd_constants
from _pydevd_bundle import pydevd_constants, pydevd_defaults
from _pydevd_bundle.pydevd_defaults import PydevdCustomization
import ast

Expand Down Expand Up @@ -69,6 +69,14 @@ def _get_setup_updated_with_protocol_and_ppid(setup, is_exec=False):
else:
pydev_log.debug('Unexpected protocol: %s', protocol)

mode = pydevd_defaults.PydevdCustomization.DEBUG_MODE
if mode:
setup['debug-mode'] = mode

preimport = pydevd_defaults.PydevdCustomization.PREIMPORT
if preimport:
setup['preimport'] = preimport

if DebugInfoHolder.PYDEVD_DEBUG_FILE:
setup['log-file'] = DebugInfoHolder.PYDEVD_DEBUG_FILE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def convert_ppid(ppid):
ArgHandlerWithParam('client'),
ArgHandlerWithParam('access-token'),
ArgHandlerWithParam('client-access-token'),
ArgHandlerWithParam('debug-mode'),
ArgHandlerWithParam('preimport'),

# Logging
ArgHandlerWithParam('log-file'),
Expand Down
54 changes: 53 additions & 1 deletion src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_defaults.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,60 @@
'''
This module holds the customization settings for the debugger.
'''

from _pydevd_bundle.pydevd_constants import QUOTED_LINE_PROTOCOL
from _pydev_bundle import pydev_log
import sys


class PydevdCustomization(object):
DEFAULT_PROTOCOL = QUOTED_LINE_PROTOCOL
DEFAULT_PROTOCOL: str = QUOTED_LINE_PROTOCOL

# Debug mode may be set to 'debugpy-dap'.
#
# In 'debugpy-dap' mode the following settings are done to PyDB:
#
# py_db.skip_suspend_on_breakpoint_exception = (BaseException,)
# py_db.skip_print_breakpoint_exception = (NameError,)
# py_db.multi_threads_single_notification = True
DEBUG_MODE: str = ''

# This may be a <sys_path_entry>;<module_name> to be pre-imported
# Something as: 'c:/temp/foo;my_module.bar'
#
# What's done in this case is something as:
#
# sys.path.insert(0, <sys_path_entry>)
# try:
# import <module_name>
# finally:
# del sys.path[0]
#
# If the pre-import fails an output message is
# sent (but apart from that debugger execution
# should continue).
PREIMPORT: str = ''


def on_pydb_init(py_db):
if PydevdCustomization.DEBUG_MODE == 'debugpy-dap':
py_db.skip_suspend_on_breakpoint_exception = (BaseException,)
py_db.skip_print_breakpoint_exception = (NameError,)
py_db.multi_threads_single_notification = True

if PydevdCustomization.PREIMPORT:
try:
sys_path_entry, module_name = PydevdCustomization.PREIMPORT.rsplit(';', maxsplit=1)
except Exception:
pydev_log.exception("Expected ';' in %s" % (PydevdCustomization.PREIMPORT,))
else:
try:
sys.path.insert(0, sys_path_entry)
try:
__import__(module_name)
finally:
sys.path.remove(sys_path_entry)
except Exception:
pydev_log.exception(
"Error importing %s (with sys.path entry: %s)" % (module_name, sys_path_entry))

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class DebugOptions(object):
'stop_on_entry',
'max_exception_stack_frames',
'gui_event_loop',
'client_os',
]

def __init__(self):
Expand All @@ -26,6 +27,7 @@ def __init__(self):
self.stop_on_entry = False
self.max_exception_stack_frames = 0
self.gui_event_loop = 'matplotlib'
self.client_os = None

def to_json(self):
dct = {}
Expand Down Expand Up @@ -55,6 +57,9 @@ def update_fom_debug_options(self, debug_options):
if 'STOP_ON_ENTRY' in debug_options:
self.stop_on_entry = debug_options.get('STOP_ON_ENTRY')

if 'CLIENT_OS_TYPE' in debug_options:
self.client_os = debug_options.get('CLIENT_OS_TYPE')

# Note: _max_exception_stack_frames cannot be set by debug options.

def update_from_args(self, args):
Expand Down Expand Up @@ -91,6 +96,9 @@ def update_from_args(self, args):
if 'guiEventLoop' in args:
self.gui_event_loop = str(args['guiEventLoop'])

if 'clientOS' in args:
self.client_os = str(args['clientOS']).upper()


def int_parser(s, default_value=0):
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
VariablesResponseBody, SetBreakpointsResponseBody, Response,
Capabilities, PydevdAuthorizeRequest, Request,
StepInTargetsResponseBody, SetFunctionBreakpointsResponseBody, BreakpointEvent,
BreakpointEventBody)
BreakpointEventBody, InitializedEvent)
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_breakpoints import get_exception_class, FunctionBreakpoint
from _pydevd_bundle.pydevd_comm_constants import (
Expand Down Expand Up @@ -380,6 +380,9 @@ def get_variable_presentation(setting, default):

self.api.set_use_libraries_filter(py_db, self._options.just_my_code)

if self._options.client_os:
self.api.set_ide_os(self._options.client_os)

path_mappings = []
for pathMapping in args.get('pathMappings', []):
localRoot = pathMapping.get('localRoot', '')
Expand Down Expand Up @@ -496,6 +499,9 @@ def _handle_launch_or_attach_request(self, py_db, request, start_reason):
self.api.set_enable_thread_notifications(py_db, True)
self._set_debug_options(py_db, request.arguments.kwargs, start_reason=start_reason)
response = pydevd_base_schema.build_response(request)

initialized_event = InitializedEvent()
py_db.writer.add_command(NetCommand(CMD_RETURN, 0, initialized_event, is_json=True))
return NetCommand(CMD_RETURN, 0, response, is_json=True)

def on_launch_request(self, py_db, request):
Expand Down
11 changes: 10 additions & 1 deletion src/debugpy/_vendored/pydevd/pydevd.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from _pydev_bundle._pydev_saved_modules import threading, time, thread
from _pydevd_bundle import pydevd_extension_utils, pydevd_frame_utils
from _pydevd_bundle.pydevd_filtering import FilesFiltering, glob_matches_path
from _pydevd_bundle import pydevd_io, pydevd_vm_type
from _pydevd_bundle import pydevd_io, pydevd_vm_type, pydevd_defaults
from _pydevd_bundle import pydevd_utils
from _pydevd_bundle import pydevd_runpy
from _pydev_bundle.pydev_console_utils import DebugConsoleStdIn
Expand Down Expand Up @@ -715,6 +715,7 @@ def new_trace_dispatch(frame, event, arg):
# Set as the global instance only after it's initialized.
set_global_debugger(self)

pydevd_defaults.on_pydb_init(self)
# Stop the tracing as the last thing before the actual shutdown for a clean exit.
atexit.register(stoptrace)

Expand Down Expand Up @@ -3279,6 +3280,14 @@ def main():
pydev_log.exception()
usage(1)

preimport = setup.get('preimport')
if preimport:
pydevd_defaults.PydevdCustomization.PREIMPORT = preimport

debug_mode = setup.get('debug-mode')
if debug_mode:
pydevd_defaults.PydevdCustomization.DEBUG_MODE = debug_mode

log_trace_level = setup.get('log-level')

# Note: the logging info could've been changed (this would happen if this is a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,10 @@ def accept_message(msg):
else:
return last
if prev != last:
print('Ignored message: %r' % (last,))
sys.stderr.write('Ignored message: %r\n' % (last,))
# Uncomment to know where in the stack it was ignored.
# import traceback
# traceback.print_stack(limit=7)

prev = last

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
import sys
port = int(sys.argv[1])
root_dirname = os.path.dirname(os.path.dirname(__file__))

if root_dirname not in sys.path:
sys.path.append(root_dirname)

import pydevd

# Ensure that pydevd uses JSON protocol
from _pydevd_bundle import pydevd_constants
from _pydevd_bundle import pydevd_defaults
pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL

# Enable some defaults related to debugpy such as sending a single notification when
# threads pause and stopping on any exception.
pydevd_defaults.PydevdCustomization.DEBUG_MODE = 'debugpy-dap'

import tempfile
with tempfile.TemporaryDirectory('w') as tempdir:
with open(os.path.join(tempdir, 'my_custom_module.py'), 'w') as stream:
stream.write("print('Loaded my_custom_module')")

pydevd_defaults.PydevdCustomization.PREIMPORT = '%s;my_custom_module' % (tempdir,)
assert 'my_custom_module' not in sys.modules

assert sys.gettrace() is None
print('enable attach to port: %s' % (port,))
pydevd._enable_attach(('127.0.0.1', port))

assert pydevd.get_global_debugger() is not None
# Set as a part of debugpy-dap
assert pydevd.get_global_debugger().multi_threads_single_notification
assert sys.gettrace() is not None

assert 'my_custom_module' in sys.modules

a = 10 # Break 1
print('wait for attach')
pydevd._wait_for_attach()

a = 20 # Break 2

print('TEST SUCEEDED!')
28 changes: 27 additions & 1 deletion src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
ExceptionOptions, Response, StoppedEvent, ContinuedEvent, ProcessEvent, InitializeRequest,
InitializeRequestArguments, TerminateArguments, TerminateRequest, TerminatedEvent,
FunctionBreakpoint, SetFunctionBreakpointsRequest, SetFunctionBreakpointsArguments,
BreakpointEvent)
BreakpointEvent, InitializedEvent)
from _pydevd_bundle.pydevd_comm_constants import file_system_encoding
from _pydevd_bundle.pydevd_constants import (int_types, IS_64BIT_PROCESS,
PY_VERSION_STR, PY_IMPL_VERSION_STR, PY_IMPL_NAME, IS_PY36_OR_GREATER,
Expand Down Expand Up @@ -3920,6 +3920,30 @@ def create_msg():
writer.finished_ok = True


def test_wait_for_attach_debugpy_mode(case_setup_remote_attach_to):
host_port = get_socket_name(close=True)

with case_setup_remote_attach_to.test_file('_debugger_case_wait_for_attach_debugpy_mode.py', host_port[1]) as writer:
time.sleep(1) # Give some time for it to pass the first breakpoint and wait in 'wait_for_attach'.
writer.start_socket_client(*host_port)

# We don't send initial messages because everything should be pre-configured to
# the DAP mode already (i.e.: making sure it works).
json_facade = JsonFacade(writer, send_json_startup_messages=False)
break2_line = writer.get_line_index_with_content('Break 2')

json_facade.write_attach()
# Make sure we also received the initialized in the attach.
assert len(json_facade.mark_messages(InitializedEvent)) == 1

json_facade.write_set_breakpoints([break2_line])

json_facade.write_make_initial_run()
json_facade.wait_for_thread_stopped(line=break2_line)
json_facade.write_continue()
writer.finished_ok = True


def test_wait_for_attach(case_setup_remote_attach_to):
host_port = get_socket_name(close=True)

Expand Down Expand Up @@ -5411,6 +5435,7 @@ def test_debug_options(case_setup, val):
stopOnEntry=val,
maxExceptionStackFrames=4 if val else 5,
guiEventLoop=gui_event_loop,
clientOS='UNIX' if val else 'WINDOWS'
)
json_facade.write_launch(**args)

Expand All @@ -5434,6 +5459,7 @@ def test_debug_options(case_setup, val):
'stopOnEntry': 'stop_on_entry',
'maxExceptionStackFrames': 'max_exception_stack_frames',
'guiEventLoop': 'gui_event_loop',
'clientOS': 'client_os',
}

assert json.loads(output.body.output) == dict((translation[key], val) for key, val in args.items())
Expand Down
18 changes: 0 additions & 18 deletions src/debugpy/adapter/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,24 +266,6 @@ def handle(self, request):
self._propagate_deferred_events()
return

if "clientOS" in request:
client_os = request("clientOS", json.enum("windows", "unix")).upper()
elif {"WindowsClient", "Windows"} & debug_options:
client_os = "WINDOWS"
elif {"UnixClient", "UNIX"} & debug_options:
client_os = "UNIX"
else:
client_os = "WINDOWS" if sys.platform == "win32" else "UNIX"
self.server.channel.request(
"setDebuggerProperty",
{
"skipSuspendOnBreakpointException": ("BaseException",),
"skipPrintBreakpointException": ("NameError",),
"multiThreadsSingleNotification": True,
"ideOS": client_os,
},
)

# Let the client know that it can begin configuring the adapter.
self.channel.send_event("initialized")

Expand Down
39 changes: 0 additions & 39 deletions src/debugpy/adapter/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,45 +84,6 @@ def __init__(self, sock):
self.ppid = None
self.channel.name = stream.name = str(self)

debugpy_dir = os.path.dirname(os.path.dirname(debugpy.__file__))
# Note: we must check if 'debugpy' is not already in sys.modules because the
# evaluation of an import at the wrong time could deadlock Python due to
# its import lock.
#
# So, in general this evaluation shouldn't do anything. It's only
# important when pydevd attaches automatically to a subprocess. In this
# case, we have to make sure that debugpy is properly put back in the game
# for users to be able to use it.v
#
# In this case (when the import is needed), this evaluation *must* be done
# before the configurationDone request is sent -- if this is not respected
# it's possible that pydevd already started secondary threads to handle
# commands, in which case it's very likely that this command would be
# evaluated at the wrong thread and the import could potentially deadlock
# the program.
#
# Note 2: the sys module is guaranteed to be in the frame globals and
# doesn't need to be imported.
inject_debugpy = """
if 'debugpy' not in sys.modules:
sys.path.insert(0, {debugpy_dir!r})
try:
import debugpy
finally:
del sys.path[0]
"""
inject_debugpy = inject_debugpy.format(debugpy_dir=debugpy_dir)

try:
self.channel.request("evaluate", {"expression": inject_debugpy})
except messaging.MessageHandlingError:
# Failure to inject is not a fatal error - such a subprocess can
# still be debugged, it just won't support "import debugpy" in user
# code - so don't terminate the session.
log.swallow_exception(
"Failed to inject debugpy into {0}:", self, level="warning"
)

with _lock:
# The server can disconnect concurrently before we get here, e.g. if
# it was force-killed. If the disconnect() handler has already run,
Expand Down
Loading

0 comments on commit 0892603

Please sign in to comment.