|
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