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)], stderr=None
83 )
84 except subprocess.CalledProcessError as e:
85 # In Windows, taskkill returns 128, 255 for no process found.
86 if e.returncode not in [128, 255]:
87 # Let's raise to let the caller log the error details in a
88 # warning and only kill the root process.
89 raise # pragma: no cover
90
91
92def _kill(pid):
93 # Not all systems (e.g. Windows) have a SIGKILL, but the C specification
94 # mandates a SIGTERM signal. While Windows is handled specifically above,
95 # let's try to be safe for other hypothetic platforms that only have
96 # SIGTERM without SIGKILL.
97 kill_signal = getattr(signal, "SIGKILL", signal.SIGTERM)
98 try:
99 os.kill(pid, kill_signal)
100 except OSError as e:
101 # if OSError is raised with [Errno 3] no such process, the process
102 # is already terminated, else, raise the error and let the top
103 # level function raise a warning and retry to kill the process.
104 if e.errno != errno.ESRCH:
105 raise # pragma: no cover
106
107
108def _posix_recursive_kill(pid):
109 """Recursively kill the descendants of a process before killing it."""
110 try:
111 children_pids = subprocess.check_output(
112 ["pgrep", "-P", str(pid)], stderr=None, text=True
113 )
114 except subprocess.CalledProcessError as e:
115 # `ps` returns 1 when no child process has been found
116 if e.returncode == 1:
117 children_pids = ""
118 else:
119 raise # pragma: no cover
120
121 # Decode the result, split the cpid and remove the trailing line
122 for cpid in children_pids.splitlines():
123 cpid = int(cpid)
124 _posix_recursive_kill(cpid)
125
126 _kill(pid)
127
128
129def get_exitcodes_terminated_worker(processes):
130 """Return a formatted string with the exitcodes of terminated workers.
131
132 If necessary, wait (up to .25s) for the system to correctly set the
133 exitcode of one terminated worker.
134 """
135 patience = 5
136
137 # Catch the exitcode of the terminated workers. There should at least be
138 # one. If not, wait a bit for the system to correctly set the exitcode of
139 # the terminated worker.
140 exitcodes = [
141 p.exitcode for p in list(processes.values()) if p.exitcode is not None
142 ]
143 while not exitcodes and patience > 0:
144 patience -= 1
145 exitcodes = [
146 p.exitcode
147 for p in list(processes.values())
148 if p.exitcode is not None
149 ]
150 time.sleep(0.05)
151
152 return _format_exitcodes(exitcodes)
153
154
155def _format_exitcodes(exitcodes):
156 """Format a list of exit code with names of the signals if possible"""
157 str_exitcodes = [
158 f"{_get_exitcode_name(e)}({e})" for e in exitcodes if e is not None
159 ]
160 return "{" + ", ".join(str_exitcodes) + "}"
161
162
163def _get_exitcode_name(exitcode):
164 if sys.platform == "win32":
165 # The exitcode are unreliable on windows (see bpo-31863).
166 # For this case, return UNKNOWN
167 return "UNKNOWN"
168
169 if exitcode < 0:
170 try:
171 import signal
172
173 return signal.Signals(-exitcode).name
174 except ValueError:
175 return "UNKNOWN"
176 elif exitcode != 255:
177 # The exitcode are unreliable on forkserver were 255 is always returned
178 # (see bpo-30589). For this case, return UNKNOWN
179 return "EXIT"
180
181 return "UNKNOWN"