""" Contains base Spawner class & default implementation """ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import errno import os import pipes import shutil import signal import sys import warnings from subprocess import Popen from tempfile import mkdtemp from sqlalchemy import inspect from tornado import gen from tornado.ioloop import PeriodicCallback from traitlets.config import LoggingConfigurable from traitlets import ( Any, Bool, Dict, Instance, Integer, Float, List, Unicode, observe, validate, ) from .objects import Server from .traitlets import Command, ByteSpecification from .utils import random_port, url_path_join, exponential_backoff 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 As JupyterHub supports multiple users, an instance of the Spawner subclass is created for each user. If there are 20 JupyterHub users, there will be 20 instances of the subclass. """ # private attributes for tracking status _spawn_pending = False _start_pending = False _stop_pending = False _proxy_pending = False _waiting_for_response = False _jupyterhub_version = None _spawn_future = None @property def _log_name(self): """Return username:servername or username Used in logging for consistency with named servers. """ if self.name: return '%s:%s' % (self.user.name, self.name) else: return self.user.name @property def pending(self): """Return the current pending event, if any Return False if nothing is pending. """ if self._spawn_pending: return 'spawn' elif self._stop_pending: return 'stop' return False @property def ready(self): """Is this server ready to use? A server is not ready if an event is pending. """ if self.pending: return False if self.server is None: return False return True @property def active(self): """Return True if the server is active. This includes fully running and ready or any pending start/stop event. """ return bool(self.pending or self.ready) authenticator = Any() hub = Any() orm_spawner = Any() db = Any() @observe('orm_spawner') def _orm_spawner_changed(self, change): if change.new and change.new.server: self._server = Server(orm_server=change.new.server) else: self._server = None user = Any() def __init_subclass__(cls, **kwargs): super().__init_subclass__() missing = [] for attr in ('start','stop', 'poll'): if getattr(Spawner, attr) is getattr(cls, attr): missing.append(attr) if missing: raise NotImplementedError("class `{}` needs to redefine the `start`," "`stop` and `poll` methods. `{}` not redefined.".format(cls.__name__, '`, `'.join(missing))) proxy_spec = Unicode() @property def server(self): if hasattr(self, '_server'): return self._server if self.orm_spawner and self.orm_spawner.server: return Server(orm_server=self.orm_spawner.server) @server.setter def server(self, server): self._server = server if self.orm_spawner: if self.orm_spawner.server is not None: # delete the old value db = inspect(self.orm_spawner.server).session db.delete(self.orm_spawner.server) if server is None: self.orm_spawner.server = None else: self.orm_spawner.server = server.orm_server @property def name(self): if self.orm_spawner: return self.orm_spawner.name return '' admin_access = Bool(False) api_token = Unicode() oauth_client_id = Unicode() will_resume = Bool(False, help="""Whether the Spawner will resume on next start Default is False where each launch of the Spawner will be a new instance. If True, an existing Spawner will resume instead of starting anew (e.g. resuming a Docker container), and API tokens in use when the Spawner stops will not be deleted. """ ) ip = Unicode('', help=""" The IP address (or hostname) the single-user server should listen on. The JupyterHub proxy implementation should be able to send packets to this interface. """ ).tag(config=True) port = Integer(0, help=""" The port for single-user servers to listen on. Defaults to `0`, which uses a randomly allocated port number each time. If set to a non-zero value, all Spawners will use the same port, which only makes sense if each server is on a different address, e.g. in containers. New in version 0.7. """ ).tag(config=True) start_timeout = Integer(60, help=""" Timeout (in seconds) before giving up on starting of single-user server. This is the timeout for start to return, not the timeout for the server to respond. Callers of spawner.start will assume that startup has failed if it takes longer than this. start should return when the server process is started and its location is known. """ ).tag(config=True) http_timeout = Integer(30, help=""" Timeout (in seconds) before giving up on a spawned HTTP server Once a server has successfully been spawned, this is the amount of time we wait before assuming that the server is unable to accept connections. """ ).tag(config=True) poll_interval = Integer(30, help=""" Interval (in seconds) on which to poll the spawner for single-user server's status. At every poll interval, each spawner's `.poll` method is called, which checks if the single-user server is still running. If it isn't running, then JupyterHub modifies its own state accordingly and removes appropriate routes from the configurable proxy. """ ).tag(config=True) _callbacks = List() _poll_callback = Any() debug = Bool(False, help="Enable debug-logging of the single-user server" ).tag(config=True) options_form = Unicode( help=""" An HTML form for options a user can specify on launching their server. The surrounding `