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
349 for key in ("curve_publickey", "curve_secretkey"):
350 if isinstance(kw.get(key), str):
351 kw[key] = kw[key].encode("ascii")
352
353 return self.client_factory(**kw)
354
355 # --------------------------------------------------------------------------
356 # Kernel management
357 # --------------------------------------------------------------------------
358
359 def resolve_path(self, path: str) -> str | None:
360 """Resolve path to given file."""
361 assert self.provisioner is not None
362 return self.provisioner.resolve_path(path)
363
364 def update_env(self, *, env: t.Dict[str, str]) -> None:
365 """
366 Allow to update the environment of a kernel manager.
367
368 This will take effect only after kernel restart when the new env is
369 passed to the new kernel.
370
371 This is useful as some of the information of the current kernel reflect
372 the state of the session that started it, and those session information
373 (like the attach file path, or name), are mutable.
374
375 .. version-added: 8.5
376 """
377 # Mypy think this is unreachable as it see _launch_args as Dict, not t.Dict
378 if (
379 isinstance(self._launch_args, dict)
380 and "env" in self._launch_args
381 and isinstance(self._launch_args["env"], dict) # type: ignore [unreachable]
382 ):
383 self._launch_args["env"].update(env) # type: ignore [unreachable]
384
385 def format_kernel_cmd(self, extra_arguments: t.List[str] | None = None) -> t.List[str]:
386 """Replace templated args (e.g. {connection_file})"""
387 extra_arguments = extra_arguments or []
388 assert self.kernel_spec is not None
389 cmd = self.kernel_spec.argv + extra_arguments
390
391 if cmd and cmd[0] in {
392 "python",
393 "python%i" % sys.version_info[0],
394 "python%i.%i" % sys.version_info[:2],
395 }:
396 # executable is 'python' or 'python3', use sys.executable.
397 # These will typically be the same,
398 # but if the current process is in an env
399 # and has been launched by abspath without
400 # activating the env, python on PATH may not be sys.executable,
401 # but it should be.
402 cmd[0] = sys.executable
403
404 # Make sure to use the realpath for the connection_file
405 # On windows, when running with the store python, the connection_file path
406 # is not usable by non python kernels because the path is being rerouted when
407 # inside of a store app.
408 # See this bug here: https://bugs.python.org/issue41196
409 ns: t.Dict[str, t.Any] = {
410 "connection_file": os.path.realpath(self.connection_file),
411 "prefix": sys.prefix,
412 }
413
414 if self.kernel_spec: # type:ignore[truthy-bool]
415 ns["resource_dir"] = self.kernel_spec.resource_dir
416 assert isinstance(self._launch_args, dict)
417
418 ns.update(self._launch_args)
419
420 pat = re.compile(r"\{([A-Za-z0-9_]+)\}")
421
422 def from_ns(match: t.Any) -> t.Any:
423 """Get the key out of ns if it's there, otherwise no change."""
424 return ns.get(match.group(1), match.group())
425
426 return [pat.sub(from_ns, arg) for arg in cmd]
427
428 async def _async_launch_kernel(self, kernel_cmd: t.List[str], **kw: t.Any) -> None:
429 """actually launch the kernel
430
431 override in a subclass to launch kernel subprocesses differently
432 Note that provisioners can now be used to customize kernel environments
433 and
434 """
435 assert self.provisioner is not None
436 connection_info = await self.provisioner.launch_kernel(kernel_cmd, **kw)
437 assert self.provisioner.has_process
438 # Provisioner provides the connection information. Load into kernel manager
439 # and write the connection file, if not already done.
440 self._reconcile_connection_info(connection_info)
441
442 _launch_kernel = run_sync(_async_launch_kernel)
443
444 # Control socket used for polite kernel shutdown
445
446 def _connect_control_socket(self) -> None:
447 if self._control_socket is None:
448 self._control_socket = self._create_connected_socket("control")
449 self._control_socket.linger = 100
450
451 def _close_control_socket(self) -> None:
452 if self._control_socket is None:
453 return
454 self._control_socket.close()
455 self._control_socket = None
456
457 async def _async_pre_start_kernel(
458 self, *, transport_encryption: str | None = None, **kw: t.Any
459 ) -> t.Tuple[t.List[str], t.Dict[str, t.Any]]:
460 """Prepares a kernel for startup in a separate process.
461
462 If random ports (port=0) are being used, this method must be called
463 before the channels are created.
464
465 Parameters
466 ----------
467 `**kw` : optional
468 keyword arguments that are passed down to build the kernel_cmd
469 and launching the kernel (e.g. Popen kwargs).
470 """
471 self.shutting_down = False
472 if transport_encryption is not None:
473 self.transport_encryption = self._transport_encryption_policy(transport_encryption)
474 self.kernel_id = self.kernel_id or kw.pop("kernel_id", str(uuid.uuid4()))
475 # save kwargs for use in restart
476 # assigning Traitlets Dicts to Dict make mypy unhappy but is ok
477 self._launch_args = kw.copy()
478 if (
479 self._transport_encryption_policy() == "required"
480 and not self._kernel_supports_curve_encryption()
481 ):
482 msg = (
483 "transport_encryption='required' but kernelspec does not declare "
484 "'curve' in `metadata.supported_encryption`."
485 )
486 raise RuntimeError(msg)
487 if self.provisioner is None: # will not be None on restarts
488 self.provisioner = KPF.instance(parent=self.parent).create_provisioner_instance(
489 self.kernel_id,
490 self.kernel_spec,
491 parent=self,
492 )
493 kw = await self.provisioner.pre_launch(**kw)
494 kernel_cmd = kw.pop("cmd")
495 return kernel_cmd, kw
496
497 pre_start_kernel = run_sync(_async_pre_start_kernel)
498
499 async def _async_post_start_kernel(self, **kw: t.Any) -> None:
500 """Performs any post startup tasks relative to the kernel.
501
502 Parameters
503 ----------
504 `**kw` : optional
505 keyword arguments that were used in the kernel process's launch.
506 """
507 self.start_restarter()
508 self._connect_control_socket()
509 assert self.provisioner is not None
510 await self.provisioner.post_launch(**kw)
511
512 post_start_kernel = run_sync(_async_post_start_kernel)
513
514 @in_pending_state
515 async def _async_start_kernel(self, **kw: t.Any) -> None:
516 """Starts a kernel on this host in a separate process.
517
518 If random ports (port=0) are being used, this method must be called
519 before the channels are created.
520
521 Parameters
522 ----------
523 `**kw` : optional
524 keyword arguments that are passed down to build the kernel_cmd
525 and launching the kernel (e.g. Popen kwargs).
526 """
527 self._attempted_start = True
528 kernel_cmd, kw = await self._async_pre_start_kernel(**kw)
529
530 # launch the kernel subprocess
531 self.log.debug("Starting kernel: %s", kernel_cmd)
532 await self._async_launch_kernel(kernel_cmd, **kw)
533 await self._async_post_start_kernel(**kw)
534
535 start_kernel = run_sync(_async_start_kernel)
536
537 async def _async_request_shutdown(self, restart: bool = False) -> None:
538 """Send a shutdown request via control channel"""
539 content = {"restart": restart}
540 msg = self.session.msg("shutdown_request", content=content)
541 # ensure control socket is connected
542 self._connect_control_socket()
543 self.session.send(self._control_socket, msg)
544 assert self.provisioner is not None
545 await self.provisioner.shutdown_requested(restart=restart)
546 self._shutdown_status = _ShutdownStatus.ShutdownRequest
547
548 request_shutdown = run_sync(_async_request_shutdown)
549
550 async def _async_finish_shutdown(
551 self,
552 waittime: float | None = None,
553 pollinterval: float = 0.1,
554 restart: bool = False,
555 ) -> None:
556 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
557
558 This does not send shutdown requests - use :meth:`request_shutdown`
559 first.
560 """
561 if waittime is None:
562 waittime = max(self.shutdown_wait_time, 0)
563 if self.provisioner: # Allow provisioner to override
564 waittime = self.provisioner.get_shutdown_wait_time(recommended=waittime)
565
566 try:
567 await asyncio.wait_for(
568 self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
569 )
570 except asyncio.TimeoutError:
571 self.log.debug("Kernel is taking too long to finish, terminating")
572 self._shutdown_status = _ShutdownStatus.SigtermRequest
573 await self._async_send_kernel_sigterm()
574
575 try:
576 await asyncio.wait_for(
577 self._async_wait(pollinterval=pollinterval), timeout=waittime / 2
578 )
579 except asyncio.TimeoutError:
580 self.log.debug("Kernel is taking too long to finish, killing")
581 self._shutdown_status = _ShutdownStatus.SigkillRequest
582 await self._async_kill_kernel(restart=restart)
583 else:
584 # Process is no longer alive, wait and clear
585 if self.has_kernel:
586 assert self.provisioner is not None
587 await self.provisioner.wait()
588
589 finish_shutdown = run_sync(_async_finish_shutdown)
590
591 async def _async_cleanup_resources(self, restart: bool = False) -> None:
592 """Clean up resources when the kernel is shut down"""
593 if not restart:
594 self.cleanup_connection_file()
595
596 self.cleanup_ipc_files()
597 self._close_control_socket()
598 self.session.parent = None
599
600 if self._created_context and not restart:
601 self.context.destroy(linger=100)
602
603 if self.provisioner:
604 await self.provisioner.cleanup(restart=restart)
605
606 cleanup_resources = run_sync(_async_cleanup_resources)
607
608 @in_pending_state
609 async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) -> None:
610 """Attempts to stop the kernel process cleanly.
611
612 This attempts to shutdown the kernels cleanly by:
613
614 1. Sending it a shutdown message over the control channel.
615 2. If that fails, the kernel is shutdown forcibly by sending it
616 a signal.
617
618 Parameters
619 ----------
620 now : bool
621 Should the kernel be forcible killed *now*. This skips the
622 first, nice shutdown attempt.
623 restart: bool
624 Will this kernel be restarted after it is shutdown. When this
625 is True, connection files will not be cleaned up.
626 """
627 if not self.owns_kernel:
628 return
629
630 self.shutting_down = True # Used by restarter to prevent race condition
631 # Stop monitoring for restarting while we shutdown.
632 self.stop_restarter()
633
634 if self.has_kernel:
635 await self._async_interrupt_kernel()
636
637 if now:
638 await self._async_kill_kernel()
639 else:
640 await self._async_request_shutdown(restart=restart)
641 # Don't send any additional kernel kill messages immediately, to give
642 # the kernel a chance to properly execute shutdown actions. Wait for at
643 # most 1s, checking every 0.1s.
644 await self._async_finish_shutdown(restart=restart)
645
646 await self._async_cleanup_resources(restart=restart)
647
648 shutdown_kernel = run_sync(_async_shutdown_kernel)
649
650 async def _async_restart_kernel(
651 self, now: bool = False, newports: bool = False, **kw: t.Any
652 ) -> None:
653 """Restarts a kernel with the arguments that were used to launch it.
654
655 Parameters
656 ----------
657 now : bool, optional
658 If True, the kernel is forcefully restarted *immediately*, without
659 having a chance to do any cleanup action. Otherwise the kernel is
660 given 1s to clean up before a forceful restart is issued.
661
662 In all cases the kernel is restarted, the only difference is whether
663 it is given a chance to perform a clean shutdown or not.
664
665 newports : bool, optional
666 If the old kernel was launched with random ports, this flag decides
667 whether the same ports and connection file will be used again.
668 If False, the same ports and connection file are used. This is
669 the default. If True, new random port numbers are chosen and a
670 new connection file is written. It is still possible that the newly
671 chosen random port numbers happen to be the same as the old ones.
672
673 `**kw` : optional
674 Any options specified here will overwrite those used to launch the
675 kernel.
676 """
677 if self._launch_args is None:
678 msg = "Cannot restart the kernel. No previous call to 'start_kernel'."
679 raise RuntimeError(msg)
680
681 # Stop currently running kernel.
682 await self._async_shutdown_kernel(now=now, restart=True)
683
684 if newports:
685 self.cleanup_random_ports()
686
687 # Start new kernel.
688 self._launch_args.update(kw)
689 await self._async_start_kernel(**self._launch_args)
690
691 restart_kernel = run_sync(_async_restart_kernel)
692
693 @property
694 def owns_kernel(self) -> bool:
695 return self._owns_kernel
696
697 @property
698 def has_kernel(self) -> bool:
699 """Has a kernel process been started that we are actively managing."""
700 return self.provisioner is not None and self.provisioner.has_process
701
702 async def _async_send_kernel_sigterm(self, restart: bool = False) -> None:
703 """similar to _kill_kernel, but with sigterm (not sigkill), but do not block"""
704 if self.has_kernel:
705 assert self.provisioner is not None
706 await self.provisioner.terminate(restart=restart)
707
708 _send_kernel_sigterm = run_sync(_async_send_kernel_sigterm)
709
710 async def _async_kill_kernel(self, restart: bool = False) -> None:
711 """Kill the running kernel.
712
713 This is a private method, callers should use shutdown_kernel(now=True).
714 """
715 if self.has_kernel:
716 assert self.provisioner is not None
717 await self.provisioner.kill(restart=restart)
718
719 # Wait until the kernel terminates.
720 try:
721 await asyncio.wait_for(self._async_wait(), timeout=5.0)
722 except asyncio.TimeoutError:
723 # Wait timed out, just log warning but continue - not much more we can do.
724 self.log.warning("Wait for final termination of kernel timed out - continuing...")
725 pass
726 else:
727 # Process is no longer alive, wait and clear
728 if self.has_kernel:
729 await self.provisioner.wait()
730
731 _kill_kernel = run_sync(_async_kill_kernel)
732
733 async def _async_interrupt_kernel(self) -> None:
734 """Interrupts the kernel by sending it a signal.
735
736 Unlike ``signal_kernel``, this operation is well supported on all
737 platforms.
738 """
739 if not self.has_kernel and self._ready is not None:
740 if isinstance(self._ready, CFuture):
741 ready = asyncio.ensure_future(t.cast(Future[t.Any], self._ready))
742 else:
743 ready = self._ready
744 # Wait for a shutdown if one is in progress.
745 if self.shutting_down:
746 await ready
747 # Wait for a startup.
748 await ready
749
750 if self.has_kernel:
751 assert self.kernel_spec is not None
752 interrupt_mode = self.kernel_spec.interrupt_mode
753 if interrupt_mode == "signal":
754 await self._async_signal_kernel(signal.SIGINT)
755
756 elif interrupt_mode == "message":
757 msg = self.session.msg("interrupt_request", content={})
758 self._connect_control_socket()
759 self.session.send(self._control_socket, msg)
760 else:
761 msg = "Cannot interrupt kernel. No kernel is running!"
762 raise RuntimeError(msg)
763
764 interrupt_kernel = run_sync(_async_interrupt_kernel)
765
766 async def _async_signal_kernel(self, signum: int) -> None:
767 """Sends a signal to the process group of the kernel (this
768 usually includes the kernel and any subprocesses spawned by
769 the kernel).
770
771 Note that since only SIGTERM is supported on Windows, this function is
772 only useful on Unix systems.
773 """
774 if self.has_kernel:
775 assert self.provisioner is not None
776 await self.provisioner.send_signal(signum)
777 else:
778 msg = "Cannot signal kernel. No kernel is running!"
779 raise RuntimeError(msg)
780
781 signal_kernel = run_sync(_async_signal_kernel)
782
783 async def _async_is_alive(self) -> bool:
784 """Is the kernel process still running?"""
785 if not self.owns_kernel:
786 return True
787
788 if self.has_kernel:
789 assert self.provisioner is not None
790 ret = await self.provisioner.poll()
791 if ret is None:
792 return True
793 return False
794
795 is_alive = run_sync(_async_is_alive)
796
797 async def _async_wait(self, pollinterval: float = 0.1) -> None:
798 # Use busy loop at 100ms intervals, polling until the process is
799 # not alive. If we find the process is no longer alive, complete
800 # its cleanup via the blocking wait(). Callers are responsible for
801 # issuing calls to wait() using a timeout (see _kill_kernel()).
802 while await self._async_is_alive():
803 await asyncio.sleep(pollinterval)
804
805
806class AsyncKernelManager(KernelManager):
807 """An async kernel manager."""
808
809 # the class to create with our `client` method
810 client_class: DottedObjectName = DottedObjectName(
811 "jupyter_client.asynchronous.AsyncKernelClient", config=True
812 )
813 client_factory: Type = Type(klass="jupyter_client.asynchronous.AsyncKernelClient", config=True)
814
815 # The PyZMQ Context to use for communication with the kernel.
816 context: Instance = Instance(zmq.asyncio.Context)
817
818 @default("context")
819 def _context_default(self) -> zmq.asyncio.Context:
820 self._created_context = True
821 return zmq.asyncio.Context()
822
823 def client( # type:ignore[override]
824 self, **kwargs: t.Any
825 ) -> AsyncKernelClient:
826 """Get a client for the manager."""
827 return super().client(**kwargs) # type:ignore[return-value]
828
829 _launch_kernel = KernelManager._async_launch_kernel # type:ignore[assignment]
830 start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_start_kernel # type:ignore[assignment]
831 pre_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_pre_start_kernel # type:ignore[assignment]
832 post_start_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_post_start_kernel # type:ignore[assignment]
833 request_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_request_shutdown # type:ignore[assignment]
834 finish_shutdown: t.Callable[..., t.Awaitable] = KernelManager._async_finish_shutdown # type:ignore[assignment]
835 cleanup_resources: t.Callable[..., t.Awaitable] = KernelManager._async_cleanup_resources # type:ignore[assignment]
836 shutdown_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_shutdown_kernel # type:ignore[assignment]
837 restart_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_restart_kernel # type:ignore[assignment]
838 _send_kernel_sigterm = KernelManager._async_send_kernel_sigterm # type:ignore[assignment]
839 _kill_kernel = KernelManager._async_kill_kernel # type:ignore[assignment]
840 interrupt_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_interrupt_kernel # type:ignore[assignment]
841 signal_kernel: t.Callable[..., t.Awaitable] = KernelManager._async_signal_kernel # type:ignore[assignment]
842 is_alive: t.Callable[..., t.Awaitable] = KernelManager._async_is_alive # type:ignore[assignment]
843
844
845KernelManagerABC.register(KernelManager)
846
847
848def start_new_kernel(
849 startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
850) -> t.Tuple[KernelManager, BlockingKernelClient]:
851 """Start a new kernel, and return its Manager and Client"""
852 km = KernelManager(kernel_name=kernel_name)
853 km.start_kernel(**kwargs)
854 kc = km.client()
855 kc.start_channels()
856 try:
857 kc.wait_for_ready(timeout=startup_timeout)
858 except RuntimeError:
859 kc.stop_channels()
860 km.shutdown_kernel()
861 raise
862
863 return km, kc
864
865
866async def start_new_async_kernel(
867 startup_timeout: float = 60, kernel_name: str = "python", **kwargs: t.Any
868) -> t.Tuple[AsyncKernelManager, AsyncKernelClient]:
869 """Start a new kernel, and return its Manager and Client"""
870 km = AsyncKernelManager(kernel_name=kernel_name)
871 await km.start_kernel(**kwargs)
872 kc = km.client()
873 kc.start_channels()
874 try:
875 await kc.wait_for_ready(timeout=startup_timeout)
876 except RuntimeError:
877 kc.stop_channels()
878 await km.shutdown_kernel()
879 raise
880
881 return (km, kc)
882
883
884@contextmanager
885def run_kernel(**kwargs: t.Any) -> t.Iterator[KernelClient]:
886 """Context manager to create a kernel in a subprocess.
887
888 The kernel is shut down when the context exits.
889
890 Returns
891 -------
892 kernel_client: connected KernelClient instance
893 """
894 km, kc = start_new_kernel(**kwargs)
895 try:
896 yield kc
897 finally:
898 kc.stop_channels()
899 km.shutdown_kernel(now=True)