104104from jupyter_core .paths import jupyter_runtime_dir , jupyter_path
105105from notebook ._sysinfo import get_sys_info
106106
107- from ._tz import utcnow
107+ from ._tz import utcnow , utcfromtimestamp
108108from .utils import url_path_join , check_pid , url_escape
109109
110110#-----------------------------------------------------------------------------
@@ -336,6 +336,26 @@ def init_handlers(self, settings):
336336 new_handlers .append ((r'(.*)' , Template404 ))
337337 return new_handlers
338338
339+ def last_activity (self ):
340+ """Get a UTC timestamp for when the server last did something.
341+
342+ Includes: API activity, kernel activity, kernel shutdown, and terminal
343+ activity.
344+ """
345+ sources = [
346+ self .settings ['started' ],
347+ self .settings ['kernel_manager' ].last_kernel_activity ,
348+ ]
349+ try :
350+ sources .append (self .settings ['api_last_activity' ])
351+ except KeyError :
352+ pass
353+ try :
354+ sources .append (self .settings ['terminal_last_activity' ])
355+ except KeyError :
356+ pass
357+ return max (sources )
358+
339359
340360class NotebookPasswordApp (JupyterApp ):
341361 """Set a password for the notebook server.
@@ -1139,6 +1159,16 @@ def _update_server_extensions(self, change):
11391159 rate_limit_window = Float (3 , config = True , help = _ ("""(sec) Time window used to
11401160 check the message and data rate limits.""" ))
11411161
1162+ shutdown_no_activity_timeout = Integer (0 , config = True ,
1163+ help = ("Shut down the server after N seconds with no kernels or "
1164+ "terminals running and no activity. "
1165+ "This can be used together with culling idle kernels "
1166+ "(MappingKernelManager.cull_idle_timeout) to "
1167+ "shutdown the notebook server when it's not in use. This is not "
1168+ "precisely timed: it may shut down up to a minute later. "
1169+ "0 (the default) disables this automatic shutdown." )
1170+ )
1171+
11421172 def parse_command_line (self , argv = None ):
11431173 super (NotebookApp , self ).parse_command_line (argv )
11441174
@@ -1426,6 +1456,37 @@ def init_mime_overrides(self):
14261456 # mimetype always needs to be text/css, so we override it here.
14271457 mimetypes .add_type ('text/css' , '.css' )
14281458
1459+
1460+ def shutdown_no_activity (self ):
1461+ """Shutdown server on timeout when there are no kernels or terminals."""
1462+ km = self .kernel_manager
1463+ if len (km ) != 0 :
1464+ return # Kernels still running
1465+
1466+ try :
1467+ term_mgr = self .web_app .settings ['terminal_manager' ]
1468+ except KeyError :
1469+ pass # Terminals not enabled
1470+ else :
1471+ if term_mgr .terminals :
1472+ return # Terminals still running
1473+
1474+ seconds_since_active = \
1475+ (utcnow () - self .web_app .last_activity ()).total_seconds ()
1476+ self .log .debug ("No activity for %d seconds." ,
1477+ seconds_since_active )
1478+ if seconds_since_active > self .shutdown_no_activity_timeout :
1479+ self .log .info ("No kernels or terminals for %d seconds; shutting down." ,
1480+ seconds_since_active )
1481+ self .stop ()
1482+
1483+ def init_shutdown_no_activity (self ):
1484+ if self .shutdown_no_activity_timeout > 0 :
1485+ self .log .info ("Will shut down after %d seconds with no kernels or terminals." ,
1486+ self .shutdown_no_activity_timeout )
1487+ pc = ioloop .PeriodicCallback (self .shutdown_no_activity , 60000 )
1488+ pc .start ()
1489+
14291490 @catch_config_error
14301491 def initialize (self , argv = None ):
14311492 super (NotebookApp , self ).initialize (argv )
@@ -1439,6 +1500,7 @@ def initialize(self, argv=None):
14391500 self .init_signal ()
14401501 self .init_server_extensions ()
14411502 self .init_mime_overrides ()
1503+ self .init_shutdown_no_activity ()
14421504
14431505 def cleanup_kernels (self ):
14441506 """Shutdown all kernels.
0 commit comments