We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Cull_Idle Instructions
I'm having trouble using the [cull_idle example](https://github.com/jupyterhub/jupyterhub/tree/master/examples/cull-idle#configure-cull-idle-to-run-as-a-hub-managed-service) as a managed service. I added the [example config section](https://github.com/jupyterhub/jupyterhub/blob/master/examples/cull-idle/jupyterhub_config.py) to the bottom of my existing `jupyterhub_config.py` file:

``` python
c = get_config()
docker_ip = '172.17.0.1' 
hub_name='127.0.0.1'

c.JupyterHub.ip = hub_name
c.JupyterHub.hub_ip = docker_ip
c.JupyterHub.debug_proxy = True
c.JupyterHub.port = 8000

c.JupyterHub.cleanup_servers = False
c.JupyterHub.cleanup_proxy = True
c.JupyterHub.ssl_key = '/etc/jupyterhub/ssl/jhub.key'
c.JupyterHub.ssl_cert = '/etc/jupyterhub/ssl/jhub.crt'


c.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'
c.DockerSpawner.hub_ip_connect = docker_ip
c.DockerSpawner.container_ip = docker_ip 


c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'
c.JupyterHub.log_level = 'DEBUG'

#CULL_IDLE SECTION
c.JupyterHub.services = [{
    'name': 'cull-idle',
    'admin': True,
    'command': 'python cull_idle_servers.py --timeout=3600'.split()}]
```

I see the resulting error:

```
[C 2016-09-20 02:28:37.026 JupyterHub app:1444] Failed to start service cull-idle
    Traceback (most recent call last):
      File "/usr/local/lib/python3.5/dist-packages/jupyterhub/app.py", line 1442, in start
        yield service.start()
      File "/usr/local/lib/python3.5/dist-packages/jupyterhub/services/service.py", line 228, in start
        env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
    AttributeError: 'NoneType' object has no attribute 'base_url'
```

Any ideas of what could be causing this? Thank you!


--- 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/services/service.py`
Content:
```
1 """A service is a process that talks to JupyterHub
2 
3 Cases:
4 
5 Managed:
6   - managed by JuyterHub (always subprocess, no custom Spawners)
7   - always a long-running process
8   - managed services are restarted automatically if they exit unexpectedly
9 Unmanaged:
10   - managed by external service (docker, systemd, etc.)
11   - do not need to be long-running processes, or processes at all
12 
13 
14 URL: needs a route added to the proxy.
15   - Public route will always be /services/service-name
16   - url specified in config
17   - if port is 0, Hub will select a port
18 
19 API access:
20   - admin: tokens will have admin-access to the API
21   - not admin: tokens will only have non-admin access
22     (not much they can do other than defer to Hub for auth)
23 
24 An externally managed service running on a URL::
25 
26     {
27         'name': 'my-service',
28         'url': 'https://host:8888',
29         'admin': True,
30         'token': 'super-secret',
31     }
32 
33 A hub-managed service with no URL:
34 
35     {
36         'name': 'cull-idle',
37         'command': ['python', '/path/to/cull-idle']
38         'admin': True,
39     }
40 """
41 
42 from getpass import getuser
43 import pipes
44 import shutil
45 from subprocess import Popen
46 from urllib.parse import urlparse
47 
48 from tornado import gen
49 
50 from traitlets import (
51     HasTraits,
52     Any, Bool, Dict, Unicode, Instance,
53     default, observe,
54 )
55 from traitlets.config import LoggingConfigurable
56 
57 from .. import orm
58 from ..traitlets import Command
59 from ..spawner import LocalProcessSpawner
60 from ..utils import url_path_join
61 
62 class _MockUser(HasTraits):
63     name = Unicode()
64     server = Instance(orm.Server, allow_none=True)
65     state = Dict()
66     service = Instance(__module__ + '.Service')
67 
68 # We probably shouldn't use a Spawner here,
69 # but there are too many concepts to share.
70 
71 class _ServiceSpawner(LocalProcessSpawner):
72     """Subclass of LocalProcessSpawner
73     
74     Removes notebook-specific-ness from LocalProcessSpawner.
75     """
76     cwd = Unicode()
77     cmd = Command(minlen=0)
78     
79     def make_preexec_fn(self, name):
80         if not name or name == getuser():
81             # no setuid if no name
82             return
83         return super().make_preexec_fn(name)
84 
85     def start(self):
86         """Start the process"""
87         env = self.get_env()
88         cmd = self.cmd
89 
90         self.log.info("Spawning %s", ' '.join(pipes.quote(s) for s in cmd))
91         try:
92             self.proc = Popen(self.cmd, env=env,
93                 preexec_fn=self.make_preexec_fn(self.user.name),
94                 start_new_session=True, # don't forward signals
95                 cwd=self.cwd or None,
96             )
97         except PermissionError:
98             # use which to get abspath
99             script = shutil.which(cmd[0]) or cmd[0]
100             self.log.error("Permission denied trying to run %r. Does %s have access to this file?",
101                 script, self.user.name,
102             )
103             raise
104 
105         self.pid = self.proc.pid
106 
107 class Service(LoggingConfigurable):
108     """An object wrapping a service specification for Hub API consumers.
109 
110     A service has inputs:
111 
112     - name: str
113         the name of the service
114     - admin: bool(false)
115         whether the service should have administrative privileges
116     - url: str (None)
117         The URL where the service is/should be.
118         If specified, the service will be added to the proxy at /services/:name
119     
120     If a service is to be managed by the Hub, it has a few extra options:
121     
122     - command: (str/Popen list)
123         Command for JupyterHub to spawn the service.
124         Only use this if the service should be a subprocess.
125         If command is not specified, it is assumed to be managed
126         by a
127     - env: dict
128         environment variables to add to the current env
129     - user: str
130         The name of a system user to become.
131         If unspecified, run as the same user as the Hub.
132     """
133     
134     # inputs:
135     name = Unicode(
136         help="""The name of the service.
137         
138         If the service has an http endpoint, it
139         """
140     ).tag(input=True)
141     admin = Bool(False,
142         help="Does the service need admin-access to the Hub API?"
143     ).tag(input=True)
144     url = Unicode(
145         help="""URL of the service.
146         
147         Only specify if the service runs an HTTP(s) endpoint that.
148         If managed, will be passed as JUPYTERHUB_SERVICE_URL env.
149         """
150     ).tag(input=True)
151     api_token = Unicode(
152         help="""The API token to use for the service.
153         
154         If unspecified, an API token will be generated for managed services.
155         """
156     ).tag(input=True)
157     # Managed service API:
158 
159     @property
160     def managed(self):
161         """Am I managed by the Hub?"""
162         return bool(self.command)
163 
164     command = Command(minlen=0,
165         help="Command to spawn this service, if managed."
166     ).tag(input=True)
167     cwd = Unicode(
168         help="""The working directory in which to run the service."""
169     ).tag(input=True)
170     environment = Dict(
171         help="""Environment variables to pass to the service.
172         Only used if the Hub is spawning the service.
173         """
174     ).tag(input=True)
175     user = Unicode(getuser(),
176         help="""The user to become when launching the service.
177 
178         If unspecified, run the service as the same user as the Hub.
179         """
180     ).tag(input=True)
181 
182     domain = Unicode()
183     host = Unicode()
184     proc = Any()
185 
186     # handles on globals:
187     proxy = Any()
188     base_url = Unicode()
189     db = Any()
190     orm = Any()
191 
192     @property
193     def server(self):
194         return self.orm.server
195 
196     @property
197     def prefix(self):
198         return url_path_join(self.base_url, 'services', self.name)
199 
200     @property
201     def proxy_path(self):
202         if not self.server:
203             return ''
204         if self.domain:
205             return url_path_join('/' + self.domain, self.server.base_url)
206         else:
207             return self.server.base_url
208 
209     def __repr__(self):
210         return "<{cls}(name={name}{managed})>".format(
211             cls=self.__class__.__name__,
212             name=self.name,
213             managed=' managed' if self.managed else '',
214         )
215 
216     def start(self):
217         """Start a managed service"""
218         if not self.managed:
219             raise RuntimeError("Cannot start unmanaged service %s" % self)
220         self.log.info("Starting service %r: %r", self.name, self.command)
221         env = {}
222         env.update(self.environment)
223 
224         env['JUPYTERHUB_SERVICE_NAME'] = self.name
225         env['JUPYTERHUB_API_TOKEN'] = self.api_token
226         env['JUPYTERHUB_API_URL'] = self.hub_api_url
227         env['JUPYTERHUB_BASE_URL'] = self.base_url
228         env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
229         env['JUPYTERHUB_SERVICE_URL'] = self.url
230 
231         self.spawner = _ServiceSpawner(
232             cmd=self.command,
233             environment=env,
234             api_token=self.api_token,
235             cwd=self.cwd,
236             user=_MockUser(
237                 name=self.user,
238                 service=self,
239                 server=self.orm.server,
240             ),
241         )
242         self.spawner.start()
243         self.proc = self.spawner.proc
244         self.spawner.add_poll_callback(self._proc_stopped)
245         self.spawner.start_polling()
246 
247     def _proc_stopped(self):
248         """Called when the service process unexpectedly exits"""
249         self.log.error("Service %s exited with status %i", self.name, self.proc.returncode)
250         self.start()
251         
252     def stop(self):
253         """Stop a managed service"""
254         if not self.managed:
255             raise RuntimeError("Cannot start unmanaged service %s" % self)
256         self.spawner.stop_polling()
257         return self.spawner.stop()
258
```
--- 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/services/service.py b/jupyterhub/services/service.py
--- a/jupyterhub/services/service.py
+++ b/jupyterhub/services/service.py
@@ -225,8 +225,9 @@
         env['JUPYTERHUB_API_TOKEN'] = self.api_token
         env['JUPYTERHUB_API_URL'] = self.hub_api_url
         env['JUPYTERHUB_BASE_URL'] = self.base_url
-        env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
-        env['JUPYTERHUB_SERVICE_URL'] = self.url
+        if self.url:
+            env['JUPYTERHUB_SERVICE_URL'] = self.url
+            env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
 
         self.spawner = _ServiceSpawner(
             cmd=self.command,
@@ -248,7 +249,7 @@
         """Called when the service process unexpectedly exits"""
         self.log.error("Service %s exited with status %i", self.name, self.proc.returncode)
         self.start()
-        
+
     def stop(self):
         """Stop a managed service"""
         if not self.managed:

{"golden_diff": "diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py\n--- a/jupyterhub/services/service.py\n+++ b/jupyterhub/services/service.py\n@@ -225,8 +225,9 @@\n         env['JUPYTERHUB_API_TOKEN'] = self.api_token\n         env['JUPYTERHUB_API_URL'] = self.hub_api_url\n         env['JUPYTERHUB_BASE_URL'] = self.base_url\n-        env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url\n-        env['JUPYTERHUB_SERVICE_URL'] = self.url\n+        if self.url:\n+            env['JUPYTERHUB_SERVICE_URL'] = self.url\n+            env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url\n \n         self.spawner = _ServiceSpawner(\n             cmd=self.command,\n@@ -248,7 +249,7 @@\n         \"\"\"Called when the service process unexpectedly exits\"\"\"\n         self.log.error(\"Service %s exited with status %i\", self.name, self.proc.returncode)\n         self.start()\n-        \n+\n     def stop(self):\n         \"\"\"Stop a managed service\"\"\"\n         if not self.managed:\n", "issue": "Cull_Idle Instructions\nI'm having trouble using the [cull_idle example](https://github.com/jupyterhub/jupyterhub/tree/master/examples/cull-idle#configure-cull-idle-to-run-as-a-hub-managed-service) as a managed service. I added the [example config section](https://github.com/jupyterhub/jupyterhub/blob/master/examples/cull-idle/jupyterhub_config.py) to the bottom of my existing `jupyterhub_config.py` file:\n\n``` python\nc = get_config()\ndocker_ip = '172.17.0.1' \nhub_name='127.0.0.1'\n\nc.JupyterHub.ip = hub_name\nc.JupyterHub.hub_ip = docker_ip\nc.JupyterHub.debug_proxy = True\nc.JupyterHub.port = 8000\n\nc.JupyterHub.cleanup_servers = False\nc.JupyterHub.cleanup_proxy = True\nc.JupyterHub.ssl_key = '/etc/jupyterhub/ssl/jhub.key'\nc.JupyterHub.ssl_cert = '/etc/jupyterhub/ssl/jhub.crt'\n\n\nc.JupyterHub.spawner_class = 'dockerspawner.DockerSpawner'\nc.DockerSpawner.hub_ip_connect = docker_ip\nc.DockerSpawner.container_ip = docker_ip \n\n\nc.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'\nc.JupyterHub.log_level = 'DEBUG'\n\n#CULL_IDLE SECTION\nc.JupyterHub.services = [{\n    'name': 'cull-idle',\n    'admin': True,\n    'command': 'python cull_idle_servers.py --timeout=3600'.split()}]\n```\n\nI see the resulting error:\n\n```\n[C 2016-09-20 02:28:37.026 JupyterHub app:1444] Failed to start service cull-idle\n    Traceback (most recent call last):\n      File \"/usr/local/lib/python3.5/dist-packages/jupyterhub/app.py\", line 1442, in start\n        yield service.start()\n      File \"/usr/local/lib/python3.5/dist-packages/jupyterhub/services/service.py\", line 228, in start\n        env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url\n    AttributeError: 'NoneType' object has no attribute 'base_url'\n```\n\nAny ideas of what could be causing this? Thank you!\n\n", "before_files": [{"content": "\"\"\"A service is a process that talks to JupyterHub\n\nCases:\n\nManaged:\n  - managed by JuyterHub (always subprocess, no custom Spawners)\n  - always a long-running process\n  - managed services are restarted automatically if they exit unexpectedly\nUnmanaged:\n  - managed by external service (docker, systemd, etc.)\n  - do not need to be long-running processes, or processes at all\n\n\nURL: needs a route added to the proxy.\n  - Public route will always be /services/service-name\n  - url specified in config\n  - if port is 0, Hub will select a port\n\nAPI access:\n  - admin: tokens will have admin-access to the API\n  - not admin: tokens will only have non-admin access\n    (not much they can do other than defer to Hub for auth)\n\nAn externally managed service running on a URL::\n\n    {\n        'name': 'my-service',\n        'url': 'https://host:8888',\n        'admin': True,\n        'token': 'super-secret',\n    }\n\nA hub-managed service with no URL:\n\n    {\n        'name': 'cull-idle',\n        'command': ['python', '/path/to/cull-idle']\n        'admin': True,\n    }\n\"\"\"\n\nfrom getpass import getuser\nimport pipes\nimport shutil\nfrom subprocess import Popen\nfrom urllib.parse import urlparse\n\nfrom tornado import gen\n\nfrom traitlets import (\n    HasTraits,\n    Any, Bool, Dict, Unicode, Instance,\n    default, observe,\n)\nfrom traitlets.config import LoggingConfigurable\n\nfrom .. import orm\nfrom ..traitlets import Command\nfrom ..spawner import LocalProcessSpawner\nfrom ..utils import url_path_join\n\nclass _MockUser(HasTraits):\n    name = Unicode()\n    server = Instance(orm.Server, allow_none=True)\n    state = Dict()\n    service = Instance(__module__ + '.Service')\n\n# We probably shouldn't use a Spawner here,\n# but there are too many concepts to share.\n\nclass _ServiceSpawner(LocalProcessSpawner):\n    \"\"\"Subclass of LocalProcessSpawner\n    \n    Removes notebook-specific-ness from LocalProcessSpawner.\n    \"\"\"\n    cwd = Unicode()\n    cmd = Command(minlen=0)\n    \n    def make_preexec_fn(self, name):\n        if not name or name == getuser():\n            # no setuid if no name\n            return\n        return super().make_preexec_fn(name)\n\n    def start(self):\n        \"\"\"Start the process\"\"\"\n        env = self.get_env()\n        cmd = self.cmd\n\n        self.log.info(\"Spawning %s\", ' '.join(pipes.quote(s) for s in cmd))\n        try:\n            self.proc = Popen(self.cmd, env=env,\n                preexec_fn=self.make_preexec_fn(self.user.name),\n                start_new_session=True, # don't forward signals\n                cwd=self.cwd or None,\n            )\n        except PermissionError:\n            # use which to get abspath\n            script = shutil.which(cmd[0]) or cmd[0]\n            self.log.error(\"Permission denied trying to run %r. Does %s have access to this file?\",\n                script, self.user.name,\n            )\n            raise\n\n        self.pid = self.proc.pid\n\nclass Service(LoggingConfigurable):\n    \"\"\"An object wrapping a service specification for Hub API consumers.\n\n    A service has inputs:\n\n    - name: str\n        the name of the service\n    - admin: bool(false)\n        whether the service should have administrative privileges\n    - url: str (None)\n        The URL where the service is/should be.\n        If specified, the service will be added to the proxy at /services/:name\n    \n    If a service is to be managed by the Hub, it has a few extra options:\n    \n    - command: (str/Popen list)\n        Command for JupyterHub to spawn the service.\n        Only use this if the service should be a subprocess.\n        If command is not specified, it is assumed to be managed\n        by a\n    - env: dict\n        environment variables to add to the current env\n    - user: str\n        The name of a system user to become.\n        If unspecified, run as the same user as the Hub.\n    \"\"\"\n    \n    # inputs:\n    name = Unicode(\n        help=\"\"\"The name of the service.\n        \n        If the service has an http endpoint, it\n        \"\"\"\n    ).tag(input=True)\n    admin = Bool(False,\n        help=\"Does the service need admin-access to the Hub API?\"\n    ).tag(input=True)\n    url = Unicode(\n        help=\"\"\"URL of the service.\n        \n        Only specify if the service runs an HTTP(s) endpoint that.\n        If managed, will be passed as JUPYTERHUB_SERVICE_URL env.\n        \"\"\"\n    ).tag(input=True)\n    api_token = Unicode(\n        help=\"\"\"The API token to use for the service.\n        \n        If unspecified, an API token will be generated for managed services.\n        \"\"\"\n    ).tag(input=True)\n    # Managed service API:\n\n    @property\n    def managed(self):\n        \"\"\"Am I managed by the Hub?\"\"\"\n        return bool(self.command)\n\n    command = Command(minlen=0,\n        help=\"Command to spawn this service, if managed.\"\n    ).tag(input=True)\n    cwd = Unicode(\n        help=\"\"\"The working directory in which to run the service.\"\"\"\n    ).tag(input=True)\n    environment = Dict(\n        help=\"\"\"Environment variables to pass to the service.\n        Only used if the Hub is spawning the service.\n        \"\"\"\n    ).tag(input=True)\n    user = Unicode(getuser(),\n        help=\"\"\"The user to become when launching the service.\n\n        If unspecified, run the service as the same user as the Hub.\n        \"\"\"\n    ).tag(input=True)\n\n    domain = Unicode()\n    host = Unicode()\n    proc = Any()\n\n    # handles on globals:\n    proxy = Any()\n    base_url = Unicode()\n    db = Any()\n    orm = Any()\n\n    @property\n    def server(self):\n        return self.orm.server\n\n    @property\n    def prefix(self):\n        return url_path_join(self.base_url, 'services', self.name)\n\n    @property\n    def proxy_path(self):\n        if not self.server:\n            return ''\n        if self.domain:\n            return url_path_join('/' + self.domain, self.server.base_url)\n        else:\n            return self.server.base_url\n\n    def __repr__(self):\n        return \"<{cls}(name={name}{managed})>\".format(\n            cls=self.__class__.__name__,\n            name=self.name,\n            managed=' managed' if self.managed else '',\n        )\n\n    def start(self):\n        \"\"\"Start a managed service\"\"\"\n        if not self.managed:\n            raise RuntimeError(\"Cannot start unmanaged service %s\" % self)\n        self.log.info(\"Starting service %r: %r\", self.name, self.command)\n        env = {}\n        env.update(self.environment)\n\n        env['JUPYTERHUB_SERVICE_NAME'] = self.name\n        env['JUPYTERHUB_API_TOKEN'] = self.api_token\n        env['JUPYTERHUB_API_URL'] = self.hub_api_url\n        env['JUPYTERHUB_BASE_URL'] = self.base_url\n        env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url\n        env['JUPYTERHUB_SERVICE_URL'] = self.url\n\n        self.spawner = _ServiceSpawner(\n            cmd=self.command,\n            environment=env,\n            api_token=self.api_token,\n            cwd=self.cwd,\n            user=_MockUser(\n                name=self.user,\n                service=self,\n                server=self.orm.server,\n            ),\n        )\n        self.spawner.start()\n        self.proc = self.spawner.proc\n        self.spawner.add_poll_callback(self._proc_stopped)\n        self.spawner.start_polling()\n\n    def _proc_stopped(self):\n        \"\"\"Called when the service process unexpectedly exits\"\"\"\n        self.log.error(\"Service %s exited with status %i\", self.name, self.proc.returncode)\n        self.start()\n        \n    def stop(self):\n        \"\"\"Stop a managed service\"\"\"\n        if not self.managed:\n            raise RuntimeError(\"Cannot start unmanaged service %s\" % self)\n        self.spawner.stop_polling()\n        return self.spawner.stop()\n", "path": "jupyterhub/services/service.py"}], "after_files": [{"content": "\"\"\"A service is a process that talks to JupyterHub\n\nCases:\n\nManaged:\n  - managed by JuyterHub (always subprocess, no custom Spawners)\n  - always a long-running process\n  - managed services are restarted automatically if they exit unexpectedly\nUnmanaged:\n  - managed by external service (docker, systemd, etc.)\n  - do not need to be long-running processes, or processes at all\n\n\nURL: needs a route added to the proxy.\n  - Public route will always be /services/service-name\n  - url specified in config\n  - if port is 0, Hub will select a port\n\nAPI access:\n  - admin: tokens will have admin-access to the API\n  - not admin: tokens will only have non-admin access\n    (not much they can do other than defer to Hub for auth)\n\nAn externally managed service running on a URL::\n\n    {\n        'name': 'my-service',\n        'url': 'https://host:8888',\n        'admin': True,\n        'token': 'super-secret',\n    }\n\nA hub-managed service with no URL:\n\n    {\n        'name': 'cull-idle',\n        'command': ['python', '/path/to/cull-idle']\n        'admin': True,\n    }\n\"\"\"\n\nfrom getpass import getuser\nimport pipes\nimport shutil\nfrom subprocess import Popen\nfrom urllib.parse import urlparse\n\nfrom tornado import gen\n\nfrom traitlets import (\n    HasTraits,\n    Any, Bool, Dict, Unicode, Instance,\n    default, observe,\n)\nfrom traitlets.config import LoggingConfigurable\n\nfrom .. import orm\nfrom ..traitlets import Command\nfrom ..spawner import LocalProcessSpawner\nfrom ..utils import url_path_join\n\nclass _MockUser(HasTraits):\n    name = Unicode()\n    server = Instance(orm.Server, allow_none=True)\n    state = Dict()\n    service = Instance(__module__ + '.Service')\n\n# We probably shouldn't use a Spawner here,\n# but there are too many concepts to share.\n\nclass _ServiceSpawner(LocalProcessSpawner):\n    \"\"\"Subclass of LocalProcessSpawner\n    \n    Removes notebook-specific-ness from LocalProcessSpawner.\n    \"\"\"\n    cwd = Unicode()\n    cmd = Command(minlen=0)\n    \n    def make_preexec_fn(self, name):\n        if not name or name == getuser():\n            # no setuid if no name\n            return\n        return super().make_preexec_fn(name)\n\n    def start(self):\n        \"\"\"Start the process\"\"\"\n        env = self.get_env()\n        cmd = self.cmd\n\n        self.log.info(\"Spawning %s\", ' '.join(pipes.quote(s) for s in cmd))\n        try:\n            self.proc = Popen(self.cmd, env=env,\n                preexec_fn=self.make_preexec_fn(self.user.name),\n                start_new_session=True, # don't forward signals\n                cwd=self.cwd or None,\n            )\n        except PermissionError:\n            # use which to get abspath\n            script = shutil.which(cmd[0]) or cmd[0]\n            self.log.error(\"Permission denied trying to run %r. Does %s have access to this file?\",\n                script, self.user.name,\n            )\n            raise\n\n        self.pid = self.proc.pid\n\nclass Service(LoggingConfigurable):\n    \"\"\"An object wrapping a service specification for Hub API consumers.\n\n    A service has inputs:\n\n    - name: str\n        the name of the service\n    - admin: bool(false)\n        whether the service should have administrative privileges\n    - url: str (None)\n        The URL where the service is/should be.\n        If specified, the service will be added to the proxy at /services/:name\n    \n    If a service is to be managed by the Hub, it has a few extra options:\n    \n    - command: (str/Popen list)\n        Command for JupyterHub to spawn the service.\n        Only use this if the service should be a subprocess.\n        If command is not specified, it is assumed to be managed\n        by a\n    - env: dict\n        environment variables to add to the current env\n    - user: str\n        The name of a system user to become.\n        If unspecified, run as the same user as the Hub.\n    \"\"\"\n    \n    # inputs:\n    name = Unicode(\n        help=\"\"\"The name of the service.\n        \n        If the service has an http endpoint, it\n        \"\"\"\n    ).tag(input=True)\n    admin = Bool(False,\n        help=\"Does the service need admin-access to the Hub API?\"\n    ).tag(input=True)\n    url = Unicode(\n        help=\"\"\"URL of the service.\n        \n        Only specify if the service runs an HTTP(s) endpoint that.\n        If managed, will be passed as JUPYTERHUB_SERVICE_URL env.\n        \"\"\"\n    ).tag(input=True)\n    api_token = Unicode(\n        help=\"\"\"The API token to use for the service.\n        \n        If unspecified, an API token will be generated for managed services.\n        \"\"\"\n    ).tag(input=True)\n    # Managed service API:\n\n    @property\n    def managed(self):\n        \"\"\"Am I managed by the Hub?\"\"\"\n        return bool(self.command)\n\n    command = Command(minlen=0,\n        help=\"Command to spawn this service, if managed.\"\n    ).tag(input=True)\n    cwd = Unicode(\n        help=\"\"\"The working directory in which to run the service.\"\"\"\n    ).tag(input=True)\n    environment = Dict(\n        help=\"\"\"Environment variables to pass to the service.\n        Only used if the Hub is spawning the service.\n        \"\"\"\n    ).tag(input=True)\n    user = Unicode(getuser(),\n        help=\"\"\"The user to become when launching the service.\n\n        If unspecified, run the service as the same user as the Hub.\n        \"\"\"\n    ).tag(input=True)\n\n    domain = Unicode()\n    host = Unicode()\n    proc = Any()\n\n    # handles on globals:\n    proxy = Any()\n    base_url = Unicode()\n    db = Any()\n    orm = Any()\n\n    @property\n    def server(self):\n        return self.orm.server\n\n    @property\n    def prefix(self):\n        return url_path_join(self.base_url, 'services', self.name)\n\n    @property\n    def proxy_path(self):\n        if not self.server:\n            return ''\n        if self.domain:\n            return url_path_join('/' + self.domain, self.server.base_url)\n        else:\n            return self.server.base_url\n\n    def __repr__(self):\n        return \"<{cls}(name={name}{managed})>\".format(\n            cls=self.__class__.__name__,\n            name=self.name,\n            managed=' managed' if self.managed else '',\n        )\n\n    def start(self):\n        \"\"\"Start a managed service\"\"\"\n        if not self.managed:\n            raise RuntimeError(\"Cannot start unmanaged service %s\" % self)\n        self.log.info(\"Starting service %r: %r\", self.name, self.command)\n        env = {}\n        env.update(self.environment)\n\n        env['JUPYTERHUB_SERVICE_NAME'] = self.name\n        env['JUPYTERHUB_API_TOKEN'] = self.api_token\n        env['JUPYTERHUB_API_URL'] = self.hub_api_url\n        env['JUPYTERHUB_BASE_URL'] = self.base_url\n        if self.url:\n            env['JUPYTERHUB_SERVICE_URL'] = self.url\n            env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url\n\n        self.spawner = _ServiceSpawner(\n            cmd=self.command,\n            environment=env,\n            api_token=self.api_token,\n            cwd=self.cwd,\n            user=_MockUser(\n                name=self.user,\n                service=self,\n                server=self.orm.server,\n            ),\n        )\n        self.spawner.start()\n        self.proc = self.spawner.proc\n        self.spawner.add_poll_callback(self._proc_stopped)\n        self.spawner.start_polling()\n\n    def _proc_stopped(self):\n        \"\"\"Called when the service process unexpectedly exits\"\"\"\n        self.log.error(\"Service %s exited with status %i\", self.name, self.proc.returncode)\n        self.start()\n\n    def stop(self):\n        \"\"\"Stop a managed service\"\"\"\n        if not self.managed:\n            raise RuntimeError(\"Cannot start unmanaged service %s\" % self)\n        self.spawner.stop_polling()\n        return self.spawner.stop()\n", "path": "jupyterhub/services/service.py"}]}