1
- import base64
1
+ import atexit
2
2
import collections
3
3
import datetime
4
+ import base64
5
+ import signal
4
6
from textwrap import dedent
5
7
6
8
from async_generator import asynccontextmanager
@@ -278,7 +280,7 @@ def __init__(self, nb, km=None, **kw):
278
280
----------
279
281
nb : NotebookNode
280
282
Notebook being executed.
281
- km : KernerlManager (optional)
283
+ km : KernelManager (optional)
282
284
Optional kernel manager. If none is provided, a kernel manager will
283
285
be created.
284
286
"""
@@ -330,23 +332,21 @@ def start_kernel_manager(self):
330
332
return self .km
331
333
332
334
async def _async_cleanup_kernel (self ):
335
+ now = self .shutdown_kernel == "immediate"
333
336
try :
334
- # Send a polite shutdown request
335
- await ensure_async (self .kc .shutdown ())
336
- try :
337
- # Queue the manager to kill the process, sometimes the built-in and above
338
- # shutdowns have not been successful or called yet, so give a direct kill
339
- # call here and recover gracefully if it's already dead.
340
- await ensure_async (self .km .shutdown_kernel (now = True ))
341
- except RuntimeError as e :
342
- # The error isn't specialized, so we have to check the message
343
- if 'No kernel is running!' not in str (e ):
344
- raise
337
+ # Queue the manager to kill the process, and recover gracefully if it's already dead.
338
+ if await ensure_async (self .km .is_alive ()):
339
+ await ensure_async (self .km .shutdown_kernel (now = now ))
340
+ except RuntimeError as e :
341
+ # The error isn't specialized, so we have to check the message
342
+ if 'No kernel is running!' not in str (e ):
343
+ raise
345
344
finally :
346
345
# Remove any state left over even if we failed to stop the kernel
347
346
await ensure_async (self .km .cleanup ())
348
- await ensure_async (self .kc .stop_channels ())
349
- self .kc = None
347
+ if getattr (self , "kc" ):
348
+ await ensure_async (self .kc .stop_channels ())
349
+ self .kc = None
350
350
351
351
_cleanup_kernel = run_sync (_async_cleanup_kernel )
352
352
@@ -438,11 +438,30 @@ async def async_setup_kernel(self, **kwargs):
438
438
439
439
When control returns from the yield it stops the client's zmq channels, and shuts
440
440
down the kernel.
441
+
442
+ Handlers for SIGINT and SIGTERM are also added to cleanup in case of unexpected shutdown.
441
443
"""
442
444
cleanup_kc = kwargs .pop ('cleanup_kc' , True )
443
445
if self .km is None :
444
446
self .start_kernel_manager ()
445
447
448
+ # self._cleanup_kernel uses run_async, which ensures the ioloop is running again.
449
+ # This is necessary as the ioloop has stopped once atexit fires.
450
+ atexit .register (self ._cleanup_kernel )
451
+
452
+ def on_signal ():
453
+ asyncio .ensure_future (self ._async_cleanup_kernel ())
454
+ atexit .unregister (self ._cleanup_kernel )
455
+
456
+ loop = asyncio .get_event_loop ()
457
+ try :
458
+ loop .add_signal_handler (signal .SIGINT , on_signal )
459
+ loop .add_signal_handler (signal .SIGTERM , on_signal )
460
+ except (NotImplementedError , RuntimeError ):
461
+ # NotImplementedError: Windows does not support signals.
462
+ # RuntimeError: Raised when add_signal_handler is called outside the main thread
463
+ pass
464
+
446
465
if not self .km .has_kernel :
447
466
await self .async_start_new_kernel_client (** kwargs )
448
467
try :
@@ -451,6 +470,13 @@ async def async_setup_kernel(self, **kwargs):
451
470
if cleanup_kc :
452
471
await self ._async_cleanup_kernel ()
453
472
473
+ atexit .unregister (self ._cleanup_kernel )
474
+ try :
475
+ loop .remove_signal_handler (signal .SIGINT )
476
+ loop .remove_signal_handler (signal .SIGTERM )
477
+ except (NotImplementedError , RuntimeError ):
478
+ pass
479
+
454
480
async def async_execute (self , reset_kc = False , ** kwargs ):
455
481
"""
456
482
Executes each code cell.
0 commit comments