Repository: microsoft/ptvsd

Issue:
PTVSD 5.* debugger Flags are not working
I'm setting the debugger flags in the launch json, but they are being ignored by the debugger. For instance, the show return value and break on system exit 0 are not working. 

Launch JSON: 
![image](https://user-images.githubusercontent.com/3315992/71121455-5c429d00-2193-11ea-88d6-7db65c4774c0.png)

DebugAdapterLogs for PTVSD 5.*
[DebugAdapterHost5.txt](https://github.com/microsoft/ptvsd/files/3980429/DebugAdapterHost5.txt)

DebugAdapterLogs for PTVSD 4.0 (for comparison)
[DebugAdapterHost4.txt](https://github.com/microsoft/ptvsd/files/3980432/DebugAdapterHost4.txt)





Discussion:
We didn't fully transition to new-style properties for all the debug options yet. We need to do so.

For some of the properties, the extension or the adapter automatically generate the corresponding option to pass to pydevd, but not all. However, it would be better for pydevd itself to handle these directly in one place.

The required behavior is straightforward: for every debug option named "Foo", we should have a corresponding Boolean property named "foo", with the same semantics. Eventually we'll retire the debug options, and keep the properties. 
I'll take a look at it.

Resolving patch:
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
--- 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
--- 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/adapter/ide.py b/src/ptvsd/adapter/ide.py
--- 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


Test patch:
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
--- /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
--- 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: