Repository: microsoft/ptvsd

Issue:
Fork/exec creates two connections from pydevd to adapter
This is causing `test_subprocess` to fail on Python 2.7, and is also the root cause of Flask test failures.

The problem is that on 2.7, `subprocess.Popen` uses fork+exec - and pydevd detours both. So first `fork()` creates a child process, and pydevd is spun up in that process and connects to the adapter. Then the child process does `exec()`, pydevd patches command line to ensure that it gets loaded first, and as it does so, it connects to the adapter again. Since both connections come from a process with the same PID, the adapter rejects the second one.

There is a bug in the adapter here as well (#1908), but the fundamental problem is that it really is the _same_ process trying to connect twice. The adapter can handle it by forcibly closing any existing connection with the same PID when a new one comes in; but I wonder if it might be better to prevent this on pydevd side, e.g. by having the code detouring `exec()` close the socket before replacing the process. Per `man execve`, the OS itself will keep the socket fd open unless it's explicitly closed, so the adapter can't just wait for it to go away.


Discussion:
@fabioz, what do you think? Is there any downside to explicitly closing the socket in pydevd? 
Another interesting side effect of this: because we use PID to generate distinct log file names, they end up clashing in this case. This made it harder to diagnose this issue, because the pydevd log created after `exec` was overwriting the older log created after `fork`. The same issue would apply to `ptvsd.server` log, if it got far enough to create one.

Perhaps we should include a timestamp in the filename?
I think closing the socket is ok and makes sense. I'll create a pull request for that.

I was thinking a bit more about it and maybe we should handle that specific case from subprocess and don't even create a connection for the fork in that case (note that we still need to handle the fork/exec case in case some user specifically does it, but it won't be common as it is now -- in this case it may be ok to just leave the log as it is without adding a timestamp just for this corner case).

What do you think?
Makes sense, if it can be detoured reliably. Attaching to that intermediate process doesn't do anything useful, but the user ends up paying the penalty of loading the debugger in it and waiting for IDE to attach to it. 

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_log.py b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_log.py
--- a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_log.py
+++ b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_log.py
@@ -16,13 +16,23 @@ class _LoggingGlobals(object):
     _debug_stream_initialized = False
 
 
-def initialize_debug_stream(force=False):
-    if _LoggingGlobals._debug_stream_initialized and not force:
-        return
+def initialize_debug_stream(reinitialize=False):
+    '''
+    :param bool reinitialize:
+        Reinitialize is used to update the debug stream after a fork (thus, if it wasn't
+        initialized, we don't need to do anything).
+    '''
+    if reinitialize:
+        if not _LoggingGlobals._debug_stream_initialized:
+            return
+    else:
+        if _LoggingGlobals._debug_stream_initialized:
+            return
+
     _LoggingGlobals._debug_stream_initialized = True
 
     # Note: we cannot initialize with sys.stderr because when forking we may end up logging things in 'os' calls.
-    _LoggingGlobals._debug_stream = NULL  
+    _LoggingGlobals._debug_stream = NULL
     _LoggingGlobals._debug_stream_filename = None
 
     if not DebugInfoHolder.PYDEVD_DEBUG_FILE:
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
@@ -93,8 +93,10 @@ def _is_managed_arg(arg):
 
 def _on_forked_process(setup_tracing=True):
     pydevd_constants.after_fork()
-    pydev_log.initialize_debug_stream(force=True)
-    pydev_log.debug('pydevd on forked process: %s', os.getpid())
+    pydev_log.initialize_debug_stream(reinitialize=True)
+
+    if setup_tracing:
+        pydev_log.debug('pydevd on forked process: %s', os.getpid())
 
     import pydevd
     pydevd.threadingCurrentThread().__pydevd_main_thread = True
@@ -574,8 +576,10 @@ def new_fork():
 
         apply_arg_patch = _get_apply_arg_patching()
 
+        is_subprocess_fork = False
         while frame is not None:
             if frame.f_code.co_name == '_execute_child' and 'subprocess' in frame.f_code.co_filename:
+                is_subprocess_fork = True
                 # If we're actually in subprocess.Popen creating a child, it may
                 # result in something which is not a Python process, (so, we
                 # don't want to connect with it in the forked version).
@@ -592,7 +596,7 @@ def new_fork():
         child_process = getattr(os, original_name)()  # fork
         if not child_process:
             if is_new_python_process:
-                _on_forked_process(setup_tracing=apply_arg_patch)
+                _on_forked_process(setup_tracing=apply_arg_patch and not is_subprocess_fork)
         else:
             if is_new_python_process:
                 send_process_created_message()


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
@@ -2392,13 +2392,6 @@ class SecondaryProcessThreadCommunication(threading.Thread):
             def run(self):
                 from tests_python.debugger_unittest import ReaderThread
                 expected_connections = 1
-                if sys.platform != 'win32' and IS_PY2 and file_to_check == '_debugger_case_python_c.py':
-                    # 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)
@@ -2544,8 +2537,6 @@ def run(self):
                 # connection.
 
                 expected_connections = 1
-                if sys.platform != 'win32' and IS_PY2:
-                    expected_connections = 2
 
                 for _ in range(expected_connections):
                     server_socket.listen(1)
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
@@ -3001,13 +3001,6 @@ 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)
@@ -3086,13 +3079,6 @@ 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)
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_utilities.py b/src/ptvsd/_vendored/pydevd/tests_python/test_utilities.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_utilities.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_utilities.py
@@ -182,7 +182,7 @@ def test_pydevd_logging_files(tmpdir):
 
         for d in (d1, d2):
             DebugInfoHolder.PYDEVD_DEBUG_FILE = os.path.join(d, 'file.txt')
-            pydev_log.initialize_debug_stream(force=True)
+            pydev_log.initialize_debug_stream(reinitialize=True)
 
             assert os.path.normpath(_LoggingGlobals._debug_stream_filename) == \
                 os.path.normpath(os.path.join(d, 'file.%s.txt' % os.getpid()))