Repository: microsoft/ptvsd

Issue:
pydevd API to ignore process by PID on terminate
This is needed to prevent `on_terminate_request` from killing the adapter process that is start by `enable_attach`


Discussion:

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
@@ -806,7 +806,7 @@ def set_source_mapping(self, py_db, source_filename, mapping):
         self.reapply_breakpoints(py_db)
         return ''
 
-    def _terminate_child_processes_windows(self):
+    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.
 
@@ -830,7 +830,7 @@ def _terminate_child_processes_windows(self):
                     except ValueError:
                         pass
                     else:
-                        if pid != list_popen.pid:
+                        if pid != list_popen.pid and pid not in dont_terminate_child_pids:
                             children_pids.append(pid)
 
             if not children_pids:
@@ -845,7 +845,7 @@ def _terminate_child_processes_windows(self):
 
                 del children_pids[:]
 
-    def _terminate_child_processes_linux_and_mac(self):
+    def _terminate_child_processes_linux_and_mac(self, dont_terminate_child_pids):
         this_pid = os.getpid()
 
         def list_children_and_stop_forking(initial_pid, stop=True):
@@ -870,6 +870,8 @@ def list_children_and_stop_forking(initial_pid, stop=True):
                     line = line.decode('ascii').strip()
                     if line:
                         pid = str(line)
+                        if pid in dont_terminate_child_pids:
+                            continue
                         children_pids.append(pid)
                         # Recursively get children.
                         children_pids.extend(list_children_and_stop_forking(pid))
@@ -921,9 +923,9 @@ def terminate_process(self, py_db):
         try:
             if py_db.terminate_child_processes:
                 if IS_WINDOWS:
-                    self._terminate_child_processes_windows()
+                    self._terminate_child_processes_windows(py_db.dont_terminate_child_pids)
                 else:
-                    self._terminate_child_processes_linux_and_mac()
+                    self._terminate_child_processes_linux_and_mac(py_db.dont_terminate_child_pids)
         finally:
             os._exit(0)
 
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
@@ -450,6 +450,9 @@ def __init__(self, set_as_global=True):
         # Determines whether we should terminate child processes when asked to terminate.
         self.terminate_child_processes = True
 
+        # List of direct child pids which should not be terminated when terminating processes.
+        self.dont_terminate_child_pids = set()
+
         # These are the breakpoints received by the PyDevdAPI. They are meant to store
         # the breakpoints in the api -- its actual contents are managed by the api.
         self.api_received_breakpoints = {}
@@ -2648,14 +2651,38 @@ def settrace_forked(setup_tracing=True):
 
 @contextmanager
 def skip_subprocess_arg_patch():
+    '''
+    May be used to skip the monkey-patching that pydevd does to
+    skip changing arguments to embed the debugger into child processes.
+
+    i.e.:
+
+    with pydevd.skip_subprocess_arg_patch():
+        subprocess.call(...)
+    '''
     from _pydev_bundle import pydev_monkey
     with pydev_monkey.skip_subprocess_arg_patch():
         yield
 
 
-#=======================================================================================================================
-# SetupHolder
-#=======================================================================================================================
+def add_dont_terminate_child_pid(pid):
+    '''
+    May be used to ask pydevd to skip the termination of some process
+    when it's asked to terminate (debug adapter protocol only).
+
+    :param int pid:
+        The pid to be ignored.
+
+    i.e.:
+
+    process = subprocess.Popen(...)
+    pydevd.add_dont_terminate_child_pid(process.pid)
+    '''
+    py_db = GetGlobalDebugger()
+    if py_db is not None:
+        py_db.dont_terminate_child_pids.add(pid)
+
+
 class SetupHolder:
 
     setup = None


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_terminate.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_terminate.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_terminate.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_terminate.py
@@ -8,9 +8,12 @@
         n = int(sys.argv[-1])
         if n != 0:
             subprocess.Popen([sys.executable, __file__, 'launch-subprocesses', str(n - 1)])
-        print('%screated %s (child of %s)' % ('\t' * (4 - n), os.getpid(), os.getppid()))
+        if hasattr(os, 'getppid'):
+            print('%screated %s (child of %s)' % ('\t' * (4 - n), os.getpid(), os.getppid()))
+        else:
+            print('%screated %s' % ('\t' * (4 - n), os.getpid()))
 
-    elif 'check-subprocesses' in sys.argv:
+    elif 'check-subprocesses' in sys.argv or 'check-subprocesses-ignore-pid' in sys.argv:
         # Recursively create a process tree such as:
         # - parent (this process)
         #    - p3
@@ -21,8 +24,12 @@
         #      - p2
         #        - p1
         #          - p0
-        subprocess.Popen([sys.executable, __file__, 'launch-subprocesses', '3'])
-        subprocess.Popen([sys.executable, __file__, 'launch-subprocesses', '3'])
+        p0 = subprocess.Popen([sys.executable, __file__, 'launch-subprocesses', '3'])
+        p1 = subprocess.Popen([sys.executable, __file__, 'launch-subprocesses', '3'])
+
+        if 'check-subprocesses-ignore-pid' in sys.argv:
+            import pydevd
+            pydevd.add_dont_terminate_child_pid(p0.pid)
 
         print('created', os.getpid())
 
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
@@ -2936,7 +2936,8 @@ def test_pydevd_systeminfo(case_setup):
 @pytest.mark.parametrize('check_subprocesses', [
     'no_subprocesses',
     'kill_subprocesses',
-    'dont_kill_subprocesses'
+    'kill_subprocesses_ignore_pid',
+    'dont_kill_subprocesses',
 ])
 def test_terminate(case_setup, scenario, check_subprocesses):
     import psutil
@@ -2948,12 +2949,15 @@ def update_command_line_args(writer, args):
         ret = debugger_unittest.AbstractWriterThread.update_command_line_args(writer, args)
         if check_subprocesses in ('kill_subprocesses', 'dont_kill_subprocesses'):
             ret.append('check-subprocesses')
+        if check_subprocesses in ('kill_subprocesses_ignore_pid',):
+            ret.append('check-subprocesses-ignore-pid')
         return ret
 
     with case_setup.test_file(
         '_debugger_case_terminate.py',
         check_test_suceeded_msg=check_test_suceeded_msg,
         update_command_line_args=update_command_line_args,
+        EXPECTED_RETURNCODE='any' if check_subprocesses == 'kill_subprocesses_ignore_pid' else 0,
         ) as writer:
         json_facade = JsonFacade(writer)
         if check_subprocesses == 'dont_kill_subprocesses':
@@ -2963,7 +2967,7 @@ def update_command_line_args(writer, args):
         response = json_facade.write_initialize(None)
         pid = response.to_dict()['body']['pydevd']['processId']
 
-        if check_subprocesses in ('kill_subprocesses', 'dont_kill_subprocesses'):
+        if check_subprocesses in ('kill_subprocesses', 'dont_kill_subprocesses', 'kill_subprocesses_ignore_pid'):
             process_ids_to_check = [pid]
             p = psutil.Process(pid)
 
@@ -2984,7 +2988,7 @@ def wait_for_child_processes():
         else:
             raise AssertionError('Unexpected: %s' % (scenario,))
 
-        if check_subprocesses in ('kill_subprocesses', 'dont_kill_subprocesses'):
+        if check_subprocesses in ('kill_subprocesses', 'dont_kill_subprocesses', 'kill_subprocesses_ignore_pid'):
 
             def is_pid_alive(pid):
                 # Note: the process may be a zombie process in Linux
@@ -3012,6 +3016,22 @@ def all_pids_exited():
 
                 wait_for_condition(all_pids_exited)
 
+            elif check_subprocesses == 'kill_subprocesses_ignore_pid':
+
+                def all_pids_exited():
+                    live_pids = get_live_pids()
+                    if len(live_pids) == 1:
+                        return False
+
+                    return True
+
+                wait_for_condition(all_pids_exited)
+
+                # Now, let's kill the remaining process ourselves.
+                for pid in get_live_pids():
+                    proc = psutil.Process(pid)
+                    proc.kill()
+
             else:  # 'dont_kill_subprocesses'
                 time.sleep(1)