Add extended docs for LocalProcessSpawner too

This commit is contained in:
YuviPanda
2016-11-28 23:01:17 -08:00
parent b0dc52781e
commit 4687fbe075

View File

@@ -658,48 +658,97 @@ def set_user_setuid(username):
class LocalProcessSpawner(Spawner): class LocalProcessSpawner(Spawner):
"""A Spawner that just uses Popen to start local processes as users. """
A Spawner that uses `subprocess.Popen` to start single-user servers as local processes.
Requires users to exist on the local system.
Requires local UNIX users matching the authenticated users to exist. Does not work on
Windows.
This is the default spawner for JupyterHub. This is the default spawner for JupyterHub.
""" """
INTERRUPT_TIMEOUT = Integer(10, INTERRUPT_TIMEOUT = Integer(
help="Seconds to wait for process to halt after SIGINT before proceeding to SIGTERM" 10,
help="""
Seconds to wait for single-user server process to halt after SIGINT.
If the process has not exited cleanly after this many seconds, a SIGTERM is sent.
"""
).tag(config=True) ).tag(config=True)
TERM_TIMEOUT = Integer(5,
help="Seconds to wait for process to halt after SIGTERM before proceeding to SIGKILL" TERM_TIMEOUT = Integer(
5,
help="""
Seconds to wait for single-user server process to halt after SIGTERM.
If the process does not exit cleanly after this many seconds of SIGTERM, a SIGKILL is sent.
"""
).tag(config=True) ).tag(config=True)
KILL_TIMEOUT = Integer(5, KILL_TIMEOUT = Integer(5,
help="Seconds to wait for process to halt after SIGKILL before giving up" help="""
Seconds to wait for process to halt after SIGKILL before giving up.
If the process does not exit cleanly after this many seconds of SIGKILL, it becomes a zombie
process. The hub process will log a warning and then give up.
"""
).tag(config=True) ).tag(config=True)
proc = Instance(Popen, allow_none=True) proc = Instance(
pid = Integer(0) Popen,
allow_none=True,
help="""
The process representing the single-user server process spawned for current user.
Is None if no process has been spawned yet.
""")
pid = Integer(
0,
help="""
The process id (pid) of the single-user server process spawned for current user.
"""
)
def make_preexec_fn(self, name): def make_preexec_fn(self, name):
"""
Return a function that can be used to set the userid of the spawned process to user with name `name`
This function can be safely passed to `preexec_fn` of `Popen`
"""
return set_user_setuid(name) return set_user_setuid(name)
def load_state(self, state): def load_state(self, state):
"""load pid from state""" """
Restore state about spawned single-user server after a hub restart.
We currently only store/restore the process id.
"""
super(LocalProcessSpawner, self).load_state(state) super(LocalProcessSpawner, self).load_state(state)
if 'pid' in state: if 'pid' in state:
self.pid = state['pid'] self.pid = state['pid']
def get_state(self): def get_state(self):
"""add pid to state""" """
Save state that is needed to restore this spawner instance after a hub restore.
We currently only store/restore the process id.
"""
state = super(LocalProcessSpawner, self).get_state() state = super(LocalProcessSpawner, self).get_state()
if self.pid: if self.pid:
state['pid'] = self.pid state['pid'] = self.pid
return state return state
def clear_state(self): def clear_state(self):
"""clear pid state""" """
Clear stored state about this spawner.
"""
super(LocalProcessSpawner, self).clear_state() super(LocalProcessSpawner, self).clear_state()
self.pid = 0 self.pid = 0
def user_env(self, env): def user_env(self, env):
"""
Augment environment of spawned process with user specific env variables.
"""
env['USER'] = self.user.name env['USER'] = self.user.name
home = pwd.getpwnam(self.user.name).pw_dir home = pwd.getpwnam(self.user.name).pw_dir
shell = pwd.getpwnam(self.user.name).pw_shell shell = pwd.getpwnam(self.user.name).pw_shell
@@ -710,16 +759,20 @@ class LocalProcessSpawner(Spawner):
if shell: if shell:
env['SHELL'] = shell env['SHELL'] = shell
return env return env
def get_env(self): def get_env(self):
"""Add user environment variables""" """
Get the complete set of environment variables to be set in the spawned process.
"""
env = super().get_env() env = super().get_env()
env = self.user_env(env) env = self.user_env(env)
return env return env
@gen.coroutine @gen.coroutine
def start(self): def start(self):
"""Start the process""" """
Start the single-user server.
"""
self.port = random_port() self.port = random_port()
cmd = [] cmd = []
env = self.get_env() env = self.get_env()
@@ -756,7 +809,12 @@ class LocalProcessSpawner(Spawner):
@gen.coroutine @gen.coroutine
def poll(self): def poll(self):
"""Poll the process""" """
Poll the spawned process to see if it is still running.
If the process is still running, we return None. If it is not running,
we return the exit code of the process if we have access to it, or 0 otherwise.
"""
# if we started the process, poll with Popen # if we started the process, poll with Popen
if self.proc is not None: if self.proc is not None:
status = self.proc.poll() status = self.proc.poll()
@@ -764,15 +822,14 @@ class LocalProcessSpawner(Spawner):
# clear state if the process is done # clear state if the process is done
self.clear_state() self.clear_state()
return status return status
# if we resumed from stored state, # if we resumed from stored state,
# we don't have the Popen handle anymore, so rely on self.pid # we don't have the Popen handle anymore, so rely on self.pid
if not self.pid: if not self.pid:
# no pid, not running # no pid, not running
self.clear_state() self.clear_state()
return 0 return 0
# send signal 0 to check if PID exists # send signal 0 to check if PID exists
# this doesn't work on Windows, but that's okay because we don't support Windows. # this doesn't work on Windows, but that's okay because we don't support Windows.
alive = yield self._signal(0) alive = yield self._signal(0)
@@ -781,10 +838,16 @@ class LocalProcessSpawner(Spawner):
return 0 return 0
else: else:
return None return None
@gen.coroutine @gen.coroutine
def _signal(self, sig): def _signal(self, sig):
"""simple implementation of signal, which we can use when we are using setuid (we are root)""" """
Send given signal to a single-user server's process.
Returns True if the process still exists, False otherwise.
The hub process is assumed to be root and hence have enough privilages to do this.
"""
try: try:
os.kill(self.pid, sig) os.kill(self.pid, sig)
except OSError as e: except OSError as e:
@@ -793,12 +856,13 @@ class LocalProcessSpawner(Spawner):
else: else:
raise raise
return True # process exists return True # process exists
@gen.coroutine @gen.coroutine
def stop(self, now=False): def stop(self, now=False):
"""stop the subprocess """
Stop the single-user server process for the current user.
if `now`, skip waiting for clean shutdown
If `now` is set to True, do not wait for the process to die. Otherwise, it'll wait.
""" """
if not now: if not now:
status = yield self.poll() status = yield self.poll()
@@ -807,7 +871,7 @@ class LocalProcessSpawner(Spawner):
self.log.debug("Interrupting %i", self.pid) self.log.debug("Interrupting %i", self.pid)
yield self._signal(signal.SIGINT) yield self._signal(signal.SIGINT)
yield self.wait_for_death(self.INTERRUPT_TIMEOUT) yield self.wait_for_death(self.INTERRUPT_TIMEOUT)
# clean shutdown failed, use TERM # clean shutdown failed, use TERM
status = yield self.poll() status = yield self.poll()
if status is not None: if status is not None:
@@ -815,7 +879,7 @@ class LocalProcessSpawner(Spawner):
self.log.debug("Terminating %i", self.pid) self.log.debug("Terminating %i", self.pid)
yield self._signal(signal.SIGTERM) yield self._signal(signal.SIGTERM)
yield self.wait_for_death(self.TERM_TIMEOUT) yield self.wait_for_death(self.TERM_TIMEOUT)
# TERM failed, use KILL # TERM failed, use KILL
status = yield self.poll() status = yield self.poll()
if status is not None: if status is not None:
@@ -828,4 +892,3 @@ class LocalProcessSpawner(Spawner):
if status is None: if status is None:
# it all failed, zombie process # it all failed, zombie process
self.log.warning("Process %i never died", self.pid) self.log.warning("Process %i never died", self.pid)