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