1"""Utilities for launching kernels"""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5import os
6import sys
7import warnings
8from subprocess import PIPE, Popen
9from typing import Any
10
11from traitlets.log import get_logger
12
13
14def launch_kernel(
15 cmd: list[str],
16 stdin: int | None = None,
17 stdout: int | None = None,
18 stderr: int | None = None,
19 env: dict[str, str] | None = None,
20 independent: bool = False,
21 cwd: str | None = None,
22 **kw: Any,
23) -> Popen:
24 """Launches a localhost kernel, binding to the specified ports.
25
26 Parameters
27 ----------
28 cmd : Popen list,
29 A string of Python code that imports and executes a kernel entry point.
30
31 stdin, stdout, stderr : optional (default None)
32 Standards streams, as defined in subprocess.Popen.
33
34 env: dict, optional
35 Environment variables passed to the kernel
36
37 independent : bool, optional (default False)
38 If set, the kernel process is guaranteed to survive if this process
39 dies. If not set, an effort is made to ensure that the kernel is killed
40 when this process dies. Note that in this case it is still good practice
41 to kill kernels manually before exiting.
42
43 cwd : path, optional
44 The working dir of the kernel process (default: cwd of this process).
45
46 **kw: optional
47 Additional arguments for Popen
48
49 Returns
50 -------
51
52 Popen instance for the kernel subprocess
53 """
54
55 # Popen will fail (sometimes with a deadlock) if stdin, stdout, and stderr
56 # are invalid. Unfortunately, there is in general no way to detect whether
57 # they are valid. The following two blocks redirect them to (temporary)
58 # pipes in certain important cases.
59
60 # If this process has been backgrounded, our stdin is invalid. Since there
61 # is no compelling reason for the kernel to inherit our stdin anyway, we'll
62 # place this one safe and always redirect.
63 redirect_in = True
64 _stdin = PIPE if stdin is None else stdin
65
66 # If this process in running on pythonw, we know that stdin, stdout, and
67 # stderr are all invalid.
68 redirect_out = sys.executable.endswith("pythonw.exe")
69 _stdout: Any
70 _stderr: Any
71 if redirect_out:
72 blackhole = open(os.devnull, "w") # noqa
73 _stdout = blackhole if stdout is None else stdout
74 _stderr = blackhole if stderr is None else stderr
75 else:
76 _stdout, _stderr = stdout, stderr
77
78 env = env if (env is not None) else os.environ.copy()
79
80 kwargs = kw.copy()
81 main_args = {
82 "stdin": _stdin,
83 "stdout": _stdout,
84 "stderr": _stderr,
85 "cwd": cwd,
86 "env": env,
87 }
88 kwargs.update(main_args)
89
90 # Spawn a kernel.
91 if sys.platform == "win32":
92 if cwd:
93 kwargs["cwd"] = cwd
94
95 from .win_interrupt import create_interrupt_event
96
97 # Create a Win32 event for interrupting the kernel
98 # and store it in an environment variable.
99 interrupt_event = create_interrupt_event()
100 env["JPY_INTERRUPT_EVENT"] = str(interrupt_event)
101 # deprecated old env name:
102 env["IPY_INTERRUPT_EVENT"] = env["JPY_INTERRUPT_EVENT"]
103
104 try:
105 from _winapi import (
106 CREATE_NEW_PROCESS_GROUP,
107 DUPLICATE_SAME_ACCESS,
108 DuplicateHandle,
109 GetCurrentProcess,
110 )
111 except: # noqa
112 from _subprocess import (
113 CREATE_NEW_PROCESS_GROUP,
114 DUPLICATE_SAME_ACCESS,
115 DuplicateHandle,
116 GetCurrentProcess,
117 )
118
119 # create a handle on the parent to be inherited
120 if independent:
121 kwargs["creationflags"] = CREATE_NEW_PROCESS_GROUP
122 else:
123 pid = GetCurrentProcess()
124 handle = DuplicateHandle(
125 pid,
126 pid,
127 pid,
128 0,
129 True,
130 DUPLICATE_SAME_ACCESS, # Inheritable by new processes.
131 )
132 env["JPY_PARENT_PID"] = str(int(handle))
133
134 # Prevent creating new console window on pythonw
135 if redirect_out:
136 kwargs["creationflags"] = (
137 kwargs.setdefault("creationflags", 0) | 0x08000000
138 ) # CREATE_NO_WINDOW
139
140 # Avoid closing the above parent and interrupt handles.
141 # close_fds is True by default on Python >=3.7
142 # or when no stream is captured on Python <3.7
143 # (we always capture stdin, so this is already False by default on <3.7)
144 kwargs["close_fds"] = False
145 else:
146 # Create a new session.
147 # This makes it easier to interrupt the kernel,
148 # because we want to interrupt the whole process group.
149 # We don't use setpgrp, which is known to cause problems for kernels starting
150 # certain interactive subprocesses, such as bash -i.
151 kwargs["start_new_session"] = True
152 if not independent:
153 env["JPY_PARENT_PID"] = str(os.getpid())
154
155 try:
156 # Allow to use ~/ in the command or its arguments
157 cmd = [os.path.expanduser(s) for s in cmd]
158 proc = Popen(cmd, **kwargs) # noqa
159 except Exception as ex:
160 try:
161 msg = "Failed to run command:\n{}\n PATH={!r}\n with kwargs:\n{!r}\n"
162 # exclude environment variables,
163 # which may contain access tokens and the like.
164 without_env = {key: value for key, value in kwargs.items() if key != "env"}
165 msg = msg.format(cmd, env.get("PATH", os.defpath), without_env)
166 get_logger().error(msg)
167 except Exception as ex2: # Don't let a formatting/logger issue lead to the wrong exception
168 warnings.warn(f"Failed to run command: '{cmd}' due to exception: {ex}", stacklevel=2)
169 warnings.warn(
170 f"The following exception occurred handling the previous failure: {ex2}",
171 stacklevel=2,
172 )
173 raise ex
174
175 if sys.platform == "win32":
176 # Attach the interrupt event to the Popen object so it can be used later.
177 proc.win32_interrupt_event = interrupt_event
178
179 # Clean up pipes created to work around Popen bug.
180 if redirect_in and stdin is None:
181 assert proc.stdin is not None
182 proc.stdin.close()
183
184 return proc
185
186
187__all__ = [
188 "launch_kernel",
189]