##// END OF EJS Templates
Added kernel shutdown support: messaging spec, zmq and client code ready....
Fernando Perez -
Show More
@@ -206,7 +206,7 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
206 return True
206 return True
207 elif key == QtCore.Qt.Key_Period:
207 elif key == QtCore.Qt.Key_Period:
208 message = 'Are you sure you want to restart the kernel?'
208 message = 'Are you sure you want to restart the kernel?'
209 self.restart_kernel(message)
209 self.restart_kernel(message, instant_death=False)
210 return True
210 return True
211 return super(FrontendWidget, self)._event_filter_console_keypress(event)
211 return super(FrontendWidget, self)._event_filter_console_keypress(event)
212
212
@@ -279,7 +279,7 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
279 if self.custom_restart:
279 if self.custom_restart:
280 self.custom_restart_kernel_died.emit(since_last_heartbeat)
280 self.custom_restart_kernel_died.emit(since_last_heartbeat)
281 else:
281 else:
282 self.restart_kernel(message)
282 self.restart_kernel(message, instant_death=True)
283
283
284 def _handle_object_info_reply(self, rep):
284 def _handle_object_info_reply(self, rep):
285 """ Handle replies for call tips.
285 """ Handle replies for call tips.
@@ -341,9 +341,16 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
341 self._append_plain_text('Kernel process is either remote or '
341 self._append_plain_text('Kernel process is either remote or '
342 'unspecified. Cannot interrupt.\n')
342 'unspecified. Cannot interrupt.\n')
343
343
344 def restart_kernel(self, message):
344 def restart_kernel(self, message, instant_death=False):
345 """ Attempts to restart the running kernel.
345 """ Attempts to restart the running kernel.
346 """
346 """
347 # FIXME: instant_death should be configurable via a checkbox in the
348 # dialog. Right now at least the heartbeat path sets it to True and
349 # the manual restart to False. But those should just be the
350 # pre-selected states of a checkbox that the user could override if so
351 # desired. But I don't know enough Qt to go implementing the checkbox
352 # now.
353
347 # We want to make sure that if this dialog is already happening, that
354 # We want to make sure that if this dialog is already happening, that
348 # other signals don't trigger it again. This can happen when the
355 # other signals don't trigger it again. This can happen when the
349 # kernel_died heartbeat signal is emitted and the user is slow to
356 # kernel_died heartbeat signal is emitted and the user is slow to
@@ -360,7 +367,8 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
360 message, buttons)
367 message, buttons)
361 if result == QtGui.QMessageBox.Yes:
368 if result == QtGui.QMessageBox.Yes:
362 try:
369 try:
363 self.kernel_manager.restart_kernel()
370 self.kernel_manager.restart_kernel(
371 instant_death=instant_death)
364 except RuntimeError:
372 except RuntimeError:
365 message = 'Kernel started externally. Cannot restart.\n'
373 message = 'Kernel started externally. Cannot restart.\n'
366 self._append_plain_text(message)
374 self._append_plain_text(message)
@@ -17,6 +17,7 b' from __future__ import print_function'
17
17
18 # Standard library imports.
18 # Standard library imports.
19 import __builtin__
19 import __builtin__
20 import atexit
20 import sys
21 import sys
21 import time
22 import time
22 import traceback
23 import traceback
@@ -30,13 +31,12 b' from IPython.utils import io'
30 from IPython.utils.jsonutil import json_clean
31 from IPython.utils.jsonutil import json_clean
31 from IPython.lib import pylabtools
32 from IPython.lib import pylabtools
32 from IPython.utils.traitlets import Instance, Float
33 from IPython.utils.traitlets import Instance, Float
33 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
34 from entry_point import (base_launch_kernel, make_argument_parser, make_kernel,
34 start_kernel
35 start_kernel)
35 from iostream import OutStream
36 from iostream import OutStream
36 from session import Session, Message
37 from session import Session, Message
37 from zmqshell import ZMQInteractiveShell
38 from zmqshell import ZMQInteractiveShell
38
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Main kernel class
41 # Main kernel class
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
@@ -68,10 +68,21 b' class Kernel(Configurable):'
68 # Units are in seconds, kernel subclasses for GUI toolkits may need to
68 # Units are in seconds, kernel subclasses for GUI toolkits may need to
69 # adapt to milliseconds.
69 # adapt to milliseconds.
70 _poll_interval = Float(0.05, config=True)
70 _poll_interval = Float(0.05, config=True)
71
72 # If the shutdown was requested over the network, we leave here the
73 # necessary reply message so it can be sent by our registered atexit
74 # handler. This ensures that the reply is only sent to clients truly at
75 # the end of our shutdown process (which happens after the underlying
76 # IPython shell's own shutdown).
77 _shutdown_message = None
71
78
72 def __init__(self, **kwargs):
79 def __init__(self, **kwargs):
73 super(Kernel, self).__init__(**kwargs)
80 super(Kernel, self).__init__(**kwargs)
74
81
82 # Before we even start up the shell, register *first* our exit handlers
83 # so they come before the shell's
84 atexit.register(self._at_shutdown)
85
75 # Initialize the InteractiveShell subclass
86 # Initialize the InteractiveShell subclass
76 self.shell = ZMQInteractiveShell.instance()
87 self.shell = ZMQInteractiveShell.instance()
77 self.shell.displayhook.session = self.session
88 self.shell.displayhook.session = self.session
@@ -82,7 +93,8 b' class Kernel(Configurable):'
82
93
83 # Build dict of handlers for message types
94 # Build dict of handlers for message types
84 msg_types = [ 'execute_request', 'complete_request',
95 msg_types = [ 'execute_request', 'complete_request',
85 'object_info_request', 'history_request' ]
96 'object_info_request', 'history_request',
97 'shutdown_request']
86 self.handlers = {}
98 self.handlers = {}
87 for msg_type in msg_types:
99 for msg_type in msg_types:
88 self.handlers[msg_type] = getattr(self, msg_type)
100 self.handlers[msg_type] = getattr(self, msg_type)
@@ -271,6 +283,11 b' class Kernel(Configurable):'
271 content, parent, ident)
283 content, parent, ident)
272 io.raw_print(msg)
284 io.raw_print(msg)
273
285
286 def shutdown_request(self, ident, parent):
287 self.shell.exit_now = True
288 self._shutdown_message = self.session.msg(u'shutdown_reply', {}, parent)
289 sys.exit(0)
290
274 #---------------------------------------------------------------------------
291 #---------------------------------------------------------------------------
275 # Protected interface
292 # Protected interface
276 #---------------------------------------------------------------------------
293 #---------------------------------------------------------------------------
@@ -360,6 +377,17 b' class Kernel(Configurable):'
360
377
361 return symbol, []
378 return symbol, []
362
379
380 def _at_shutdown(self):
381 """Actions taken at shutdown by the kernel, called by python's atexit.
382 """
383 # io.rprint("Kernel at_shutdown") # dbg
384 if self._shutdown_message is not None:
385 self.reply_socket.send_json(self._shutdown_message)
386 io.raw_print(self._shutdown_message)
387 # A very short sleep to give zmq time to flush its message buffers
388 # before Python truly shuts down.
389 time.sleep(0.01)
390
363
391
364 class QtKernel(Kernel):
392 class QtKernel(Kernel):
365 """A Kernel subclass with Qt support."""
393 """A Kernel subclass with Qt support."""
@@ -367,10 +395,9 b' class QtKernel(Kernel):'
367 def start(self):
395 def start(self):
368 """Start a kernel with QtPy4 event loop integration."""
396 """Start a kernel with QtPy4 event loop integration."""
369
397
370 from PyQt4 import QtGui, QtCore
398 from PyQt4 import QtCore
371 from IPython.lib.guisupport import (
399 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
372 get_app_qt4, start_event_loop_qt4
400
373 )
374 self.app = get_app_qt4([" "])
401 self.app = get_app_qt4([" "])
375 self.app.setQuitOnLastWindowClosed(False)
402 self.app.setQuitOnLastWindowClosed(False)
376 self.timer = QtCore.QTimer()
403 self.timer = QtCore.QTimer()
@@ -388,6 +415,7 b' class WxKernel(Kernel):'
388
415
389 import wx
416 import wx
390 from IPython.lib.guisupport import start_event_loop_wx
417 from IPython.lib.guisupport import start_event_loop_wx
418
391 doi = self.do_one_iteration
419 doi = self.do_one_iteration
392 # Wx uses milliseconds
420 # Wx uses milliseconds
393 poll_interval = int(1000*self._poll_interval)
421 poll_interval = int(1000*self._poll_interval)
@@ -304,6 +304,23 b' class XReqSocketChannel(ZmqSocketChannel):'
304 self._queue_request(msg)
304 self._queue_request(msg)
305 return msg['header']['msg_id']
305 return msg['header']['msg_id']
306
306
307 def shutdown(self):
308 """Request an immediate kernel shutdown.
309
310 Upon receipt of the (empty) reply, client code can safely assume that
311 the kernel has shut down and it's safe to forcefully terminate it if
312 it's still alive.
313
314 The kernel will send the reply via a function registered with Python's
315 atexit module, ensuring it's truly done as the kernel is done with all
316 normal operation.
317 """
318 # Send quit message to kernel. Once we implement kernel-side setattr,
319 # this should probably be done that way, but for now this will do.
320 msg = self.session.msg('shutdown_request', {})
321 self._queue_request(msg)
322 return msg['header']['msg_id']
323
307 def _handle_events(self, socket, events):
324 def _handle_events(self, socket, events):
308 if events & POLLERR:
325 if events & POLLERR:
309 self._handle_err()
326 self._handle_err()
@@ -700,14 +717,11 b' class KernelManager(HasTraits):'
700 """ Attempts to the stop the kernel process cleanly. If the kernel
717 """ Attempts to the stop the kernel process cleanly. If the kernel
701 cannot be stopped, it is killed, if possible.
718 cannot be stopped, it is killed, if possible.
702 """
719 """
703 # Send quit message to kernel. Once we implement kernel-side setattr,
720 self.xreq_channel.shutdown()
704 # this should probably be done that way, but for now this will do.
705 self.xreq_channel.execute('get_ipython().exit_now=True', silent=True)
706
707 # Don't send any additional kernel kill messages immediately, to give
721 # Don't send any additional kernel kill messages immediately, to give
708 # the kernel a chance to properly execute shutdown actions. Wait for at
722 # the kernel a chance to properly execute shutdown actions. Wait for at
709 # most 2s, checking every 0.1s.
723 # most 1s, checking every 0.1s.
710 for i in range(20):
724 for i in range(10):
711 if self.is_alive:
725 if self.is_alive:
712 time.sleep(0.1)
726 time.sleep(0.1)
713 else:
727 else:
@@ -716,18 +730,31 b' class KernelManager(HasTraits):'
716 # OK, we've waited long enough.
730 # OK, we've waited long enough.
717 if self.has_kernel:
731 if self.has_kernel:
718 self.kill_kernel()
732 self.kill_kernel()
719
733
720 def restart_kernel(self):
734 def restart_kernel(self, instant_death=False):
721 """Restarts a kernel with the same arguments that were used to launch
735 """Restarts a kernel with the same arguments that were used to launch
722 it. If the old kernel was launched with random ports, the same ports
736 it. If the old kernel was launched with random ports, the same ports
723 will be used for the new kernel.
737 will be used for the new kernel.
738
739 Parameters
740 ----------
741 instant_death : bool, optional
742 If True, the kernel is forcefully restarted *immediately*, without
743 having a chance to do any cleanup action. Otherwise the kernel is
744 given 1s to clean up before a forceful restart is issued.
745
746 In all cases the kernel is restarted, the only difference is whether
747 it is given a chance to perform a clean shutdown or not.
724 """
748 """
725 if self._launch_args is None:
749 if self._launch_args is None:
726 raise RuntimeError("Cannot restart the kernel. "
750 raise RuntimeError("Cannot restart the kernel. "
727 "No previous call to 'start_kernel'.")
751 "No previous call to 'start_kernel'.")
728 else:
752 else:
729 if self.has_kernel:
753 if self.has_kernel:
730 self.kill_kernel()
754 if instant_death:
755 self.kill_kernel()
756 else:
757 self.shutdown_kernel()
731 self.start_kernel(**self._launch_args)
758 self.start_kernel(**self._launch_args)
732
759
733 @property
760 @property
@@ -755,6 +782,8 b' class KernelManager(HasTraits):'
755 @property
782 @property
756 def is_alive(self):
783 def is_alive(self):
757 """Is the kernel process still running?"""
784 """Is the kernel process still running?"""
785 # FIXME: not using a heartbeat means this method is broken for any
786 # remote kernel, it's only capable of handling local kernels.
758 if self.kernel is not None:
787 if self.kernel is not None:
759 if self.kernel.poll() is None:
788 if self.kernel.poll() is None:
760 return True
789 return True
@@ -534,7 +534,49 b' Message type: ``history_reply``::'
534 # respectively.
534 # respectively.
535 'history' : dict,
535 'history' : dict,
536 }
536 }
537
538
539 Kernel shutdown
540 ---------------
541
542 The clients can request the kernel to shut itself down; this is used in
543 multiple cases:
544
545 - when the user chooses to close the client application via a menu or window
546 control.
547 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
548 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
549 IPythonQt client) to force a kernel restart to get a clean kernel without
550 losing client-side state like history or inlined figures.
551
552 The client sends a shutdown request to the kernel, and once it receives the
553 reply message (which is otherwise empty), it can assume that the kernel has
554 completed shutdown safely.
555
556 Upon their own shutdown, client applications will typically execute a last
557 minute sanity check and forcefully terminate any kernel that is still alive, to
558 avoid leaving stray processes in the user's machine.
559
560 For both shutdown request and reply, there is no actual content that needs to
561 be sent, so the content dict is empty.
562
563 Message type: ``shutdown_request``::
564
565 content = {
566 }
567
568 Message type: ``shutdown_reply``::
569
570 content = {
571 }
572
573 .. Note::
574
575 When the clients detect a dead kernel thanks to inactivity on the heartbeat
576 socket, they simply send a forceful process termination signal, since a dead
577 process is unlikely to respond in any useful way to messages.
537
578
579
538 Messages on the PUB/SUB socket
580 Messages on the PUB/SUB socket
539 ==============================
581 ==============================
540
582
General Comments 0
You need to be logged in to leave comments. Login now