##// END OF EJS Templates
Merge pull request #5764 from takluyver/parallel-kernel-shutdown...
Thomas Kluyver -
r16726:a26a2e70 merge
parent child Browse files
Show More
@@ -1,413 +1,420 b''
1 """Base class to manage a running kernel"""
1 """Base class to manage a running kernel"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 # Standard library imports
8 # Standard library imports
9 import os
9 import os
10 import re
10 import re
11 import signal
11 import signal
12 import sys
12 import sys
13 import time
13 import time
14 import warnings
14 import warnings
15
15
16 import zmq
16 import zmq
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import LoggingConfigurable
19 from IPython.config.configurable import LoggingConfigurable
20 from IPython.utils.importstring import import_item
20 from IPython.utils.importstring import import_item
21 from IPython.utils.localinterfaces import is_local_ip, local_ips
21 from IPython.utils.localinterfaces import is_local_ip, local_ips
22 from IPython.utils.path import get_ipython_dir
22 from IPython.utils.path import get_ipython_dir
23 from IPython.utils.traitlets import (
23 from IPython.utils.traitlets import (
24 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
24 Any, Instance, Unicode, List, Bool, Type, DottedObjectName
25 )
25 )
26 from IPython.kernel import (
26 from IPython.kernel import (
27 make_ipkernel_cmd,
27 make_ipkernel_cmd,
28 launch_kernel,
28 launch_kernel,
29 kernelspec,
29 kernelspec,
30 )
30 )
31 from .connect import ConnectionFileMixin
31 from .connect import ConnectionFileMixin
32 from .zmq.session import Session
32 from .zmq.session import Session
33 from .managerabc import (
33 from .managerabc import (
34 KernelManagerABC
34 KernelManagerABC
35 )
35 )
36
36
37
37
38 class KernelManager(LoggingConfigurable, ConnectionFileMixin):
38 class KernelManager(LoggingConfigurable, ConnectionFileMixin):
39 """Manages a single kernel in a subprocess on this host.
39 """Manages a single kernel in a subprocess on this host.
40
40
41 This version starts kernels with Popen.
41 This version starts kernels with Popen.
42 """
42 """
43
43
44 # The PyZMQ Context to use for communication with the kernel.
44 # The PyZMQ Context to use for communication with the kernel.
45 context = Instance(zmq.Context)
45 context = Instance(zmq.Context)
46 def _context_default(self):
46 def _context_default(self):
47 return zmq.Context.instance()
47 return zmq.Context.instance()
48
48
49 # The Session to use for communication with the kernel.
49 # The Session to use for communication with the kernel.
50 session = Instance(Session)
50 session = Instance(Session)
51 def _session_default(self):
51 def _session_default(self):
52 return Session(parent=self)
52 return Session(parent=self)
53
53
54 # the class to create with our `client` method
54 # the class to create with our `client` method
55 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
55 client_class = DottedObjectName('IPython.kernel.blocking.BlockingKernelClient')
56 client_factory = Type()
56 client_factory = Type()
57 def _client_class_changed(self, name, old, new):
57 def _client_class_changed(self, name, old, new):
58 self.client_factory = import_item(str(new))
58 self.client_factory = import_item(str(new))
59
59
60 # The kernel process with which the KernelManager is communicating.
60 # The kernel process with which the KernelManager is communicating.
61 # generally a Popen instance
61 # generally a Popen instance
62 kernel = Any()
62 kernel = Any()
63
63
64 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
64 kernel_spec_manager = Instance(kernelspec.KernelSpecManager)
65
65
66 def _kernel_spec_manager_default(self):
66 def _kernel_spec_manager_default(self):
67 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
67 return kernelspec.KernelSpecManager(ipython_dir=self.ipython_dir)
68
68
69 kernel_name = Unicode('python')
69 kernel_name = Unicode('python')
70
70
71 kernel_spec = Instance(kernelspec.KernelSpec)
71 kernel_spec = Instance(kernelspec.KernelSpec)
72
72
73 def _kernel_spec_default(self):
73 def _kernel_spec_default(self):
74 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
74 return self.kernel_spec_manager.get_kernel_spec(self.kernel_name)
75
75
76 def _kernel_name_changed(self, name, old, new):
76 def _kernel_name_changed(self, name, old, new):
77 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
77 self.kernel_spec = self.kernel_spec_manager.get_kernel_spec(new)
78 self.ipython_kernel = new in {'python', 'python2', 'python3'}
78 self.ipython_kernel = new in {'python', 'python2', 'python3'}
79
79
80 kernel_cmd = List(Unicode, config=True,
80 kernel_cmd = List(Unicode, config=True,
81 help="""DEPRECATED: Use kernel_name instead.
81 help="""DEPRECATED: Use kernel_name instead.
82
82
83 The Popen Command to launch the kernel.
83 The Popen Command to launch the kernel.
84 Override this if you have a custom kernel.
84 Override this if you have a custom kernel.
85 If kernel_cmd is specified in a configuration file,
85 If kernel_cmd is specified in a configuration file,
86 IPython does not pass any arguments to the kernel,
86 IPython does not pass any arguments to the kernel,
87 because it cannot make any assumptions about the
87 because it cannot make any assumptions about the
88 arguments that the kernel understands. In particular,
88 arguments that the kernel understands. In particular,
89 this means that the kernel does not receive the
89 this means that the kernel does not receive the
90 option --debug if it given on the IPython command line.
90 option --debug if it given on the IPython command line.
91 """
91 """
92 )
92 )
93
93
94 def _kernel_cmd_changed(self, name, old, new):
94 def _kernel_cmd_changed(self, name, old, new):
95 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
95 warnings.warn("Setting kernel_cmd is deprecated, use kernel_spec to "
96 "start different kernels.")
96 "start different kernels.")
97 self.ipython_kernel = False
97 self.ipython_kernel = False
98
98
99 ipython_kernel = Bool(True)
99 ipython_kernel = Bool(True)
100
100
101 ipython_dir = Unicode()
101 ipython_dir = Unicode()
102 def _ipython_dir_default(self):
102 def _ipython_dir_default(self):
103 return get_ipython_dir()
103 return get_ipython_dir()
104
104
105 # Protected traits
105 # Protected traits
106 _launch_args = Any()
106 _launch_args = Any()
107 _control_socket = Any()
107 _control_socket = Any()
108
108
109 _restarter = Any()
109 _restarter = Any()
110
110
111 autorestart = Bool(False, config=True,
111 autorestart = Bool(False, config=True,
112 help="""Should we autorestart the kernel if it dies."""
112 help="""Should we autorestart the kernel if it dies."""
113 )
113 )
114
114
115 def __del__(self):
115 def __del__(self):
116 self._close_control_socket()
116 self._close_control_socket()
117 self.cleanup_connection_file()
117 self.cleanup_connection_file()
118
118
119 #--------------------------------------------------------------------------
119 #--------------------------------------------------------------------------
120 # Kernel restarter
120 # Kernel restarter
121 #--------------------------------------------------------------------------
121 #--------------------------------------------------------------------------
122
122
123 def start_restarter(self):
123 def start_restarter(self):
124 pass
124 pass
125
125
126 def stop_restarter(self):
126 def stop_restarter(self):
127 pass
127 pass
128
128
129 def add_restart_callback(self, callback, event='restart'):
129 def add_restart_callback(self, callback, event='restart'):
130 """register a callback to be called when a kernel is restarted"""
130 """register a callback to be called when a kernel is restarted"""
131 if self._restarter is None:
131 if self._restarter is None:
132 return
132 return
133 self._restarter.add_callback(callback, event)
133 self._restarter.add_callback(callback, event)
134
134
135 def remove_restart_callback(self, callback, event='restart'):
135 def remove_restart_callback(self, callback, event='restart'):
136 """unregister a callback to be called when a kernel is restarted"""
136 """unregister a callback to be called when a kernel is restarted"""
137 if self._restarter is None:
137 if self._restarter is None:
138 return
138 return
139 self._restarter.remove_callback(callback, event)
139 self._restarter.remove_callback(callback, event)
140
140
141 #--------------------------------------------------------------------------
141 #--------------------------------------------------------------------------
142 # create a Client connected to our Kernel
142 # create a Client connected to our Kernel
143 #--------------------------------------------------------------------------
143 #--------------------------------------------------------------------------
144
144
145 def client(self, **kwargs):
145 def client(self, **kwargs):
146 """Create a client configured to connect to our kernel"""
146 """Create a client configured to connect to our kernel"""
147 if self.client_factory is None:
147 if self.client_factory is None:
148 self.client_factory = import_item(self.client_class)
148 self.client_factory = import_item(self.client_class)
149
149
150 kw = {}
150 kw = {}
151 kw.update(self.get_connection_info())
151 kw.update(self.get_connection_info())
152 kw.update(dict(
152 kw.update(dict(
153 connection_file=self.connection_file,
153 connection_file=self.connection_file,
154 session=self.session,
154 session=self.session,
155 parent=self,
155 parent=self,
156 ))
156 ))
157
157
158 # add kwargs last, for manual overrides
158 # add kwargs last, for manual overrides
159 kw.update(kwargs)
159 kw.update(kwargs)
160 return self.client_factory(**kw)
160 return self.client_factory(**kw)
161
161
162 #--------------------------------------------------------------------------
162 #--------------------------------------------------------------------------
163 # Kernel management
163 # Kernel management
164 #--------------------------------------------------------------------------
164 #--------------------------------------------------------------------------
165
165
166 def format_kernel_cmd(self, **kw):
166 def format_kernel_cmd(self, **kw):
167 """replace templated args (e.g. {connection_file})"""
167 """replace templated args (e.g. {connection_file})"""
168 if self.kernel_cmd:
168 if self.kernel_cmd:
169 cmd = self.kernel_cmd
169 cmd = self.kernel_cmd
170 elif self.kernel_name == 'python':
170 elif self.kernel_name == 'python':
171 # The native kernel gets special handling
171 # The native kernel gets special handling
172 cmd = make_ipkernel_cmd(
172 cmd = make_ipkernel_cmd(
173 'from IPython.kernel.zmq.kernelapp import main; main()',
173 'from IPython.kernel.zmq.kernelapp import main; main()',
174 **kw
174 **kw
175 )
175 )
176 else:
176 else:
177 cmd = self.kernel_spec.argv
177 cmd = self.kernel_spec.argv
178
178
179 ns = dict(connection_file=self.connection_file)
179 ns = dict(connection_file=self.connection_file)
180 ns.update(self._launch_args)
180 ns.update(self._launch_args)
181
181
182 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
182 pat = re.compile(r'\{([A-Za-z0-9_]+)\}')
183 def from_ns(match):
183 def from_ns(match):
184 """Get the key out of ns if it's there, otherwise no change."""
184 """Get the key out of ns if it's there, otherwise no change."""
185 return ns.get(match.group(1), match.group())
185 return ns.get(match.group(1), match.group())
186
186
187 return [ pat.sub(from_ns, arg) for arg in cmd ]
187 return [ pat.sub(from_ns, arg) for arg in cmd ]
188
188
189 def _launch_kernel(self, kernel_cmd, **kw):
189 def _launch_kernel(self, kernel_cmd, **kw):
190 """actually launch the kernel
190 """actually launch the kernel
191
191
192 override in a subclass to launch kernel subprocesses differently
192 override in a subclass to launch kernel subprocesses differently
193 """
193 """
194 return launch_kernel(kernel_cmd, **kw)
194 return launch_kernel(kernel_cmd, **kw)
195
195
196 # Control socket used for polite kernel shutdown
196 # Control socket used for polite kernel shutdown
197
197
198 def _connect_control_socket(self):
198 def _connect_control_socket(self):
199 if self._control_socket is None:
199 if self._control_socket is None:
200 self._control_socket = self.connect_control()
200 self._control_socket = self.connect_control()
201 self._control_socket.linger = 100
201 self._control_socket.linger = 100
202
202
203 def _close_control_socket(self):
203 def _close_control_socket(self):
204 if self._control_socket is None:
204 if self._control_socket is None:
205 return
205 return
206 self._control_socket.close()
206 self._control_socket.close()
207 self._control_socket = None
207 self._control_socket = None
208
208
209 def start_kernel(self, **kw):
209 def start_kernel(self, **kw):
210 """Starts a kernel on this host in a separate process.
210 """Starts a kernel on this host in a separate process.
211
211
212 If random ports (port=0) are being used, this method must be called
212 If random ports (port=0) are being used, this method must be called
213 before the channels are created.
213 before the channels are created.
214
214
215 Parameters
215 Parameters
216 ----------
216 ----------
217 **kw : optional
217 **kw : optional
218 keyword arguments that are passed down to build the kernel_cmd
218 keyword arguments that are passed down to build the kernel_cmd
219 and launching the kernel (e.g. Popen kwargs).
219 and launching the kernel (e.g. Popen kwargs).
220 """
220 """
221 if self.transport == 'tcp' and not is_local_ip(self.ip):
221 if self.transport == 'tcp' and not is_local_ip(self.ip):
222 raise RuntimeError("Can only launch a kernel on a local interface. "
222 raise RuntimeError("Can only launch a kernel on a local interface. "
223 "Make sure that the '*_address' attributes are "
223 "Make sure that the '*_address' attributes are "
224 "configured properly. "
224 "configured properly. "
225 "Currently valid addresses are: %s" % local_ips()
225 "Currently valid addresses are: %s" % local_ips()
226 )
226 )
227
227
228 # write connection file / get default ports
228 # write connection file / get default ports
229 self.write_connection_file()
229 self.write_connection_file()
230
230
231 # save kwargs for use in restart
231 # save kwargs for use in restart
232 self._launch_args = kw.copy()
232 self._launch_args = kw.copy()
233 # build the Popen cmd
233 # build the Popen cmd
234 kernel_cmd = self.format_kernel_cmd(**kw)
234 kernel_cmd = self.format_kernel_cmd(**kw)
235 if self.kernel_cmd:
235 if self.kernel_cmd:
236 # If kernel_cmd has been set manually, don't refer to a kernel spec
236 # If kernel_cmd has been set manually, don't refer to a kernel spec
237 env = os.environ
237 env = os.environ
238 else:
238 else:
239 # Environment variables from kernel spec are added to os.environ
239 # Environment variables from kernel spec are added to os.environ
240 env = os.environ.copy()
240 env = os.environ.copy()
241 env.update(self.kernel_spec.env or {})
241 env.update(self.kernel_spec.env or {})
242 # launch the kernel subprocess
242 # launch the kernel subprocess
243 self.kernel = self._launch_kernel(kernel_cmd, env=env,
243 self.kernel = self._launch_kernel(kernel_cmd, env=env,
244 ipython_kernel=self.ipython_kernel,
244 ipython_kernel=self.ipython_kernel,
245 **kw)
245 **kw)
246 self.start_restarter()
246 self.start_restarter()
247 self._connect_control_socket()
247 self._connect_control_socket()
248
248
249 def _send_shutdown_request(self, restart=False):
249 def request_shutdown(self, restart=False):
250 """TODO: send a shutdown request via control channel"""
250 """Send a shutdown request via control channel
251
252 On Windows, this just kills kernels instead, because the shutdown
253 messages don't work.
254 """
251 content = dict(restart=restart)
255 content = dict(restart=restart)
252 msg = self.session.msg("shutdown_request", content=content)
256 msg = self.session.msg("shutdown_request", content=content)
253 self.session.send(self._control_socket, msg)
257 self.session.send(self._control_socket, msg)
254
258
259 def finish_shutdown(self, waittime=1, pollinterval=0.1):
260 """Wait for kernel shutdown, then kill process if it doesn't shutdown.
261
262 This does not send shutdown requests - use :meth:`request_shutdown`
263 first.
264 """
265 for i in range(int(waittime/pollinterval)):
266 if self.is_alive():
267 time.sleep(pollinterval)
268 else:
269 break
270 else:
271 # OK, we've waited long enough.
272 if self.has_kernel:
273 self._kill_kernel()
274
275 def cleanup(self, connection_file=True):
276 """Clean up resources when the kernel is shut down"""
277 if connection_file:
278 self.cleanup_connection_file()
279
280 self.cleanup_ipc_files()
281 self._close_control_socket()
282
255 def shutdown_kernel(self, now=False, restart=False):
283 def shutdown_kernel(self, now=False, restart=False):
256 """Attempts to the stop the kernel process cleanly.
284 """Attempts to the stop the kernel process cleanly.
257
285
258 This attempts to shutdown the kernels cleanly by:
286 This attempts to shutdown the kernels cleanly by:
259
287
260 1. Sending it a shutdown message over the shell channel.
288 1. Sending it a shutdown message over the shell channel.
261 2. If that fails, the kernel is shutdown forcibly by sending it
289 2. If that fails, the kernel is shutdown forcibly by sending it
262 a signal.
290 a signal.
263
291
264 Parameters
292 Parameters
265 ----------
293 ----------
266 now : bool
294 now : bool
267 Should the kernel be forcible killed *now*. This skips the
295 Should the kernel be forcible killed *now*. This skips the
268 first, nice shutdown attempt.
296 first, nice shutdown attempt.
269 restart: bool
297 restart: bool
270 Will this kernel be restarted after it is shutdown. When this
298 Will this kernel be restarted after it is shutdown. When this
271 is True, connection files will not be cleaned up.
299 is True, connection files will not be cleaned up.
272 """
300 """
273 # Stop monitoring for restarting while we shutdown.
301 # Stop monitoring for restarting while we shutdown.
274 self.stop_restarter()
302 self.stop_restarter()
275
303
276 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
304 if now:
277 if now or sys.platform == 'win32':
305 self._kill_kernel()
278 if self.has_kernel:
279 self._kill_kernel()
280 else:
306 else:
307 self.request_shutdown(restart=restart)
281 # Don't send any additional kernel kill messages immediately, to give
308 # Don't send any additional kernel kill messages immediately, to give
282 # the kernel a chance to properly execute shutdown actions. Wait for at
309 # the kernel a chance to properly execute shutdown actions. Wait for at
283 # most 1s, checking every 0.1s.
310 # most 1s, checking every 0.1s.
284 self._send_shutdown_request(restart=restart)
311 self.finish_shutdown()
285 for i in range(10):
286 if self.is_alive():
287 time.sleep(0.1)
288 else:
289 break
290 else:
291 # OK, we've waited long enough.
292 if self.has_kernel:
293 self._kill_kernel()
294
312
295 if not restart:
313 self.cleanup(connection_file=not restart)
296 self.cleanup_connection_file()
297 self.cleanup_ipc_files()
298 else:
299 self.cleanup_ipc_files()
300
301 self._close_control_socket()
302
314
303 def restart_kernel(self, now=False, **kw):
315 def restart_kernel(self, now=False, **kw):
304 """Restarts a kernel with the arguments that were used to launch it.
316 """Restarts a kernel with the arguments that were used to launch it.
305
317
306 If the old kernel was launched with random ports, the same ports will be
318 If the old kernel was launched with random ports, the same ports will be
307 used for the new kernel. The same connection file is used again.
319 used for the new kernel. The same connection file is used again.
308
320
309 Parameters
321 Parameters
310 ----------
322 ----------
311 now : bool, optional
323 now : bool, optional
312 If True, the kernel is forcefully restarted *immediately*, without
324 If True, the kernel is forcefully restarted *immediately*, without
313 having a chance to do any cleanup action. Otherwise the kernel is
325 having a chance to do any cleanup action. Otherwise the kernel is
314 given 1s to clean up before a forceful restart is issued.
326 given 1s to clean up before a forceful restart is issued.
315
327
316 In all cases the kernel is restarted, the only difference is whether
328 In all cases the kernel is restarted, the only difference is whether
317 it is given a chance to perform a clean shutdown or not.
329 it is given a chance to perform a clean shutdown or not.
318
330
319 **kw : optional
331 **kw : optional
320 Any options specified here will overwrite those used to launch the
332 Any options specified here will overwrite those used to launch the
321 kernel.
333 kernel.
322 """
334 """
323 if self._launch_args is None:
335 if self._launch_args is None:
324 raise RuntimeError("Cannot restart the kernel. "
336 raise RuntimeError("Cannot restart the kernel. "
325 "No previous call to 'start_kernel'.")
337 "No previous call to 'start_kernel'.")
326 else:
338 else:
327 # Stop currently running kernel.
339 # Stop currently running kernel.
328 self.shutdown_kernel(now=now, restart=True)
340 self.shutdown_kernel(now=now, restart=True)
329
341
330 # Start new kernel.
342 # Start new kernel.
331 self._launch_args.update(kw)
343 self._launch_args.update(kw)
332 self.start_kernel(**self._launch_args)
344 self.start_kernel(**self._launch_args)
333
345
334 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
335 # unless there is some delay here.
336 if sys.platform == 'win32':
337 time.sleep(0.2)
338
339 @property
346 @property
340 def has_kernel(self):
347 def has_kernel(self):
341 """Has a kernel been started that we are managing."""
348 """Has a kernel been started that we are managing."""
342 return self.kernel is not None
349 return self.kernel is not None
343
350
344 def _kill_kernel(self):
351 def _kill_kernel(self):
345 """Kill the running kernel.
352 """Kill the running kernel.
346
353
347 This is a private method, callers should use shutdown_kernel(now=True).
354 This is a private method, callers should use shutdown_kernel(now=True).
348 """
355 """
349 if self.has_kernel:
356 if self.has_kernel:
350
357
351 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
358 # Signal the kernel to terminate (sends SIGKILL on Unix and calls
352 # TerminateProcess() on Win32).
359 # TerminateProcess() on Win32).
353 try:
360 try:
354 self.kernel.kill()
361 self.kernel.kill()
355 except OSError as e:
362 except OSError as e:
356 # In Windows, we will get an Access Denied error if the process
363 # In Windows, we will get an Access Denied error if the process
357 # has already terminated. Ignore it.
364 # has already terminated. Ignore it.
358 if sys.platform == 'win32':
365 if sys.platform == 'win32':
359 if e.winerror != 5:
366 if e.winerror != 5:
360 raise
367 raise
361 # On Unix, we may get an ESRCH error if the process has already
368 # On Unix, we may get an ESRCH error if the process has already
362 # terminated. Ignore it.
369 # terminated. Ignore it.
363 else:
370 else:
364 from errno import ESRCH
371 from errno import ESRCH
365 if e.errno != ESRCH:
372 if e.errno != ESRCH:
366 raise
373 raise
367
374
368 # Block until the kernel terminates.
375 # Block until the kernel terminates.
369 self.kernel.wait()
376 self.kernel.wait()
370 self.kernel = None
377 self.kernel = None
371 else:
378 else:
372 raise RuntimeError("Cannot kill kernel. No kernel is running!")
379 raise RuntimeError("Cannot kill kernel. No kernel is running!")
373
380
374 def interrupt_kernel(self):
381 def interrupt_kernel(self):
375 """Interrupts the kernel by sending it a signal.
382 """Interrupts the kernel by sending it a signal.
376
383
377 Unlike ``signal_kernel``, this operation is well supported on all
384 Unlike ``signal_kernel``, this operation is well supported on all
378 platforms.
385 platforms.
379 """
386 """
380 if self.has_kernel:
387 if self.has_kernel:
381 if sys.platform == 'win32':
388 if sys.platform == 'win32':
382 from .zmq.parentpoller import ParentPollerWindows as Poller
389 from .zmq.parentpoller import ParentPollerWindows as Poller
383 Poller.send_interrupt(self.kernel.win32_interrupt_event)
390 Poller.send_interrupt(self.kernel.win32_interrupt_event)
384 else:
391 else:
385 self.kernel.send_signal(signal.SIGINT)
392 self.kernel.send_signal(signal.SIGINT)
386 else:
393 else:
387 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
394 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
388
395
389 def signal_kernel(self, signum):
396 def signal_kernel(self, signum):
390 """Sends a signal to the kernel.
397 """Sends a signal to the kernel.
391
398
392 Note that since only SIGTERM is supported on Windows, this function is
399 Note that since only SIGTERM is supported on Windows, this function is
393 only useful on Unix systems.
400 only useful on Unix systems.
394 """
401 """
395 if self.has_kernel:
402 if self.has_kernel:
396 self.kernel.send_signal(signum)
403 self.kernel.send_signal(signum)
397 else:
404 else:
398 raise RuntimeError("Cannot signal kernel. No kernel is running!")
405 raise RuntimeError("Cannot signal kernel. No kernel is running!")
399
406
400 def is_alive(self):
407 def is_alive(self):
401 """Is the kernel process still running?"""
408 """Is the kernel process still running?"""
402 if self.has_kernel:
409 if self.has_kernel:
403 if self.kernel.poll() is None:
410 if self.kernel.poll() is None:
404 return True
411 return True
405 else:
412 else:
406 return False
413 return False
407 else:
414 else:
408 # we don't have a kernel
415 # we don't have a kernel
409 return False
416 return False
410
417
411
418
412 KernelManagerABC.register(KernelManager)
419 KernelManagerABC.register(KernelManager)
413
420
@@ -1,302 +1,322 b''
1 """A kernel manager for multiple kernels
1 """A kernel manager for multiple kernels
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 from __future__ import absolute_import
19 from __future__ import absolute_import
20
20
21 import os
21 import os
22 import uuid
22 import uuid
23
23
24 import zmq
24 import zmq
25
25
26 from IPython.config.configurable import LoggingConfigurable
26 from IPython.config.configurable import LoggingConfigurable
27 from IPython.utils.importstring import import_item
27 from IPython.utils.importstring import import_item
28 from IPython.utils.traitlets import (
28 from IPython.utils.traitlets import (
29 Instance, Dict, Unicode, Any, DottedObjectName
29 Instance, Dict, Unicode, Any, DottedObjectName
30 )
30 )
31 from IPython.utils.py3compat import unicode_type
31 from IPython.utils.py3compat import unicode_type
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes
34 # Classes
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class DuplicateKernelError(Exception):
37 class DuplicateKernelError(Exception):
38 pass
38 pass
39
39
40
40
41
41
42 def kernel_method(f):
42 def kernel_method(f):
43 """decorator for proxying MKM.method(kernel_id) to individual KMs by ID"""
43 """decorator for proxying MKM.method(kernel_id) to individual KMs by ID"""
44 def wrapped(self, kernel_id, *args, **kwargs):
44 def wrapped(self, kernel_id, *args, **kwargs):
45 # get the kernel
45 # get the kernel
46 km = self.get_kernel(kernel_id)
46 km = self.get_kernel(kernel_id)
47 method = getattr(km, f.__name__)
47 method = getattr(km, f.__name__)
48 # call the kernel's method
48 # call the kernel's method
49 r = method(*args, **kwargs)
49 r = method(*args, **kwargs)
50 # last thing, call anything defined in the actual class method
50 # last thing, call anything defined in the actual class method
51 # such as logging messages
51 # such as logging messages
52 f(self, kernel_id, *args, **kwargs)
52 f(self, kernel_id, *args, **kwargs)
53 # return the method result
53 # return the method result
54 return r
54 return r
55 return wrapped
55 return wrapped
56
56
57
57
58 class MultiKernelManager(LoggingConfigurable):
58 class MultiKernelManager(LoggingConfigurable):
59 """A class for managing multiple kernels."""
59 """A class for managing multiple kernels."""
60
60
61 kernel_manager_class = DottedObjectName(
61 kernel_manager_class = DottedObjectName(
62 "IPython.kernel.ioloop.IOLoopKernelManager", config=True,
62 "IPython.kernel.ioloop.IOLoopKernelManager", config=True,
63 help="""The kernel manager class. This is configurable to allow
63 help="""The kernel manager class. This is configurable to allow
64 subclassing of the KernelManager for customized behavior.
64 subclassing of the KernelManager for customized behavior.
65 """
65 """
66 )
66 )
67 def _kernel_manager_class_changed(self, name, old, new):
67 def _kernel_manager_class_changed(self, name, old, new):
68 self.kernel_manager_factory = import_item(new)
68 self.kernel_manager_factory = import_item(new)
69
69
70 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
70 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
71 def _kernel_manager_factory_default(self):
71 def _kernel_manager_factory_default(self):
72 return import_item(self.kernel_manager_class)
72 return import_item(self.kernel_manager_class)
73
73
74 context = Instance('zmq.Context')
74 context = Instance('zmq.Context')
75 def _context_default(self):
75 def _context_default(self):
76 return zmq.Context.instance()
76 return zmq.Context.instance()
77
77
78 connection_dir = Unicode('')
78 connection_dir = Unicode('')
79
79
80 _kernels = Dict()
80 _kernels = Dict()
81
81
82 def list_kernel_ids(self):
82 def list_kernel_ids(self):
83 """Return a list of the kernel ids of the active kernels."""
83 """Return a list of the kernel ids of the active kernels."""
84 # Create a copy so we can iterate over kernels in operations
84 # Create a copy so we can iterate over kernels in operations
85 # that delete keys.
85 # that delete keys.
86 return list(self._kernels.keys())
86 return list(self._kernels.keys())
87
87
88 def __len__(self):
88 def __len__(self):
89 """Return the number of running kernels."""
89 """Return the number of running kernels."""
90 return len(self.list_kernel_ids())
90 return len(self.list_kernel_ids())
91
91
92 def __contains__(self, kernel_id):
92 def __contains__(self, kernel_id):
93 return kernel_id in self._kernels
93 return kernel_id in self._kernels
94
94
95 def start_kernel(self, **kwargs):
95 def start_kernel(self, **kwargs):
96 """Start a new kernel.
96 """Start a new kernel.
97
97
98 The caller can pick a kernel_id by passing one in as a keyword arg,
98 The caller can pick a kernel_id by passing one in as a keyword arg,
99 otherwise one will be picked using a uuid.
99 otherwise one will be picked using a uuid.
100
100
101 To silence the kernel's stdout/stderr, call this using::
101 To silence the kernel's stdout/stderr, call this using::
102
102
103 km.start_kernel(stdout=PIPE, stderr=PIPE)
103 km.start_kernel(stdout=PIPE, stderr=PIPE)
104
104
105 """
105 """
106 kernel_id = kwargs.pop('kernel_id', unicode_type(uuid.uuid4()))
106 kernel_id = kwargs.pop('kernel_id', unicode_type(uuid.uuid4()))
107 if kernel_id in self:
107 if kernel_id in self:
108 raise DuplicateKernelError('Kernel already exists: %s' % kernel_id)
108 raise DuplicateKernelError('Kernel already exists: %s' % kernel_id)
109 # kernel_manager_factory is the constructor for the KernelManager
109 # kernel_manager_factory is the constructor for the KernelManager
110 # subclass we are using. It can be configured as any Configurable,
110 # subclass we are using. It can be configured as any Configurable,
111 # including things like its transport and ip.
111 # including things like its transport and ip.
112 km = self.kernel_manager_factory(connection_file=os.path.join(
112 km = self.kernel_manager_factory(connection_file=os.path.join(
113 self.connection_dir, "kernel-%s.json" % kernel_id),
113 self.connection_dir, "kernel-%s.json" % kernel_id),
114 parent=self, autorestart=True, log=self.log
114 parent=self, autorestart=True, log=self.log
115 )
115 )
116 km.start_kernel(**kwargs)
116 km.start_kernel(**kwargs)
117 self._kernels[kernel_id] = km
117 self._kernels[kernel_id] = km
118 return kernel_id
118 return kernel_id
119
119
120 @kernel_method
120 @kernel_method
121 def shutdown_kernel(self, kernel_id, now=False):
121 def shutdown_kernel(self, kernel_id, now=False, restart=False):
122 """Shutdown a kernel by its kernel uuid.
122 """Shutdown a kernel by its kernel uuid.
123
123
124 Parameters
124 Parameters
125 ==========
125 ==========
126 kernel_id : uuid
126 kernel_id : uuid
127 The id of the kernel to shutdown.
127 The id of the kernel to shutdown.
128 now : bool
128 now : bool
129 Should the kernel be shutdown forcibly using a signal.
129 Should the kernel be shutdown forcibly using a signal.
130 restart : bool
131 Will the kernel be restarted?
130 """
132 """
131 self.log.info("Kernel shutdown: %s" % kernel_id)
133 self.log.info("Kernel shutdown: %s" % kernel_id)
132 self.remove_kernel(kernel_id)
134 self.remove_kernel(kernel_id)
133
135
136 @kernel_method
137 def request_shutdown(self, kernel_id, restart=False):
138 """Ask a kernel to shut down by its kernel uuid"""
139
140 @kernel_method
141 def finish_shutdown(self, kernel_id, waittime=1, pollinterval=0.1):
142 """Wait for a kernel to finish shutting down, and kill it if it doesn't
143 """
144 self.log.info("Kernel shutdown: %s" % kernel_id)
145
146 @kernel_method
147 def cleanup(self, kernel_id, connection_file=True):
148 """Clean up a kernel's resources"""
149
134 def remove_kernel(self, kernel_id):
150 def remove_kernel(self, kernel_id):
135 """remove a kernel from our mapping.
151 """remove a kernel from our mapping.
136
152
137 Mainly so that a kernel can be removed if it is already dead,
153 Mainly so that a kernel can be removed if it is already dead,
138 without having to call shutdown_kernel.
154 without having to call shutdown_kernel.
139
155
140 The kernel object is returned.
156 The kernel object is returned.
141 """
157 """
142 return self._kernels.pop(kernel_id)
158 return self._kernels.pop(kernel_id)
143
159
144 def shutdown_all(self, now=False):
160 def shutdown_all(self, now=False):
145 """Shutdown all kernels."""
161 """Shutdown all kernels."""
146 for kid in self.list_kernel_ids():
162 kids = self.list_kernel_ids()
147 self.shutdown_kernel(kid, now=now)
163 for kid in kids:
164 self.request_shutdown(kid)
165 for kid in kids:
166 self.finish_shutdown(kid)
167 self.cleanup(kid)
148
168
149 @kernel_method
169 @kernel_method
150 def interrupt_kernel(self, kernel_id):
170 def interrupt_kernel(self, kernel_id):
151 """Interrupt (SIGINT) the kernel by its uuid.
171 """Interrupt (SIGINT) the kernel by its uuid.
152
172
153 Parameters
173 Parameters
154 ==========
174 ==========
155 kernel_id : uuid
175 kernel_id : uuid
156 The id of the kernel to interrupt.
176 The id of the kernel to interrupt.
157 """
177 """
158 self.log.info("Kernel interrupted: %s" % kernel_id)
178 self.log.info("Kernel interrupted: %s" % kernel_id)
159
179
160 @kernel_method
180 @kernel_method
161 def signal_kernel(self, kernel_id, signum):
181 def signal_kernel(self, kernel_id, signum):
162 """Sends a signal to the kernel by its uuid.
182 """Sends a signal to the kernel by its uuid.
163
183
164 Note that since only SIGTERM is supported on Windows, this function
184 Note that since only SIGTERM is supported on Windows, this function
165 is only useful on Unix systems.
185 is only useful on Unix systems.
166
186
167 Parameters
187 Parameters
168 ==========
188 ==========
169 kernel_id : uuid
189 kernel_id : uuid
170 The id of the kernel to signal.
190 The id of the kernel to signal.
171 """
191 """
172 self.log.info("Signaled Kernel %s with %s" % (kernel_id, signum))
192 self.log.info("Signaled Kernel %s with %s" % (kernel_id, signum))
173
193
174 @kernel_method
194 @kernel_method
175 def restart_kernel(self, kernel_id, now=False):
195 def restart_kernel(self, kernel_id, now=False):
176 """Restart a kernel by its uuid, keeping the same ports.
196 """Restart a kernel by its uuid, keeping the same ports.
177
197
178 Parameters
198 Parameters
179 ==========
199 ==========
180 kernel_id : uuid
200 kernel_id : uuid
181 The id of the kernel to interrupt.
201 The id of the kernel to interrupt.
182 """
202 """
183 self.log.info("Kernel restarted: %s" % kernel_id)
203 self.log.info("Kernel restarted: %s" % kernel_id)
184
204
185 @kernel_method
205 @kernel_method
186 def is_alive(self, kernel_id):
206 def is_alive(self, kernel_id):
187 """Is the kernel alive.
207 """Is the kernel alive.
188
208
189 This calls KernelManager.is_alive() which calls Popen.poll on the
209 This calls KernelManager.is_alive() which calls Popen.poll on the
190 actual kernel subprocess.
210 actual kernel subprocess.
191
211
192 Parameters
212 Parameters
193 ==========
213 ==========
194 kernel_id : uuid
214 kernel_id : uuid
195 The id of the kernel.
215 The id of the kernel.
196 """
216 """
197
217
198 def _check_kernel_id(self, kernel_id):
218 def _check_kernel_id(self, kernel_id):
199 """check that a kernel id is valid"""
219 """check that a kernel id is valid"""
200 if kernel_id not in self:
220 if kernel_id not in self:
201 raise KeyError("Kernel with id not found: %s" % kernel_id)
221 raise KeyError("Kernel with id not found: %s" % kernel_id)
202
222
203 def get_kernel(self, kernel_id):
223 def get_kernel(self, kernel_id):
204 """Get the single KernelManager object for a kernel by its uuid.
224 """Get the single KernelManager object for a kernel by its uuid.
205
225
206 Parameters
226 Parameters
207 ==========
227 ==========
208 kernel_id : uuid
228 kernel_id : uuid
209 The id of the kernel.
229 The id of the kernel.
210 """
230 """
211 self._check_kernel_id(kernel_id)
231 self._check_kernel_id(kernel_id)
212 return self._kernels[kernel_id]
232 return self._kernels[kernel_id]
213
233
214 @kernel_method
234 @kernel_method
215 def add_restart_callback(self, kernel_id, callback, event='restart'):
235 def add_restart_callback(self, kernel_id, callback, event='restart'):
216 """add a callback for the KernelRestarter"""
236 """add a callback for the KernelRestarter"""
217
237
218 @kernel_method
238 @kernel_method
219 def remove_restart_callback(self, kernel_id, callback, event='restart'):
239 def remove_restart_callback(self, kernel_id, callback, event='restart'):
220 """remove a callback for the KernelRestarter"""
240 """remove a callback for the KernelRestarter"""
221
241
222 @kernel_method
242 @kernel_method
223 def get_connection_info(self, kernel_id):
243 def get_connection_info(self, kernel_id):
224 """Return a dictionary of connection data for a kernel.
244 """Return a dictionary of connection data for a kernel.
225
245
226 Parameters
246 Parameters
227 ==========
247 ==========
228 kernel_id : uuid
248 kernel_id : uuid
229 The id of the kernel.
249 The id of the kernel.
230
250
231 Returns
251 Returns
232 =======
252 =======
233 connection_dict : dict
253 connection_dict : dict
234 A dict of the information needed to connect to a kernel.
254 A dict of the information needed to connect to a kernel.
235 This includes the ip address and the integer port
255 This includes the ip address and the integer port
236 numbers of the different channels (stdin_port, iopub_port,
256 numbers of the different channels (stdin_port, iopub_port,
237 shell_port, hb_port).
257 shell_port, hb_port).
238 """
258 """
239
259
240 @kernel_method
260 @kernel_method
241 def connect_iopub(self, kernel_id, identity=None):
261 def connect_iopub(self, kernel_id, identity=None):
242 """Return a zmq Socket connected to the iopub channel.
262 """Return a zmq Socket connected to the iopub channel.
243
263
244 Parameters
264 Parameters
245 ==========
265 ==========
246 kernel_id : uuid
266 kernel_id : uuid
247 The id of the kernel
267 The id of the kernel
248 identity : bytes (optional)
268 identity : bytes (optional)
249 The zmq identity of the socket
269 The zmq identity of the socket
250
270
251 Returns
271 Returns
252 =======
272 =======
253 stream : zmq Socket or ZMQStream
273 stream : zmq Socket or ZMQStream
254 """
274 """
255
275
256 @kernel_method
276 @kernel_method
257 def connect_shell(self, kernel_id, identity=None):
277 def connect_shell(self, kernel_id, identity=None):
258 """Return a zmq Socket connected to the shell channel.
278 """Return a zmq Socket connected to the shell channel.
259
279
260 Parameters
280 Parameters
261 ==========
281 ==========
262 kernel_id : uuid
282 kernel_id : uuid
263 The id of the kernel
283 The id of the kernel
264 identity : bytes (optional)
284 identity : bytes (optional)
265 The zmq identity of the socket
285 The zmq identity of the socket
266
286
267 Returns
287 Returns
268 =======
288 =======
269 stream : zmq Socket or ZMQStream
289 stream : zmq Socket or ZMQStream
270 """
290 """
271
291
272 @kernel_method
292 @kernel_method
273 def connect_stdin(self, kernel_id, identity=None):
293 def connect_stdin(self, kernel_id, identity=None):
274 """Return a zmq Socket connected to the stdin channel.
294 """Return a zmq Socket connected to the stdin channel.
275
295
276 Parameters
296 Parameters
277 ==========
297 ==========
278 kernel_id : uuid
298 kernel_id : uuid
279 The id of the kernel
299 The id of the kernel
280 identity : bytes (optional)
300 identity : bytes (optional)
281 The zmq identity of the socket
301 The zmq identity of the socket
282
302
283 Returns
303 Returns
284 =======
304 =======
285 stream : zmq Socket or ZMQStream
305 stream : zmq Socket or ZMQStream
286 """
306 """
287
307
288 @kernel_method
308 @kernel_method
289 def connect_hb(self, kernel_id, identity=None):
309 def connect_hb(self, kernel_id, identity=None):
290 """Return a zmq Socket connected to the hb channel.
310 """Return a zmq Socket connected to the hb channel.
291
311
292 Parameters
312 Parameters
293 ==========
313 ==========
294 kernel_id : uuid
314 kernel_id : uuid
295 The id of the kernel
315 The id of the kernel
296 identity : bytes (optional)
316 identity : bytes (optional)
297 The zmq identity of the socket
317 The zmq identity of the socket
298
318
299 Returns
319 Returns
300 =======
320 =======
301 stream : zmq Socket or ZMQStream
321 stream : zmq Socket or ZMQStream
302 """
322 """
General Comments 0
You need to be logged in to leave comments. Login now