Skip to content

Commit 78c83ad

Browse files
authored
Merge pull request #869 from Carreau/kill-sp
2 parents 8d87867 + aaad575 commit 78c83ad

File tree

2 files changed

+107
-25
lines changed

2 files changed

+107
-25
lines changed

ipykernel/kernelbase.py

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,49 @@
55

66
import asyncio
77
import concurrent.futures
8-
from datetime import datetime
9-
from functools import partial
8+
import inspect
109
import itertools
1110
import logging
12-
import inspect
1311
import os
14-
from signal import signal, default_int_handler, SIGINT
15-
import sys
1612
import socket
13+
import sys
1714
import time
1815
import uuid
1916
import warnings
17+
from datetime import datetime
18+
from functools import partial
19+
from signal import SIGINT, SIGTERM, Signals, default_int_handler, signal
20+
21+
if sys.platform != "win32":
22+
from signal import SIGKILL
23+
else:
24+
SIGKILL = "windown-SIGKILL-sentinel"
25+
26+
2027
try:
2128
import psutil
2229
except ImportError:
2330
psutil = None
2431

32+
2533
try:
2634
# jupyter_client >= 5, use tz-aware now
2735
from jupyter_client.session import utcnow as now
2836
except ImportError:
2937
# jupyter_client < 5, use local now()
3038
now = datetime.now
3139

40+
import zmq
41+
from IPython.core.error import StdinNotImplementedError
42+
from jupyter_client.session import Session
3243
from tornado import ioloop
3344
from tornado.queues import Queue, QueueEmpty
34-
import zmq
45+
from traitlets import (Any, Bool, Dict, Float, Instance, Integer, List, Set,
46+
Unicode, default, observe)
47+
from traitlets.config.configurable import SingletonConfigurable
3548
from zmq.eventloop.zmqstream import ZMQStream
3649

37-
from traitlets.config.configurable import SingletonConfigurable
38-
from IPython.core.error import StdinNotImplementedError
3950
from ipykernel.jsonutil import json_clean
40-
from traitlets import (
41-
Any, Instance, Float, Dict, List, Set, Integer, Unicode, Bool,
42-
observe, default
43-
)
44-
45-
from jupyter_client.session import Session
4651

4752
from ._version import kernel_protocol_version
4853

@@ -796,14 +801,12 @@ async def comm_info_request(self, stream, ident, parent):
796801
reply_content, parent, ident)
797802
self.log.debug("%s", msg)
798803

799-
async def interrupt_request(self, stream, ident, parent):
800-
pid = os.getpid()
801-
pgid = os.getpgid(pid)
802-
804+
def _send_interupt_children(self):
803805
if os.name == "nt":
804806
self.log.error("Interrupt message not supported on Windows")
805-
806807
else:
808+
pid = os.getpid()
809+
pgid = os.getpgid(pid)
807810
# Prefer process-group over process
808811
if pgid and hasattr(os, "killpg"):
809812
try:
@@ -816,6 +819,8 @@ async def interrupt_request(self, stream, ident, parent):
816819
except OSError:
817820
pass
818821

822+
async def interrupt_request(self, stream, ident, parent):
823+
self._send_interupt_children()
819824
content = parent['content']
820825
self.session.send(stream, 'interrupt_reply', content, parent, ident=ident)
821826
return
@@ -830,7 +835,7 @@ async def shutdown_request(self, stream, ident, parent):
830835
content, parent
831836
)
832837

833-
self._at_shutdown()
838+
await self._at_shutdown()
834839

835840
self.log.debug('Stopping control ioloop')
836841
control_io_loop = self.control_stream.io_loop
@@ -1131,10 +1136,86 @@ def _input_request(self, prompt, ident, parent, password=False):
11311136
raise EOFError
11321137
return value
11331138

1134-
def _at_shutdown(self):
1139+
def _killpg(self, signal):
1140+
"""
1141+
similar to killpg but use psutil if it can on windows
1142+
or if pgid is none
1143+
1144+
"""
1145+
pgid = os.getpgid(os.getpid())
1146+
if pgid and hasattr(os, "killpg"):
1147+
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
1162+
1163+
async def _progressively_terminate_all_children(self):
1164+
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)
1176+
1177+
sleeps = (0.01, 0.03, 0.1, 0.3, 1, 3, 10)
1178+
children = psutil.Process().children(recursive=True)
1179+
if not children:
1180+
self.log.debug("Kernel has no children.")
1181+
return
1182+
self.log.debug(f"Trying to interrupt then kill subprocesses : {children}")
1183+
self._send_interupt_children()
1184+
1185+
for signum in (SIGTERM, SIGKILL):
1186+
self.log.debug(
1187+
f"Will try to send {signum} ({Signals(signum)!r}) to subprocesses :{children}"
1188+
)
1189+
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)
1200+
self.log.debug(
1201+
f"Will sleep {delay}s before checking for children and retrying. {children}"
1202+
)
1203+
await asyncio.sleep(delay)
1204+
1205+
async def _at_shutdown(self):
11351206
"""Actions taken at shutdown by the kernel, called by python's atexit.
11361207
"""
1137-
if self._shutdown_message is not None:
1138-
self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
1139-
self.log.debug("%s", self._shutdown_message)
1140-
self.control_stream.flush(zmq.POLLOUT)
1208+
try:
1209+
await self._progressively_terminate_all_children()
1210+
except Exception as e:
1211+
self.log.exception("Exception during subprocesses termination %s", e)
1212+
1213+
finally:
1214+
if self._shutdown_message is not None:
1215+
self.session.send(
1216+
self.iopub_socket,
1217+
self._shutdown_message,
1218+
ident=self._topic("shutdown"),
1219+
)
1220+
self.log.debug("%s", self._shutdown_message)
1221+
self.control_stream.flush(zmq.POLLOUT)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def run(self):
6868
'tornado>=4.2,<7.0',
6969
'matplotlib-inline>=0.1.0,<0.2.0',
7070
'appnope;platform_system=="Darwin"',
71+
'psutil;platform_system=="Windows"',
7172
'nest_asyncio',
7273
],
7374
extras_require={

0 commit comments

Comments
 (0)