diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index b3e57c95..7b9918f9 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -111,9 +111,10 @@ class APIHandler(BaseHandler): for name, spawner in user.spawners.items(): if spawner.ready: servers[name] = s = {'name': name} - s['pending'] = spawner.pending or None + if spawner.pending: + s['pending'] = spawner.pending if spawner.server: - s['url'] = user.url + name + s['url'] = user.url + name + '/' return model def group_model(self, group): diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 853fb34a..63b9e995 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -185,6 +185,8 @@ class UserServerAPIHandler(APIHandler): user = self.find_user(name) if server_name and not self.allow_named_servers: raise web.HTTPError(400, "Named servers are not enabled.") + if self.allow_named_servers and not server_name: + server_name = user.default_server_name() spawner = user.spawners[server_name] pending = spawner.pending if pending == 'spawn': @@ -194,8 +196,6 @@ class UserServerAPIHandler(APIHandler): elif pending: raise web.HTTPError(400, "%s is pending %s" % (spawner._log_name, pending)) - self._check_pending(spawner, accepted='spawn') - if spawner.ready: # include notify, so that a server that died is noticed immediately # set _spawn_pending flag to prevent races while we wait diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 8283bd48..6d0ba398 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -20,7 +20,7 @@ from .. import __version__ from .. import orm from ..objects import Server from ..spawner import LocalProcessSpawner -from ..utils import url_path_join, exponential_backoff +from ..utils import default_server_name, url_path_join, exponential_backoff # pattern for the authentication token header auth_header_pat = re.compile(r'^(?:token|bearer)\s+([^\s]+)$', flags=re.IGNORECASE) @@ -377,6 +377,9 @@ class BaseHandler(RequestHandler): @gen.coroutine def spawn_single_user(self, user, server_name='', options=None): user_server_name = user.name + if self.allow_named_servers and not server_name: + server_name = default_server_name(user) + if server_name: user_server_name = '%s:%s' % (user.name, server_name) @@ -517,20 +520,24 @@ class BaseHandler(RequestHandler): spawner._stop_pending = True @gen.coroutine - def stop(f=None): + def stop(): """Stop the server 1. remove it from the proxy 2. stop the server 3. notice that it stopped """ - yield self.proxy.delete_user(user, name) - yield user.stop(name) + tic = IOLoop.current().time() + try: + yield self.proxy.delete_user(user, name) + yield user.stop(name) + finally: + spawner._stop_pending = False toc = IOLoop.current().time() self.log.info("User %s server took %.3f seconds to stop", user.name, toc - tic) try: - yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), stop) + yield gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), stop()) except gen.TimeoutError: if spawner._stop_pending: # hit timeout, but stop is still pending diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index e2aa17e4..bda7dd44 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -231,9 +231,9 @@ class Proxy(LoggingConfigurable): user.name, spawner.proxy_spec, spawner.server.host, ) - if spawner.pending: + if spawner.pending and spawner.pending != 'spawn': raise RuntimeError( - "%s is pending %s, shouldn't be added to the proxy yet!" % (spawner._log_name) + "%s is pending %s, shouldn't be added to the proxy yet!" % (spawner._log_name, spawner.pending) ) yield self.add_route( diff --git a/jupyterhub/tests/test_named_servers.py b/jupyterhub/tests/test_named_servers.py index 5115766d..1ffd7fe3 100644 --- a/jupyterhub/tests/test_named_servers.py +++ b/jupyterhub/tests/test_named_servers.py @@ -38,6 +38,27 @@ def test_create_named_server(app, named_servers): assert prefix == user.spawners[servername].server.base_url assert prefix.endswith('/user/%s/%s/' % (username, servername)) + r = yield api_request(app, 'users', username) + r.raise_for_status() + + user_model = r.json() + user_model.pop('last_activity') + assert user_model == { + 'name': username, + 'groups': [], + 'kind': 'user', + 'admin': False, + 'pending': None, + 'server': None, + 'servers': { + name: { + 'name': name, + 'url': url_path_join(user.url, name, '/'), + } + for name in ['1', servername] + }, + } + @pytest.mark.gen_test def test_delete_named_server(app, named_servers): @@ -69,9 +90,9 @@ def test_delete_named_server(app, named_servers): 'servers': { name: { 'name': name, - 'url': url_path_join(user.url, name), + 'url': url_path_join(user.url, name, '/'), } - for name in ['1', servername] + for name in ['1'] }, } diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 654d9b7c..a3e6f57f 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -317,8 +317,6 @@ class User(HasTraits): url of the server will be /user/:name/:server_name """ db = self.db - if self.allow_named_servers and not server_name: - server_name = default_server_name(self) base_url = url_path_join(self.base_url, server_name) + '/'