provisioner_base.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. """Kernel Provisioner Classes"""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import os
  5. from abc import ABC, ABCMeta, abstractmethod
  6. from typing import Any, Union
  7. from traitlets.config import Instance, LoggingConfigurable, Unicode
  8. from ..connect import KernelConnectionInfo
  9. class KernelProvisionerMeta(ABCMeta, type(LoggingConfigurable)): # type: ignore[misc]
  10. pass
  11. class KernelProvisionerBase(ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta): # type: ignore[metaclass]
  12. """
  13. Abstract base class defining methods for KernelProvisioner classes.
  14. A majority of methods are abstract (requiring implementations via a subclass) while
  15. some are optional and others provide implementations common to all instances.
  16. Subclasses should be aware of which methods require a call to the superclass.
  17. Many of these methods model those of :class:`subprocess.Popen` for parity with
  18. previous versions where the kernel process was managed directly.
  19. """
  20. # The kernel specification associated with this provisioner
  21. kernel_spec: Any = Instance("jupyter_client.kernelspec.KernelSpec", allow_none=True)
  22. kernel_id: Union[str, Unicode] = Unicode(None, allow_none=True)
  23. connection_info: KernelConnectionInfo = {}
  24. @property
  25. @abstractmethod
  26. def has_process(self) -> bool:
  27. """
  28. Returns true if this provisioner is currently managing a process.
  29. This property is asserted to be True immediately following a call to
  30. the provisioner's :meth:`launch_kernel` method.
  31. """
  32. pass
  33. @abstractmethod
  34. async def poll(self) -> int | None:
  35. """
  36. Checks if kernel process is still running.
  37. If running, None is returned, otherwise the process's integer-valued exit code is returned.
  38. This method is called from :meth:`KernelManager.is_alive`.
  39. """
  40. pass
  41. @abstractmethod
  42. async def wait(self) -> int | None:
  43. """
  44. Waits for kernel process to terminate.
  45. This method is called from `KernelManager.finish_shutdown()` and
  46. `KernelManager.kill_kernel()` when terminating a kernel gracefully or
  47. immediately, respectively.
  48. """
  49. pass
  50. @abstractmethod
  51. async def send_signal(self, signum: int) -> None:
  52. """
  53. Sends signal identified by signum to the kernel process.
  54. This method is called from `KernelManager.signal_kernel()` to send the
  55. kernel process a signal.
  56. """
  57. pass
  58. @abstractmethod
  59. async def kill(self, restart: bool = False) -> None:
  60. """
  61. Kill the kernel process.
  62. This is typically accomplished via a SIGKILL signal, which cannot be caught.
  63. This method is called from `KernelManager.kill_kernel()` when terminating
  64. a kernel immediately.
  65. restart is True if this operation will precede a subsequent launch_kernel request.
  66. """
  67. pass
  68. @abstractmethod
  69. async def terminate(self, restart: bool = False) -> None:
  70. """
  71. Terminates the kernel process.
  72. This is typically accomplished via a SIGTERM signal, which can be caught, allowing
  73. the kernel provisioner to perform possible cleanup of resources. This method is
  74. called indirectly from `KernelManager.finish_shutdown()` during a kernel's
  75. graceful termination.
  76. restart is True if this operation precedes a start launch_kernel request.
  77. """
  78. pass
  79. @abstractmethod
  80. async def launch_kernel(self, cmd: list[str], **kwargs: Any) -> KernelConnectionInfo:
  81. """
  82. Launch the kernel process and return its connection information.
  83. This method is called from `KernelManager.launch_kernel()` during the
  84. kernel manager's start kernel sequence.
  85. """
  86. pass
  87. @abstractmethod
  88. async def cleanup(self, restart: bool = False) -> None:
  89. """
  90. Cleanup any resources allocated on behalf of the kernel provisioner.
  91. This method is called from `KernelManager.cleanup_resources()` as part of
  92. its shutdown kernel sequence.
  93. restart is True if this operation precedes a start launch_kernel request.
  94. """
  95. pass
  96. async def shutdown_requested(self, restart: bool = False) -> None:
  97. """
  98. Allows the provisioner to determine if the kernel's shutdown has been requested.
  99. This method is called from `KernelManager.request_shutdown()` as part of
  100. its shutdown sequence.
  101. This method is optional and is primarily used in scenarios where the provisioner
  102. may need to perform other operations in preparation for a kernel's shutdown.
  103. """
  104. pass
  105. async def pre_launch(self, **kwargs: Any) -> dict[str, Any]:
  106. """
  107. Perform any steps in preparation for kernel process launch.
  108. This includes applying additional substitutions to the kernel launch command
  109. and environment. It also includes preparation of launch parameters.
  110. NOTE: Subclass implementations are advised to call this method as it applies
  111. environment variable substitutions from the local environment and calls the
  112. provisioner's :meth:`_finalize_env()` method to allow each provisioner the
  113. ability to cleanup the environment variables that will be used by the kernel.
  114. This method is called from `KernelManager.pre_start_kernel()` as part of its
  115. start kernel sequence.
  116. Returns the (potentially updated) keyword arguments that are passed to
  117. :meth:`launch_kernel()`.
  118. """
  119. env = kwargs.pop("env", os.environ).copy()
  120. env.update(self.__apply_env_substitutions(env))
  121. self._finalize_env(env)
  122. kwargs["env"] = env
  123. return kwargs
  124. async def post_launch(self, **kwargs: Any) -> None:
  125. """
  126. Perform any steps following the kernel process launch.
  127. This method is called from `KernelManager.post_start_kernel()` as part of its
  128. start kernel sequence.
  129. """
  130. pass
  131. async def get_provisioner_info(self) -> dict[str, Any]:
  132. """
  133. Captures the base information necessary for persistence relative to this instance.
  134. This enables applications that subclass `KernelManager` to persist a kernel provisioner's
  135. relevant information to accomplish functionality like disaster recovery or high availability
  136. by calling this method via the kernel manager's `provisioner` attribute.
  137. NOTE: The superclass method must always be called first to ensure proper serialization.
  138. """
  139. provisioner_info: dict[str, Any] = {}
  140. provisioner_info["kernel_id"] = self.kernel_id
  141. provisioner_info["connection_info"] = self.connection_info
  142. return provisioner_info
  143. async def load_provisioner_info(self, provisioner_info: dict) -> None:
  144. """
  145. Loads the base information necessary for persistence relative to this instance.
  146. The inverse of `get_provisioner_info()`, this enables applications that subclass
  147. `KernelManager` to re-establish communication with a provisioner that is managing
  148. a (presumably) remote kernel from an entirely different process that the original
  149. provisioner.
  150. NOTE: The superclass method must always be called first to ensure proper deserialization.
  151. """
  152. self.kernel_id = provisioner_info["kernel_id"]
  153. self.connection_info = provisioner_info["connection_info"]
  154. def get_shutdown_wait_time(self, recommended: float = 5.0) -> float:
  155. """
  156. Returns the time allowed for a complete shutdown. This may vary by provisioner.
  157. This method is called from `KernelManager.finish_shutdown()` during the graceful
  158. phase of its kernel shutdown sequence.
  159. The recommended value will typically be what is configured in the kernel manager.
  160. """
  161. return recommended
  162. def get_stable_start_time(self, recommended: float = 10.0) -> float:
  163. """
  164. Returns the expected upper bound for a kernel (re-)start to complete.
  165. This may vary by provisioner.
  166. The recommended value will typically be what is configured in the kernel restarter.
  167. """
  168. return recommended
  169. def resolve_path(self, path: str) -> str | None:
  170. """
  171. Returns the path resolved relative to kernel working directory.
  172. For example, path `my_code.py` for a kernel started in `/tmp/`
  173. should result in `/tmp/my_code.py`, while path `~/test.py` for
  174. a kernel started in `/home/my_user/` should resolve to the
  175. (fully specified) `/home/my_user/test.py` path.
  176. The provisioner may choose not to resolve any paths, or restrict
  177. the resolution to paths local to the kernel working directory
  178. to prevent path traversal and exposure of file system layout.
  179. """
  180. return None
  181. def _finalize_env(self, env: dict[str, str]) -> None:
  182. """
  183. Ensures env is appropriate prior to launch.
  184. This method is called from `KernelProvisionerBase.pre_launch()` during the kernel's
  185. start sequence.
  186. NOTE: Subclasses should be sure to call super()._finalize_env(env)
  187. """
  188. if self.kernel_spec.language and self.kernel_spec.language.lower().startswith("python"):
  189. # Don't allow PYTHONEXECUTABLE to be passed to kernel process.
  190. # If set, it can bork all the things.
  191. env.pop("PYTHONEXECUTABLE", None)
  192. def __apply_env_substitutions(self, substitution_values: dict[str, str]) -> dict[str, str]:
  193. """
  194. Walks entries in the kernelspec's env stanza and applies substitutions from current env.
  195. This method is called from `KernelProvisionerBase.pre_launch()` during the kernel's
  196. start sequence.
  197. Returns the substituted list of env entries.
  198. NOTE: This method is private and is not intended to be overridden by provisioners.
  199. """
  200. substituted_env = {}
  201. if self.kernel_spec:
  202. from string import Template
  203. # For each templated env entry, fill any templated references
  204. # matching names of env variables with those values and build
  205. # new dict with substitutions.
  206. templated_env = self.kernel_spec.env
  207. for k, v in templated_env.items():
  208. substituted_env.update({k: Template(v).safe_substitute(substitution_values)})
  209. return substituted_env