diff --git a/jupyterhub/apihandlers/__init__.py b/jupyterhub/apihandlers/__init__.py index 367595c0..ec07df9b 100644 --- a/jupyterhub/apihandlers/__init__.py +++ b/jupyterhub/apihandlers/__init__.py @@ -1,10 +1,11 @@ from .base import * from .auth import * +from .hub import * from .proxy import * from .users import * -from . import auth, proxy, users +from . import auth, hub, proxy, users default_handlers = [] -for mod in (auth, proxy, users): +for mod in (auth, hub, proxy, users): default_handlers.extend(mod.default_handlers) diff --git a/jupyterhub/apihandlers/hub.py b/jupyterhub/apihandlers/hub.py new file mode 100644 index 00000000..c31765ff --- /dev/null +++ b/jupyterhub/apihandlers/hub.py @@ -0,0 +1,51 @@ +"""API handlers for administering the Hub itself""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import web +from tornado.ioloop import IOLoop + +from ..utils import admin_only +from .base import APIHandler + +class ShutdownAPIHandler(APIHandler): + + @admin_only + def post(self): + """POST /api/shutdown triggers a clean shutdown + + URL parameters: + + - servers: specify whether single-user servers should be terminated + - proxy: specify whether the proxy should be terminated + """ + from ..app import JupyterHub + app = JupyterHub.instance() + proxy = self.get_argument('proxy', '').lower() + if proxy == 'false': + app.cleanup_proxy = False + elif proxy == 'true': + app.cleanup_proxy = True + elif proxy: + raise web.HTTPError(400, "proxy must be true or false, got %r" % proxy) + servers = self.get_argument('servers', '').lower() + if servers == 'false': + app.cleanup_servers = False + elif servers == 'true': + app.cleanup_servers = True + elif servers: + raise web.HTTPError(400, "servers must be true or false, got %r" % servers) + + # finish the request + self.set_status(202) + self.finish() + + # stop the eventloop, which will trigger cleanup + loop = IOLoop.current() + loop.add_callback(loop.stop) + + +default_handlers = [ + (r"/api/shutdown", ShutdownAPIHandler), +] diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 09e4d7ea..1bbe5eba 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -846,6 +846,7 @@ class JupyterHub(Application): @gen.coroutine def cleanup(self): """Shutdown our various subprocesses and cleanup runtime files.""" + futures = [] if self.cleanup_servers: self.log.info("Cleaning up single-user servers...") @@ -868,7 +869,7 @@ class JupyterHub(Application): yield f except Exception as e: self.log.error("Failed to stop user: %s", e) - + self.db.commit() if self.pid_file and os.path.exists(self.pid_file): @@ -963,9 +964,16 @@ class JupyterHub(Application): # start the webserver self.http_server = tornado.httpserver.HTTPServer(self.tornado_application, xheaders=True) self.http_server.listen(self.hub_port) + atexit.register(self.atexit) + + def atexit(self): + """atexit callback""" # run the cleanup step (in a new loop, because the interrupted one is unclean) + IOLoop.clear_current() + loop = IOLoop() + loop.make_current() + loop.run_sync(self.cleanup) - atexit.register(lambda : IOLoop().run_sync(self.cleanup)) def stop(self): if not self.io_loop: