first comit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .factory import KernelProvisionerFactory # noqa
|
||||
from .local_provisioner import LocalProvisioner # noqa
|
||||
from .provisioner_base import KernelProvisionerBase # noqa
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,200 @@
|
||||
"""Kernel Provisioner Classes"""
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import glob
|
||||
import sys
|
||||
from os import getenv, path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
# See compatibility note on `group` keyword in https://docs.python.org/3/library/importlib.metadata.html#entry-points
|
||||
if sys.version_info < (3, 10): # pragma: no cover
|
||||
from importlib_metadata import EntryPoint, entry_points # type:ignore[import-not-found]
|
||||
else: # pragma: no cover
|
||||
from importlib.metadata import EntryPoint, entry_points
|
||||
|
||||
from traitlets.config import SingletonConfigurable, Unicode, default
|
||||
|
||||
from .provisioner_base import KernelProvisionerBase
|
||||
|
||||
|
||||
class KernelProvisionerFactory(SingletonConfigurable):
|
||||
"""
|
||||
:class:`KernelProvisionerFactory` is responsible for creating provisioner instances.
|
||||
|
||||
A singleton instance, `KernelProvisionerFactory` is also used by the :class:`KernelSpecManager`
|
||||
to validate `kernel_provisioner` references found in kernel specifications to confirm their
|
||||
availability (in cases where the kernel specification references a kernel provisioner that has
|
||||
not been installed into the current Python environment).
|
||||
|
||||
It's ``default_provisioner_name`` attribute can be used to specify the default provisioner
|
||||
to use when a kernel_spec is found to not reference a provisioner. It's value defaults to
|
||||
`"local-provisioner"` which identifies the local provisioner implemented by
|
||||
:class:`LocalProvisioner`.
|
||||
"""
|
||||
|
||||
GROUP_NAME = "jupyter_client.kernel_provisioners"
|
||||
provisioners: Dict[str, EntryPoint] = {}
|
||||
|
||||
default_provisioner_name_env = "JUPYTER_DEFAULT_PROVISIONER_NAME"
|
||||
default_provisioner_name = Unicode(
|
||||
config=True,
|
||||
help="""Indicates the name of the provisioner to use when no kernel_provisioner
|
||||
entry is present in the kernelspec.""",
|
||||
)
|
||||
|
||||
@default("default_provisioner_name")
|
||||
def _default_provisioner_name_default(self) -> str:
|
||||
"""The default provisioner name."""
|
||||
return getenv(self.default_provisioner_name_env, "local-provisioner")
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"""Initialize a kernel provisioner factory."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
for ep in KernelProvisionerFactory._get_all_provisioners():
|
||||
self.provisioners[ep.name] = ep
|
||||
|
||||
def is_provisioner_available(self, kernel_spec: Any) -> bool:
|
||||
"""
|
||||
Reads the associated ``kernel_spec`` to determine the provisioner and returns whether it
|
||||
exists as an entry_point (True) or not (False). If the referenced provisioner is not
|
||||
in the current cache or cannot be loaded via entry_points, a warning message is issued
|
||||
indicating it is not available.
|
||||
"""
|
||||
is_available: bool = True
|
||||
provisioner_cfg = self._get_provisioner_config(kernel_spec)
|
||||
provisioner_name = str(provisioner_cfg.get("provisioner_name"))
|
||||
if not self._check_availability(provisioner_name):
|
||||
is_available = False
|
||||
self.log.warning(
|
||||
f"Kernel '{kernel_spec.display_name}' is referencing a kernel "
|
||||
f"provisioner ('{provisioner_name}') that is not available. "
|
||||
f"Ensure the appropriate package has been installed and retry."
|
||||
)
|
||||
return is_available
|
||||
|
||||
def create_provisioner_instance(
|
||||
self, kernel_id: str, kernel_spec: Any, parent: Any
|
||||
) -> KernelProvisionerBase:
|
||||
"""
|
||||
Reads the associated ``kernel_spec`` to see if it has a `kernel_provisioner` stanza.
|
||||
If one exists, it instantiates an instance. If a kernel provisioner is not
|
||||
specified in the kernel specification, a default provisioner stanza is fabricated
|
||||
and instantiated corresponding to the current value of ``default_provisioner_name`` trait.
|
||||
The instantiated instance is returned.
|
||||
|
||||
If the provisioner is found to not exist (not registered via entry_points),
|
||||
`ModuleNotFoundError` is raised.
|
||||
"""
|
||||
provisioner_cfg = self._get_provisioner_config(kernel_spec)
|
||||
provisioner_name = str(provisioner_cfg.get("provisioner_name"))
|
||||
if not self._check_availability(provisioner_name):
|
||||
msg = f"Kernel provisioner '{provisioner_name}' has not been registered."
|
||||
raise ModuleNotFoundError(msg)
|
||||
|
||||
self.log.debug(
|
||||
f"Instantiating kernel '{kernel_spec.display_name}' with "
|
||||
f"kernel provisioner: {provisioner_name}"
|
||||
)
|
||||
provisioner_class = self.provisioners[provisioner_name].load()
|
||||
provisioner_config = provisioner_cfg.get("config")
|
||||
provisioner: KernelProvisionerBase = provisioner_class(
|
||||
kernel_id=kernel_id, kernel_spec=kernel_spec, parent=parent, **provisioner_config
|
||||
)
|
||||
return provisioner
|
||||
|
||||
def _check_availability(self, provisioner_name: str) -> bool:
|
||||
"""
|
||||
Checks that the given provisioner is available.
|
||||
|
||||
If the given provisioner is not in the current set of loaded provisioners an attempt
|
||||
is made to fetch the named entry point and, if successful, loads it into the cache.
|
||||
|
||||
:param provisioner_name:
|
||||
:return:
|
||||
"""
|
||||
is_available = True
|
||||
if provisioner_name not in self.provisioners:
|
||||
try:
|
||||
ep = self._get_provisioner(provisioner_name)
|
||||
self.provisioners[provisioner_name] = ep # Update cache
|
||||
except Exception:
|
||||
is_available = False
|
||||
return is_available
|
||||
|
||||
def _get_provisioner_config(self, kernel_spec: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Return the kernel_provisioner stanza from the kernel_spec.
|
||||
|
||||
Checks the kernel_spec's metadata dictionary for a kernel_provisioner entry.
|
||||
If found, it is returned, else one is created relative to the DEFAULT_PROVISIONER
|
||||
and returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kernel_spec : Any - this is a KernelSpec type but listed as Any to avoid circular import
|
||||
The kernel specification object from which the provisioner dictionary is derived.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The provisioner portion of the kernel_spec. If one does not exist, it will contain
|
||||
the default information. If no `config` sub-dictionary exists, an empty `config`
|
||||
dictionary will be added.
|
||||
"""
|
||||
env_provisioner = kernel_spec.metadata.get("kernel_provisioner", {})
|
||||
if "provisioner_name" in env_provisioner: # If no provisioner_name, return default
|
||||
if (
|
||||
"config" not in env_provisioner
|
||||
): # if provisioner_name, but no config stanza, add one
|
||||
env_provisioner.update({"config": {}})
|
||||
return env_provisioner # Return what we found (plus config stanza if necessary)
|
||||
return {"provisioner_name": self.default_provisioner_name, "config": {}}
|
||||
|
||||
def get_provisioner_entries(self) -> Dict[str, str]:
|
||||
"""
|
||||
Returns a dictionary of provisioner entries.
|
||||
|
||||
The key is the provisioner name for its entry point. The value is the colon-separated
|
||||
string of the entry point's module name and object name.
|
||||
"""
|
||||
entries = {}
|
||||
for name, ep in self.provisioners.items():
|
||||
entries[name] = ep.value
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _get_all_provisioners() -> List[EntryPoint]:
|
||||
"""Wrapper around entry_points (to fetch the set of provisioners) - primarily to facilitate testing."""
|
||||
return entry_points(group=KernelProvisionerFactory.GROUP_NAME)
|
||||
|
||||
def _get_provisioner(self, name: str) -> EntryPoint:
|
||||
"""Wrapper around entry_points (to fetch a single provisioner) - primarily to facilitate testing."""
|
||||
eps = entry_points(group=KernelProvisionerFactory.GROUP_NAME, name=name)
|
||||
if eps:
|
||||
return eps[0]
|
||||
|
||||
# Check if the entrypoint name is 'local-provisioner'. Although this should never
|
||||
# happen, we have seen cases where the previous distribution of jupyter_client has
|
||||
# remained which doesn't include kernel-provisioner entrypoints (so 'local-provisioner'
|
||||
# is deemed not found even though its definition is in THIS package). In such cases,
|
||||
# the entrypoints package uses what it first finds - which is the older distribution
|
||||
# resulting in a violation of a supposed invariant condition. To address this scenario,
|
||||
# we will log a warning message indicating this situation, then build the entrypoint
|
||||
# instance ourselves - since we have that information.
|
||||
if name == "local-provisioner":
|
||||
distros = glob.glob(f"{path.dirname(path.dirname(__file__))}-*")
|
||||
self.log.warning(
|
||||
f"Kernel Provisioning: The 'local-provisioner' is not found. This is likely "
|
||||
f"due to the presence of multiple jupyter_client distributions and a previous "
|
||||
f"distribution is being used as the source for entrypoints - which does not "
|
||||
f"include 'local-provisioner'. That distribution should be removed such that "
|
||||
f"only the version-appropriate distribution remains (version >= 7). Until "
|
||||
f"then, a 'local-provisioner' entrypoint will be automatically constructed "
|
||||
f"and used.\nThe candidate distribution locations are: {distros}"
|
||||
)
|
||||
return EntryPoint(
|
||||
"local-provisioner", "jupyter_client.provisioning", "LocalProvisioner"
|
||||
)
|
||||
|
||||
raise
|
||||
@@ -0,0 +1,242 @@
|
||||
"""Kernel Provisioner Classes"""
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from ..connect import KernelConnectionInfo, LocalPortCache
|
||||
from ..launcher import launch_kernel
|
||||
from ..localinterfaces import is_local_ip, local_ips
|
||||
from .provisioner_base import KernelProvisionerBase
|
||||
|
||||
|
||||
class LocalProvisioner(KernelProvisionerBase): # type:ignore[misc]
|
||||
"""
|
||||
:class:`LocalProvisioner` is a concrete class of ABC :py:class:`KernelProvisionerBase`
|
||||
and is the out-of-box default implementation used when no kernel provisioner is
|
||||
specified in the kernel specification (``kernel.json``). It provides functional
|
||||
parity to existing applications by launching the kernel locally and using
|
||||
:class:`subprocess.Popen` to manage its lifecycle.
|
||||
|
||||
This class is intended to be subclassed for customizing local kernel environments
|
||||
and serve as a reference implementation for other custom provisioners.
|
||||
"""
|
||||
|
||||
process = None
|
||||
_exit_future = None
|
||||
pid = None
|
||||
pgid = None
|
||||
ip = None
|
||||
ports_cached = False
|
||||
|
||||
@property
|
||||
def has_process(self) -> bool:
|
||||
return self.process is not None
|
||||
|
||||
async def poll(self) -> Optional[int]:
|
||||
"""Poll the provisioner."""
|
||||
ret = 0
|
||||
if self.process:
|
||||
ret = self.process.poll() # type:ignore[unreachable]
|
||||
return ret
|
||||
|
||||
async def wait(self) -> Optional[int]:
|
||||
"""Wait for the provisioner process."""
|
||||
ret = 0
|
||||
if self.process:
|
||||
# Use busy loop at 100ms intervals, polling until the process is
|
||||
# not alive. If we find the process is no longer alive, complete
|
||||
# its cleanup via the blocking wait(). Callers are responsible for
|
||||
# issuing calls to wait() using a timeout (see kill()).
|
||||
while await self.poll() is None: # type:ignore[unreachable]
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Process is no longer alive, wait and clear
|
||||
ret = self.process.wait()
|
||||
# Make sure all the fds get closed.
|
||||
for attr in ["stdout", "stderr", "stdin"]:
|
||||
fid = getattr(self.process, attr)
|
||||
if fid:
|
||||
fid.close()
|
||||
self.process = None # allow has_process to now return False
|
||||
return ret
|
||||
|
||||
async def send_signal(self, signum: int) -> None:
|
||||
"""Sends a signal to the process group of the kernel (this
|
||||
usually includes the kernel and any subprocesses spawned by
|
||||
the kernel).
|
||||
|
||||
Note that since only SIGTERM is supported on Windows, we will
|
||||
check if the desired signal is for interrupt and apply the
|
||||
applicable code on Windows in that case.
|
||||
"""
|
||||
if self.process:
|
||||
if signum == signal.SIGINT and sys.platform == "win32": # type:ignore[unreachable]
|
||||
from ..win_interrupt import send_interrupt
|
||||
|
||||
send_interrupt(self.process.win32_interrupt_event)
|
||||
return
|
||||
|
||||
# Prefer process-group over process
|
||||
if self.pgid and hasattr(os, "killpg"):
|
||||
try:
|
||||
os.killpg(self.pgid, signum)
|
||||
return
|
||||
except OSError:
|
||||
pass # We'll retry sending the signal to only the process below
|
||||
|
||||
# If we're here, send the signal to the process and let caller handle exceptions
|
||||
self.process.send_signal(signum)
|
||||
return
|
||||
|
||||
async def kill(self, restart: bool = False) -> None:
|
||||
"""Kill the provisioner and optionally restart."""
|
||||
if self.process:
|
||||
if hasattr(signal, "SIGKILL"): # type:ignore[unreachable]
|
||||
# If available, give preference to signalling the process-group over `kill()`.
|
||||
try:
|
||||
await self.send_signal(signal.SIGKILL)
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
self.process.kill()
|
||||
except OSError as e:
|
||||
LocalProvisioner._tolerate_no_process(e)
|
||||
|
||||
async def terminate(self, restart: bool = False) -> None:
|
||||
"""Terminate the provisioner and optionally restart."""
|
||||
if self.process:
|
||||
if hasattr(signal, "SIGTERM"): # type:ignore[unreachable]
|
||||
# If available, give preference to signalling the process group over `terminate()`.
|
||||
try:
|
||||
await self.send_signal(signal.SIGTERM)
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
self.process.terminate()
|
||||
except OSError as e:
|
||||
LocalProvisioner._tolerate_no_process(e)
|
||||
|
||||
@staticmethod
|
||||
def _tolerate_no_process(os_error: OSError) -> None:
|
||||
# In Windows, we will get an Access Denied error if the process
|
||||
# has already terminated. Ignore it.
|
||||
if sys.platform == "win32":
|
||||
if os_error.winerror != 5:
|
||||
raise
|
||||
# On Unix, we may get an ESRCH error (or ProcessLookupError instance) if
|
||||
# the process has already terminated. Ignore it.
|
||||
else:
|
||||
from errno import ESRCH
|
||||
|
||||
if not isinstance(os_error, ProcessLookupError) or os_error.errno != ESRCH:
|
||||
raise
|
||||
|
||||
async def cleanup(self, restart: bool = False) -> None:
|
||||
"""Clean up the resources used by the provisioner and optionally restart."""
|
||||
if self.ports_cached and not restart:
|
||||
# provisioner is about to be destroyed, return cached ports
|
||||
lpc = LocalPortCache.instance()
|
||||
ports = (
|
||||
self.connection_info["shell_port"],
|
||||
self.connection_info["iopub_port"],
|
||||
self.connection_info["stdin_port"],
|
||||
self.connection_info["hb_port"],
|
||||
self.connection_info["control_port"],
|
||||
)
|
||||
for port in ports:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(port, int)
|
||||
lpc.return_port(port)
|
||||
|
||||
async def pre_launch(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""Perform any steps in preparation for kernel process launch.
|
||||
|
||||
This includes applying additional substitutions to the kernel launch command and env.
|
||||
It also includes preparation of launch parameters.
|
||||
|
||||
Returns the updated kwargs.
|
||||
"""
|
||||
|
||||
# This should be considered temporary until a better division of labor can be defined.
|
||||
km = self.parent
|
||||
if km:
|
||||
if km.transport == "tcp" and not is_local_ip(km.ip):
|
||||
msg = (
|
||||
"Can only launch a kernel on a local interface. "
|
||||
f"This one is not: {km.ip}."
|
||||
"Make sure that the '*_address' attributes are "
|
||||
"configured properly. "
|
||||
f"Currently valid addresses are: {local_ips()}"
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
# build the Popen cmd
|
||||
extra_arguments = kwargs.pop("extra_arguments", [])
|
||||
|
||||
# write connection file / get default ports
|
||||
# TODO - change when handshake pattern is adopted
|
||||
if km.cache_ports and not self.ports_cached:
|
||||
lpc = LocalPortCache.instance()
|
||||
km.shell_port = lpc.find_available_port(km.ip)
|
||||
km.iopub_port = lpc.find_available_port(km.ip)
|
||||
km.stdin_port = lpc.find_available_port(km.ip)
|
||||
km.hb_port = lpc.find_available_port(km.ip)
|
||||
km.control_port = lpc.find_available_port(km.ip)
|
||||
self.ports_cached = True
|
||||
if "env" in kwargs:
|
||||
jupyter_session = kwargs["env"].get("JPY_SESSION_NAME", "")
|
||||
km.write_connection_file(jupyter_session=jupyter_session)
|
||||
else:
|
||||
km.write_connection_file()
|
||||
self.connection_info = km.get_connection_info()
|
||||
|
||||
kernel_cmd = km.format_kernel_cmd(
|
||||
extra_arguments=extra_arguments
|
||||
) # This needs to remain here for b/c
|
||||
else:
|
||||
extra_arguments = kwargs.pop("extra_arguments", [])
|
||||
kernel_cmd = self.kernel_spec.argv + extra_arguments
|
||||
|
||||
return await super().pre_launch(cmd=kernel_cmd, **kwargs)
|
||||
|
||||
async def launch_kernel(self, cmd: List[str], **kwargs: Any) -> KernelConnectionInfo:
|
||||
"""Launch a kernel with a command."""
|
||||
scrubbed_kwargs = LocalProvisioner._scrub_kwargs(kwargs)
|
||||
self.process = launch_kernel(cmd, **scrubbed_kwargs)
|
||||
pgid = None
|
||||
if hasattr(os, "getpgid"):
|
||||
try:
|
||||
pgid = os.getpgid(self.process.pid)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.pid = self.process.pid
|
||||
self.pgid = pgid
|
||||
return self.connection_info
|
||||
|
||||
@staticmethod
|
||||
def _scrub_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Remove any keyword arguments that Popen does not tolerate."""
|
||||
keywords_to_scrub: List[str] = ["extra_arguments", "kernel_id"]
|
||||
scrubbed_kwargs = kwargs.copy()
|
||||
for kw in keywords_to_scrub:
|
||||
scrubbed_kwargs.pop(kw, None)
|
||||
return scrubbed_kwargs
|
||||
|
||||
async def get_provisioner_info(self) -> Dict:
|
||||
"""Captures the base information necessary for persistence relative to this instance."""
|
||||
provisioner_info = await super().get_provisioner_info()
|
||||
provisioner_info.update({"pid": self.pid, "pgid": self.pgid, "ip": self.ip})
|
||||
return provisioner_info
|
||||
|
||||
async def load_provisioner_info(self, provisioner_info: Dict) -> None:
|
||||
"""Loads the base information necessary for persistence relative to this instance."""
|
||||
await super().load_provisioner_info(provisioner_info)
|
||||
self.pid = provisioner_info["pid"]
|
||||
self.pgid = provisioner_info["pgid"]
|
||||
self.ip = provisioner_info["ip"]
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Kernel Provisioner Classes"""
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import os
|
||||
from abc import ABC, ABCMeta, abstractmethod
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from traitlets.config import Instance, LoggingConfigurable, Unicode
|
||||
|
||||
from ..connect import KernelConnectionInfo
|
||||
|
||||
|
||||
class KernelProvisionerMeta(ABCMeta, type(LoggingConfigurable)): # type: ignore[misc]
|
||||
pass
|
||||
|
||||
|
||||
class KernelProvisionerBase( # type:ignore[misc]
|
||||
ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta
|
||||
):
|
||||
"""
|
||||
Abstract base class defining methods for KernelProvisioner classes.
|
||||
|
||||
A majority of methods are abstract (requiring implementations via a subclass) while
|
||||
some are optional and others provide implementations common to all instances.
|
||||
Subclasses should be aware of which methods require a call to the superclass.
|
||||
|
||||
Many of these methods model those of :class:`subprocess.Popen` for parity with
|
||||
previous versions where the kernel process was managed directly.
|
||||
"""
|
||||
|
||||
# The kernel specification associated with this provisioner
|
||||
kernel_spec: Any = Instance("jupyter_client.kernelspec.KernelSpec", allow_none=True)
|
||||
kernel_id: Union[str, Unicode] = Unicode(None, allow_none=True)
|
||||
connection_info: KernelConnectionInfo = {}
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def has_process(self) -> bool:
|
||||
"""
|
||||
Returns true if this provisioner is currently managing a process.
|
||||
|
||||
This property is asserted to be True immediately following a call to
|
||||
the provisioner's :meth:`launch_kernel` method.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def poll(self) -> Optional[int]:
|
||||
"""
|
||||
Checks if kernel process is still running.
|
||||
|
||||
If running, None is returned, otherwise the process's integer-valued exit code is returned.
|
||||
This method is called from :meth:`KernelManager.is_alive`.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def wait(self) -> Optional[int]:
|
||||
"""
|
||||
Waits for kernel process to terminate.
|
||||
|
||||
This method is called from `KernelManager.finish_shutdown()` and
|
||||
`KernelManager.kill_kernel()` when terminating a kernel gracefully or
|
||||
immediately, respectively.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_signal(self, signum: int) -> None:
|
||||
"""
|
||||
Sends signal identified by signum to the kernel process.
|
||||
|
||||
This method is called from `KernelManager.signal_kernel()` to send the
|
||||
kernel process a signal.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def kill(self, restart: bool = False) -> None:
|
||||
"""
|
||||
Kill the kernel process.
|
||||
|
||||
This is typically accomplished via a SIGKILL signal, which cannot be caught.
|
||||
This method is called from `KernelManager.kill_kernel()` when terminating
|
||||
a kernel immediately.
|
||||
|
||||
restart is True if this operation will precede a subsequent launch_kernel request.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def terminate(self, restart: bool = False) -> None:
|
||||
"""
|
||||
Terminates the kernel process.
|
||||
|
||||
This is typically accomplished via a SIGTERM signal, which can be caught, allowing
|
||||
the kernel provisioner to perform possible cleanup of resources. This method is
|
||||
called indirectly from `KernelManager.finish_shutdown()` during a kernel's
|
||||
graceful termination.
|
||||
|
||||
restart is True if this operation precedes a start launch_kernel request.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def launch_kernel(self, cmd: List[str], **kwargs: Any) -> KernelConnectionInfo:
|
||||
"""
|
||||
Launch the kernel process and return its connection information.
|
||||
|
||||
This method is called from `KernelManager.launch_kernel()` during the
|
||||
kernel manager's start kernel sequence.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def cleanup(self, restart: bool = False) -> None:
|
||||
"""
|
||||
Cleanup any resources allocated on behalf of the kernel provisioner.
|
||||
|
||||
This method is called from `KernelManager.cleanup_resources()` as part of
|
||||
its shutdown kernel sequence.
|
||||
|
||||
restart is True if this operation precedes a start launch_kernel request.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def shutdown_requested(self, restart: bool = False) -> None:
|
||||
"""
|
||||
Allows the provisioner to determine if the kernel's shutdown has been requested.
|
||||
|
||||
This method is called from `KernelManager.request_shutdown()` as part of
|
||||
its shutdown sequence.
|
||||
|
||||
This method is optional and is primarily used in scenarios where the provisioner
|
||||
may need to perform other operations in preparation for a kernel's shutdown.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def pre_launch(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Perform any steps in preparation for kernel process launch.
|
||||
|
||||
This includes applying additional substitutions to the kernel launch command
|
||||
and environment. It also includes preparation of launch parameters.
|
||||
|
||||
NOTE: Subclass implementations are advised to call this method as it applies
|
||||
environment variable substitutions from the local environment and calls the
|
||||
provisioner's :meth:`_finalize_env()` method to allow each provisioner the
|
||||
ability to cleanup the environment variables that will be used by the kernel.
|
||||
|
||||
This method is called from `KernelManager.pre_start_kernel()` as part of its
|
||||
start kernel sequence.
|
||||
|
||||
Returns the (potentially updated) keyword arguments that are passed to
|
||||
:meth:`launch_kernel()`.
|
||||
"""
|
||||
env = kwargs.pop("env", os.environ).copy()
|
||||
env.update(self.__apply_env_substitutions(env))
|
||||
self._finalize_env(env)
|
||||
kwargs["env"] = env
|
||||
|
||||
return kwargs
|
||||
|
||||
async def post_launch(self, **kwargs: Any) -> None:
|
||||
"""
|
||||
Perform any steps following the kernel process launch.
|
||||
|
||||
This method is called from `KernelManager.post_start_kernel()` as part of its
|
||||
start kernel sequence.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def get_provisioner_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Captures the base information necessary for persistence relative to this instance.
|
||||
|
||||
This enables applications that subclass `KernelManager` to persist a kernel provisioner's
|
||||
relevant information to accomplish functionality like disaster recovery or high availability
|
||||
by calling this method via the kernel manager's `provisioner` attribute.
|
||||
|
||||
NOTE: The superclass method must always be called first to ensure proper serialization.
|
||||
"""
|
||||
provisioner_info: Dict[str, Any] = {}
|
||||
provisioner_info["kernel_id"] = self.kernel_id
|
||||
provisioner_info["connection_info"] = self.connection_info
|
||||
return provisioner_info
|
||||
|
||||
async def load_provisioner_info(self, provisioner_info: Dict) -> None:
|
||||
"""
|
||||
Loads the base information necessary for persistence relative to this instance.
|
||||
|
||||
The inverse of `get_provisioner_info()`, this enables applications that subclass
|
||||
`KernelManager` to re-establish communication with a provisioner that is managing
|
||||
a (presumably) remote kernel from an entirely different process that the original
|
||||
provisioner.
|
||||
|
||||
NOTE: The superclass method must always be called first to ensure proper deserialization.
|
||||
"""
|
||||
self.kernel_id = provisioner_info["kernel_id"]
|
||||
self.connection_info = provisioner_info["connection_info"]
|
||||
|
||||
def get_shutdown_wait_time(self, recommended: float = 5.0) -> float:
|
||||
"""
|
||||
Returns the time allowed for a complete shutdown. This may vary by provisioner.
|
||||
|
||||
This method is called from `KernelManager.finish_shutdown()` during the graceful
|
||||
phase of its kernel shutdown sequence.
|
||||
|
||||
The recommended value will typically be what is configured in the kernel manager.
|
||||
"""
|
||||
return recommended
|
||||
|
||||
def get_stable_start_time(self, recommended: float = 10.0) -> float:
|
||||
"""
|
||||
Returns the expected upper bound for a kernel (re-)start to complete.
|
||||
This may vary by provisioner.
|
||||
|
||||
The recommended value will typically be what is configured in the kernel restarter.
|
||||
"""
|
||||
return recommended
|
||||
|
||||
def _finalize_env(self, env: Dict[str, str]) -> None:
|
||||
"""
|
||||
Ensures env is appropriate prior to launch.
|
||||
|
||||
This method is called from `KernelProvisionerBase.pre_launch()` during the kernel's
|
||||
start sequence.
|
||||
|
||||
NOTE: Subclasses should be sure to call super()._finalize_env(env)
|
||||
"""
|
||||
if self.kernel_spec.language and self.kernel_spec.language.lower().startswith("python"):
|
||||
# Don't allow PYTHONEXECUTABLE to be passed to kernel process.
|
||||
# If set, it can bork all the things.
|
||||
env.pop("PYTHONEXECUTABLE", None)
|
||||
|
||||
def __apply_env_substitutions(self, substitution_values: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
Walks entries in the kernelspec's env stanza and applies substitutions from current env.
|
||||
|
||||
This method is called from `KernelProvisionerBase.pre_launch()` during the kernel's
|
||||
start sequence.
|
||||
|
||||
Returns the substituted list of env entries.
|
||||
|
||||
NOTE: This method is private and is not intended to be overridden by provisioners.
|
||||
"""
|
||||
substituted_env = {}
|
||||
if self.kernel_spec:
|
||||
from string import Template
|
||||
|
||||
# For each templated env entry, fill any templated references
|
||||
# matching names of env variables with those values and build
|
||||
# new dict with substitutions.
|
||||
templated_env = self.kernel_spec.env
|
||||
for k, v in templated_env.items():
|
||||
substituted_env.update({k: Template(v).safe_substitute(substitution_values)})
|
||||
return substituted_env
|
||||
Reference in New Issue
Block a user