1import os
2import sys
3import time
4import errno
5import signal
6import warnings
7import subprocess
8import traceback
9
10try:
11 import psutil
12except ImportError:
13 psutil = None
14
15
16def kill_process_tree(process, use_psutil=True):
17 """Terminate process and its descendants with SIGKILL"""
18 if use_psutil and psutil is not None:
19 _kill_process_tree_with_psutil(process)
20 else:
21 _kill_process_tree_without_psutil(process)
22
23
24def recursive_terminate(process, use_psutil=True):
25 warnings.warn(
26 "recursive_terminate is deprecated in loky 3.2, use kill_process_tree"
27 "instead",
28 DeprecationWarning,
29 )
30 kill_process_tree(process, use_psutil=use_psutil)
31
32
33def _kill_process_tree_with_psutil(process):
34 try:
35 descendants = psutil.Process(process.pid).children(recursive=True)
36 except psutil.NoSuchProcess:
37 return
38
39 # Kill the descendants in reverse order to avoid killing the parents before
40 # the descendant in cases where there are more processes nested.
41 for descendant in descendants[::-1]:
42 try:
43 descendant.kill()
44 except psutil.NoSuchProcess:
45 pass
46
47 try:
48 psutil.Process(process.pid).kill()
49 except psutil.NoSuchProcess:
50 pass
51 process.join()
52
53
54def _kill_process_tree_without_psutil(process):
55 """Terminate a process and its descendants."""
56 try:
57 if sys.platform == "win32":
58 _windows_taskkill_process_tree(process.pid)
59 else:
60 _posix_recursive_kill(process.pid)
61 except Exception: # pragma: no cover
62 details = traceback.format_exc()
63 warnings.warn(
64 "Failed to kill subprocesses on this platform. Please install"
65 "psutil: https://github.com/giampaolo/psutil\n"
66 f"Details:\n{details}"
67 )
68 # In case we cannot introspect or kill the descendants, we fall back to
69 # only killing the main process.
70 #
71 # Note: on Windows, process.kill() is an alias for process.terminate()
72 # which in turns calls the Win32 API function TerminateProcess().
73 process.kill()
74 process.join()
75
76
77def _windows_taskkill_process_tree(pid):
78 # On windows, the taskkill function with option `/T` terminate a given
79 # process pid and its children.
80 try:
81 subprocess.check_output(
82 ["taskkill", "/F", "/T", "/PID", str(pid)],
83 stderr=None,
84 creationflags=subprocess.CREATE_NO_WINDOW,
85 )
86 except subprocess.CalledProcessError as e:
87 # In Windows, taskkill returns 128, 255 for no process found.
88 if e.returncode not in [128, 255]:
89 # Let's raise to let the caller log the error details in a
90 # warning and only kill the root process.
91 raise # pragma: no cover
92
93
94def _kill(pid):
95 # Not all systems (e.g. Windows) have a SIGKILL, but the C specification
96 # mandates a SIGTERM signal. While Windows is handled specifically above,
97 # let's try to be safe for other hypothetic platforms that only have
98 # SIGTERM without SIGKILL.
99 kill_signal = getattr(signal, "SIGKILL", signal.SIGTERM)
100 try:
101 os.kill(pid, kill_signal)
102 except OSError as e:
103 # if OSError is raised with [Errno 3] no such process, the process
104 # is already terminated, else, raise the error and let the top
105 # level function raise a warning and retry to kill the process.
106 if e.errno != errno.ESRCH:
107 raise # pragma: no cover
108
109
110def _posix_recursive_kill(pid):
111 """Recursively kill the descendants of a process before killing it."""
112 try:
113 children_pids = subprocess.check_output(
114 ["pgrep", "-P", str(pid)], stderr=None, text=True
115 )
116 except subprocess.CalledProcessError as e:
117 # `ps` returns 1 when no child process has been found
118 if e.returncode == 1:
119 children_pids = ""
120 else:
121 raise # pragma: no cover
122
123 # Decode the result, split the cpid and remove the trailing line
124 for cpid in children_pids.splitlines():
125 cpid = int(cpid)
126 _posix_recursive_kill(cpid)
127
128 _kill(pid)
129
130
131def get_exitcodes_terminated_worker(processes):
132 """Return a formatted string with the exitcodes of terminated workers.
133
134 If necessary, wait (up to .25s) for the system to correctly set the
135 exitcode of one terminated worker.
136 """
137 patience = 5
138
139 # Catch the exitcode of the terminated workers. There should at least be
140 # one. If not, wait a bit for the system to correctly set the exitcode of
141 # the terminated worker.
142 exitcodes = [
143 p.exitcode for p in list(processes.values()) if p.exitcode is not None
144 ]
145 while not exitcodes and patience > 0:
146 patience -= 1
147 exitcodes = [
148 p.exitcode
149 for p in list(processes.values())
150 if p.exitcode is not None
151 ]
152 time.sleep(0.05)
153
154 return _format_exitcodes(exitcodes)
155
156
157def _format_exitcodes(exitcodes):
158 """Format a list of exit code with names of the signals if possible"""
159 str_exitcodes = [
160 f"{_get_exitcode_name(e)}({e})" for e in exitcodes if e is not None
161 ]
162 return "{" + ", ".join(str_exitcodes) + "}"
163
164
165def _get_exitcode_name(exitcode):
166 if sys.platform == "win32":
167 # The exitcode are unreliable on windows (see bpo-31863).
168 # For this case, return UNKNOWN
169 return "UNKNOWN"
170
171 if exitcode < 0:
172 try:
173 import signal
174
175 return signal.Signals(-exitcode).name
176 except ValueError:
177 return "UNKNOWN"
178 elif exitcode != 255:
179 # The exitcode are unreliable on forkserver were 255 is always returned
180 # (see bpo-30589). For this case, return UNKNOWN
181 return "EXIT"
182
183 return "UNKNOWN"