5
5
6
6
import asyncio
7
7
import concurrent .futures
8
- from datetime import datetime
9
- from functools import partial
8
+ import inspect
10
9
import itertools
11
10
import logging
12
- import inspect
13
11
import os
14
- from signal import signal , default_int_handler , SIGINT
15
- import sys
16
12
import socket
13
+ import sys
17
14
import time
18
15
import uuid
19
16
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
+
20
27
try :
21
28
import psutil
22
29
except ImportError :
23
30
psutil = None
24
31
32
+
25
33
try :
26
34
# jupyter_client >= 5, use tz-aware now
27
35
from jupyter_client .session import utcnow as now
28
36
except ImportError :
29
37
# jupyter_client < 5, use local now()
30
38
now = datetime .now
31
39
40
+ import zmq
41
+ from IPython .core .error import StdinNotImplementedError
42
+ from jupyter_client .session import Session
32
43
from tornado import ioloop
33
44
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
35
48
from zmq .eventloop .zmqstream import ZMQStream
36
49
37
- from traitlets .config .configurable import SingletonConfigurable
38
- from IPython .core .error import StdinNotImplementedError
39
50
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
46
51
47
52
from ._version import kernel_protocol_version
48
53
@@ -796,14 +801,12 @@ async def comm_info_request(self, stream, ident, parent):
796
801
reply_content , parent , ident )
797
802
self .log .debug ("%s" , msg )
798
803
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 ):
803
805
if os .name == "nt" :
804
806
self .log .error ("Interrupt message not supported on Windows" )
805
-
806
807
else :
808
+ pid = os .getpid ()
809
+ pgid = os .getpgid (pid )
807
810
# Prefer process-group over process
808
811
if pgid and hasattr (os , "killpg" ):
809
812
try :
@@ -816,6 +819,8 @@ async def interrupt_request(self, stream, ident, parent):
816
819
except OSError :
817
820
pass
818
821
822
+ async def interrupt_request (self , stream , ident , parent ):
823
+ self ._send_interupt_children ()
819
824
content = parent ['content' ]
820
825
self .session .send (stream , 'interrupt_reply' , content , parent , ident = ident )
821
826
return
@@ -830,7 +835,7 @@ async def shutdown_request(self, stream, ident, parent):
830
835
content , parent
831
836
)
832
837
833
- self ._at_shutdown ()
838
+ await self ._at_shutdown ()
834
839
835
840
self .log .debug ('Stopping control ioloop' )
836
841
control_io_loop = self .control_stream .io_loop
@@ -1131,10 +1136,86 @@ def _input_request(self, prompt, ident, parent, password=False):
1131
1136
raise EOFError
1132
1137
return value
1133
1138
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 ):
1135
1206
"""Actions taken at shutdown by the kernel, called by python's atexit.
1136
1207
"""
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