1"""Base class to manage a running kernel"""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5import asyncio
6import functools
7import os
8import re
9import signal
10import sys
11import typing as t
12import uuid
13import warnings
14from asyncio.futures import Future
15from concurrent.futures import Future as CFuture
16from contextlib import contextmanager
17from enum import Enum
18
19import zmq
20from jupyter_core.utils import run_sync
21from traitlets import (
22 Any,
23 Bool,
24 CaselessStrEnum,
25 Dict,
26 DottedObjectName,
27 Float,
28 Instance,
29 TraitError,
30 Type,
31 Unicode,
32 default,
33 observe,
34 observe_compat,
35 validate,
36)
37from traitlets.utils.importstring import import_item
38
39from . import kernelspec
40from .asynchronous import AsyncKernelClient
41from .blocking import BlockingKernelClient
42from .client import KernelClient
43from .connect import ConnectionFileMixin
44from .managerabc import KernelManagerABC
45from .provisioning import KernelProvisionerBase
46from .provisioning import KernelProvisionerFactory as KPF # noqa
47
48# After an upgrade to Sphinx 9 and myst 5, the doc build started to fail
49# with the following error: :8: (ERROR/3) Unexpected indentation.
50# This seems to be due to the docstring of the wrapper function inside
51# in_pending_state. However, removing the docstring doe snot fix the issue
52# since we use the :undoc-members: directive with automodule.
53# The workaround is to explicitly set what we want to document
54
55__all__ = [
56 "AsyncKernelManager",
57 "KernelManager",
58 "in_pending_state",
59 "run_kernel",
60 "start_new_async_kernel",
61 "start_new_kernel",
62]
63
64
65class _ShutdownStatus(Enum):
66 """
67
68 This is so far used only for testing in order to track the internal state of
69 the shutdown logic, and verifying which path is taken for which
70 missbehavior.
71
72 """
73
74 Unset = None
75 ShutdownRequest = "ShutdownRequest"
76 SigtermRequest = "SigtermRequest"
77 SigkillRequest = "SigkillRequest"
78
79
80F = t.TypeVar("F", bound=t.Callable[..., t.Any])
81
82
83def _get_future() -> t.Union[Future, CFuture]:
84 """Get an appropriate Future object"""
85 try:
86 asyncio.get_running_loop()
87 return Future()
88 except RuntimeError:
89 # No event loop running, use concurrent future
90 return CFuture()
91
92
93def in_pending_state(method: F) -> F:
94 """Sets the kernel to a pending state by
95 creating a fresh Future for the KernelManager's `ready`
96 attribute. Once the method is finished, set the Future's results.
97 """
98
99 @t.no_type_check
100 @functools.wraps(method)
101 async def wrapper(self: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
102 """Create a future for the decorated method."""
103 if self._attempted_start or not self._ready:
104 self._ready = _get_future()
105 try:
106 # call wrapped method, await, and set the result or exception.
107 out = await method(self, *args, **kwargs)
108 # Add a small sleep to ensure tests can capture the state before done
109 await asyncio.sleep(0.01)
110 if self.owns_kernel:
111 self._ready.set_result(None)
112 return out
113 except Exception as e:
114 self._ready.set_exception(e)
115 self.log.exception(self._ready.exception())
116 raise e
117
118 return t.cast(F, wrapper)
119
120
121class KernelManager(ConnectionFileMixin):
122 """Manages a single kernel in a subprocess on this host.
123
124 This version starts kernels with Popen.
125 """
126
127 _ready: t.Union[Future, CFuture] | None
128
129 def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
130 """Initialize a kernel manager."""
131 if args:
132 warnings.warn(
133 "Passing positional only arguments to "
134 "`KernelManager.__init__` is deprecated since jupyter_client"
135 " 8.6, and will become an error on future versions. Positional "
136 " arguments have been ignored since jupyter_client 7.0",
137 DeprecationWarning,
138 stacklevel=2,
139 )
140 self._owns_kernel = kwargs.pop("owns_kernel", True)
141 super().__init__(**kwargs)
142 self._shutdown_status = _ShutdownStatus.Unset
143 self._attempted_start = False
144 self._ready = None
145
146 _created_context: Bool = Bool(False)
147
148 # The PyZMQ Context to use for communication with the kernel.
149 context: Instance = Instance(zmq.Context)
150
151 @default("context")
152 def _context_default(self) -> zmq.Context:
153 self._created_context = True
154 return zmq.Context()
155
156 # the class to create with our `client` method
157 client_class: DottedObjectName = DottedObjectName(
158 "jupyter_client.blocking.BlockingKernelClient", config=True
159 )
160 client_factory: Type = Type(klass=KernelClient, config=True)
161
162 transport_encryption: CaselessStrEnum = CaselessStrEnum(
163 ["disabled", "auto", "required"],
164 default_value="disabled",
165 config=True,
166 help=(
167 "Transport encryption policy for manager-side provisioning of CurveZMQ server keys for kernels. "
168 "'disabled' (default) does not provision Curve credentials, 'auto' provisions when the kernelspec "
169 "declares support, and 'required' enforces provisioning and fails startup if transport encryption "
170 "cannot be applied."
171 ),
172 )
173
174 @validate("transport_encryption")
175 def _validate_transport_encryption(self, proposal: dict) -> str:
176 value = proposal["value"]
177 if value in ("auto", "required") and not zmq.has("curve"):
178 msg = (
179 f"transport_encryption={value!r} requires CurveZMQ support, "
180 "but zmq.has('curve') returned False. "
181 "Install pyzmq with libzmq compiled with libsodium to enable CurveZMQ."
182 )
183 raise TraitError(msg)
184 return value
185
186 def _transport_encryption_policy(self, value: str | None = None) -> str:
187 """Normalize transport encryption input into one of the supported policy values."""
188 if value is None:
189 value = self.transport_encryption
190 normalized = str(value).lower()
191 if normalized not in {"disabled", "auto", "required"}:
192 msg = (
193 "transport_encryption must be one of: 'disabled', 'auto', 'required' "
194 f"(got: {value!r})"
195 )
196 raise ValueError(msg)
197 return normalized
198
199 def _kernel_supports_curve_encryption(self) -> bool:
200 """Whether kernelspec metadata declares support for Curve transport encryption."""
201 if self.kernel_spec is None:
202 return False
203 metadata = getattr(self.kernel_spec, "metadata", {}) or {}
204 supported_encryption = metadata.get("supported_encryption")
205 if supported_encryption is None:
206 return False
207 if isinstance(supported_encryption, str):
208 return supported_encryption.strip().lower() == "curve"
209 if isinstance(supported_encryption, (list, tuple, set)):
210 normalized = {str(item).strip().lower() for item in supported_encryption}
211 return "curve" in normalized
212 return False
213
214 @default("client_factory")
215 def _client_factory_default(self) -> Type:
216 return import_item(self.client_class)
217
218 @observe("client_class")
219 def _client_class_changed(self, change: t.Dict[str, DottedObjectName]) -> None:
220 self.client_factory = import_item(str(change["new"]))
221
222 kernel_id: t.Union[str, Unicode] = Unicode(None, allow_none=True)
223
224 # The kernel provisioner with which this KernelManager is communicating.
225 # This will generally be a LocalProvisioner instance unless the kernelspec
226 # indicates otherwise.
227 provisioner: KernelProvisionerBase | None = None
228
229 kernel_spec_manager: Instance = Instance(kernelspec.KernelSpecManager)
230
231 @default("kernel_spec_manager")
232 def _kernel_spec_manager_default(self) -> kernelspec.KernelSpecManager:
233 return kernelspec.KernelSpecManager(data_dir=self.data_dir)
234
235 @observe("kernel_spec_manager")
236 @observe_compat
237 def _kernel_spec_manager_changed(self, change: t.Dict[str, Instance]) -> None:
238 self._kernel_spec = None
239
240 shutdown_wait_time: Float = Float(
241 5.0,
242 config=True,
243 help="Time to wait for a kernel to terminate before killing it, "
244 "in seconds. When a shutdown request is initiated, the kernel "
245 "will be immediately sent an interrupt (SIGINT), followed"
246 "by a shutdown_request message, after 1/2 of `shutdown_wait_time`"
247 "it will be sent a terminate (SIGTERM) request, and finally at "
248 "the end of `shutdown_wait_time` will be killed (SIGKILL). terminate "
249 "and kill may be equivalent on windows. Note that this value can be"
250 "overridden by the in-use kernel provisioner since shutdown times may"
251 "vary by provisioned environment.",
252 )
253
254 kernel_name: t.Union[str, Unicode] = Unicode(kernelspec.NATIVE_KERNEL_NAME)
255
256 @observe("kernel_name")
257 def _kernel_name_changed(self, change: t.Dict[str, str]) -> None:
258 self._kernel_spec = None
259 if change["new"] == "python":
260 self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
261
262 _kernel_spec: kernelspec.KernelSpec | None = None
263
264 @property
265 def kernel_spec(self) -> kernelspec.KernelSpec | None:
266 if self._kernel_spec is None and self.kernel_name != "":
267 self._kernel_spec = self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
268 return self._kernel_spec
269
270 cache_ports: Bool = Bool(
271 False,
272 config=True,
273 help="True if the MultiKernelManager should cache ports for this KernelManager instance",
274 )
275
276 @default("cache_ports")
277 def _default_cache_ports(self) -> bool:
278 return self.transport == "tcp"
279
280 @property
281 def ready(self) -> t.Union[CFuture, Future]:
282 """A future that resolves when the kernel process has started for the first time"""
283 if not self._ready:
284 self._ready = _get_future()
285 return self._ready
286
287 @property
288 def ipykernel(self) -> bool:
289 return self.kernel_name in {"python", "python2", "python3"}
290
291 # Protected traits
292 _launch_args: t.Optional["Dict[str, Any]"] = Dict(allow_none=True)
293 _control_socket: Any = Any()
294
295 _restarter: Any = Any()
296
297 autorestart: Bool = Bool(
298 True, config=True, help="""Should we autorestart the kernel if it dies."""
299 )
300
301 shutting_down: bool = False
302
303 def __del__(self) -> None:
304 self._close_control_socket()
305 self.cleanup_connection_file()
306
307 # --------------------------------------------------------------------------
308 # Kernel restarter
309 # --------------------------------------------------------------------------
310
311 def start_restarter(self) -> None:
312 """Start the kernel restarter."""
313 pass
314
315 def stop_restarter(self) -> None:
316 """Stop the kernel restarter."""
317 pass
318
319 def add_restart_callback(self, callback: t.Callable, event: str = "restart") -> None:
320 """Register a callback to be called when a kernel is restarted"""
321 if self._restarter is None:
322 return
323 self._restarter.add_callback(callback, event)
324
325 def remove_restart_callback(self, callback: t.Callable, event: str = "restart") -> None:
326 """Unregister a callback to be called when a kernel is restarted"""
327 if self._restarter is None:
328 return
329 self._restarter.remove_callback(callback, event)
330
331 # --------------------------------------------------------------------------
332 # create a Client connected to our Kernel
333 # --------------------------------------------------------------------------
334
335 def client(self, **kwargs: t.Any) -> BlockingKernelClient:
336 """Create a client configured to connect to our kernel"""
337 kw: dict = {}
338 kw.update(self.get_connection_info(session=True))
339 kw.update(
340 {
341 "connection_file": self.connection_file,
342 "parent": self,
343 }
344 )
345
346 # add kwargs last, for manual overrides
347 kw.update(kwargs)
348 return self.client_factory(**kw)
349
350 # --------------------------------------------------------------------------
351 # Kernel management
352 # --------------------------------------------------------------------------
353
354 def resolve_path(self, path: str) -> str | None:
355 """Resolve path to given file."""
356 assert self.provisioner is not None
357 return self.provisioner.resolve_path(path)
358
359 def update_env(self, *, env: t.Dict[str, str]) -> None:
360 """
361 Allow to update the environment of a kernel manager.
362
363 This will take effect only after kernel restart when the new env is
364 passed to the new kernel.
365
366 This is useful as some of the information of the current kernel reflect
367 the state of the session that started it, and those session information
368 (like the attach file path, or name), are mutable.
369
370 .. version-added: 8.5
371 """
372 # Mypy think this is unreachable as it see _launch_args as Dict, not t.Dict
373 if (
374 isinstance(self._launch_args, dict)
375 and "env" in self._launch_args
376 and isinstance(self._launch_args["env"], dict) # type: ignore [unreachable]
377 ):
378 self._launch_args["env"].update(env) # type: ignore [unreachable]
379
380 def format_kernel_cmd(self, extra_arguments: t.List[str] | None = None) -> t.List[str]:
381 """Replace templated args (e.g. {connection_file})"""
382 extra_arguments = extra_arguments or []
383 assert self.kernel_spec is not None
384 cmd = self.kernel_spec.argv + extra_arguments
385
386 if cmd and cmd[0] in {
387 "python",
388 "python%i" % sys.version_info[0],
389 "python%i.%i" % sys.version_info[:2],
390 }:
391 # executable is 'python' or 'python3', use sys.executable.
392 # These will typically be the same,
393 # but if the current process is in an env
394 # and has been launched by abspath without
395 # activating the env, python on PATH may not be sys.executable,
396 # but it should be.
397 cmd[0] = sys.executable
398
399 # Make sure to use the realpath for the connection_file
400 # On windows, when running with the store python, the connection_file path
401 # is not usable by non python kernels because the path is being rerouted when
402 # inside of a store app.
403 # See this bug here: https://bugs.python.org/issue41196
404 ns: t.Dict[str, t.Any] = {
405 "connection_file": os.path.realpath(self.connection_file),
406 "prefix": sys.prefix,
407 }
408
409 if self.kernel_spec: # type:ignore[truthy-bool]
410 ns["resource_dir"] = self.kernel_spec.resource_dir
411 assert isinstance(self._launch_args, dict)
412
413 ns.update(self._launch_args)
414
415 pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
416
417 def from_ns(match: t.Any) -> t.Any:
418 """Get the key out of ns if it's there, otherwise no change."""
419 return ns.get(match.group(1), match.group())
420
421 return [pat.sub(from_ns, arg) for arg in cmd]
422
423 async def _async_launch_kernel(self, kernel_cmd: t.List[str], **kw: t.Any) -> None:
424 """actually launch the kernel
425
426 override in a subclass to launch kernel subprocesses differently
427 Note that provisioners can now be used to customize kernel environments
428 and
429 """
430 assert self.provisioner is not None
431 connection_info = await self.provisioner.launch_kernel(kernel_cmd, **kw)
432 assert self.provisioner.has_process
433 # Provisioner provides the connection information. Load into kernel manager
434 # and write the connection file, if not already done.
435 self._reconcile_connection_info(connection_info)
436
437 _launch_kernel = run_sync(_async_launch_kernel)
438
439 # Control socket used for polite kernel shutdown
440
441 def _connect_control_socket(self) -> None:
442 if self._control_socket is None:
443 self._control_socket = self._create_connected_socket("control")
444 self._control_socket.linger = 100
445
446 def _close_control_socket(self) -> None:
447 if self._control_socket is None:
448 return
449 self._control_socket.close()
450 self._control_socket = None
451
452 async def _async_pre_start_kernel(
453 self, *, transport_encryption: str | None = None, **kw: t.Any
454 ) -> t.Tuple[t.List[str], t.Dict[str, t.Any]]:
455 """Prepares a kernel for startup in a separate process.
456
457 If random ports (port=0) are being used, this method must be called
458 before the channels are created.
459
460 Parameters
461 ----------
462 `**kw` : optional
463 keyword arguments that are passed down to build the kernel_cmd
464 and launching the kernel (e.g. Popen kwargs).
465 """
466 self.shutting_down = False
467 if transport_encryption is not None:
468 self.transport_encryption = self._transport_encryption_policy(transport_encryption)
469 self.kernel_id = self.kernel_id or kw.pop("kernel_id", str(uuid.uuid4()))
470 # save kwargs for use in restart
471 # assigning Traitlets Dicts to Dict make mypy unhappy but is ok
472 self._launch_args = kw.copy()
473 if (
474 self._transport_encryption_policy() == "required"
475 and not self._kernel_supports_curve_encryption()
476 ):
477 msg = (
478 "transport_encryption='required' but kernelspec does not declare "
479 "metadata.supported_encryption='curve'."
480 )
481 raise RuntimeError(msg)
482 if self.provisioner is None: # will not be None on restarts
483 self.provisioner = KPF.instance(parent=self.parent).create_provisioner_instance(
484 self.kernel_id,
485 self.kernel_spec,
486 parent=self,
487 )
488 kw = await self.provisioner.pre_launch(**kw)
489 kernel_cmd = kw.pop("cmd")
490 return kernel_cmd, kw
491
492 pre_start_kernel = run_sync(_async_pre_start_kernel)
493
494 async def _async_post_start_kernel(self, **kw: t.Any) -> None:
495 """Performs any post startup tasks relative to the kernel.
496
497 Parameters
498 ----------
499 `**kw` : optional
500 keyword arguments that were used in the kernel process's launch.
501 """
502 self.start_restarter()
503 self._connect_control_socket()
504 assert self.provisioner is not None
505 await self.provisioner.post_launch(**kw)
506
507 post_start_kernel = run_sync(_async_post_start_kernel)
508
509 @in_pending_state
510 async def _async_start_kernel(self, **kw: t.Any) -> None:
511 """Starts a kernel on this host in a separate process.
512
513 If random ports (port=0) are being used, this method must be called
514 before the channels are created.
515
516 Parameters
517 ----------
518 `**kw` : optional
519 keyword arguments that are passed down to build the kernel_cmd
520 and launching the kernel (e.g. Popen kwargs).
521 """
522 self._attempted_start = True
523 kernel_cmd, kw = await self._async_pre_start_kernel(**kw)
524
525 # launch the kernel subprocess
526 self.log.debug("Starting kernel: %s", kernel_cmd)
527 await self._async_launch_kernel(kernel_cmd, **kw)
528 await self._async_post_start_kernel(**kw)
529
530 start_kernel = run_sync(_async_start_kernel)
531
532 async def _async_request_shutdown(self, restart: bool = False) -> None:
533 """Send a shutdown request via control channel"""
534 content = {"restart": restart}
535 msg = self.session.msg("shutdown_request", content=content)
536 # ensure control socket is connected
537 self._connect_control_socket()
538 self.session.send(self._control_socket, msg)
539 assert self.provisioner is not None
540 await self.provisioner.shutdown_requested(restart=restart)
541 self._shutdown_status = _ShutdownStatus.ShutdownRequest
542
543 request_shutdown = run_sync(_async_request_shutdown)
544
545 async def _async_finish_shutdown(
546 self,
547 waittime: float | None = None,
548 pollinterval: float = 0.1,
549 restart: bool = False,
550 ) -> None:
551 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
552
553 This does not send shutdown requests - use :meth:`request_shutdown`
554 first.
555 """
556 if waittime is None:
557 waittime = max(self.shutdown_wait_time, 0)
558 if self.provisioner: # Allow provisioner to override
559 waittime = self.provisioner.get_shutdown_wait_time(recommended=waittime)
560
561 try:
562 await asyncio.wait_for(
563 self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
564 )
565 except asyncio.TimeoutError:
566 self.log.debug("Kernel is taking too long to finish, terminating")
567 self._shutdown_status = _ShutdownStatus.SigtermRequest
568 await self._async_send_kernel_sigterm()
569
570 try:
571 await asyncio.wait_for(
572 self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
573 )
574 except asyncio.TimeoutError:
575 self.log.debug("Kernel is taking too long to finish, killing")
576 self._shutdown_status = _ShutdownStatus.SigkillRequest
577 await self._async_kill_kernel(restart=restart)
578 else:
579 # Process is no longer alive, wait and clear
580 if self.has_kernel:
581 assert self.provisioner is not None
582 await self.provisioner.wait()
583
584 finish_shutdown = run_sync(_async_finish_shutdown)
585
586 async def _async_cleanup_resources(self, restart: bool = False) -> None:
587 """Clean up resources when the kernel is shut down"""
588 if not restart:
589 self.cleanup_connection_file()
590
591 self.cleanup_ipc_files()
592 self._close_control_socket()
593 self.session.parent = None
594
595 if self._created_context and not restart:
596 self.context.destroy(linger=100)
597
598 if self.provisioner:
599 await self.provisioner.cleanup(restart=restart)
600
601 cleanup_resources = run_sync(_async_cleanup_resources)
602
603 @in_pending_state
604 async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) -> None:
605 """Attempts to stop the kernel process cleanly.
606
607 This attempts to shutdown the kernels cleanly by:
608
609 1. Sending it a shutdown message over the control channel.
610 2. If that fails, the kernel is shutdown forcibly by sending it
611 a signal.
612
613 Parameters
614 ----------
615 now : bool
616 Should the kernel be forcible killed *now*. This skips the
617 first, nice shutdown attempt.
618 restart: bool
619 Will this kernel be restarted after it is shutdown. When this
620 is True, connection files will not be cleaned up.
621 """
622 if not self.owns_kernel:
623 return
624
625 self.shutting_down = True # Used by restarter to prevent race condition
626 # Stop monitoring for restarting while we shutdown.
627 self.stop_restarter()
628
629 if self.has_kernel:
630 await self._async_interrupt_kernel()
631
632 if now:
633 await self._async_kill_kernel()
634 else:
635 await self._async_request_shutdown(restart=restart)
636 # Don't send any additional kernel kill messages immediately, to give
637 # the kernel a chance to properly execute shutdown actions. Wait for at
638 # most 1s, checking every 0.1s.
639 await self._async_finish_shutdown(restart=restart)
640
641 await self._async_cleanup_resources(restart=restart)
642
643 shutdown_kernel = run_sync(_async_shutdown_kernel)
644
645 async def _async_restart_kernel(
646 self, now: bool = False, newports: bool = False, **kw: t.Any
647 ) -> None:
648 """Restarts a kernel with the arguments that were used to launch it.
649
650 Parameters
651 ----------
652 now : bool, optional
653 If True, the kernel is forcefully restarted *immediately*, without
654 having a chance to do any cleanup action. Otherwise the kernel is
655 given 1s to clean up before a forceful restart is issued.
656
657 In all cases the kernel is restarted, the only difference is whether
658 it is given a chance to perform a clean shutdown or not.
659
660 newports : bool, optional
661 If the old kernel was launched with random ports, this flag decides
662 whether the same ports and connection file will be used again.
663 If False, the same ports and connection file are used. This is
664 the default. If True, new random port numbers are chosen and a
665 new connection file is written. It is still possible that the newly
666 chosen random port numbers happen to be the same as the old ones.
667
668 `**kw` : optional
669 Any options specified here will overwrite those used to launch the
670 kernel.
671 """
672 if self._launch_args is None:
673 msg = "Cannot restart the kernel. No previous call to 'start_kernel'."
674 raise RuntimeError(msg)
675
676 # Stop currently running kernel.
677 await self._async_shutdown_kernel(now=now, restart=True)
678
679 if newports:
680 self.cleanup_random_ports()
681
682 # Start new kernel.
683 self._launch_args.update(kw)
684 await self._async_start_kernel(**self._launch_args)
685
686 restart_kernel = run_sync(_async_restart_kernel)
687
688 @property
689 def owns_kernel(self) -> bool:
690 return self._owns_kernel
691
692 @property
693 def has_kernel(self) -> bool:
694 """Has a kernel process been started that we are actively managing."""
695 return self.provisioner is not None and self.provisioner.has_process
696
697 async def _async_send_kernel_sigterm(self, restart: bool = False) -> None:
698 """similar to _kill_kernel, but with sigterm (not sigkill), but do not block"""
699 if self.has_kernel:
700 assert self.provisioner is not None
701 await self.provisioner.terminate(restart=restart)
702
703 _send_kernel_sigterm = run_sync(_async_send_kernel_sigterm)
704
705 async def _async_kill_kernel(self, restart: bool = False) -> None:
706 """Kill the running kernel.
707
708 This is a private method, callers should use shutdown_kernel(now=True).
709 """
710 if self.has_kernel:
711 assert self.provisioner is not None
712 await self.provisioner.kill(restart=restart)
713
714 # Wait until the kernel terminates.
715 try:
716 await asyncio.wait_for(self._async_wait(), timeout=5.0)
717 except asyncio.TimeoutError:
718 # Wait timed out, just log warning but continue - not much more we can do.
719 self.log.warning("Wait for final termination of kernel timed out - continuing...")
720 pass
721 else:
722 # Process is no longer alive, wait and clear
723 if self.has_kernel:
724 await self.provisioner.wait()
725
726 _kill_kernel = run_sync(_async_kill_kernel)
727
728 async def _async_interrupt_kernel(self) -> None:
729 """Interrupts the kernel by sending it a signal.
730
731 Unlike ``signal_kernel``, this operation is well supported on all
732 platforms.
733 """
734 if not self.has_kernel and self._ready is not None:
735 if isinstance(self._ready, CFuture):
736 ready = asyncio.ensure_future(t.cast(Future[t.Any], self._ready))
737 else:
738 ready = self._ready
739 # Wait for a shutdown if one is in progress.
740 if self.shutting_down:
741 await ready
742 # Wait for a startup.
743 await ready
744
745 if self.has_kernel:
746 assert self.kernel_spec is not None
747 interrupt_mode = self.kernel_spec.interrupt_mode
748 if interrupt_mode == "signal":
749 await self._async_signal_kernel(signal.SIGINT)
750
751 elif interrupt_mode == "message":
752 msg = self.session.msg("interrupt_request", content={})
753 self._connect_control_socket()
754 self.session.send(self._control_socket, msg)
755 else:
756 msg = "Cannot interrupt kernel. No kernel is running!"
757 raise RuntimeError(msg)
758
759 interrupt_kernel = run_sync(_async_interrupt_kernel)
760
761 async def _async_signal_kernel(self, signum: int) -> None:
762 """Sends a signal to the process group of the kernel (this
763 usually includes the kernel and any subprocesses spawned by
764 the kernel).
765
766 Note that since only SIGTERM is supported on Windows, this function is
767 only useful on Unix systems.
768 """
769 if self.has_kernel:
770 assert self.provisioner is not None
771 await self.provisioner.send_signal(signum)
772 else:
773 msg = "Cannot signal kernel. No kernel is running!"
774 raise RuntimeError(msg)
775
776 signal_kernel = run_sync(_async_signal_kernel)
777
778 async def _async_is_alive(self) -> bool:
779 """Is the kernel process still running?"""
780 if not self.owns_kernel:
781 return True
782
783 if self.has_kernel:
784 assert self.provisioner is not None
785 ret = await self.provisioner.poll()
786 if ret is None:
787 return True
788 return False
789
790 is_alive = run_sync(_async_is_alive)
791
792 async def _async_wait(self, pollinterval: float = 0.1) -> None:
793 # Use busy loop at 100ms intervals, polling until the process is
794 # not alive. If we find the process is no longer alive, complete
795 # its cleanup via the blocking wait(). Callers are responsible for
796 # issuing calls to wait() using a timeout (see _kill_kernel()).
797 while await self._async_is_alive():
798 await asyncio.sleep(pollinterval)
799
800
801class AsyncKernelManager(KernelManager):
802 """An async kernel manager."""
803
804 # the class to create with our `client` method
805 client_class: DottedObjectName = DottedObjectName(
806 "jupyter_client.asynchronous.AsyncKernelClient", config=True
807 )
808 client_factory: Type = Type(klass="jupyter_client.asynchronous.AsyncKernelClient", config=True)
809
810 # The PyZMQ Context to use for communication with the kernel.
811 context: Instance = Instance(zmq.asyncio.Context)
812
813 @default("context")
814 def _context_default(self) -> zmq.asyncio.Context:
815 self._created_context = True
816 return zmq.asyncio.Context()
817
818 def client( # type:ignore[override]
819 self, **kwargs: t.Any
820 ) -> AsyncKernelClient:
821 """Get a client for the manager."""
822 return super().client(**kwargs) # type:ignore[return-value]
823
824 _launch_kernel = KernelManager._async_launch_kernel # type:ignore[assignment]
825 start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_start_kernel # type:ignore[assignment]
826 pre_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_pre_start_kernel # type:ignore[assignment]
827 post_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_post_start_kernel # type:ignore[assignment]
828 request_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_request_shutdown # type:ignore[assignment]
829 finish_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_finish_shutdown # type:ignore[assignment]
830 cleanup_resources: t.Callable[..., t.Awaitable] = KernelManager._async_cleanup_resources # type:ignore[assignment]
831 shutdown_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_shutdown_kernel # type:ignore[assignment]
832 restart_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_restart_kernel # type:ignore[assignment]
833 _send_kernel_sigterm = KernelManager._async_send_kernel_sigterm # type:ignore[assignment]
834 _kill_kernel = KernelManager._async_kill_kernel # type:ignore[assignment]
835 interrupt_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_interrupt_kernel # type:ignore[assignment]
836 signal_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_signal_kernel # type:ignore[assignment]
837 is_alive: t.Callable[..., t.Awaitable] = KernelManager._async_is_alive # type:ignore[assignment]
838
839
840KernelManagerABC.register(KernelManager)
841
842
843def start_new_kernel(
844 startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
845) -> t.Tuple[KernelManager, BlockingKernelClient]:
846 """Start a new kernel, and return its Manager and Client"""
847 km = KernelManager(kernel_name=kernel_name)
848 km.start_kernel(**kwargs)
849 kc = km.client()
850 kc.start_channels()
851 try:
852 kc.wait_for_ready(timeout=startup_timeout)
853 except RuntimeError:
854 kc.stop_channels()
855 km.shutdown_kernel()
856 raise
857
858 return km, kc
859
860
861async def start_new_async_kernel(
862 startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
863) -> t.Tuple[AsyncKernelManager, AsyncKernelClient]:
864 """Start a new kernel, and return its Manager and Client"""
865 km = AsyncKernelManager(kernel_name=kernel_name)
866 await km.start_kernel(**kwargs)
867 kc = km.client()
868 kc.start_channels()
869 try:
870 await kc.wait_for_ready(timeout=startup_timeout)
871 except RuntimeError:
872 kc.stop_channels()
873 await km.shutdown_kernel()
874 raise
875
876 return (km, kc)
877
878
879@contextmanager
880def run_kernel(**kwargs: t.Any) -> t.Iterator[KernelClient]:
881 """Context manager to create a kernel in a subprocess.
882
883 The kernel is shut down when the context exits.
884
885 Returns
886 -------
887 kernel_client: connected KernelClient instance
888 """
889 km, kc = start_new_kernel(**kwargs)
890 try:
891 yield kc
892 finally:
893 kc.stop_channels()
894 km.shutdown_kernel(now=True)