Files
jupyterhub/multiuser/spawner.py
2014-08-18 15:20:43 -07:00

186 lines
5.0 KiB
Python

"""Class for spawning single-user notebook servers."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import errno
import os
import signal
import sys
import time
from subprocess import Popen
from tornado import gen
from IPython.config import LoggingConfigurable
from IPython.utils.traitlets import (
Any, Dict, Instance, Integer, List, Unicode,
)
from .utils import random_port, wait_for_server
class Spawner(LoggingConfigurable):
"""Base class for spawning single-user notebook servers.
Subclass this, and override the following methods:
- load_state
- get_state
- start
- stop
- poll
"""
user = Any()
hub = Any()
api_token = Unicode()
env_prefix = Unicode('IPY_')
def _env_key(self, d, key, value):
d['%s%s' % (self.env_prefix, key)] = value
env = Dict()
def _env_default(self):
env = os.environ.copy()
self._env_key(env, 'COOKIE_SECRET', self.user.server.cookie_secret)
self._env_key(env, 'API_TOKEN', self.api_token)
return env
@classmethod
def fromJSON(cls, state, **kwargs):
"""Create a new instance, and load its JSON state
state will be a dict, loaded from JSON in the database.
"""
inst = cls(**kwargs)
inst.load_state(state)
return inst
def load_state(self, state):
"""load state from the database
This is the extensible part of state
Override in a subclass if there is state to load.
See Also
--------
get_state
"""
pass
def get_state(self):
"""store the state necessary for load_state
A black box of extra state for custom spawners
Returns
-------
state: dict
a JSONable dict of state
"""
return {}
def get_args(self):
"""Return the arguments to be passed after self.cmd"""
return [
'--user=%s' % self.user.name,
'--port=%i' % self.user.server.port,
'--cookie-name=%s' % self.user.server.cookie_name,
'--base-url=%s' % self.user.server.base_url,
'--hub-prefix=%s' % self.hub.server.base_url,
'--hub-api-url=%s' % self.hub.api_host_url,
]
def start(self):
raise NotImplementedError("Override in subclass")
def stop(self):
raise NotImplementedError("Override in subclass")
def poll(self):
raise NotImplementedError("Override in subclass")
class PopenSpawner(Spawner):
"""A Spawner that just uses Popen to start local processes."""
cmd = List(Unicode, config=True,
help="""The command used for starting notebooks."""
)
def _cmd_default(self):
# should have sudo -u self.user
return [sys.executable, '-m', 'multiuser.singleuser']
proc = Instance(Popen)
pid = Integer()
def load_state(self, state):
self.pid = state['pid']
def get_state(self):
return dict(pid=self.pid)
def start(self):
self.user.server.port = random_port()
cmd = self.cmd + self.get_args()
self.log.info("Spawning %r", cmd)
self.proc = Popen(cmd, env=self.env,
# don't forward signals
preexec_fn=os.setpgrp,
)
self.pid = self.proc.pid
def poll(self):
# if we started the process, poll with Popen
if self.proc is not None:
return self.proc.poll()
# if we resumed from stored state,
# we don't have the Popen handle anymore
# this doesn't work on Windows.
# multi-user doesn't support Windows.
try:
os.kill(self.pid, 0)
except OSError as e:
if e.errno == errno.ESRCH:
# no such process, return exitcode == 0, since we don't know
return 0
else:
# None indicates the process is running
return None
def _wait_for_death(self, timeout=10):
"""wait for the process to die, up to timeout seconds"""
for i in range(int(timeout * 10)):
if self.poll() is None:
break
else:
time.sleep(0.1)
def stop(self, now=False):
"""stop the subprocess"""
if not now:
# double-sigint to request clean shutdown
os.kill(self.pid, signal.SIGINT)
os.kill(self.pid, signal.SIGINT)
self._wait_for_death(10)
# clean shutdown failed, use TERM
if self.poll() is None:
os.kill(self.pid, signal.SIGTERM)
self._wait_for_death(5)
# TERM failed, use KILL
if self.poll() is None:
os.kill(self.pid, signal.SIGKILL)
self._wait_for_death(5)
# it all failed, zombie process