55
66import asyncio
77import concurrent .futures
8- from datetime import datetime
9- from functools import partial
8+ import inspect
109import itertools
1110import logging
12- import inspect
1311import os
14- from signal import signal , default_int_handler , SIGINT
15- import sys
1612import socket
13+ import sys
1714import time
1815import uuid
1916import 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+
2027try :
2128 import psutil
2229except ImportError :
2330 psutil = None
2431
32+
2533try :
2634 # jupyter_client >= 5, use tz-aware now
2735 from jupyter_client .session import utcnow as now
2836except 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
3243from tornado import ioloop
3344from 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
3548from zmq .eventloop .zmqstream import ZMQStream
3649
37- from traitlets .config .configurable import SingletonConfigurable
38- from IPython .core .error import StdinNotImplementedError
3950from 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
4752from ._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 )
0 commit comments