We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Container dying after startup leads to infinite redirection loop
We are using jupyterhub with the dockerspawner module. Once successfully logged in, we observed that if the docker container is spawned but dies a second or so later (for whatever reason), the redirection from /hub/user/username to /user/username will enter an infinite redirection loop. I suspect the reason is
that there's nothing listening at /user/username (because the container server is not there), so jupyterhub redirects you to /hub/user/username as default action in that case, which in turn tries to send you back to /user/username because you are logged in.

It's an unusual condition and we are running outside the basic design, but if you have any feedback on this it would be great.

jupyterhub version 0.6.1


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `jupyterhub/user.py`
Content:
```
1 # Copyright (c) Jupyter Development Team.
2 # Distributed under the terms of the Modified BSD License.
3 
4 from datetime import datetime, timedelta
5 from urllib.parse import quote, urlparse
6 
7 from tornado import gen
8 from tornado.log import app_log
9 
10 from sqlalchemy import inspect
11 
12 from .utils import url_path_join
13 
14 from . import orm
15 from traitlets import HasTraits, Any, Dict, observe, default
16 from .spawner import LocalProcessSpawner
17 
18 
19 class UserDict(dict):
20     """Like defaultdict, but for users
21     
22     Getting by a user id OR an orm.User instance returns a User wrapper around the orm user.
23     """
24     def __init__(self, db_factory, settings):
25         self.db_factory = db_factory
26         self.settings = settings
27         super().__init__()
28     
29     @property
30     def db(self):
31         return self.db_factory()
32     
33     def __contains__(self, key):
34         if isinstance(key, (User, orm.User)):
35             key = key.id
36         return dict.__contains__(self, key)
37     
38     def __getitem__(self, key):
39         if isinstance(key, User):
40             key = key.id
41         elif isinstance(key, str):
42             orm_user = self.db.query(orm.User).filter(orm.User.name==key).first()
43             if orm_user is None:
44                 raise KeyError("No such user: %s" % key)
45             else:
46                 key = orm_user
47         if isinstance(key, orm.User):
48             # users[orm_user] returns User(orm_user)
49             orm_user = key
50             if orm_user.id not in self:
51                 user = self[orm_user.id] = User(orm_user, self.settings)
52                 return user
53             user = dict.__getitem__(self, orm_user.id)
54             user.db = self.db
55             return user
56         elif isinstance(key, int):
57             id = key
58             if id not in self:
59                 orm_user = self.db.query(orm.User).filter(orm.User.id==id).first()
60                 if orm_user is None:
61                     raise KeyError("No such user: %s" % id)
62                 user = self[id] = User(orm_user, self.settings)
63             return dict.__getitem__(self, id)
64         else:
65             raise KeyError(repr(key))
66     
67     def __delitem__(self, key):
68         user = self[key]
69         user_id = user.id
70         db = self.db
71         db.delete(user.orm_user)
72         db.commit()
73         dict.__delitem__(self, user_id)
74 
75 
76 class User(HasTraits):
77     
78     @default('log')
79     def _log_default(self):
80         return app_log
81     
82     settings = Dict()
83     
84     db = Any(allow_none=True)
85     @default('db')
86     def _db_default(self):
87         if self.orm_user:
88             return inspect(self.orm_user).session
89     @observe('db')
90     def _db_changed(self, change):
91         """Changing db session reacquires ORM User object"""
92         # db session changed, re-get orm User
93         if self.orm_user:
94             id = self.orm_user.id
95             self.orm_user = change['new'].query(orm.User).filter(orm.User.id==id).first()
96         self.spawner.db = self.db
97     
98     orm_user = None
99     spawner = None
100     spawn_pending = False
101     stop_pending = False
102     
103     @property
104     def authenticator(self):
105         return self.settings.get('authenticator', None)
106     
107     @property
108     def spawner_class(self):
109         return self.settings.get('spawner_class', LocalProcessSpawner)
110     
111     def __init__(self, orm_user, settings, **kwargs):
112         self.orm_user = orm_user
113         self.settings = settings
114         super().__init__(**kwargs)
115         
116         hub = self.db.query(orm.Hub).first()
117         
118         self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe=''))
119         self.base_url = url_path_join(
120             self.settings.get('base_url', '/'), 'user', self.escaped_name)
121         
122         self.spawner = self.spawner_class(
123             user=self,
124             db=self.db,
125             hub=hub,
126             authenticator=self.authenticator,
127             config=self.settings.get('config'),
128         )
129     
130     # pass get/setattr to ORM user
131     
132     def __getattr__(self, attr):
133         if hasattr(self.orm_user, attr):
134             return getattr(self.orm_user, attr)
135         else:
136             raise AttributeError(attr)
137     
138     def __setattr__(self, attr, value):
139         if self.orm_user and hasattr(self.orm_user, attr):
140             setattr(self.orm_user, attr, value)
141         else:
142             super().__setattr__(attr, value)
143     
144     def __repr__(self):
145         return repr(self.orm_user)
146     
147     @property
148     def running(self):
149         """property for whether a user has a running server"""
150         if self.spawn_pending or self.stop_pending:
151             return False # server is not running if spawn or stop is still pending
152         if self.server is None:
153             return False
154         return True
155     
156     @property
157     def escaped_name(self):
158         """My name, escaped for use in URLs, cookies, etc."""
159         return quote(self.name, safe='@')
160     
161     @property
162     def proxy_path(self):
163         if self.settings.get('subdomain_host'):
164             return url_path_join('/' + self.domain, self.base_url)
165         else:
166             return self.base_url
167     
168     @property
169     def domain(self):
170         """Get the domain for my server."""
171         # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
172         return self.escaped_name + '.' + self.settings['domain']
173     
174     @property
175     def host(self):
176         """Get the *host* for my server (proto://domain[:port])"""
177         # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
178         parsed = urlparse(self.settings['subdomain_host'])
179         h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)
180         return h
181     
182     @property
183     def url(self):
184         """My URL
185         
186         Full name.domain/path if using subdomains, otherwise just my /base/url
187         """
188         if self.settings.get('subdomain_host'):
189             return '{host}{path}'.format(
190                 host=self.host,
191                 path=self.base_url,
192             )
193         else:
194             return self.base_url
195     
196     @gen.coroutine
197     def spawn(self, options=None):
198         """Start the user's spawner"""
199         db = self.db
200         
201         self.server = orm.Server(
202             cookie_name=self.cookie_name,
203             base_url=self.base_url,
204         )
205         db.add(self.server)
206         db.commit()
207         
208         api_token = self.new_api_token()
209         db.commit()
210         
211         spawner = self.spawner
212         spawner.user_options = options or {}
213         # we are starting a new server, make sure it doesn't restore state
214         spawner.clear_state()
215         spawner.api_token = api_token
216 
217         # trigger pre-spawn hook on authenticator
218         authenticator = self.authenticator
219         if (authenticator):
220             yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))
221 
222         self.spawn_pending = True
223         # wait for spawner.start to return
224         try:
225             f = spawner.start()
226             # commit any changes in spawner.start (always commit db changes before yield)
227             db.commit()
228             ip_port = yield gen.with_timeout(timedelta(seconds=spawner.start_timeout), f)
229             if ip_port:
230                 # get ip, port info from return value of start()
231                 self.server.ip, self.server.port = ip_port
232             else:
233                 # prior to 0.7, spawners had to store this info in user.server themselves.
234                 # Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
235                 self.log.warning("DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7")
236         except Exception as e:
237             if isinstance(e, gen.TimeoutError):
238                 self.log.warning("{user}'s server failed to start in {s} seconds, giving up".format(
239                     user=self.name, s=spawner.start_timeout,
240                 ))
241                 e.reason = 'timeout'
242             else:
243                 self.log.error("Unhandled error starting {user}'s server: {error}".format(
244                     user=self.name, error=e,
245                 ))
246                 e.reason = 'error'
247             try:
248                 yield self.stop()
249             except Exception:
250                 self.log.error("Failed to cleanup {user}'s server that failed to start".format(
251                     user=self.name,
252                 ), exc_info=True)
253             # raise original exception
254             raise e
255         spawner.start_polling()
256 
257         # store state
258         self.state = spawner.get_state()
259         self.last_activity = datetime.utcnow()
260         db.commit()
261         self.spawn_pending = False
262         try:
263             yield self.server.wait_up(http=True, timeout=spawner.http_timeout)
264         except Exception as e:
265             if isinstance(e, TimeoutError):
266                 self.log.warning(
267                     "{user}'s server never showed up at {url} "
268                     "after {http_timeout} seconds. Giving up".format(
269                         user=self.name,
270                         url=self.server.url,
271                         http_timeout=spawner.http_timeout,
272                     )
273                 )
274                 e.reason = 'timeout'
275             else:
276                 e.reason = 'error'
277                 self.log.error("Unhandled error waiting for {user}'s server to show up at {url}: {error}".format(
278                     user=self.name, url=self.server.url, error=e,
279                 ))
280             try:
281                 yield self.stop()
282             except Exception:
283                 self.log.error("Failed to cleanup {user}'s server that failed to start".format(
284                     user=self.name,
285                 ), exc_info=True)
286             # raise original TimeoutError
287             raise e
288         return self
289 
290     @gen.coroutine
291     def stop(self):
292         """Stop the user's spawner
293         
294         and cleanup after it.
295         """
296         self.spawn_pending = False
297         spawner = self.spawner
298         self.spawner.stop_polling()
299         self.stop_pending = True
300         try:
301             api_token = self.spawner.api_token
302             status = yield spawner.poll()
303             if status is None:
304                 yield self.spawner.stop()
305             spawner.clear_state()
306             self.state = spawner.get_state()
307             self.last_activity = datetime.utcnow()
308             # cleanup server entry, API token from defunct server
309             if self.server:
310                 # cleanup server entry from db
311                 self.db.delete(self.server)
312             orm_token = orm.APIToken.find(self.db, api_token)
313             if orm_token:
314                 self.db.delete(orm_token)
315             self.server = None
316             self.db.commit()
317         finally:
318             self.stop_pending = False
319             # trigger post-spawner hook on authenticator
320             auth = spawner.authenticator
321             if auth:
322                 yield gen.maybe_future(
323                     auth.post_spawn_stop(self, spawner)
324                 )
325 
326
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/jupyterhub/user.py b/jupyterhub/user.py
--- a/jupyterhub/user.py
+++ b/jupyterhub/user.py
@@ -258,7 +258,6 @@
         self.state = spawner.get_state()
         self.last_activity = datetime.utcnow()
         db.commit()
-        self.spawn_pending = False
         try:
             yield self.server.wait_up(http=True, timeout=spawner.http_timeout)
         except Exception as e:
@@ -285,6 +284,7 @@
                 ), exc_info=True)
             # raise original TimeoutError
             raise e
+        self.spawn_pending = False
         return self
 
     @gen.coroutine

{"golden_diff": "diff --git a/jupyterhub/user.py b/jupyterhub/user.py\n--- a/jupyterhub/user.py\n+++ b/jupyterhub/user.py\n@@ -258,7 +258,6 @@\n         self.state = spawner.get_state()\n         self.last_activity = datetime.utcnow()\n         db.commit()\n-        self.spawn_pending = False\n         try:\n             yield self.server.wait_up(http=True, timeout=spawner.http_timeout)\n         except Exception as e:\n@@ -285,6 +284,7 @@\n                 ), exc_info=True)\n             # raise original TimeoutError\n             raise e\n+        self.spawn_pending = False\n         return self\n \n     @gen.coroutine\n", "issue": "Container dying after startup leads to infinite redirection loop\nWe are using jupyterhub with the dockerspawner module. Once successfully logged in, we observed that if the docker container is spawned but dies a second or so later (for whatever reason), the redirection from /hub/user/username to /user/username will enter an infinite redirection loop. I suspect the reason is\nthat there's nothing listening at /user/username (because the container server is not there), so jupyterhub redirects you to /hub/user/username as default action in that case, which in turn tries to send you back to /user/username because you are logged in.\n\nIt's an unusual condition and we are running outside the basic design, but if you have any feedback on this it would be great.\n\njupyterhub version 0.6.1\n\n", "before_files": [{"content": "# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom datetime import datetime, timedelta\nfrom urllib.parse import quote, urlparse\n\nfrom tornado import gen\nfrom tornado.log import app_log\n\nfrom sqlalchemy import inspect\n\nfrom .utils import url_path_join\n\nfrom . import orm\nfrom traitlets import HasTraits, Any, Dict, observe, default\nfrom .spawner import LocalProcessSpawner\n\n\nclass UserDict(dict):\n    \"\"\"Like defaultdict, but for users\n    \n    Getting by a user id OR an orm.User instance returns a User wrapper around the orm user.\n    \"\"\"\n    def __init__(self, db_factory, settings):\n        self.db_factory = db_factory\n        self.settings = settings\n        super().__init__()\n    \n    @property\n    def db(self):\n        return self.db_factory()\n    \n    def __contains__(self, key):\n        if isinstance(key, (User, orm.User)):\n            key = key.id\n        return dict.__contains__(self, key)\n    \n    def __getitem__(self, key):\n        if isinstance(key, User):\n            key = key.id\n        elif isinstance(key, str):\n            orm_user = self.db.query(orm.User).filter(orm.User.name==key).first()\n            if orm_user is None:\n                raise KeyError(\"No such user: %s\" % key)\n            else:\n                key = orm_user\n        if isinstance(key, orm.User):\n            # users[orm_user] returns User(orm_user)\n            orm_user = key\n            if orm_user.id not in self:\n                user = self[orm_user.id] = User(orm_user, self.settings)\n                return user\n            user = dict.__getitem__(self, orm_user.id)\n            user.db = self.db\n            return user\n        elif isinstance(key, int):\n            id = key\n            if id not in self:\n                orm_user = self.db.query(orm.User).filter(orm.User.id==id).first()\n                if orm_user is None:\n                    raise KeyError(\"No such user: %s\" % id)\n                user = self[id] = User(orm_user, self.settings)\n            return dict.__getitem__(self, id)\n        else:\n            raise KeyError(repr(key))\n    \n    def __delitem__(self, key):\n        user = self[key]\n        user_id = user.id\n        db = self.db\n        db.delete(user.orm_user)\n        db.commit()\n        dict.__delitem__(self, user_id)\n\n\nclass User(HasTraits):\n    \n    @default('log')\n    def _log_default(self):\n        return app_log\n    \n    settings = Dict()\n    \n    db = Any(allow_none=True)\n    @default('db')\n    def _db_default(self):\n        if self.orm_user:\n            return inspect(self.orm_user).session\n    @observe('db')\n    def _db_changed(self, change):\n        \"\"\"Changing db session reacquires ORM User object\"\"\"\n        # db session changed, re-get orm User\n        if self.orm_user:\n            id = self.orm_user.id\n            self.orm_user = change['new'].query(orm.User).filter(orm.User.id==id).first()\n        self.spawner.db = self.db\n    \n    orm_user = None\n    spawner = None\n    spawn_pending = False\n    stop_pending = False\n    \n    @property\n    def authenticator(self):\n        return self.settings.get('authenticator', None)\n    \n    @property\n    def spawner_class(self):\n        return self.settings.get('spawner_class', LocalProcessSpawner)\n    \n    def __init__(self, orm_user, settings, **kwargs):\n        self.orm_user = orm_user\n        self.settings = settings\n        super().__init__(**kwargs)\n        \n        hub = self.db.query(orm.Hub).first()\n        \n        self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe=''))\n        self.base_url = url_path_join(\n            self.settings.get('base_url', '/'), 'user', self.escaped_name)\n        \n        self.spawner = self.spawner_class(\n            user=self,\n            db=self.db,\n            hub=hub,\n            authenticator=self.authenticator,\n            config=self.settings.get('config'),\n        )\n    \n    # pass get/setattr to ORM user\n    \n    def __getattr__(self, attr):\n        if hasattr(self.orm_user, attr):\n            return getattr(self.orm_user, attr)\n        else:\n            raise AttributeError(attr)\n    \n    def __setattr__(self, attr, value):\n        if self.orm_user and hasattr(self.orm_user, attr):\n            setattr(self.orm_user, attr, value)\n        else:\n            super().__setattr__(attr, value)\n    \n    def __repr__(self):\n        return repr(self.orm_user)\n    \n    @property\n    def running(self):\n        \"\"\"property for whether a user has a running server\"\"\"\n        if self.spawn_pending or self.stop_pending:\n            return False # server is not running if spawn or stop is still pending\n        if self.server is None:\n            return False\n        return True\n    \n    @property\n    def escaped_name(self):\n        \"\"\"My name, escaped for use in URLs, cookies, etc.\"\"\"\n        return quote(self.name, safe='@')\n    \n    @property\n    def proxy_path(self):\n        if self.settings.get('subdomain_host'):\n            return url_path_join('/' + self.domain, self.base_url)\n        else:\n            return self.base_url\n    \n    @property\n    def domain(self):\n        \"\"\"Get the domain for my server.\"\"\"\n        # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment\n        return self.escaped_name + '.' + self.settings['domain']\n    \n    @property\n    def host(self):\n        \"\"\"Get the *host* for my server (proto://domain[:port])\"\"\"\n        # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment\n        parsed = urlparse(self.settings['subdomain_host'])\n        h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)\n        return h\n    \n    @property\n    def url(self):\n        \"\"\"My URL\n        \n        Full name.domain/path if using subdomains, otherwise just my /base/url\n        \"\"\"\n        if self.settings.get('subdomain_host'):\n            return '{host}{path}'.format(\n                host=self.host,\n                path=self.base_url,\n            )\n        else:\n            return self.base_url\n    \n    @gen.coroutine\n    def spawn(self, options=None):\n        \"\"\"Start the user's spawner\"\"\"\n        db = self.db\n        \n        self.server = orm.Server(\n            cookie_name=self.cookie_name,\n            base_url=self.base_url,\n        )\n        db.add(self.server)\n        db.commit()\n        \n        api_token = self.new_api_token()\n        db.commit()\n        \n        spawner = self.spawner\n        spawner.user_options = options or {}\n        # we are starting a new server, make sure it doesn't restore state\n        spawner.clear_state()\n        spawner.api_token = api_token\n\n        # trigger pre-spawn hook on authenticator\n        authenticator = self.authenticator\n        if (authenticator):\n            yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))\n\n        self.spawn_pending = True\n        # wait for spawner.start to return\n        try:\n            f = spawner.start()\n            # commit any changes in spawner.start (always commit db changes before yield)\n            db.commit()\n            ip_port = yield gen.with_timeout(timedelta(seconds=spawner.start_timeout), f)\n            if ip_port:\n                # get ip, port info from return value of start()\n                self.server.ip, self.server.port = ip_port\n            else:\n                # prior to 0.7, spawners had to store this info in user.server themselves.\n                # Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.\n                self.log.warning(\"DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7\")\n        except Exception as e:\n            if isinstance(e, gen.TimeoutError):\n                self.log.warning(\"{user}'s server failed to start in {s} seconds, giving up\".format(\n                    user=self.name, s=spawner.start_timeout,\n                ))\n                e.reason = 'timeout'\n            else:\n                self.log.error(\"Unhandled error starting {user}'s server: {error}\".format(\n                    user=self.name, error=e,\n                ))\n                e.reason = 'error'\n            try:\n                yield self.stop()\n            except Exception:\n                self.log.error(\"Failed to cleanup {user}'s server that failed to start\".format(\n                    user=self.name,\n                ), exc_info=True)\n            # raise original exception\n            raise e\n        spawner.start_polling()\n\n        # store state\n        self.state = spawner.get_state()\n        self.last_activity = datetime.utcnow()\n        db.commit()\n        self.spawn_pending = False\n        try:\n            yield self.server.wait_up(http=True, timeout=spawner.http_timeout)\n        except Exception as e:\n            if isinstance(e, TimeoutError):\n                self.log.warning(\n                    \"{user}'s server never showed up at {url} \"\n                    \"after {http_timeout} seconds. Giving up\".format(\n                        user=self.name,\n                        url=self.server.url,\n                        http_timeout=spawner.http_timeout,\n                    )\n                )\n                e.reason = 'timeout'\n            else:\n                e.reason = 'error'\n                self.log.error(\"Unhandled error waiting for {user}'s server to show up at {url}: {error}\".format(\n                    user=self.name, url=self.server.url, error=e,\n                ))\n            try:\n                yield self.stop()\n            except Exception:\n                self.log.error(\"Failed to cleanup {user}'s server that failed to start\".format(\n                    user=self.name,\n                ), exc_info=True)\n            # raise original TimeoutError\n            raise e\n        return self\n\n    @gen.coroutine\n    def stop(self):\n        \"\"\"Stop the user's spawner\n        \n        and cleanup after it.\n        \"\"\"\n        self.spawn_pending = False\n        spawner = self.spawner\n        self.spawner.stop_polling()\n        self.stop_pending = True\n        try:\n            api_token = self.spawner.api_token\n            status = yield spawner.poll()\n            if status is None:\n                yield self.spawner.stop()\n            spawner.clear_state()\n            self.state = spawner.get_state()\n            self.last_activity = datetime.utcnow()\n            # cleanup server entry, API token from defunct server\n            if self.server:\n                # cleanup server entry from db\n                self.db.delete(self.server)\n            orm_token = orm.APIToken.find(self.db, api_token)\n            if orm_token:\n                self.db.delete(orm_token)\n            self.server = None\n            self.db.commit()\n        finally:\n            self.stop_pending = False\n            # trigger post-spawner hook on authenticator\n            auth = spawner.authenticator\n            if auth:\n                yield gen.maybe_future(\n                    auth.post_spawn_stop(self, spawner)\n                )\n\n", "path": "jupyterhub/user.py"}], "after_files": [{"content": "# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom datetime import datetime, timedelta\nfrom urllib.parse import quote, urlparse\n\nfrom tornado import gen\nfrom tornado.log import app_log\n\nfrom sqlalchemy import inspect\n\nfrom .utils import url_path_join\n\nfrom . import orm\nfrom traitlets import HasTraits, Any, Dict, observe, default\nfrom .spawner import LocalProcessSpawner\n\n\nclass UserDict(dict):\n    \"\"\"Like defaultdict, but for users\n    \n    Getting by a user id OR an orm.User instance returns a User wrapper around the orm user.\n    \"\"\"\n    def __init__(self, db_factory, settings):\n        self.db_factory = db_factory\n        self.settings = settings\n        super().__init__()\n    \n    @property\n    def db(self):\n        return self.db_factory()\n    \n    def __contains__(self, key):\n        if isinstance(key, (User, orm.User)):\n            key = key.id\n        return dict.__contains__(self, key)\n    \n    def __getitem__(self, key):\n        if isinstance(key, User):\n            key = key.id\n        elif isinstance(key, str):\n            orm_user = self.db.query(orm.User).filter(orm.User.name==key).first()\n            if orm_user is None:\n                raise KeyError(\"No such user: %s\" % key)\n            else:\n                key = orm_user\n        if isinstance(key, orm.User):\n            # users[orm_user] returns User(orm_user)\n            orm_user = key\n            if orm_user.id not in self:\n                user = self[orm_user.id] = User(orm_user, self.settings)\n                return user\n            user = dict.__getitem__(self, orm_user.id)\n            user.db = self.db\n            return user\n        elif isinstance(key, int):\n            id = key\n            if id not in self:\n                orm_user = self.db.query(orm.User).filter(orm.User.id==id).first()\n                if orm_user is None:\n                    raise KeyError(\"No such user: %s\" % id)\n                user = self[id] = User(orm_user, self.settings)\n            return dict.__getitem__(self, id)\n        else:\n            raise KeyError(repr(key))\n    \n    def __delitem__(self, key):\n        user = self[key]\n        user_id = user.id\n        db = self.db\n        db.delete(user.orm_user)\n        db.commit()\n        dict.__delitem__(self, user_id)\n\n\nclass User(HasTraits):\n    \n    @default('log')\n    def _log_default(self):\n        return app_log\n    \n    settings = Dict()\n    \n    db = Any(allow_none=True)\n    @default('db')\n    def _db_default(self):\n        if self.orm_user:\n            return inspect(self.orm_user).session\n    @observe('db')\n    def _db_changed(self, change):\n        \"\"\"Changing db session reacquires ORM User object\"\"\"\n        # db session changed, re-get orm User\n        if self.orm_user:\n            id = self.orm_user.id\n            self.orm_user = change['new'].query(orm.User).filter(orm.User.id==id).first()\n        self.spawner.db = self.db\n    \n    orm_user = None\n    spawner = None\n    spawn_pending = False\n    stop_pending = False\n    \n    @property\n    def authenticator(self):\n        return self.settings.get('authenticator', None)\n    \n    @property\n    def spawner_class(self):\n        return self.settings.get('spawner_class', LocalProcessSpawner)\n    \n    def __init__(self, orm_user, settings, **kwargs):\n        self.orm_user = orm_user\n        self.settings = settings\n        super().__init__(**kwargs)\n        \n        hub = self.db.query(orm.Hub).first()\n        \n        self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe=''))\n        self.base_url = url_path_join(\n            self.settings.get('base_url', '/'), 'user', self.escaped_name)\n        \n        self.spawner = self.spawner_class(\n            user=self,\n            db=self.db,\n            hub=hub,\n            authenticator=self.authenticator,\n            config=self.settings.get('config'),\n        )\n    \n    # pass get/setattr to ORM user\n    \n    def __getattr__(self, attr):\n        if hasattr(self.orm_user, attr):\n            return getattr(self.orm_user, attr)\n        else:\n            raise AttributeError(attr)\n    \n    def __setattr__(self, attr, value):\n        if self.orm_user and hasattr(self.orm_user, attr):\n            setattr(self.orm_user, attr, value)\n        else:\n            super().__setattr__(attr, value)\n    \n    def __repr__(self):\n        return repr(self.orm_user)\n    \n    @property\n    def running(self):\n        \"\"\"property for whether a user has a running server\"\"\"\n        if self.spawn_pending or self.stop_pending:\n            return False # server is not running if spawn or stop is still pending\n        if self.server is None:\n            return False\n        return True\n    \n    @property\n    def escaped_name(self):\n        \"\"\"My name, escaped for use in URLs, cookies, etc.\"\"\"\n        return quote(self.name, safe='@')\n    \n    @property\n    def proxy_path(self):\n        if self.settings.get('subdomain_host'):\n            return url_path_join('/' + self.domain, self.base_url)\n        else:\n            return self.base_url\n    \n    @property\n    def domain(self):\n        \"\"\"Get the domain for my server.\"\"\"\n        # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment\n        return self.escaped_name + '.' + self.settings['domain']\n    \n    @property\n    def host(self):\n        \"\"\"Get the *host* for my server (proto://domain[:port])\"\"\"\n        # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment\n        parsed = urlparse(self.settings['subdomain_host'])\n        h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)\n        return h\n    \n    @property\n    def url(self):\n        \"\"\"My URL\n        \n        Full name.domain/path if using subdomains, otherwise just my /base/url\n        \"\"\"\n        if self.settings.get('subdomain_host'):\n            return '{host}{path}'.format(\n                host=self.host,\n                path=self.base_url,\n            )\n        else:\n            return self.base_url\n    \n    @gen.coroutine\n    def spawn(self, options=None):\n        \"\"\"Start the user's spawner\"\"\"\n        db = self.db\n        \n        self.server = orm.Server(\n            cookie_name=self.cookie_name,\n            base_url=self.base_url,\n        )\n        db.add(self.server)\n        db.commit()\n        \n        api_token = self.new_api_token()\n        db.commit()\n        \n        spawner = self.spawner\n        spawner.user_options = options or {}\n        # we are starting a new server, make sure it doesn't restore state\n        spawner.clear_state()\n        spawner.api_token = api_token\n\n        # trigger pre-spawn hook on authenticator\n        authenticator = self.authenticator\n        if (authenticator):\n            yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner))\n\n        self.spawn_pending = True\n        # wait for spawner.start to return\n        try:\n            f = spawner.start()\n            # commit any changes in spawner.start (always commit db changes before yield)\n            db.commit()\n            ip_port = yield gen.with_timeout(timedelta(seconds=spawner.start_timeout), f)\n            if ip_port:\n                # get ip, port info from return value of start()\n                self.server.ip, self.server.port = ip_port\n            else:\n                # prior to 0.7, spawners had to store this info in user.server themselves.\n                # Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.\n                self.log.warning(\"DEPRECATION: Spawner.start should return (ip, port) in JupyterHub >= 0.7\")\n        except Exception as e:\n            if isinstance(e, gen.TimeoutError):\n                self.log.warning(\"{user}'s server failed to start in {s} seconds, giving up\".format(\n                    user=self.name, s=spawner.start_timeout,\n                ))\n                e.reason = 'timeout'\n            else:\n                self.log.error(\"Unhandled error starting {user}'s server: {error}\".format(\n                    user=self.name, error=e,\n                ))\n                e.reason = 'error'\n            try:\n                yield self.stop()\n            except Exception:\n                self.log.error(\"Failed to cleanup {user}'s server that failed to start\".format(\n                    user=self.name,\n                ), exc_info=True)\n            # raise original exception\n            raise e\n        spawner.start_polling()\n\n        # store state\n        self.state = spawner.get_state()\n        self.last_activity = datetime.utcnow()\n        db.commit()\n        try:\n            yield self.server.wait_up(http=True, timeout=spawner.http_timeout)\n        except Exception as e:\n            if isinstance(e, TimeoutError):\n                self.log.warning(\n                    \"{user}'s server never showed up at {url} \"\n                    \"after {http_timeout} seconds. Giving up\".format(\n                        user=self.name,\n                        url=self.server.url,\n                        http_timeout=spawner.http_timeout,\n                    )\n                )\n                e.reason = 'timeout'\n            else:\n                e.reason = 'error'\n                self.log.error(\"Unhandled error waiting for {user}'s server to show up at {url}: {error}\".format(\n                    user=self.name, url=self.server.url, error=e,\n                ))\n            try:\n                yield self.stop()\n            except Exception:\n                self.log.error(\"Failed to cleanup {user}'s server that failed to start\".format(\n                    user=self.name,\n                ), exc_info=True)\n            # raise original TimeoutError\n            raise e\n        self.spawn_pending = False\n        return self\n\n    @gen.coroutine\n    def stop(self):\n        \"\"\"Stop the user's spawner\n        \n        and cleanup after it.\n        \"\"\"\n        self.spawn_pending = False\n        spawner = self.spawner\n        self.spawner.stop_polling()\n        self.stop_pending = True\n        try:\n            api_token = self.spawner.api_token\n            status = yield spawner.poll()\n            if status is None:\n                yield self.spawner.stop()\n            spawner.clear_state()\n            self.state = spawner.get_state()\n            self.last_activity = datetime.utcnow()\n            # cleanup server entry, API token from defunct server\n            if self.server:\n                # cleanup server entry from db\n                self.db.delete(self.server)\n            orm_token = orm.APIToken.find(self.db, api_token)\n            if orm_token:\n                self.db.delete(orm_token)\n            self.server = None\n            self.db.commit()\n        finally:\n            self.stop_pending = False\n            # trigger post-spawner hook on authenticator\n            auth = spawner.authenticator\n            if auth:\n                yield gen.maybe_future(\n                    auth.post_spawn_stop(self, spawner)\n                )\n\n", "path": "jupyterhub/user.py"}]}