Repository: microsoft/ptvsd

Issue:
adapter: access tokens
Adapter should implement the counterpart to #1710 in pydevd. This includes:

1. A command line argument that allows to specify the server access token (used by `ptvsd` in `enable_attach()` mode).
1. Generation of a cryptographically secure client access token.
1. Passing the server token to pydevd in the "initialize" request when connecting.
1. Verifying that the client token returned by pydevd in the "initialize" response matches the generated client token.



Discussion:

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
--- a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
+++ b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
@@ -6,6 +6,7 @@
 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 _pydev_bundle import pydev_log
+from contextlib import contextmanager
 
 try:
     xrange
@@ -18,6 +19,21 @@
 
 pydev_src_dir = os.path.dirname(os.path.dirname(__file__))
 
+_arg_patch = threading.local()
+
+
+@contextmanager
+def skip_subprocess_arg_patch():
+    _arg_patch.apply_arg_patching = False
+    try:
+        yield
+    finally:
+        _arg_patch.apply_arg_patching = True
+
+
+def _get_apply_arg_patching():
+    return getattr(_arg_patch, 'apply_arg_patching', True)
+
 
 def _get_python_c_args(host, port, indC, args, setup):
     host_literal = "'" + host + "'" if host is not None else 'None'
@@ -44,10 +60,10 @@ def _is_managed_arg(arg):
     return False
 
 
-def _on_forked_process():
+def _on_forked_process(setup_tracing=True):
     import pydevd
     pydevd.threadingCurrentThread().__pydevd_main_thread = True
-    pydevd.settrace_forked()
+    pydevd.settrace_forked(setup_tracing=setup_tracing)
 
 
 def _on_set_trace_for_new_thread(global_debugger):
@@ -357,9 +373,10 @@ def new_execl(path, *args):
         os.execlp(file, arg0, arg1, ...)
         os.execlpe(file, arg0, arg1, ..., env)
         """
-        import os
-        args = patch_args(args)
-        send_process_created_message()
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
         return getattr(os, original_name)(path, *args)
 
     return new_execl
@@ -372,9 +389,11 @@ def new_execv(path, args):
         os.execv(path, args)
         os.execvp(file, args)
         """
-        import os
-        send_process_created_message()
-        return getattr(os, original_name)(path, patch_args(args))
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
+        return getattr(os, original_name)(path, args)
 
     return new_execv
 
@@ -386,9 +405,11 @@ def create_execve(original_name):
     """
 
     def new_execve(path, args, env):
-        import os
-        send_process_created_message()
-        return getattr(os, original_name)(path, patch_args(args), env)
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
+        return getattr(os, original_name)(path, args, env)
 
     return new_execve
 
@@ -400,9 +421,10 @@ def new_spawnl(mode, path, *args):
         os.spawnl(mode, path, arg0, arg1, ...)
         os.spawnlp(mode, file, arg0, arg1, ...)
         """
-        import os
-        args = patch_args(args)
-        send_process_created_message()
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
         return getattr(os, original_name)(mode, path, *args)
 
     return new_spawnl
@@ -415,9 +437,11 @@ def new_spawnv(mode, path, args):
         os.spawnv(mode, path, args)
         os.spawnvp(mode, file, args)
         """
-        import os
-        send_process_created_message()
-        return getattr(os, original_name)(mode, path, patch_args(args))
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
+        return getattr(os, original_name)(mode, path, args)
 
     return new_spawnv
 
@@ -429,9 +453,11 @@ def create_spawnve(original_name):
     """
 
     def new_spawnve(mode, path, args, env):
-        import os
-        send_process_created_message()
-        return getattr(os, original_name)(mode, path, patch_args(args), env)
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
+        return getattr(os, original_name)(mode, path, args, env)
 
     return new_spawnve
 
@@ -443,8 +469,10 @@ def create_fork_exec(original_name):
 
     def new_fork_exec(args, *other_args):
         import _posixsubprocess  # @UnresolvedImport
-        args = patch_args(args)
-        send_process_created_message()
+        if _get_apply_arg_patching():
+            args = patch_args(args)
+            send_process_created_message()
+
         return getattr(_posixsubprocess, original_name)(args, *other_args)
 
     return new_fork_exec
@@ -476,8 +504,12 @@ def new_CreateProcess(app_name, cmd_line, *args):
             import _subprocess
         except ImportError:
             import _winapi as _subprocess
-        send_process_created_message()
-        return getattr(_subprocess, original_name)(app_name, patch_arg_str_win(cmd_line), *args)
+
+        if _get_apply_arg_patching():
+            cmd_line = patch_arg_str_win(cmd_line)
+            send_process_created_message()
+
+        return getattr(_subprocess, original_name)(app_name, cmd_line, *args)
 
     return new_CreateProcess
 
@@ -501,12 +533,12 @@ def new_CreateProcess(*args):
 def create_fork(original_name):
 
     def new_fork():
-        import os
-
         # A simple fork will result in a new python process
         is_new_python_process = True
         frame = sys._getframe()
 
+        apply_arg_patch = _get_apply_arg_patching()
+
         while frame is not None:
             if frame.f_code.co_name == '_execute_child' and 'subprocess' in frame.f_code.co_filename:
                 # If we're actually in subprocess.Popen creating a child, it may
@@ -525,7 +557,7 @@ def new_fork():
         child_process = getattr(os, original_name)()  # fork
         if not child_process:
             if is_new_python_process:
-                _on_forked_process()
+                _on_forked_process(setup_tracing=apply_arg_patch)
         else:
             if is_new_python_process:
                 send_process_created_message()
diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py
--- a/src/ptvsd/_vendored/pydevd/pydevd.py
+++ b/src/ptvsd/_vendored/pydevd/pydevd.py
@@ -2594,7 +2594,7 @@ def dispatch():
     return host, port
 
 
-def settrace_forked():
+def settrace_forked(setup_tracing=True):
     '''
     When creating a fork from a process in the debugger, we need to reset the whole debugger environment!
     '''
@@ -2610,31 +2610,40 @@ def settrace_forked():
     access_token = setup.get('access-token')
     ide_access_token = setup.get('ide-access-token')
 
-    from _pydevd_frame_eval.pydevd_frame_eval_main import clear_thread_local_info
-    host, port = dispatch()
+    if setup_tracing:
+        from _pydevd_frame_eval.pydevd_frame_eval_main import clear_thread_local_info
+        host, port = dispatch()
 
     import pydevd_tracing
     pydevd_tracing.restore_sys_set_trace_func()
 
-    if port is not None:
-        global _debugger_setup
-        _debugger_setup = False
-
-        custom_frames_container_init()
+    if setup_tracing:
+        if port is not None:
+            global _debugger_setup
+            _debugger_setup = False
+
+            custom_frames_container_init()
+
+            if clear_thread_local_info is not None:
+                clear_thread_local_info()
+
+            settrace(
+                    host,
+                    port=port,
+                    suspend=False,
+                    trace_only_current_thread=False,
+                    overwrite_prev_trace=True,
+                    patch_multiprocessing=True,
+                    access_token=access_token,
+                    ide_access_token=ide_access_token,
+            )
 
-        if clear_thread_local_info is not None:
-            clear_thread_local_info()
 
-        settrace(
-                host,
-                port=port,
-                suspend=False,
-                trace_only_current_thread=False,
-                overwrite_prev_trace=True,
-                patch_multiprocessing=True,
-                access_token=access_token,
-                ide_access_token=ide_access_token,
-        )
+@contextmanager
+def skip_subprocess_arg_patch():
+    from _pydev_bundle import pydev_monkey
+    with pydev_monkey.skip_subprocess_arg_patch():
+        yield
 
 
 #=======================================================================================================================


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_no_subprocess_patching.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_no_subprocess_patching.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_no_subprocess_patching.py
@@ -0,0 +1,35 @@
+import subprocess
+import sys
+import os
+from contextlib import contextmanager
+env = os.environ.copy()
+pythonpath = env.get('PYTHONPATH', '')
+env['PYTHONPATH'] = os.path.dirname(__file__)
+
+
+def call():
+    print("called")  # break 1 here
+
+
+@contextmanager
+def dummy_context_manager():
+    yield
+
+
+if __name__ == '__main__':
+    if 'apply-multiprocessing-patch' in sys.argv:
+        context_manager = dummy_context_manager  # It's applied by default.
+    else:
+        import pydevd
+        context_manager = pydevd.skip_subprocess_arg_patch
+
+    with context_manager():
+        p = subprocess.Popen(
+            [sys.executable, '-c', 'import _debugger_case_no_subprocess_patching;_debugger_case_no_subprocess_patching.call()'],
+            stdout=subprocess.PIPE,
+            env=env,
+        )
+
+    stdout, stderr = p.communicate()
+    assert b'called' in stdout, 'Did not find b"called" in: %s' % (stdout,)
+    print('TEST SUCEEDED!')  # break 2 here
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
@@ -2762,6 +2762,85 @@ def test_listen_dap_messages(case_setup):
         writer.finished_ok = True
 
 
+@pytest.mark.parametrize('apply_multiprocessing_patch', [True, False])
+def test_no_subprocess_patching(case_setup_multiprocessing, apply_multiprocessing_patch):
+    import threading
+    from tests_python.debugger_unittest import AbstractWriterThread
+
+    def update_command_line_args(writer, args):
+        ret = debugger_unittest.AbstractWriterThread.update_command_line_args(writer, args)
+        ret.insert(ret.index('--qt-support'), '--multiprocess')
+        if apply_multiprocessing_patch:
+            ret.append('apply-multiprocessing-patch')
+        return ret
+
+    with case_setup_multiprocessing.test_file(
+            '_debugger_case_no_subprocess_patching.py',
+            update_command_line_args=update_command_line_args
+        ) as writer:
+        json_facade = JsonFacade(writer)
+        json_facade.write_launch()
+
+        break1_line = writer.get_line_index_with_content('break 1 here')
+        break2_line = writer.get_line_index_with_content('break 2 here')
+        json_facade.write_set_breakpoints([break1_line, break2_line])
+
+        server_socket = writer.server_socket
+
+        class SecondaryProcessWriterThread(AbstractWriterThread):
+
+            TEST_FILE = writer.get_main_filename()
+            _sequence = -1
+
+        class SecondaryProcessThreadCommunication(threading.Thread):
+
+            def run(self):
+                from tests_python.debugger_unittest import ReaderThread
+                expected_connections = 1
+                if sys.platform != 'win32' and IS_PY2:
+                    # Note: on linux on Python 2 CPython subprocess.call will actually
+                    # create a fork first (at which point it'll connect) and then, later on it'll
+                    # call the main (as if it was a clean process as if PyDB wasn't created
+                    # the first time -- the debugger will still work, but it'll do an additional
+                    # connection).
+                    expected_connections = 2
+
+                for _ in range(expected_connections):
+                    server_socket.listen(1)
+                    self.server_socket = server_socket
+                    new_sock, addr = server_socket.accept()
+
+                    reader_thread = ReaderThread(new_sock)
+                    reader_thread.name = '  *** Multiprocess Reader Thread'
+                    reader_thread.start()
+
+                    writer2 = SecondaryProcessWriterThread()
+                    writer2.reader_thread = reader_thread
+                    writer2.sock = new_sock
+                    json_facade2 = JsonFacade(writer2)
+
+                    json_facade2.write_set_breakpoints([break1_line, break2_line])
+                    json_facade2.write_make_initial_run()
+
+                json_facade2.wait_for_thread_stopped()
+                json_facade2.write_continue()
+
+        if apply_multiprocessing_patch:
+            secondary_process_thread_communication = SecondaryProcessThreadCommunication()
+            secondary_process_thread_communication.start()
+            time.sleep(.1)
+
+        json_facade.write_make_initial_run()
+        json_facade.wait_for_thread_stopped()
+        json_facade.write_continue()
+
+        if apply_multiprocessing_patch:
+            secondary_process_thread_communication.join(10)
+            if secondary_process_thread_communication.is_alive():
+                raise AssertionError('The SecondaryProcessThreadCommunication did not finish')
+        writer.finished_ok = True
+
+
 def test_pydevd_systeminfo(case_setup):
     with case_setup.test_file('_debugger_case_print.py') as writer:
         json_facade = JsonFacade(writer)