##// END OF EJS Templates
Separate parent functionality from ParentPollerWindows...
Thomas Kluyver -
Show More
@@ -0,0 +1,39 b''
1 """Use a Windows event to interrupt a child process like SIGINT.
2
3 The child needs to explicitly listen for this - see
4 ipython_kernel.parentpoller.ParentPollerWindows for a Python implementation.
5 """
6
7 import ctypes
8
9 def create_interrupt_event():
10 """Create an interrupt event handle.
11
12 The parent process should call this to create the
13 interrupt event that is passed to the child process. It should store
14 this handle and use it with ``send_interrupt`` to interrupt the child
15 process.
16 """
17 # Create a security attributes struct that permits inheritance of the
18 # handle by new processes.
19 # FIXME: We can clean up this mess by requiring pywin32 for IPython.
20 class SECURITY_ATTRIBUTES(ctypes.Structure):
21 _fields_ = [ ("nLength", ctypes.c_int),
22 ("lpSecurityDescriptor", ctypes.c_void_p),
23 ("bInheritHandle", ctypes.c_int) ]
24 sa = SECURITY_ATTRIBUTES()
25 sa_p = ctypes.pointer(sa)
26 sa.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
27 sa.lpSecurityDescriptor = 0
28 sa.bInheritHandle = 1
29
30 return ctypes.windll.kernel32.CreateEventA(
31 sa_p, # lpEventAttributes
32 False, # bManualReset
33 False, # bInitialState
34 '') # lpName
35
36 def send_interrupt(interrupt_handle):
37 """ Sends an interrupt event using the specified handle.
38 """
39 ctypes.windll.kernel32.SetEvent(interrupt_handle)
@@ -1,148 +1,114 b''
1 1 # Copyright (c) IPython Development Team.
2 2 # Distributed under the terms of the Modified BSD License.
3 3
4 4 try:
5 5 import ctypes
6 6 except:
7 7 ctypes = None
8 8 import os
9 9 import platform
10 10 import signal
11 11 import time
12 12 try:
13 13 from _thread import interrupt_main # Py 3
14 14 except ImportError:
15 15 from thread import interrupt_main # Py 2
16 16 from threading import Thread
17 17
18 18 from IPython.utils.warn import warn
19 19
20 20
21 21 class ParentPollerUnix(Thread):
22 22 """ A Unix-specific daemon thread that terminates the program immediately
23 23 when the parent process no longer exists.
24 24 """
25 25
26 26 def __init__(self):
27 27 super(ParentPollerUnix, self).__init__()
28 28 self.daemon = True
29 29
30 30 def run(self):
31 31 # We cannot use os.waitpid because it works only for child processes.
32 32 from errno import EINTR
33 33 while True:
34 34 try:
35 35 if os.getppid() == 1:
36 36 os._exit(1)
37 37 time.sleep(1.0)
38 38 except OSError as e:
39 39 if e.errno == EINTR:
40 40 continue
41 41 raise
42 42
43 43
44 44 class ParentPollerWindows(Thread):
45 45 """ A Windows-specific daemon thread that listens for a special event that
46 46 signals an interrupt and, optionally, terminates the program immediately
47 47 when the parent process no longer exists.
48 48 """
49 49
50 50 def __init__(self, interrupt_handle=None, parent_handle=None):
51 51 """ Create the poller. At least one of the optional parameters must be
52 52 provided.
53 53
54 54 Parameters
55 55 ----------
56 56 interrupt_handle : HANDLE (int), optional
57 57 If provided, the program will generate a Ctrl+C event when this
58 58 handle is signaled.
59 59
60 60 parent_handle : HANDLE (int), optional
61 61 If provided, the program will terminate immediately when this
62 62 handle is signaled.
63 63 """
64 64 assert(interrupt_handle or parent_handle)
65 65 super(ParentPollerWindows, self).__init__()
66 66 if ctypes is None:
67 67 raise ImportError("ParentPollerWindows requires ctypes")
68 68 self.daemon = True
69 69 self.interrupt_handle = interrupt_handle
70 70 self.parent_handle = parent_handle
71 71
72 @staticmethod
73 def create_interrupt_event():
74 """ Create an interrupt event handle.
75
76 The parent process should use this static method for creating the
77 interrupt event that is passed to the child process. It should store
78 this handle and use it with ``send_interrupt`` to interrupt the child
79 process.
80 """
81 # Create a security attributes struct that permits inheritance of the
82 # handle by new processes.
83 # FIXME: We can clean up this mess by requiring pywin32 for IPython.
84 class SECURITY_ATTRIBUTES(ctypes.Structure):
85 _fields_ = [ ("nLength", ctypes.c_int),
86 ("lpSecurityDescriptor", ctypes.c_void_p),
87 ("bInheritHandle", ctypes.c_int) ]
88 sa = SECURITY_ATTRIBUTES()
89 sa_p = ctypes.pointer(sa)
90 sa.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
91 sa.lpSecurityDescriptor = 0
92 sa.bInheritHandle = 1
93
94 return ctypes.windll.kernel32.CreateEventA(
95 sa_p, # lpEventAttributes
96 False, # bManualReset
97 False, # bInitialState
98 '') # lpName
99
100 @staticmethod
101 def send_interrupt(interrupt_handle):
102 """ Sends an interrupt event using the specified handle.
103 """
104 ctypes.windll.kernel32.SetEvent(interrupt_handle)
105
106 72 def run(self):
107 73 """ Run the poll loop. This method never returns.
108 74 """
109 75 try:
110 76 from _winapi import WAIT_OBJECT_0, INFINITE
111 77 except ImportError:
112 78 from _subprocess import WAIT_OBJECT_0, INFINITE
113 79
114 80 # Build the list of handle to listen on.
115 81 handles = []
116 82 if self.interrupt_handle:
117 83 handles.append(self.interrupt_handle)
118 84 if self.parent_handle:
119 85 handles.append(self.parent_handle)
120 86 arch = platform.architecture()[0]
121 87 c_int = ctypes.c_int64 if arch.startswith('64') else ctypes.c_int
122 88
123 89 # Listen forever.
124 90 while True:
125 91 result = ctypes.windll.kernel32.WaitForMultipleObjects(
126 92 len(handles), # nCount
127 93 (c_int * len(handles))(*handles), # lpHandles
128 94 False, # bWaitAll
129 95 INFINITE) # dwMilliseconds
130 96
131 97 if WAIT_OBJECT_0 <= result < len(handles):
132 98 handle = handles[result - WAIT_OBJECT_0]
133 99
134 100 if handle == self.interrupt_handle:
135 101 # check if signal handler is callable
136 102 # to avoid 'int not callable' error (Python issue #23395)
137 103 if callable(signal.getsignal(signal.SIGINT)):
138 104 interrupt_main()
139 105
140 106 elif handle == self.parent_handle:
141 107 os._exit(1)
142 108 elif result < 0:
143 109 # wait failed, just give up and stop polling.
144 110 warn("""Parent poll failed. If the frontend dies,
145 111 the kernel may be left running. Please let us know
146 112 about your system (bitness, Python, etc.) at
147 113 ipython-dev@scipy.org""")
148 114 return
@@ -1,226 +1,226 b''
1 1 """Utilities for launching kernels
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import sys
9 9 from subprocess import Popen, PIPE
10 10
11 11 from IPython.utils.encoding import getdefaultencoding
12 12 from IPython.utils.py3compat import cast_bytes_py2
13 13
14 14
15 15 def swallow_argv(argv, aliases=None, flags=None):
16 16 """strip frontend-specific aliases and flags from an argument list
17 17
18 18 For use primarily in frontend apps that want to pass a subset of command-line
19 19 arguments through to a subprocess, where frontend-specific flags and aliases
20 20 should be removed from the list.
21 21
22 22 Parameters
23 23 ----------
24 24
25 25 argv : list(str)
26 26 The starting argv, to be filtered
27 27 aliases : container of aliases (dict, list, set, etc.)
28 28 The frontend-specific aliases to be removed
29 29 flags : container of flags (dict, list, set, etc.)
30 30 The frontend-specific flags to be removed
31 31
32 32 Returns
33 33 -------
34 34
35 35 argv : list(str)
36 36 The argv list, excluding flags and aliases that have been stripped
37 37 """
38 38
39 39 if aliases is None:
40 40 aliases = set()
41 41 if flags is None:
42 42 flags = set()
43 43
44 44 stripped = list(argv) # copy
45 45
46 46 swallow_next = False
47 47 was_flag = False
48 48 for a in argv:
49 49 if a == '--':
50 50 break
51 51 if swallow_next:
52 52 swallow_next = False
53 53 # last arg was an alias, remove the next one
54 54 # *unless* the last alias has a no-arg flag version, in which
55 55 # case, don't swallow the next arg if it's also a flag:
56 56 if not (was_flag and a.startswith('-')):
57 57 stripped.remove(a)
58 58 continue
59 59 if a.startswith('-'):
60 60 split = a.lstrip('-').split('=')
61 61 name = split[0]
62 62 # we use startswith because argparse accepts any arg to be specified
63 63 # by any leading section, as long as it is unique,
64 64 # so `--no-br` means `--no-browser` in the notebook, etc.
65 65 if any(alias.startswith(name) for alias in aliases):
66 66 stripped.remove(a)
67 67 if len(split) == 1:
68 68 # alias passed with arg via space
69 69 swallow_next = True
70 70 # could have been a flag that matches an alias, e.g. `existing`
71 71 # in which case, we might not swallow the next arg
72 72 was_flag = name in flags
73 73 elif len(split) == 1 and any(flag.startswith(name) for flag in flags):
74 74 # strip flag, but don't swallow next, as flags don't take args
75 75 stripped.remove(a)
76 76
77 77 # return shortened list
78 78 return stripped
79 79
80 80
81 81 def make_ipkernel_cmd(mod='ipython_kernel', executable=None, extra_arguments=[], **kw):
82 82 """Build Popen command list for launching an IPython kernel.
83 83
84 84 Parameters
85 85 ----------
86 86 mod : str, optional (default 'ipython_kernel')
87 87 A string of an IPython module whose __main__ starts an IPython kernel
88 88
89 89 executable : str, optional (default sys.executable)
90 90 The Python executable to use for the kernel process.
91 91
92 92 extra_arguments : list, optional
93 93 A list of extra arguments to pass when executing the launch code.
94 94
95 95 Returns
96 96 -------
97 97
98 98 A Popen command list
99 99 """
100 100 if executable is None:
101 101 executable = sys.executable
102 102 arguments = [ executable, '-m', mod, '-f', '{connection_file}' ]
103 103 arguments.extend(extra_arguments)
104 104
105 105 return arguments
106 106
107 107
108 108 def launch_kernel(cmd, stdin=None, stdout=None, stderr=None, env=None,
109 109 independent=False,
110 110 cwd=None,
111 111 **kw
112 112 ):
113 113 """ Launches a localhost kernel, binding to the specified ports.
114 114
115 115 Parameters
116 116 ----------
117 117 cmd : Popen list,
118 118 A string of Python code that imports and executes a kernel entry point.
119 119
120 120 stdin, stdout, stderr : optional (default None)
121 121 Standards streams, as defined in subprocess.Popen.
122 122
123 123 independent : bool, optional (default False)
124 124 If set, the kernel process is guaranteed to survive if this process
125 125 dies. If not set, an effort is made to ensure that the kernel is killed
126 126 when this process dies. Note that in this case it is still good practice
127 127 to kill kernels manually before exiting.
128 128
129 129 cwd : path, optional
130 130 The working dir of the kernel process (default: cwd of this process).
131 131
132 132 Returns
133 133 -------
134 134
135 135 Popen instance for the kernel subprocess
136 136 """
137 137
138 138 # Popen will fail (sometimes with a deadlock) if stdin, stdout, and stderr
139 139 # are invalid. Unfortunately, there is in general no way to detect whether
140 140 # they are valid. The following two blocks redirect them to (temporary)
141 141 # pipes in certain important cases.
142 142
143 143 # If this process has been backgrounded, our stdin is invalid. Since there
144 144 # is no compelling reason for the kernel to inherit our stdin anyway, we'll
145 145 # place this one safe and always redirect.
146 146 redirect_in = True
147 147 _stdin = PIPE if stdin is None else stdin
148 148
149 149 # If this process in running on pythonw, we know that stdin, stdout, and
150 150 # stderr are all invalid.
151 151 redirect_out = sys.executable.endswith('pythonw.exe')
152 152 if redirect_out:
153 153 blackhole = open(os.devnull, 'w')
154 154 _stdout = blackhole if stdout is None else stdout
155 155 _stderr = blackhole if stderr is None else stderr
156 156 else:
157 157 _stdout, _stderr = stdout, stderr
158 158
159 159 env = env if (env is not None) else os.environ.copy()
160 160
161 161 encoding = getdefaultencoding(prefer_stream=False)
162 162 kwargs = dict(
163 163 stdin=_stdin,
164 164 stdout=_stdout,
165 165 stderr=_stderr,
166 166 cwd=cwd,
167 167 env=env,
168 168 )
169 169
170 170 # Spawn a kernel.
171 171 if sys.platform == 'win32':
172 172 # Popen on Python 2 on Windows cannot handle unicode args or cwd
173 173 cmd = [ cast_bytes_py2(c, encoding) for c in cmd ]
174 174 if cwd:
175 175 cwd = cast_bytes_py2(cwd, sys.getfilesystemencoding() or 'ascii')
176 176 kwargs['cwd'] = cwd
177 177
178 from jupyter_client.parentpoller import ParentPollerWindows
178 from .win_interrupt import create_interrupt_event
179 179 # Create a Win32 event for interrupting the kernel
180 180 # and store it in an environment variable.
181 interrupt_event = ParentPollerWindows.create_interrupt_event()
181 interrupt_event = create_interrupt_event()
182 182 env["JPY_INTERRUPT_EVENT"] = str(interrupt_event)
183 183 # deprecated old env name:
184 184 env["IPY_INTERRUPT_EVENT"] = env["JPY_INTERRUPT_EVENT"]
185 185
186 186 try:
187 187 from _winapi import DuplicateHandle, GetCurrentProcess, \
188 188 DUPLICATE_SAME_ACCESS, CREATE_NEW_PROCESS_GROUP
189 189 except:
190 190 from _subprocess import DuplicateHandle, GetCurrentProcess, \
191 191 DUPLICATE_SAME_ACCESS, CREATE_NEW_PROCESS_GROUP
192 192 # Launch the kernel process
193 193 if independent:
194 194 kwargs['creationflags'] = CREATE_NEW_PROCESS_GROUP
195 195 else:
196 196 pid = GetCurrentProcess()
197 197 handle = DuplicateHandle(pid, pid, pid, 0,
198 198 True, # Inheritable by new processes.
199 199 DUPLICATE_SAME_ACCESS)
200 200 env['JPY_PARENT_PID'] = str(int(handle))
201 201
202 202 proc = Popen(cmd, **kwargs)
203 203
204 204 # Attach the interrupt event to the Popen objet so it can be used later.
205 205 proc.win32_interrupt_event = interrupt_event
206 206
207 207 else:
208 208 if independent:
209 209 kwargs['preexec_fn'] = lambda: os.setsid()
210 210 else:
211 211 env['JPY_PARENT_PID'] = str(os.getpid())
212 212
213 213 proc = Popen(cmd, **kwargs)
214 214
215 215 # Clean up pipes created to work around Popen bug.
216 216 if redirect_in:
217 217 if stdin is None:
218 218 proc.stdin.close()
219 219
220 220 return proc
221 221
222 222 __all__ = [
223 223 'swallow_argv',
224 224 'make_ipkernel_cmd',
225 225 'launch_kernel',
226 226 ]
@@ -1,442 +1,442 b''
1 1 """Base class to manage a running kernel"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from contextlib import contextmanager
9 9 import os
10 10 import re
11 11 import signal
12 12 import sys
13 13 import time
14 14 import warnings
15 15 try:
16 16 from queue import Empty # Py 3
17 17 except ImportError:
18 18 from Queue import Empty # Py 2
19 19
20 20 import zmq
21 21
22 22 from IPython.utils.importstring import import_item
23 23 from IPython.utils.localinterfaces import is_local_ip, local_ips
24 24 from IPython.utils.path import get_ipython_dir
25 25 from IPython.utils.traitlets import (
26 26 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
27 27 )
28 28 from jupyter_client import (
29 29 launch_kernel,
30 30 kernelspec,
31 31 )
32 32 from .connect import ConnectionFileMixin
33 33 from .session import Session
34 34 from .managerabc import (
35 35 KernelManagerABC
36 36 )
37 37
38 38
39 39 class KernelManager(ConnectionFileMixin):
40 40 """Manages a single kernel in a subprocess on this host.
41 41
42 42 This version starts kernels with Popen.
43 43 """
44 44
45 45 # The PyZMQ Context to use for communication with the kernel.
46 46 context = Instance(zmq.Context)
47 47 def _context_default(self):
48 48 return zmq.Context.instance()
49 49
50 50 # the class to create with our `client` method
51 51 client_class = DottedObjectName('jupyter_client.blocking.BlockingKernelClient')
52 52 client_factory = Type(allow_none=True)
53 53 def _client_class_changed(self, name, old, new):
54 54 self.client_factory = import_item(str(new))
55 55
56 56 # The kernel process with which the KernelManager is communicating.
57 57 # generally a Popen instance
58 58 kernel = Any()
59 59
60 60 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
61 61
62 62 def _kernel_spec_manager_default(self):
63 63 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
64 64
65 65 kernel_name = Unicode(kernelspec.NATIVE_KERNEL_NAME)
66 66
67 67 kernel_spec = Instance(kernelspec.KernelSpec)
68 68
69 69 def _kernel_spec_default(self):
70 70 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
71 71
72 72 def _kernel_name_changed(self, name, old, new):
73 73 if new == 'python':
74 74 self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
75 75 # This triggered another run of this function, so we can exit now
76 76 return
77 77 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
78 78 self.ipython_kernel = new in {'python', 'python2', 'python3'}
79 79
80 80 kernel_cmd = List(Unicode, config=True,
81 81 help="""DEPRECATED: Use kernel_name instead.
82 82
83 83 The Popen Command to launch the kernel.
84 84 Override this if you have a custom kernel.
85 85 If kernel_cmd is specified in a configuration file,
86 86 IPython does not pass any arguments to the kernel,
87 87 because it cannot make any assumptions about the
88 88 arguments that the kernel understands. In particular,
89 89 this means that the kernel does not receive the
90 90 option --debug if it given on the IPython command line.
91 91 """
92 92 )
93 93
94 94 def _kernel_cmd_changed(self, name, old, new):
95 95 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
96 96 "start different kernels.")
97 97 self.ipython_kernel = False
98 98
99 99 ipython_kernel = Bool(True)
100 100
101 101 ipython_dir = Unicode()
102 102 def _ipython_dir_default(self):
103 103 return get_ipython_dir()
104 104
105 105 # Protected traits
106 106 _launch_args = Any()
107 107 _control_socket = Any()
108 108
109 109 _restarter = Any()
110 110
111 111 autorestart = Bool(False, config=True,
112 112 help="""Should we autorestart the kernel if it dies."""
113 113 )
114 114
115 115 def __del__(self):
116 116 self._close_control_socket()
117 117 self.cleanup_connection_file()
118 118
119 119 #--------------------------------------------------------------------------
120 120 # Kernel restarter
121 121 #--------------------------------------------------------------------------
122 122
123 123 def start_restarter(self):
124 124 pass
125 125
126 126 def stop_restarter(self):
127 127 pass
128 128
129 129 def add_restart_callback(self, callback, event='restart'):
130 130 """register a callback to be called when a kernel is restarted"""
131 131 if self._restarter is None:
132 132 return
133 133 self._restarter.add_callback(callback, event)
134 134
135 135 def remove_restart_callback(self, callback, event='restart'):
136 136 """unregister a callback to be called when a kernel is restarted"""
137 137 if self._restarter is None:
138 138 return
139 139 self._restarter.remove_callback(callback, event)
140 140
141 141 #--------------------------------------------------------------------------
142 142 # create a Client connected to our Kernel
143 143 #--------------------------------------------------------------------------
144 144
145 145 def client(self, **kwargs):
146 146 """Create a client configured to connect to our kernel"""
147 147 if self.client_factory is None:
148 148 self.client_factory = import_item(self.client_class)
149 149
150 150 kw = {}
151 151 kw.update(self.get_connection_info())
152 152 kw.update(dict(
153 153 connection_file=self.connection_file,
154 154 session=self.session,
155 155 parent=self,
156 156 ))
157 157
158 158 # add kwargs last, for manual overrides
159 159 kw.update(kwargs)
160 160 return self.client_factory(**kw)
161 161
162 162 #--------------------------------------------------------------------------
163 163 # Kernel management
164 164 #--------------------------------------------------------------------------
165 165
166 166 def format_kernel_cmd(self, extra_arguments=None):
167 167 """replace templated args (e.g. {connection_file})"""
168 168 extra_arguments = extra_arguments or []
169 169 if self.kernel_cmd:
170 170 cmd = self.kernel_cmd + extra_arguments
171 171 else:
172 172 cmd = self.kernel_spec.argv + extra_arguments
173 173
174 174 ns = dict(connection_file=self.connection_file)
175 175 ns.update(self._launch_args)
176 176
177 177 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
178 178 def from_ns(match):
179 179 """Get the key out of ns if it's there, otherwise no change."""
180 180 return ns.get(match.group(1), match.group())
181 181
182 182 return [ pat.sub(from_ns, arg) for arg in cmd ]
183 183
184 184 def _launch_kernel(self, kernel_cmd, **kw):
185 185 """actually launch the kernel
186 186
187 187 override in a subclass to launch kernel subprocesses differently
188 188 """
189 189 return launch_kernel(kernel_cmd, **kw)
190 190
191 191 # Control socket used for polite kernel shutdown
192 192
193 193 def _connect_control_socket(self):
194 194 if self._control_socket is None:
195 195 self._control_socket = self.connect_control()
196 196 self._control_socket.linger = 100
197 197
198 198 def _close_control_socket(self):
199 199 if self._control_socket is None:
200 200 return
201 201 self._control_socket.close()
202 202 self._control_socket = None
203 203
204 204 def start_kernel(self, **kw):
205 205 """Starts a kernel on this host in a separate process.
206 206
207 207 If random ports (port=0) are being used, this method must be called
208 208 before the channels are created.
209 209
210 210 Parameters
211 211 ----------
212 212 **kw : optional
213 213 keyword arguments that are passed down to build the kernel_cmd
214 214 and launching the kernel (e.g. Popen kwargs).
215 215 """
216 216 if self.transport == 'tcp' and not is_local_ip(self.ip):
217 217 raise RuntimeError("Can only launch a kernel on a local interface. "
218 218 "Make sure that the '*_address' attributes are "
219 219 "configured properly. "
220 220 "Currently valid addresses are: %s" % local_ips()
221 221 )
222 222
223 223 # write connection file / get default ports
224 224 self.write_connection_file()
225 225
226 226 # save kwargs for use in restart
227 227 self._launch_args = kw.copy()
228 228 # build the Popen cmd
229 229 extra_arguments = kw.pop('extra_arguments', [])
230 230 kernel_cmd = self.format_kernel_cmd(extra_arguments=extra_arguments)
231 231 if self.kernel_cmd:
232 232 # If kernel_cmd has been set manually, don't refer to a kernel spec
233 233 env = os.environ
234 234 else:
235 235 # Environment variables from kernel spec are added to os.environ
236 236 env = os.environ.copy()
237 237 env.update(self.kernel_spec.env or {})
238 238 # launch the kernel subprocess
239 239 self.kernel = self._launch_kernel(kernel_cmd, env=env,
240 240 **kw)
241 241 self.start_restarter()
242 242 self._connect_control_socket()
243 243
244 244 def request_shutdown(self, restart=False):
245 245 """Send a shutdown request via control channel
246 246
247 247 On Windows, this just kills kernels instead, because the shutdown
248 248 messages don't work.
249 249 """
250 250 content = dict(restart=restart)
251 251 msg = self.session.msg("shutdown_request", content=content)
252 252 self.session.send(self._control_socket, msg)
253 253
254 254 def finish_shutdown(self, waittime=1, pollinterval=0.1):
255 255 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
256 256
257 257 This does not send shutdown requests - use :meth:`request_shutdown`
258 258 first.
259 259 """
260 260 for i in range(int(waittime/pollinterval)):
261 261 if self.is_alive():
262 262 time.sleep(pollinterval)
263 263 else:
264 264 break
265 265 else:
266 266 # OK, we've waited long enough.
267 267 if self.has_kernel:
268 268 self._kill_kernel()
269 269
270 270 def cleanup(self, connection_file=True):
271 271 """Clean up resources when the kernel is shut down"""
272 272 if connection_file:
273 273 self.cleanup_connection_file()
274 274
275 275 self.cleanup_ipc_files()
276 276 self._close_control_socket()
277 277
278 278 def shutdown_kernel(self, now=False, restart=False):
279 279 """Attempts to the stop the kernel process cleanly.
280 280
281 281 This attempts to shutdown the kernels cleanly by:
282 282
283 283 1. Sending it a shutdown message over the shell channel.
284 284 2. If that fails, the kernel is shutdown forcibly by sending it
285 285 a signal.
286 286
287 287 Parameters
288 288 ----------
289 289 now : bool
290 290 Should the kernel be forcible killed *now*. This skips the
291 291 first, nice shutdown attempt.
292 292 restart: bool
293 293 Will this kernel be restarted after it is shutdown. When this
294 294 is True, connection files will not be cleaned up.
295 295 """
296 296 # Stop monitoring for restarting while we shutdown.
297 297 self.stop_restarter()
298 298
299 299 if now:
300 300 self._kill_kernel()
301 301 else:
302 302 self.request_shutdown(restart=restart)
303 303 # Don't send any additional kernel kill messages immediately, to give
304 304 # the kernel a chance to properly execute shutdown actions. Wait for at
305 305 # most 1s, checking every 0.1s.
306 306 self.finish_shutdown()
307 307
308 308 self.cleanup(connection_file=not restart)
309 309
310 310 def restart_kernel(self, now=False, **kw):
311 311 """Restarts a kernel with the arguments that were used to launch it.
312 312
313 313 If the old kernel was launched with random ports, the same ports will be
314 314 used for the new kernel. The same connection file is used again.
315 315
316 316 Parameters
317 317 ----------
318 318 now : bool, optional
319 319 If True, the kernel is forcefully restarted *immediately*, without
320 320 having a chance to do any cleanup action. Otherwise the kernel is
321 321 given 1s to clean up before a forceful restart is issued.
322 322
323 323 In all cases the kernel is restarted, the only difference is whether
324 324 it is given a chance to perform a clean shutdown or not.
325 325
326 326 **kw : optional
327 327 Any options specified here will overwrite those used to launch the
328 328 kernel.
329 329 """
330 330 if self._launch_args is None:
331 331 raise RuntimeError("Cannot restart the kernel. "
332 332 "No previous call to 'start_kernel'.")
333 333 else:
334 334 # Stop currently running kernel.
335 335 self.shutdown_kernel(now=now, restart=True)
336 336
337 337 # Start new kernel.
338 338 self._launch_args.update(kw)
339 339 self.start_kernel(**self._launch_args)
340 340
341 341 @property
342 342 def has_kernel(self):
343 343 """Has a kernel been started that we are managing."""
344 344 return self.kernel is not None
345 345
346 346 def _kill_kernel(self):
347 347 """Kill the running kernel.
348 348
349 349 This is a private method, callers should use shutdown_kernel(now=True).
350 350 """
351 351 if self.has_kernel:
352 352
353 353 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
354 354 # TerminateProcess() on Win32).
355 355 try:
356 356 self.kernel.kill()
357 357 except OSError as e:
358 358 # In Windows, we will get an Access Denied error if the process
359 359 # has already terminated. Ignore it.
360 360 if sys.platform == 'win32':
361 361 if e.winerror != 5:
362 362 raise
363 363 # On Unix, we may get an ESRCH error if the process has already
364 364 # terminated. Ignore it.
365 365 else:
366 366 from errno import ESRCH
367 367 if e.errno != ESRCH:
368 368 raise
369 369
370 370 # Block until the kernel terminates.
371 371 self.kernel.wait()
372 372 self.kernel = None
373 373 else:
374 374 raise RuntimeError("Cannot kill kernel. No kernel is running!")
375 375
376 376 def interrupt_kernel(self):
377 377 """Interrupts the kernel by sending it a signal.
378 378
379 379 Unlike ``signal_kernel``, this operation is well supported on all
380 380 platforms.
381 381 """
382 382 if self.has_kernel:
383 383 if sys.platform == 'win32':
384 from .parentpoller import ParentPollerWindows as Poller
385 Poller.send_interrupt(self.kernel.win32_interrupt_event)
384 from .win_interrupt import send_interrupt
385 send_interrupt(self.kernel.win32_interrupt_event)
386 386 else:
387 387 self.kernel.send_signal(signal.SIGINT)
388 388 else:
389 389 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
390 390
391 391 def signal_kernel(self, signum):
392 392 """Sends a signal to the kernel.
393 393
394 394 Note that since only SIGTERM is supported on Windows, this function is
395 395 only useful on Unix systems.
396 396 """
397 397 if self.has_kernel:
398 398 self.kernel.send_signal(signum)
399 399 else:
400 400 raise RuntimeError("Cannot signal kernel. No kernel is running!")
401 401
402 402 def is_alive(self):
403 403 """Is the kernel process still running?"""
404 404 if self.has_kernel:
405 405 if self.kernel.poll() is None:
406 406 return True
407 407 else:
408 408 return False
409 409 else:
410 410 # we don't have a kernel
411 411 return False
412 412
413 413
414 414 KernelManagerABC.register(KernelManager)
415 415
416 416
417 417 def start_new_kernel(startup_timeout=60, kernel_name='python', **kwargs):
418 418 """Start a new kernel, and return its Manager and Client"""
419 419 km = KernelManager(kernel_name=kernel_name)
420 420 km.start_kernel(**kwargs)
421 421 kc = km.client()
422 422 kc.start_channels()
423 423 kc.wait_for_ready()
424 424
425 425 return km, kc
426 426
427 427 @contextmanager
428 428 def run_kernel(**kwargs):
429 429 """Context manager to create a kernel in a subprocess.
430 430
431 431 The kernel is shut down when the context exits.
432 432
433 433 Returns
434 434 -------
435 435 kernel_client: connected KernelClient instance
436 436 """
437 437 km, kc = start_new_kernel(**kwargs)
438 438 try:
439 439 yield kc
440 440 finally:
441 441 kc.stop_channels()
442 442 km.shutdown_kernel(now=True)
General Comments 0
You need to be logged in to leave comments. Login now