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