Repository: microsoft/ptvsd

Issue:
"pydevdSystemInfo" does not report "ppid" on Python 2.7 on Windows
`os.getppid()` was not available on Windows until Python 3.2. Pydevd catches any exceptions from that call, and omits the property from JSON, so this doesn't crash the debugger. But because the adapter doesn't receive "ppid" for the incoming server connection, it doesn't know which of the existing debug sessions is the parent, and doesn't send the subprocess notification to any of them. This effectively breaks multiproc debugging on 2.7.




Discussion:
Pydevd might have to propagate the parent PID via command line when spawning the child process to ensure that it's always available. Or perhaps there's a way to re-implement `os.getppid()` in 2.7 in a way similar to how it's done in 3.2.

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py
@@ -24,8 +24,7 @@
 from pydevd_tracing import get_exception_traceback_str
 import os
 import subprocess
-import time
-from _pydev_bundle.pydev_is_thread_alive import is_thread_alive
+import ctypes
 
 try:
     import dis
@@ -808,32 +807,42 @@ def set_source_mapping(self, py_db, source_filename, mapping):
         self.reapply_breakpoints(py_db)
         return ''
 
+    def get_ppid(self):
+        '''
+        Provides the parent pid (even for older versions of Python on Windows).
+        '''
+        ppid = None
+
+        try:
+            ppid = os.getppid()
+        except AttributeError:
+            pass
+
+        if ppid is None and IS_WINDOWS:
+            ppid = self._get_windows_ppid()
+
+        return ppid
+
+    def _get_windows_ppid(self):
+        this_pid = os.getpid()
+        for ppid, pid in _list_ppid_and_pid():
+            if pid == this_pid:
+                return ppid
+
+        return None
+
     def _terminate_child_processes_windows(self, dont_terminate_child_pids):
         this_pid = os.getpid()
         for _ in range(50):  # Try this at most 50 times before giving up.
 
             # Note: we can't kill the process itself with taskkill, so, we
             # list immediate children, kill that tree and then exit this process.
-            list_popen = self._popen(
-                ['wmic', 'process', 'where', '(ParentProcessId=%s)' % (this_pid,), 'get', 'ProcessId'],
-                stdout=subprocess.PIPE,
-                stderr=subprocess.STDOUT
-            )
-            if list_popen is None:
-                break  # We couldn't create the process.
 
-            stdout, _ = list_popen.communicate()
             children_pids = []
-            for line in stdout.splitlines():
-                line = line.strip()
-                if line and line != b'ProcessId':
-                    try:
-                        pid = int(line)
-                    except ValueError:
-                        pass
-                    else:
-                        if pid != list_popen.pid and pid not in dont_terminate_child_pids:
-                            children_pids.append(pid)
+            for ppid, pid in _list_ppid_and_pid():
+                if ppid == this_pid:
+                    if pid not in dont_terminate_child_pids:
+                        children_pids.append(pid)
 
             if not children_pids:
                 break
@@ -943,3 +952,36 @@ def request_terminate_process(self, py_db):
         py_db.terminate_requested = True
         run_as_pydevd_daemon_thread(py_db, self._terminate_if_commands_processed, py_db)
 
+
+def _list_ppid_and_pid():
+    _TH32CS_SNAPPROCESS = 0x00000002
+
+    class PROCESSENTRY32(ctypes.Structure):
+        _fields_ = [("dwSize", ctypes.c_ulong),
+                    ("cntUsage", ctypes.c_ulong),
+                    ("th32ProcessID", ctypes.c_ulong),
+                    ("th32DefaultHeapID", ctypes.c_ulong),
+                    ("th32ModuleID", ctypes.c_ulong),
+                    ("cntThreads", ctypes.c_ulong),
+                    ("th32ParentProcessID", ctypes.c_ulong),
+                    ("pcPriClassBase", ctypes.c_ulong),
+                    ("dwFlags", ctypes.c_ulong),
+                    ("szExeFile", ctypes.c_char * 260)]
+
+    kernel32 = ctypes.windll.kernel32
+    snapshot = kernel32.CreateToolhelp32Snapshot(_TH32CS_SNAPPROCESS, 0)
+    ppid_and_pids = []
+    try:
+        process_entry = PROCESSENTRY32()
+        process_entry.dwSize = ctypes.sizeof(PROCESSENTRY32)
+        if not kernel32.Process32First(snapshot, ctypes.byref(process_entry)):
+            pydev_log.critical('Process32First failed (getting process from CreateToolhelp32Snapshot).')
+        else:
+            while True:
+                ppid_and_pids.append((process_entry.th32ParentProcessID, process_entry.th32ProcessID))
+                if not kernel32.Process32Next(snapshot, ctypes.byref(process_entry)):
+                    break
+    finally:
+        kernel32.CloseHandle(snapshot)
+        
+    return ppid_and_pids
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
@@ -969,10 +969,7 @@ def on_pydevdsysteminfo_request(self, py_db, request):
         except AttributeError:
             pid = None
 
-        try:
-            ppid = os.getppid()
-        except AttributeError:
-            ppid = None
+        ppid = self.api.get_ppid()
 
         try:
             impl_desc = platform.python_implementation()


Test patch:
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
@@ -4,8 +4,9 @@
 from _pydevd_bundle.pydevd_utils import convert_dap_log_message_to_expression
 from tests_python.debug_constants import IS_PY26, IS_PY3K
 import sys
-from _pydevd_bundle.pydevd_constants import IS_CPYTHON
+from _pydevd_bundle.pydevd_constants import IS_CPYTHON, IS_WINDOWS
 import pytest
+import os
 
 
 def test_is_main_thread():
@@ -305,7 +306,6 @@ def test_find_main_thread_id():
     _check_in_separate_process('check_fix_main_thread_id_multiple_threads', '_pydevd_test_find_main_thread_id')
 
     import subprocess
-    import os
     import pydevd
     cwd, environ = _build_launch_env()
 
@@ -322,3 +322,17 @@ def test_find_main_thread_id():
         env=environ,
         cwd=cwd
     )
+
+
+@pytest.mark.skipif(not IS_WINDOWS, reason='Windows-only test.')
+def test_get_ppid():
+    from _pydevd_bundle.pydevd_api import PyDevdAPI
+    api = PyDevdAPI()
+    if IS_PY3K:
+        # On python 3 we can check that our internal api which is used for Python 2 gives the
+        # same result as os.getppid.
+        ppid = os.getppid()
+        assert api._get_windows_ppid() == ppid
+    else:
+        assert api._get_windows_ppid() is not None
+