|
24 | 24 | SIGKILL = "windown-SIGKILL-sentinel" |
25 | 25 |
|
26 | 26 |
|
27 | | -try: |
28 | | - import psutil |
29 | | -except ImportError: |
30 | | - psutil = None |
31 | 27 |
|
32 | 28 |
|
33 | 29 | try: |
|
37 | 33 | # jupyter_client < 5, use local now() |
38 | 34 | now = datetime.now |
39 | 35 |
|
| 36 | +import psutil |
40 | 37 | import zmq |
41 | 38 | from IPython.core.error import StdinNotImplementedError |
42 | 39 | from jupyter_client.session import Session |
@@ -808,7 +805,8 @@ def _send_interupt_children(self): |
808 | 805 | pid = os.getpid() |
809 | 806 | pgid = os.getpgid(pid) |
810 | 807 | # Prefer process-group over process |
811 | | - if pgid and hasattr(os, "killpg"): |
| 808 | + # but only if the kernel is the leader of the process group |
| 809 | + if pgid and pgid == pid and hasattr(os, "killpg"): |
812 | 810 | try: |
813 | 811 | os.killpg(pgid, SIGINT) |
814 | 812 | return |
@@ -897,8 +895,6 @@ async def usage_request(self, stream, ident, parent): |
897 | 895 | reply_content = { |
898 | 896 | 'hostname': socket.gethostname() |
899 | 897 | } |
900 | | - if psutil is None: |
901 | | - return reply_content |
902 | 898 | current_process = psutil.Process() |
903 | 899 | all_processes = [current_process] + current_process.children(recursive=True) |
904 | 900 | process_metric_value = self.get_process_metric_value |
@@ -1136,67 +1132,62 @@ def _input_request(self, prompt, ident, parent, password=False): |
1136 | 1132 | raise EOFError |
1137 | 1133 | return value |
1138 | 1134 |
|
1139 | | - def _killpg(self, signal): |
| 1135 | + def _signal_children(self, signum): |
1140 | 1136 | """ |
1141 | | - similar to killpg but use psutil if it can on windows |
1142 | | - or if pgid is none |
| 1137 | + Send a signal to all our children |
1143 | 1138 |
|
| 1139 | + Like `killpg`, but does not include the current process |
| 1140 | + (or possible parents). |
1144 | 1141 | """ |
1145 | | - pgid = os.getpgid(os.getpid()) |
1146 | | - if pgid and hasattr(os, "killpg"): |
| 1142 | + for p in self._process_children(): |
| 1143 | + self.log.debug(f"Sending {Signals(signum)!r} to subprocess {p}") |
1147 | 1144 | try: |
1148 | | - os.killpg(pgid, signal) |
1149 | | - except (OSError) as e: |
1150 | | - self.log.exception(f"OSError running killpg, not killing children.") |
1151 | | - return |
1152 | | - elif psutil is not None: |
1153 | | - children = parent.children(recursive=True) |
1154 | | - for p in children: |
1155 | | - try: |
1156 | | - if signal == SIGTERM: |
1157 | | - p.terminate() |
1158 | | - elif signal == SIGKILL: |
1159 | | - p.kill() |
1160 | | - except psutil.NoSuchProcess: |
1161 | | - pass |
| 1145 | + if signum == SIGTERM: |
| 1146 | + p.terminate() |
| 1147 | + elif signum == SIGKILL: |
| 1148 | + p.kill() |
| 1149 | + else: |
| 1150 | + p.send_signal(signum) |
| 1151 | + except psutil.NoSuchProcess: |
| 1152 | + pass |
1162 | 1153 |
|
1163 | | - async def _progressively_terminate_all_children(self): |
| 1154 | + def _process_children(self): |
| 1155 | + """Retrieve child processes in the kernel's process group |
1164 | 1156 |
|
1165 | | - pgid = os.getpgid(os.getpid()) |
1166 | | - if psutil is None: |
1167 | | - # blindly send quickly sigterm/sigkill to processes if psutil not there. |
1168 | | - self.log.info("Please install psutil for a cleaner subprocess shutdown.") |
1169 | | - self._send_interupt_children() |
1170 | | - await asyncio.sleep(0.05) |
1171 | | - self.log.debug("Sending SIGTERM to {pgid}") |
1172 | | - self._killpg(SIGTERM) |
1173 | | - await asyncio.sleep(0.05) |
1174 | | - self.log.debug("Sending SIGKILL to {pgid}") |
1175 | | - self._killpg(pgid, SIGKILL) |
| 1157 | + Avoids: |
| 1158 | + - including parents and self with killpg |
| 1159 | + - including all children that may have forked-off a new group |
| 1160 | + """ |
| 1161 | + kernel_process = psutil.Process() |
| 1162 | + all_children = kernel_process.children(recursive=True) |
| 1163 | + if os.name == "nt": |
| 1164 | + return all_children |
| 1165 | + kernel_pgid = os.getpgrp() |
| 1166 | + process_group_children = [] |
| 1167 | + for child in all_children: |
| 1168 | + try: |
| 1169 | + child_pgid = os.getpgid(child.pid) |
| 1170 | + except OSError: |
| 1171 | + pass |
| 1172 | + else: |
| 1173 | + if child_pgid == kernel_pgid: |
| 1174 | + process_group_children.append(child) |
| 1175 | + return process_group_children |
1176 | 1176 |
|
| 1177 | + async def _progressively_terminate_all_children(self): |
1177 | 1178 | sleeps = (0.01, 0.03, 0.1, 0.3, 1, 3, 10) |
1178 | | - children = psutil.Process().children(recursive=True) |
1179 | | - if not children: |
| 1179 | + if not self._process_children(): |
1180 | 1180 | self.log.debug("Kernel has no children.") |
1181 | 1181 | return |
1182 | | - self.log.debug(f"Trying to interrupt then kill subprocesses : {children}") |
1183 | | - self._send_interupt_children() |
1184 | 1182 |
|
1185 | 1183 | for signum in (SIGTERM, SIGKILL): |
1186 | | - self.log.debug( |
1187 | | - f"Will try to send {signum} ({Signals(signum)!r}) to subprocesses :{children}" |
1188 | | - ) |
1189 | 1184 | for delay in sleeps: |
1190 | | - children = psutil.Process().children(recursive=True) |
1191 | | - try: |
1192 | | - if not children: |
1193 | | - self.log.warning( |
1194 | | - "No more children, continuing shutdown routine." |
1195 | | - ) |
1196 | | - return |
1197 | | - except psutil.NoSuchProcess: |
1198 | | - pass |
1199 | | - self._killpg(15) |
| 1185 | + children = self._process_children() |
| 1186 | + if not children: |
| 1187 | + self.log.debug("No more children, continuing shutdown routine.") |
| 1188 | + return |
| 1189 | + # signals only children, not current process |
| 1190 | + self._signal_children(signum) |
1200 | 1191 | self.log.debug( |
1201 | 1192 | f"Will sleep {delay}s before checking for children and retrying. {children}" |
1202 | 1193 | ) |
|
0 commit comments