diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py index 308ee12fe..5521e55db 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_json_debug_options.py @@ -1,5 +1,5 @@ import sys -import platform +import json try: import urllib urllib.unquote # noqa @@ -7,8 +7,96 @@ import urllib.parse as urllib +class DebugOptions(object): + + __slots__ = [ + 'debug_stdlib', + 'redirect_output', + 'show_return_value', + 'break_system_exit_zero', + 'django_debug', + 'flask_debug', + 'stop_on_entry', + 'max_exception_stack_frames', + ] + + def __init__(self): + self.debug_stdlib = False + self.redirect_output = False + self.show_return_value = False + self.break_system_exit_zero = False + self.django_debug = False + self.flask_debug = False + self.stop_on_entry = False + self.max_exception_stack_frames = 0 + + def to_json(self): + dct = {} + for s in self.__slots__: + dct[s] = getattr(self, s) + return json.dumps(dct) + + def update_fom_debug_options(self, debug_options): + if 'DEBUG_STDLIB' in debug_options: + self.debug_stdlib = debug_options.get('DEBUG_STDLIB') + + if 'REDIRECT_OUTPUT' in debug_options: + self.redirect_output = debug_options.get('REDIRECT_OUTPUT') + + if 'SHOW_RETURN_VALUE' in debug_options: + self.show_return_value = debug_options.get('SHOW_RETURN_VALUE') + + if 'BREAK_SYSTEMEXIT_ZERO' in debug_options: + self.break_system_exit_zero = debug_options.get('BREAK_SYSTEMEXIT_ZERO') + + if 'DJANGO_DEBUG' in debug_options: + self.django_debug = debug_options.get('DJANGO_DEBUG') + + if 'FLASK_DEBUG' in debug_options: + self.flask_debug = debug_options.get('FLASK_DEBUG') + + if 'STOP_ON_ENTRY' in debug_options: + self.stop_on_entry = debug_options.get('STOP_ON_ENTRY') + + # Note: _max_exception_stack_frames cannot be set by debug options. + + def update_from_args(self, args): + if 'debugStdLib' in args: + self.debug_stdlib = bool_parser(args['debugStdLib']) + + if 'redirectOutput' in args: + self.redirect_output = bool_parser(args['redirectOutput']) + + if 'showReturnValue' in args: + self.show_return_value = bool_parser(args['showReturnValue']) + + if 'breakOnSystemExitZero' in args: + self.break_system_exit_zero = bool_parser(args['breakOnSystemExitZero']) + + if 'django' in args: + self.django_debug = bool_parser(args['django']) + + if 'flask' in args: + self.flask_debug = bool_parser(args['flask']) + + if 'jinja' in args: + self.flask_debug = bool_parser(args['jinja']) + + if 'stopOnEntry' in args: + self.stop_on_entry = bool_parser(args['stopOnEntry']) + + self.max_exception_stack_frames = int_parser(args.get('maxExceptionStackFrames', 0)) + + +def int_parser(s, default_value=0): + try: + return int(s) + except Exception: + return default_value + + def bool_parser(s): - return s in ("True", "true", "1") + return s in ("True", "true", "1", True, 1) if sys.version_info >= (3,): @@ -35,9 +123,6 @@ def unquote(s): 'WAIT_ON_NORMAL_EXIT': bool_parser, 'BREAK_SYSTEMEXIT_ZERO': bool_parser, 'REDIRECT_OUTPUT': bool_parser, - 'VERSION': unquote, - 'INTERPRETER_OPTIONS': unquote, - 'WEB_BROWSER_URL': unquote, 'DJANGO_DEBUG': bool_parser, 'FLASK_DEBUG': bool_parser, 'FIX_FILE_PATH_CASE': bool_parser, diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index a275f842c..43ef33ce1 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -23,7 +23,7 @@ CMD_STEP_INTO_MY_CODE, CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE, file_system_encoding, CMD_STEP_RETURN_MY_CODE, CMD_STEP_RETURN) from _pydevd_bundle.pydevd_filtering import ExcludeFilter -from _pydevd_bundle.pydevd_json_debug_options import _extract_debug_options +from _pydevd_bundle.pydevd_json_debug_options import _extract_debug_options, DebugOptions from _pydevd_bundle.pydevd_net_command import NetCommand from _pydevd_bundle.pydevd_utils import convert_dap_log_message_to_expression from _pydevd_bundle.pydevd_constants import (PY_IMPL_NAME, DebugInfoHolder, PY_VERSION_STR, @@ -117,7 +117,7 @@ class PyDevJsonCommandProcessor(object): def __init__(self, from_json): self.from_json = from_json self.api = PyDevdAPI() - self._debug_options = {} + self._options = DebugOptions() self._next_breakpoint_id = partial(next, itertools.count(0)) self._goto_targets_map = IDMap() self._launch_or_attach_request_done = False @@ -325,14 +325,14 @@ def _set_debug_options(self, py_db, args, start_reason): self.api.set_exclude_filters(py_db, exclude_filters) - self._debug_options = _extract_debug_options( + debug_options = _extract_debug_options( args.get('options'), args.get('debugOptions'), ) - self._debug_options['args'] = args + self._options.update_fom_debug_options(debug_options) + self._options.update_from_args(args) - debug_stdlib = self._debug_options.get('DEBUG_STDLIB', False) - self.api.set_use_libraries_filter(py_db, not debug_stdlib) + self.api.set_use_libraries_filter(py_db, not self._options.debug_stdlib) path_mappings = [] for pathMapping in args.get('pathMappings', []): @@ -345,21 +345,21 @@ def _set_debug_options(self, py_db, args, start_reason): if bool(path_mappings): pydevd_file_utils.setup_client_server_paths(path_mappings) - if self._debug_options.get('REDIRECT_OUTPUT', False): + if self._options.redirect_output: py_db.enable_output_redirection(True, True) else: py_db.enable_output_redirection(False, False) - self.api.set_show_return_values(py_db, self._debug_options.get('SHOW_RETURN_VALUE', False)) + self.api.set_show_return_values(py_db, self._options.show_return_value) - if not self._debug_options.get('BREAK_SYSTEMEXIT_ZERO', False): + if not self._options.break_system_exit_zero: ignore_system_exit_codes = [0] - if self._debug_options.get('DJANGO_DEBUG', False): + if self._options.django_debug: ignore_system_exit_codes += [3] self.api.set_ignore_system_exit_codes(py_db, ignore_system_exit_codes) - if self._debug_options.get('STOP_ON_ENTRY', False) and start_reason == 'launch': + if self._options.stop_on_entry and start_reason == 'launch': self.api.stop_on_entry() def _send_process_event(self, py_db, start_method): @@ -557,9 +557,9 @@ def on_setbreakpoints_request(self, py_db, request): suspend_policy = 'ALL' if not filename.lower().endswith('.py'): # Note: check based on original file, not mapping. - if self._debug_options.get('DJANGO_DEBUG', False): + if self._options.django_debug: btype = 'django-line' - elif self._debug_options.get('FLASK_DEBUG', False): + elif self._options.flask_debug: btype = 'jinja2-line' breakpoints_set = [] @@ -688,9 +688,9 @@ def on_setexceptionbreakpoints_request(self, py_db, request): if break_raised or break_uncaught: btype = None - if self._debug_options.get('DJANGO_DEBUG', False): + if self._options.django_debug: btype = 'django' - elif self._debug_options.get('FLASK_DEBUG', False): + elif self._options.flask_debug: btype = 'jinja2' if btype: @@ -723,7 +723,7 @@ def on_exceptioninfo_request(self, py_db, request): # : :type exception_into_arguments: ExceptionInfoArguments exception_into_arguments = request.arguments thread_id = exception_into_arguments.threadId - max_frames = int(self._debug_options['args'].get('maxExceptionStackFrames', 0)) + max_frames = self._options.max_exception_stack_frames self.api.request_exception_info_json(py_db, request, thread_id, max_frames) def on_scopes_request(self, py_db, request): diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_debug_options.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_debug_options.py new file mode 100644 index 000000000..4050a92d5 --- /dev/null +++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_debug_options.py @@ -0,0 +1,11 @@ + +import pydevd +# Some hackery to get the PyDevJsonCommandProcessor which is not exposed. +try: + json_command_processor = pydevd.get_global_debugger().reader.process_net_command_json.__self__ +except: + json_command_processor = pydevd.get_global_debugger().reader.process_net_command_json.im_self + +print(json_command_processor._options.to_json()) + +print('TEST SUCEEDED!') diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py index 26eb87d52..c5961a736 100644 --- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py @@ -3432,6 +3432,48 @@ def update_command_line_args(self, args): writer.finished_ok = True +@pytest.mark.parametrize('val', [True, False]) +def test_debug_options(case_setup, val): + with case_setup.test_file('_debugger_case_debug_options.py') as writer: + json_facade = JsonFacade(writer) + args = dict( + debugStdLib=val, + redirectOutput=True, # Always redirect the output regardless of other values. + showReturnValue=val, + breakOnSystemExitZero=val, + django=val, + flask=val, + stopOnEntry=val, + maxExceptionStackFrames=4 if val else 5, + ) + json_facade.write_launch(**args) + + json_facade.write_make_initial_run() + if args['stopOnEntry']: + json_facade.wait_for_thread_stopped('entry') + json_facade.write_continue() + + output = json_facade.wait_for_json_message( + OutputEvent, lambda msg: msg.body.category == 'stdout' and msg.body.output.startswith('{')and msg.body.output.endswith('}')) + + # The values printed are internal values from _pydevd_bundle.pydevd_json_debug_options.DebugOptions, + # not the parameters we passed. + translation = { + 'django': 'django_debug', + 'flask': 'flask_debug', + 'debugStdLib': 'debug_stdlib', + 'redirectOutput': 'redirect_output', + 'showReturnValue': 'show_return_value', + 'breakOnSystemExitZero': 'break_system_exit_zero', + 'stopOnEntry': 'stop_on_entry', + 'maxExceptionStackFrames': 'max_exception_stack_frames', + } + + assert json.loads(output.body.output) == dict((translation[key], val) for key, val in args.items()) + json_facade.wait_for_terminated() + writer.finished_ok = True + + def test_send_json_message(case_setup): with case_setup.test_file('_debugger_case_custom_message.py') as writer: diff --git a/src/ptvsd/adapter/ide.py b/src/ptvsd/adapter/ide.py index 44da84af8..59043bc73 100644 --- a/src/ptvsd/adapter/ide.py +++ b/src/ptvsd/adapter/ide.py @@ -156,6 +156,7 @@ def initialize_request(self, request): # See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 # for the sequence of request and events necessary to orchestrate the start. def _start_message_handler(f): + @components.Component.message_handler def handle(self, request): assert request.is_request("launch", "attach") @@ -179,11 +180,16 @@ def handle(self, request): self._initialize_request = None arguments = request.arguments - if self.launcher and "RedirectOutput" in debug_options: - # The launcher is doing output redirection, so we don't need the - # server to do it, as well. - arguments = dict(arguments) - arguments["debugOptions"] = list(debug_options - {"RedirectOutput"}) + if self.launcher: + if "RedirectOutput" in debug_options: + # The launcher is doing output redirection, so we don't need the + # server to do it, as well. + arguments = dict(arguments) + arguments["debugOptions"] = list(debug_options - {"RedirectOutput"}) + + if arguments.get("redirectOutput"): + arguments = dict(arguments) + del arguments["redirectOutput"] # pydevd doesn't send "initialized", and responds to the start request # immediately, without waiting for "configurationDone". If it changes