File size: 5,757 Bytes
7885a28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import os
import sys
import time
import errno
import signal
import warnings
import subprocess
import traceback

try:
    import psutil
except ImportError:
    psutil = None


def kill_process_tree(process, use_psutil=True):
    """Terminate process and its descendants with SIGKILL"""
    if use_psutil and psutil is not None:
        _kill_process_tree_with_psutil(process)
    else:
        _kill_process_tree_without_psutil(process)


def recursive_terminate(process, use_psutil=True):
    warnings.warn(
        "recursive_terminate is deprecated in loky 3.2, use kill_process_tree"
        "instead",
        DeprecationWarning,
    )
    kill_process_tree(process, use_psutil=use_psutil)


def _kill_process_tree_with_psutil(process):
    try:
        descendants = psutil.Process(process.pid).children(recursive=True)
    except psutil.NoSuchProcess:
        return

    # Kill the descendants in reverse order to avoid killing the parents before
    # the descendant in cases where there are more processes nested.
    for descendant in descendants[::-1]:
        try:
            descendant.kill()
        except psutil.NoSuchProcess:
            pass

    try:
        psutil.Process(process.pid).kill()
    except psutil.NoSuchProcess:
        pass
    process.join()


def _kill_process_tree_without_psutil(process):
    """Terminate a process and its descendants."""
    try:
        if sys.platform == "win32":
            _windows_taskkill_process_tree(process.pid)
        else:
            _posix_recursive_kill(process.pid)
    except Exception:  # pragma: no cover
        details = traceback.format_exc()
        warnings.warn(
            "Failed to kill subprocesses on this platform. Please install"
            "psutil: https://github.com/giampaolo/psutil\n"
            f"Details:\n{details}"
        )
        # In case we cannot introspect or kill the descendants, we fall back to
        # only killing the main process.
        #
        # Note: on Windows, process.kill() is an alias for process.terminate()
        # which in turns calls the Win32 API function TerminateProcess().
        process.kill()
    process.join()


def _windows_taskkill_process_tree(pid):
    # On windows, the taskkill function with option `/T` terminate a given
    # process pid and its children.
    try:
        subprocess.check_output(
            ["taskkill", "/F", "/T", "/PID", str(pid)], stderr=None
        )
    except subprocess.CalledProcessError as e:
        # In Windows, taskkill returns 128, 255 for no process found.
        if e.returncode not in [128, 255]:
            # Let's raise to let the caller log the error details in a
            # warning and only kill the root process.
            raise  # pragma: no cover


def _kill(pid):
    # Not all systems (e.g. Windows) have a SIGKILL, but the C specification
    # mandates a SIGTERM signal. While Windows is handled specifically above,
    # let's try to be safe for other hypothetic platforms that only have
    # SIGTERM without SIGKILL.
    kill_signal = getattr(signal, "SIGKILL", signal.SIGTERM)
    try:
        os.kill(pid, kill_signal)
    except OSError as e:
        # if OSError is raised with [Errno 3] no such process, the process
        # is already terminated, else, raise the error and let the top
        # level function raise a warning and retry to kill the process.
        if e.errno != errno.ESRCH:
            raise  # pragma: no cover


def _posix_recursive_kill(pid):
    """Recursively kill the descendants of a process before killing it."""
    try:
        children_pids = subprocess.check_output(
            ["pgrep", "-P", str(pid)], stderr=None, text=True
        )
    except subprocess.CalledProcessError as e:
        # `ps` returns 1 when no child process has been found
        if e.returncode == 1:
            children_pids = ""
        else:
            raise  # pragma: no cover

    # Decode the result, split the cpid and remove the trailing line
    for cpid in children_pids.splitlines():
        cpid = int(cpid)
        _posix_recursive_kill(cpid)

    _kill(pid)


def get_exitcodes_terminated_worker(processes):
    """Return a formatted string with the exitcodes of terminated workers.

    If necessary, wait (up to .25s) for the system to correctly set the
    exitcode of one terminated worker.
    """
    patience = 5

    # Catch the exitcode of the terminated workers. There should at least be
    # one. If not, wait a bit for the system to correctly set the exitcode of
    # the terminated worker.
    exitcodes = [
        p.exitcode for p in list(processes.values()) if p.exitcode is not None
    ]
    while not exitcodes and patience > 0:
        patience -= 1
        exitcodes = [
            p.exitcode
            for p in list(processes.values())
            if p.exitcode is not None
        ]
        time.sleep(0.05)

    return _format_exitcodes(exitcodes)


def _format_exitcodes(exitcodes):
    """Format a list of exit code with names of the signals if possible"""
    str_exitcodes = [
        f"{_get_exitcode_name(e)}({e})" for e in exitcodes if e is not None
    ]
    return "{" + ", ".join(str_exitcodes) + "}"


def _get_exitcode_name(exitcode):
    if sys.platform == "win32":
        # The exitcode are unreliable  on windows (see bpo-31863).
        # For this case, return UNKNOWN
        return "UNKNOWN"

    if exitcode < 0:
        try:
            import signal

            return signal.Signals(-exitcode).name
        except ValueError:
            return "UNKNOWN"
    elif exitcode != 255:
        # The exitcode are unreliable on forkserver were 255 is always returned
        # (see bpo-30589). For this case, return UNKNOWN
        return "EXIT"

    return "UNKNOWN"