Provide api to skip terminating child processes on terminate. Fixes #1786

This commit is contained in:
Fabio Zadrozny 2019-09-23 09:47:48 -03:00
parent 3908192ce7
commit 5fcc3e4cd5
4 changed files with 71 additions and 15 deletions

View file

@ -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)

View file

@ -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

View file

@ -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())

View file

@ -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)