You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			807 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			807 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
"""Base class to manage a running kernel"""
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
import asyncio
 | 
						|
import functools
 | 
						|
import os
 | 
						|
import re
 | 
						|
import signal
 | 
						|
import sys
 | 
						|
import typing as t
 | 
						|
import uuid
 | 
						|
import warnings
 | 
						|
from asyncio.futures import Future
 | 
						|
from concurrent.futures import Future as CFuture
 | 
						|
from contextlib import contextmanager
 | 
						|
from enum import Enum
 | 
						|
 | 
						|
import zmq
 | 
						|
from jupyter_core.utils import run_sync
 | 
						|
from traitlets import (
 | 
						|
    Any,
 | 
						|
    Bool,
 | 
						|
    Dict,
 | 
						|
    DottedObjectName,
 | 
						|
    Float,
 | 
						|
    Instance,
 | 
						|
    Type,
 | 
						|
    Unicode,
 | 
						|
    default,
 | 
						|
    observe,
 | 
						|
    observe_compat,
 | 
						|
)
 | 
						|
from traitlets.utils.importstring import import_item
 | 
						|
 | 
						|
from . import kernelspec
 | 
						|
from .asynchronous import AsyncKernelClient
 | 
						|
from .blocking import BlockingKernelClient
 | 
						|
from .client import KernelClient
 | 
						|
from .connect import ConnectionFileMixin
 | 
						|
from .managerabc import KernelManagerABC
 | 
						|
from .provisioning import KernelProvisionerBase
 | 
						|
from .provisioning import KernelProvisionerFactory as KPF  # noqa
 | 
						|
 | 
						|
 | 
						|
class _ShutdownStatus(Enum):
 | 
						|
    """
 | 
						|
 | 
						|
    This is so far used only for testing in order to track the internal state of
 | 
						|
    the shutdown logic, and verifying which path is taken for which
 | 
						|
    missbehavior.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    Unset = None
 | 
						|
    ShutdownRequest = "ShutdownRequest"
 | 
						|
    SigtermRequest = "SigtermRequest"
 | 
						|
    SigkillRequest = "SigkillRequest"
 | 
						|
 | 
						|
 | 
						|
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
 | 
						|
 | 
						|
 | 
						|
def _get_future() -> t.Union[Future, CFuture]:
 | 
						|
    """Get an appropriate Future object"""
 | 
						|
    try:
 | 
						|
        asyncio.get_running_loop()
 | 
						|
        return Future()
 | 
						|
    except RuntimeError:
 | 
						|
        # No event loop running, use concurrent future
 | 
						|
        return CFuture()
 | 
						|
 | 
						|
 | 
						|
def in_pending_state(method: F) -> F:
 | 
						|
    """Sets the kernel to a pending state by
 | 
						|
    creating a fresh Future for the KernelManager's `ready`
 | 
						|
    attribute. Once the method is finished, set the Future's results.
 | 
						|
    """
 | 
						|
 | 
						|
    @t.no_type_check
 | 
						|
    @functools.wraps(method)
 | 
						|
    async def wrapper(self: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
 | 
						|
        """Create a future for the decorated method."""
 | 
						|
        if self._attempted_start or not self._ready:
 | 
						|
            self._ready = _get_future()
 | 
						|
        try:
 | 
						|
            # call wrapped method, await, and set the result or exception.
 | 
						|
            out = await method(self, *args, **kwargs)
 | 
						|
            # Add a small sleep to ensure tests can capture the state before done
 | 
						|
            await asyncio.sleep(0.01)
 | 
						|
            if self.owns_kernel:
 | 
						|
                self._ready.set_result(None)
 | 
						|
            return out
 | 
						|
        except Exception as e:
 | 
						|
            self._ready.set_exception(e)
 | 
						|
            self.log.exception(self._ready.exception())
 | 
						|
            raise e
 | 
						|
 | 
						|
    return t.cast(F, wrapper)
 | 
						|
 | 
						|
 | 
						|
class KernelManager(ConnectionFileMixin):
 | 
						|
    """Manages a single kernel in a subprocess on this host.
 | 
						|
 | 
						|
    This version starts kernels with Popen.
 | 
						|
    """
 | 
						|
 | 
						|
    _ready: t.Optional[t.Union[Future, CFuture]]
 | 
						|
 | 
						|
    def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
 | 
						|
        """Initialize a kernel manager."""
 | 
						|
        if args:
 | 
						|
            warnings.warn(
 | 
						|
                "Passing positional only arguments to "
 | 
						|
                "`KernelManager.__init__` is deprecated since jupyter_client"
 | 
						|
                " 8.6, and will become an error on future versions. Positional "
 | 
						|
                " arguments have been ignored since jupyter_client 7.0",
 | 
						|
                DeprecationWarning,
 | 
						|
                stacklevel=2,
 | 
						|
            )
 | 
						|
        self._owns_kernel = kwargs.pop("owns_kernel", True)
 | 
						|
        super().__init__(**kwargs)
 | 
						|
        self._shutdown_status = _ShutdownStatus.Unset
 | 
						|
        self._attempted_start = False
 | 
						|
        self._ready = None
 | 
						|
 | 
						|
    _created_context: Bool = Bool(False)
 | 
						|
 | 
						|
    # The PyZMQ Context to use for communication with the kernel.
 | 
						|
    context: Instance = Instance(zmq.Context)
 | 
						|
 | 
						|
    @default("context")
 | 
						|
    def _context_default(self) -> zmq.Context:
 | 
						|
        self._created_context = True
 | 
						|
        return zmq.Context()
 | 
						|
 | 
						|
    # the class to create with our `client` method
 | 
						|
    client_class: DottedObjectName = DottedObjectName(
 | 
						|
        "jupyter_client.blocking.BlockingKernelClient"
 | 
						|
    )
 | 
						|
    client_factory: Type = Type(klass=KernelClient)
 | 
						|
 | 
						|
    @default("client_factory")
 | 
						|
    def _client_factory_default(self) -> Type:
 | 
						|
        return import_item(self.client_class)
 | 
						|
 | 
						|
    @observe("client_class")
 | 
						|
    def _client_class_changed(self, change: t.Dict[str, DottedObjectName]) -> None:
 | 
						|
        self.client_factory = import_item(str(change["new"]))
 | 
						|
 | 
						|
    kernel_id: t.Union[str, Unicode] = Unicode(None, allow_none=True)
 | 
						|
 | 
						|
    # The kernel provisioner with which this KernelManager is communicating.
 | 
						|
    # This will generally be a LocalProvisioner instance unless the kernelspec
 | 
						|
    # indicates otherwise.
 | 
						|
    provisioner: t.Optional[KernelProvisionerBase] = None
 | 
						|
 | 
						|
    kernel_spec_manager: Instance = Instance(kernelspec.KernelSpecManager)
 | 
						|
 | 
						|
    @default("kernel_spec_manager")
 | 
						|
    def _kernel_spec_manager_default(self) -> kernelspec.KernelSpecManager:
 | 
						|
        return kernelspec.KernelSpecManager(data_dir=self.data_dir)
 | 
						|
 | 
						|
    @observe("kernel_spec_manager")
 | 
						|
    @observe_compat
 | 
						|
    def _kernel_spec_manager_changed(self, change: t.Dict[str, Instance]) -> None:
 | 
						|
        self._kernel_spec = None
 | 
						|
 | 
						|
    shutdown_wait_time: Float = Float(
 | 
						|
        5.0,
 | 
						|
        config=True,
 | 
						|
        help="Time to wait for a kernel to terminate before killing it, "
 | 
						|
        "in seconds. When a shutdown request is initiated, the kernel "
 | 
						|
        "will be immediately sent an interrupt (SIGINT), followed"
 | 
						|
        "by a shutdown_request message, after 1/2 of `shutdown_wait_time`"
 | 
						|
        "it will be sent a terminate (SIGTERM) request, and finally at "
 | 
						|
        "the end of `shutdown_wait_time` will be killed (SIGKILL). terminate "
 | 
						|
        "and kill may be equivalent on windows.  Note that this value can be"
 | 
						|
        "overridden by the in-use kernel provisioner since shutdown times may"
 | 
						|
        "vary by provisioned environment.",
 | 
						|
    )
 | 
						|
 | 
						|
    kernel_name: t.Union[str, Unicode] = Unicode(kernelspec.NATIVE_KERNEL_NAME)
 | 
						|
 | 
						|
    @observe("kernel_name")
 | 
						|
    def _kernel_name_changed(self, change: t.Dict[str, str]) -> None:
 | 
						|
        self._kernel_spec = None
 | 
						|
        if change["new"] == "python":
 | 
						|
            self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
 | 
						|
 | 
						|
    _kernel_spec: t.Optional[kernelspec.KernelSpec] = None
 | 
						|
 | 
						|
    @property
 | 
						|
    def kernel_spec(self) -> t.Optional[kernelspec.KernelSpec]:
 | 
						|
        if self._kernel_spec is None and self.kernel_name != "":
 | 
						|
            self._kernel_spec = self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
 | 
						|
        return self._kernel_spec
 | 
						|
 | 
						|
    cache_ports: Bool = Bool(
 | 
						|
        False,
 | 
						|
        config=True,
 | 
						|
        help="True if the MultiKernelManager should cache ports for this KernelManager instance",
 | 
						|
    )
 | 
						|
 | 
						|
    @default("cache_ports")
 | 
						|
    def _default_cache_ports(self) -> bool:
 | 
						|
        return self.transport == "tcp"
 | 
						|
 | 
						|
    @property
 | 
						|
    def ready(self) -> t.Union[CFuture, Future]:
 | 
						|
        """A future that resolves when the kernel process has started for the first time"""
 | 
						|
        if not self._ready:
 | 
						|
            self._ready = _get_future()
 | 
						|
        return self._ready
 | 
						|
 | 
						|
    @property
 | 
						|
    def ipykernel(self) -> bool:
 | 
						|
        return self.kernel_name in {"python", "python2", "python3"}
 | 
						|
 | 
						|
    # Protected traits
 | 
						|
    _launch_args: t.Optional["Dict[str, Any]"] = Dict(allow_none=True)
 | 
						|
    _control_socket: Any = Any()
 | 
						|
 | 
						|
    _restarter: Any = Any()
 | 
						|
 | 
						|
    autorestart: Bool = Bool(
 | 
						|
        True, config=True, help="""Should we autorestart the kernel if it dies."""
 | 
						|
    )
 | 
						|
 | 
						|
    shutting_down: bool = False
 | 
						|
 | 
						|
    def __del__(self) -> None:
 | 
						|
        self._close_control_socket()
 | 
						|
        self.cleanup_connection_file()
 | 
						|
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
    # Kernel restarter
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
 | 
						|
    def start_restarter(self) -> None:
 | 
						|
        """Start the kernel restarter."""
 | 
						|
        pass
 | 
						|
 | 
						|
    def stop_restarter(self) -> None:
 | 
						|
        """Stop the kernel restarter."""
 | 
						|
        pass
 | 
						|
 | 
						|
    def add_restart_callback(self, callback: t.Callable, event: str = "restart") -> None:
 | 
						|
        """Register a callback to be called when a kernel is restarted"""
 | 
						|
        if self._restarter is None:
 | 
						|
            return
 | 
						|
        self._restarter.add_callback(callback, event)
 | 
						|
 | 
						|
    def remove_restart_callback(self, callback: t.Callable, event: str = "restart") -> None:
 | 
						|
        """Unregister a callback to be called when a kernel is restarted"""
 | 
						|
        if self._restarter is None:
 | 
						|
            return
 | 
						|
        self._restarter.remove_callback(callback, event)
 | 
						|
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
    # create a Client connected to our Kernel
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
 | 
						|
    def client(self, **kwargs: t.Any) -> BlockingKernelClient:
 | 
						|
        """Create a client configured to connect to our kernel"""
 | 
						|
        kw: dict = {}
 | 
						|
        kw.update(self.get_connection_info(session=True))
 | 
						|
        kw.update(
 | 
						|
            {
 | 
						|
                "connection_file": self.connection_file,
 | 
						|
                "parent": self,
 | 
						|
            }
 | 
						|
        )
 | 
						|
 | 
						|
        # add kwargs last, for manual overrides
 | 
						|
        kw.update(kwargs)
 | 
						|
        return self.client_factory(**kw)
 | 
						|
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
    # Kernel management
 | 
						|
    # --------------------------------------------------------------------------
 | 
						|
 | 
						|
    def update_env(self, *, env: t.Dict[str, str]) -> None:
 | 
						|
        """
 | 
						|
        Allow to update the environment of a kernel manager.
 | 
						|
 | 
						|
        This will take effect only after kernel restart when the new env is
 | 
						|
        passed to the new kernel.
 | 
						|
 | 
						|
        This is useful as some of the information of the current kernel reflect
 | 
						|
        the state of the session that started it, and those session information
 | 
						|
        (like the attach file path, or name), are mutable.
 | 
						|
 | 
						|
        .. version-added: 8.5
 | 
						|
        """
 | 
						|
        # Mypy think this is unreachable as it see _launch_args as Dict, not t.Dict
 | 
						|
        if (
 | 
						|
            isinstance(self._launch_args, dict)
 | 
						|
            and "env" in self._launch_args
 | 
						|
            and isinstance(self._launch_args["env"], dict)  # type: ignore [unreachable]
 | 
						|
        ):
 | 
						|
            self._launch_args["env"].update(env)  # type: ignore [unreachable]
 | 
						|
 | 
						|
    def format_kernel_cmd(self, extra_arguments: t.Optional[t.List[str]] = None) -> t.List[str]:
 | 
						|
        """Replace templated args (e.g. {connection_file})"""
 | 
						|
        extra_arguments = extra_arguments or []
 | 
						|
        assert self.kernel_spec is not None
 | 
						|
        cmd = self.kernel_spec.argv + extra_arguments
 | 
						|
 | 
						|
        if cmd and cmd[0] in {
 | 
						|
            "python",
 | 
						|
            "python%i" % sys.version_info[0],
 | 
						|
            "python%i.%i" % sys.version_info[:2],
 | 
						|
        }:
 | 
						|
            # executable is 'python' or 'python3', use sys.executable.
 | 
						|
            # These will typically be the same,
 | 
						|
            # but if the current process is in an env
 | 
						|
            # and has been launched by abspath without
 | 
						|
            # activating the env, python on PATH may not be sys.executable,
 | 
						|
            # but it should be.
 | 
						|
            cmd[0] = sys.executable
 | 
						|
 | 
						|
        # Make sure to use the realpath for the connection_file
 | 
						|
        # On windows, when running with the store python, the connection_file path
 | 
						|
        # is not usable by non python kernels because the path is being rerouted when
 | 
						|
        # inside of a store app.
 | 
						|
        # See this bug here: https://bugs.python.org/issue41196
 | 
						|
        ns: t.Dict[str, t.Any] = {
 | 
						|
            "connection_file": os.path.realpath(self.connection_file),
 | 
						|
            "prefix": sys.prefix,
 | 
						|
        }
 | 
						|
 | 
						|
        if self.kernel_spec:  # type:ignore[truthy-bool]
 | 
						|
            ns["resource_dir"] = self.kernel_spec.resource_dir
 | 
						|
        assert isinstance(self._launch_args, dict)
 | 
						|
 | 
						|
        ns.update(self._launch_args)
 | 
						|
 | 
						|
        pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
 | 
						|
 | 
						|
        def from_ns(match: t.Any) -> t.Any:
 | 
						|
            """Get the key out of ns if it's there, otherwise no change."""
 | 
						|
            return ns.get(match.group(1), match.group())
 | 
						|
 | 
						|
        return [pat.sub(from_ns, arg) for arg in cmd]
 | 
						|
 | 
						|
    async def _async_launch_kernel(self, kernel_cmd: t.List[str], **kw: t.Any) -> None:
 | 
						|
        """actually launch the kernel
 | 
						|
 | 
						|
        override in a subclass to launch kernel subprocesses differently
 | 
						|
        Note that provisioners can now be used to customize kernel environments
 | 
						|
        and
 | 
						|
        """
 | 
						|
        assert self.provisioner is not None
 | 
						|
        connection_info = await self.provisioner.launch_kernel(kernel_cmd, **kw)
 | 
						|
        assert self.provisioner.has_process
 | 
						|
        # Provisioner provides the connection information.  Load into kernel manager
 | 
						|
        # and write the connection file, if not already done.
 | 
						|
        self._reconcile_connection_info(connection_info)
 | 
						|
 | 
						|
    _launch_kernel = run_sync(_async_launch_kernel)
 | 
						|
 | 
						|
    # Control socket used for polite kernel shutdown
 | 
						|
 | 
						|
    def _connect_control_socket(self) -> None:
 | 
						|
        if self._control_socket is None:
 | 
						|
            self._control_socket = self._create_connected_socket("control")
 | 
						|
            self._control_socket.linger = 100
 | 
						|
 | 
						|
    def _close_control_socket(self) -> None:
 | 
						|
        if self._control_socket is None:
 | 
						|
            return
 | 
						|
        self._control_socket.close()
 | 
						|
        self._control_socket = None
 | 
						|
 | 
						|
    async def _async_pre_start_kernel(
 | 
						|
        self, **kw: t.Any
 | 
						|
    ) -> t.Tuple[t.List[str], t.Dict[str, t.Any]]:
 | 
						|
        """Prepares a kernel for startup in a separate process.
 | 
						|
 | 
						|
        If random ports (port=0) are being used, this method must be called
 | 
						|
        before the channels are created.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        `**kw` : optional
 | 
						|
             keyword arguments that are passed down to build the kernel_cmd
 | 
						|
             and launching the kernel (e.g. Popen kwargs).
 | 
						|
        """
 | 
						|
        self.shutting_down = False
 | 
						|
        self.kernel_id = self.kernel_id or kw.pop("kernel_id", str(uuid.uuid4()))
 | 
						|
        # save kwargs for use in restart
 | 
						|
        # assigning Traitlets Dicts to Dict make mypy unhappy but is ok
 | 
						|
        self._launch_args = kw.copy()  # type:ignore [assignment]
 | 
						|
        if self.provisioner is None:  # will not be None on restarts
 | 
						|
            self.provisioner = KPF.instance(parent=self.parent).create_provisioner_instance(
 | 
						|
                self.kernel_id,
 | 
						|
                self.kernel_spec,
 | 
						|
                parent=self,
 | 
						|
            )
 | 
						|
        kw = await self.provisioner.pre_launch(**kw)
 | 
						|
        kernel_cmd = kw.pop("cmd")
 | 
						|
        return kernel_cmd, kw
 | 
						|
 | 
						|
    pre_start_kernel = run_sync(_async_pre_start_kernel)
 | 
						|
 | 
						|
    async def _async_post_start_kernel(self, **kw: t.Any) -> None:
 | 
						|
        """Performs any post startup tasks relative to the kernel.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        `**kw` : optional
 | 
						|
             keyword arguments that were used in the kernel process's launch.
 | 
						|
        """
 | 
						|
        self.start_restarter()
 | 
						|
        self._connect_control_socket()
 | 
						|
        assert self.provisioner is not None
 | 
						|
        await self.provisioner.post_launch(**kw)
 | 
						|
 | 
						|
    post_start_kernel = run_sync(_async_post_start_kernel)
 | 
						|
 | 
						|
    @in_pending_state
 | 
						|
    async def _async_start_kernel(self, **kw: t.Any) -> None:
 | 
						|
        """Starts a kernel on this host in a separate process.
 | 
						|
 | 
						|
        If random ports (port=0) are being used, this method must be called
 | 
						|
        before the channels are created.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        `**kw` : optional
 | 
						|
             keyword arguments that are passed down to build the kernel_cmd
 | 
						|
             and launching the kernel (e.g. Popen kwargs).
 | 
						|
        """
 | 
						|
        self._attempted_start = True
 | 
						|
        kernel_cmd, kw = await self._async_pre_start_kernel(**kw)
 | 
						|
 | 
						|
        # launch the kernel subprocess
 | 
						|
        self.log.debug("Starting kernel: %s", kernel_cmd)
 | 
						|
        await self._async_launch_kernel(kernel_cmd, **kw)
 | 
						|
        await self._async_post_start_kernel(**kw)
 | 
						|
 | 
						|
    start_kernel = run_sync(_async_start_kernel)
 | 
						|
 | 
						|
    async def _async_request_shutdown(self, restart: bool = False) -> None:
 | 
						|
        """Send a shutdown request via control channel"""
 | 
						|
        content = {"restart": restart}
 | 
						|
        msg = self.session.msg("shutdown_request", content=content)
 | 
						|
        # ensure control socket is connected
 | 
						|
        self._connect_control_socket()
 | 
						|
        self.session.send(self._control_socket, msg)
 | 
						|
        assert self.provisioner is not None
 | 
						|
        await self.provisioner.shutdown_requested(restart=restart)
 | 
						|
        self._shutdown_status = _ShutdownStatus.ShutdownRequest
 | 
						|
 | 
						|
    request_shutdown = run_sync(_async_request_shutdown)
 | 
						|
 | 
						|
    async def _async_finish_shutdown(
 | 
						|
        self,
 | 
						|
        waittime: t.Optional[float] = None,
 | 
						|
        pollinterval: float = 0.1,
 | 
						|
        restart: bool = False,
 | 
						|
    ) -> None:
 | 
						|
        """Wait for kernel shutdown, then kill process if it doesn't shutdown.
 | 
						|
 | 
						|
        This does not send shutdown requests - use :meth:`request_shutdown`
 | 
						|
        first.
 | 
						|
        """
 | 
						|
        if waittime is None:
 | 
						|
            waittime = max(self.shutdown_wait_time, 0)
 | 
						|
        if self.provisioner:  # Allow provisioner to override
 | 
						|
            waittime = self.provisioner.get_shutdown_wait_time(recommended=waittime)
 | 
						|
 | 
						|
        try:
 | 
						|
            await asyncio.wait_for(
 | 
						|
                self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
 | 
						|
            )
 | 
						|
        except asyncio.TimeoutError:
 | 
						|
            self.log.debug("Kernel is taking too long to finish, terminating")
 | 
						|
            self._shutdown_status = _ShutdownStatus.SigtermRequest
 | 
						|
            await self._async_send_kernel_sigterm()
 | 
						|
 | 
						|
        try:
 | 
						|
            await asyncio.wait_for(
 | 
						|
                self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
 | 
						|
            )
 | 
						|
        except asyncio.TimeoutError:
 | 
						|
            self.log.debug("Kernel is taking too long to finish, killing")
 | 
						|
            self._shutdown_status = _ShutdownStatus.SigkillRequest
 | 
						|
            await self._async_kill_kernel(restart=restart)
 | 
						|
        else:
 | 
						|
            # Process is no longer alive, wait and clear
 | 
						|
            if self.has_kernel:
 | 
						|
                assert self.provisioner is not None
 | 
						|
                await self.provisioner.wait()
 | 
						|
 | 
						|
    finish_shutdown = run_sync(_async_finish_shutdown)
 | 
						|
 | 
						|
    async def _async_cleanup_resources(self, restart: bool = False) -> None:
 | 
						|
        """Clean up resources when the kernel is shut down"""
 | 
						|
        if not restart:
 | 
						|
            self.cleanup_connection_file()
 | 
						|
 | 
						|
        self.cleanup_ipc_files()
 | 
						|
        self._close_control_socket()
 | 
						|
        self.session.parent = None
 | 
						|
 | 
						|
        if self._created_context and not restart:
 | 
						|
            self.context.destroy(linger=100)
 | 
						|
 | 
						|
        if self.provisioner:
 | 
						|
            await self.provisioner.cleanup(restart=restart)
 | 
						|
 | 
						|
    cleanup_resources = run_sync(_async_cleanup_resources)
 | 
						|
 | 
						|
    @in_pending_state
 | 
						|
    async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) -> None:
 | 
						|
        """Attempts to stop the kernel process cleanly.
 | 
						|
 | 
						|
        This attempts to shutdown the kernels cleanly by:
 | 
						|
 | 
						|
        1. Sending it a shutdown message over the control channel.
 | 
						|
        2. If that fails, the kernel is shutdown forcibly by sending it
 | 
						|
           a signal.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        now : bool
 | 
						|
            Should the kernel be forcible killed *now*. This skips the
 | 
						|
            first, nice shutdown attempt.
 | 
						|
        restart: bool
 | 
						|
            Will this kernel be restarted after it is shutdown. When this
 | 
						|
            is True, connection files will not be cleaned up.
 | 
						|
        """
 | 
						|
        if not self.owns_kernel:
 | 
						|
            return
 | 
						|
 | 
						|
        self.shutting_down = True  # Used by restarter to prevent race condition
 | 
						|
        # Stop monitoring for restarting while we shutdown.
 | 
						|
        self.stop_restarter()
 | 
						|
 | 
						|
        if self.has_kernel:
 | 
						|
            await self._async_interrupt_kernel()
 | 
						|
 | 
						|
        if now:
 | 
						|
            await self._async_kill_kernel()
 | 
						|
        else:
 | 
						|
            await self._async_request_shutdown(restart=restart)
 | 
						|
            # Don't send any additional kernel kill messages immediately, to give
 | 
						|
            # the kernel a chance to properly execute shutdown actions. Wait for at
 | 
						|
            # most 1s, checking every 0.1s.
 | 
						|
            await self._async_finish_shutdown(restart=restart)
 | 
						|
 | 
						|
        await self._async_cleanup_resources(restart=restart)
 | 
						|
 | 
						|
    shutdown_kernel = run_sync(_async_shutdown_kernel)
 | 
						|
 | 
						|
    async def _async_restart_kernel(
 | 
						|
        self, now: bool = False, newports: bool = False, **kw: t.Any
 | 
						|
    ) -> None:
 | 
						|
        """Restarts a kernel with the arguments that were used to launch it.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        now : bool, optional
 | 
						|
            If True, the kernel is forcefully restarted *immediately*, without
 | 
						|
            having a chance to do any cleanup action.  Otherwise the kernel is
 | 
						|
            given 1s to clean up before a forceful restart is issued.
 | 
						|
 | 
						|
            In all cases the kernel is restarted, the only difference is whether
 | 
						|
            it is given a chance to perform a clean shutdown or not.
 | 
						|
 | 
						|
        newports : bool, optional
 | 
						|
            If the old kernel was launched with random ports, this flag decides
 | 
						|
            whether the same ports and connection file will be used again.
 | 
						|
            If False, the same ports and connection file are used. This is
 | 
						|
            the default. If True, new random port numbers are chosen and a
 | 
						|
            new connection file is written. It is still possible that the newly
 | 
						|
            chosen random port numbers happen to be the same as the old ones.
 | 
						|
 | 
						|
        `**kw` : optional
 | 
						|
            Any options specified here will overwrite those used to launch the
 | 
						|
            kernel.
 | 
						|
        """
 | 
						|
        if self._launch_args is None:
 | 
						|
            msg = "Cannot restart the kernel. No previous call to 'start_kernel'."
 | 
						|
            raise RuntimeError(msg)
 | 
						|
 | 
						|
        # Stop currently running kernel.
 | 
						|
        await self._async_shutdown_kernel(now=now, restart=True)
 | 
						|
 | 
						|
        if newports:
 | 
						|
            self.cleanup_random_ports()
 | 
						|
 | 
						|
        # Start new kernel.
 | 
						|
        self._launch_args.update(kw)
 | 
						|
        await self._async_start_kernel(**self._launch_args)
 | 
						|
 | 
						|
    restart_kernel = run_sync(_async_restart_kernel)
 | 
						|
 | 
						|
    @property
 | 
						|
    def owns_kernel(self) -> bool:
 | 
						|
        return self._owns_kernel
 | 
						|
 | 
						|
    @property
 | 
						|
    def has_kernel(self) -> bool:
 | 
						|
        """Has a kernel process been started that we are actively managing."""
 | 
						|
        return self.provisioner is not None and self.provisioner.has_process
 | 
						|
 | 
						|
    async def _async_send_kernel_sigterm(self, restart: bool = False) -> None:
 | 
						|
        """similar to _kill_kernel, but with sigterm (not sigkill), but do not block"""
 | 
						|
        if self.has_kernel:
 | 
						|
            assert self.provisioner is not None
 | 
						|
            await self.provisioner.terminate(restart=restart)
 | 
						|
 | 
						|
    _send_kernel_sigterm = run_sync(_async_send_kernel_sigterm)
 | 
						|
 | 
						|
    async def _async_kill_kernel(self, restart: bool = False) -> None:
 | 
						|
        """Kill the running kernel.
 | 
						|
 | 
						|
        This is a private method, callers should use shutdown_kernel(now=True).
 | 
						|
        """
 | 
						|
        if self.has_kernel:
 | 
						|
            assert self.provisioner is not None
 | 
						|
            await self.provisioner.kill(restart=restart)
 | 
						|
 | 
						|
            # Wait until the kernel terminates.
 | 
						|
            try:
 | 
						|
                await asyncio.wait_for(self._async_wait(), timeout=5.0)
 | 
						|
            except asyncio.TimeoutError:
 | 
						|
                # Wait timed out, just log warning but continue - not much more we can do.
 | 
						|
                self.log.warning("Wait for final termination of kernel timed out - continuing...")
 | 
						|
                pass
 | 
						|
            else:
 | 
						|
                # Process is no longer alive, wait and clear
 | 
						|
                if self.has_kernel:
 | 
						|
                    await self.provisioner.wait()
 | 
						|
 | 
						|
    _kill_kernel = run_sync(_async_kill_kernel)
 | 
						|
 | 
						|
    async def _async_interrupt_kernel(self) -> None:
 | 
						|
        """Interrupts the kernel by sending it a signal.
 | 
						|
 | 
						|
        Unlike ``signal_kernel``, this operation is well supported on all
 | 
						|
        platforms.
 | 
						|
        """
 | 
						|
        if not self.has_kernel and self._ready is not None:
 | 
						|
            if isinstance(self._ready, CFuture):
 | 
						|
                ready = asyncio.ensure_future(t.cast(Future[t.Any], self._ready))
 | 
						|
            else:
 | 
						|
                ready = self._ready
 | 
						|
            # Wait for a shutdown if one is in progress.
 | 
						|
            if self.shutting_down:
 | 
						|
                await ready
 | 
						|
            # Wait for a startup.
 | 
						|
            await ready
 | 
						|
 | 
						|
        if self.has_kernel:
 | 
						|
            assert self.kernel_spec is not None
 | 
						|
            interrupt_mode = self.kernel_spec.interrupt_mode
 | 
						|
            if interrupt_mode == "signal":
 | 
						|
                await self._async_signal_kernel(signal.SIGINT)
 | 
						|
 | 
						|
            elif interrupt_mode == "message":
 | 
						|
                msg = self.session.msg("interrupt_request", content={})
 | 
						|
                self._connect_control_socket()
 | 
						|
                self.session.send(self._control_socket, msg)
 | 
						|
        else:
 | 
						|
            msg = "Cannot interrupt kernel. No kernel is running!"
 | 
						|
            raise RuntimeError(msg)
 | 
						|
 | 
						|
    interrupt_kernel = run_sync(_async_interrupt_kernel)
 | 
						|
 | 
						|
    async def _async_signal_kernel(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, this function is
 | 
						|
        only useful on Unix systems.
 | 
						|
        """
 | 
						|
        if self.has_kernel:
 | 
						|
            assert self.provisioner is not None
 | 
						|
            await self.provisioner.send_signal(signum)
 | 
						|
        else:
 | 
						|
            msg = "Cannot signal kernel. No kernel is running!"
 | 
						|
            raise RuntimeError(msg)
 | 
						|
 | 
						|
    signal_kernel = run_sync(_async_signal_kernel)
 | 
						|
 | 
						|
    async def _async_is_alive(self) -> bool:
 | 
						|
        """Is the kernel process still running?"""
 | 
						|
        if not self.owns_kernel:
 | 
						|
            return True
 | 
						|
 | 
						|
        if self.has_kernel:
 | 
						|
            assert self.provisioner is not None
 | 
						|
            ret = await self.provisioner.poll()
 | 
						|
            if ret is None:
 | 
						|
                return True
 | 
						|
        return False
 | 
						|
 | 
						|
    is_alive = run_sync(_async_is_alive)
 | 
						|
 | 
						|
    async def _async_wait(self, pollinterval: float = 0.1) -> None:
 | 
						|
        # 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_kernel()).
 | 
						|
        while await self._async_is_alive():
 | 
						|
            await asyncio.sleep(pollinterval)
 | 
						|
 | 
						|
 | 
						|
class AsyncKernelManager(KernelManager):
 | 
						|
    """An async kernel manager."""
 | 
						|
 | 
						|
    # the class to create with our `client` method
 | 
						|
    client_class: DottedObjectName = DottedObjectName(
 | 
						|
        "jupyter_client.asynchronous.AsyncKernelClient"
 | 
						|
    )
 | 
						|
    client_factory: Type = Type(klass="jupyter_client.asynchronous.AsyncKernelClient")
 | 
						|
 | 
						|
    # The PyZMQ Context to use for communication with the kernel.
 | 
						|
    context: Instance = Instance(zmq.asyncio.Context)
 | 
						|
 | 
						|
    @default("context")
 | 
						|
    def _context_default(self) -> zmq.asyncio.Context:
 | 
						|
        self._created_context = True
 | 
						|
        return zmq.asyncio.Context()
 | 
						|
 | 
						|
    def client(  # type:ignore[override]
 | 
						|
        self, **kwargs: t.Any
 | 
						|
    ) -> AsyncKernelClient:
 | 
						|
        """Get a client for the manager."""
 | 
						|
        return super().client(**kwargs)  # type:ignore[return-value]
 | 
						|
 | 
						|
    _launch_kernel = KernelManager._async_launch_kernel  # type:ignore[assignment]
 | 
						|
    start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_start_kernel  # type:ignore[assignment]
 | 
						|
    pre_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_pre_start_kernel  # type:ignore[assignment]
 | 
						|
    post_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_post_start_kernel  # type:ignore[assignment]
 | 
						|
    request_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_request_shutdown  # type:ignore[assignment]
 | 
						|
    finish_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_finish_shutdown  # type:ignore[assignment]
 | 
						|
    cleanup_resources: t.Callable[..., t.Awaitable] = KernelManager._async_cleanup_resources  # type:ignore[assignment]
 | 
						|
    shutdown_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_shutdown_kernel  # type:ignore[assignment]
 | 
						|
    restart_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_restart_kernel  # type:ignore[assignment]
 | 
						|
    _send_kernel_sigterm = KernelManager._async_send_kernel_sigterm  # type:ignore[assignment]
 | 
						|
    _kill_kernel = KernelManager._async_kill_kernel  # type:ignore[assignment]
 | 
						|
    interrupt_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_interrupt_kernel  # type:ignore[assignment]
 | 
						|
    signal_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_signal_kernel  # type:ignore[assignment]
 | 
						|
    is_alive: t.Callable[..., t.Awaitable] = KernelManager._async_is_alive  # type:ignore[assignment]
 | 
						|
 | 
						|
 | 
						|
KernelManagerABC.register(KernelManager)
 | 
						|
 | 
						|
 | 
						|
def start_new_kernel(
 | 
						|
    startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
 | 
						|
) -> t.Tuple[KernelManager, BlockingKernelClient]:
 | 
						|
    """Start a new kernel, and return its Manager and Client"""
 | 
						|
    km = KernelManager(kernel_name=kernel_name)
 | 
						|
    km.start_kernel(**kwargs)
 | 
						|
    kc = km.client()
 | 
						|
    kc.start_channels()
 | 
						|
    try:
 | 
						|
        kc.wait_for_ready(timeout=startup_timeout)
 | 
						|
    except RuntimeError:
 | 
						|
        kc.stop_channels()
 | 
						|
        km.shutdown_kernel()
 | 
						|
        raise
 | 
						|
 | 
						|
    return km, kc
 | 
						|
 | 
						|
 | 
						|
async def start_new_async_kernel(
 | 
						|
    startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
 | 
						|
) -> t.Tuple[AsyncKernelManager, AsyncKernelClient]:
 | 
						|
    """Start a new kernel, and return its Manager and Client"""
 | 
						|
    km = AsyncKernelManager(kernel_name=kernel_name)
 | 
						|
    await km.start_kernel(**kwargs)
 | 
						|
    kc = km.client()
 | 
						|
    kc.start_channels()
 | 
						|
    try:
 | 
						|
        await kc.wait_for_ready(timeout=startup_timeout)
 | 
						|
    except RuntimeError:
 | 
						|
        kc.stop_channels()
 | 
						|
        await km.shutdown_kernel()
 | 
						|
        raise
 | 
						|
 | 
						|
    return (km, kc)
 | 
						|
 | 
						|
 | 
						|
@contextmanager
 | 
						|
def run_kernel(**kwargs: t.Any) -> t.Iterator[KernelClient]:
 | 
						|
    """Context manager to create a kernel in a subprocess.
 | 
						|
 | 
						|
    The kernel is shut down when the context exits.
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    kernel_client: connected KernelClient instance
 | 
						|
    """
 | 
						|
    km, kc = start_new_kernel(**kwargs)
 | 
						|
    try:
 | 
						|
        yield kc
 | 
						|
    finally:
 | 
						|
        kc.stop_channels()
 | 
						|
        km.shutdown_kernel(now=True)
 |