diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 271b922d..d697046b 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -806,7 +806,7 @@ class PyDevdAPI(object): 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 @@ class PyDevdAPI(object): 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 @@ class PyDevdAPI(object): 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 @@ class PyDevdAPI(object): 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 @@ class PyDevdAPI(object): 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 index c03a4837..3c6b63b0 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd.py +++ b/src/ptvsd/_vendored/pydevd/pydevd.py @@ -450,6 +450,9 @@ class PyDB(object): # 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 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 index 4b9ccdac..d241d9e7 100644 --- 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 @@ if __name__ == '__main__': 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 @@ if __name__ == '__main__': # - 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 index 1ce11263..ad6dc263 100644 --- 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 test_terminate(case_setup, scenario, check_subprocesses): 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 test_terminate(case_setup, scenario, check_subprocesses): 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 test_terminate(case_setup, scenario, check_subprocesses): 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 test_terminate(case_setup, scenario, check_subprocesses): 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)