##// 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 206 return True
207 207 elif key == QtCore.Qt.Key_Period:
208 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 210 return True
211 211 return super(FrontendWidget, self)._event_filter_console_keypress(event)
212 212
@@ -279,7 +279,7 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
279 279 if self.custom_restart:
280 280 self.custom_restart_kernel_died.emit(since_last_heartbeat)
281 281 else:
282 self.restart_kernel(message)
282 self.restart_kernel(message, instant_death=True)
283 283
284 284 def _handle_object_info_reply(self, rep):
285 285 """ Handle replies for call tips.
@@ -341,9 +341,16 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
341 341 self._append_plain_text('Kernel process is either remote or '
342 342 'unspecified. Cannot interrupt.\n')
343 343
344 def restart_kernel(self, message):
344 def restart_kernel(self, message, instant_death=False):
345 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 354 # We want to make sure that if this dialog is already happening, that
348 355 # other signals don't trigger it again. This can happen when the
349 356 # kernel_died heartbeat signal is emitted and the user is slow to
@@ -360,7 +367,8 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
360 367 message, buttons)
361 368 if result == QtGui.QMessageBox.Yes:
362 369 try:
363 self.kernel_manager.restart_kernel()
370 self.kernel_manager.restart_kernel(
371 instant_death=instant_death)
364 372 except RuntimeError:
365 373 message = 'Kernel started externally. Cannot restart.\n'
366 374 self._append_plain_text(message)
@@ -17,6 +17,7 b' from __future__ import print_function'
17 17
18 18 # Standard library imports.
19 19 import __builtin__
20 import atexit
20 21 import sys
21 22 import time
22 23 import traceback
@@ -30,13 +31,12 b' from IPython.utils import io'
30 31 from IPython.utils.jsonutil import json_clean
31 32 from IPython.lib import pylabtools
32 33 from IPython.utils.traitlets import Instance, Float
33 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
34 start_kernel
34 from entry_point import (base_launch_kernel, make_argument_parser, make_kernel,
35 start_kernel)
35 36 from iostream import OutStream
36 37 from session import Session, Message
37 38 from zmqshell import ZMQInteractiveShell
38 39
39
40 40 #-----------------------------------------------------------------------------
41 41 # Main kernel class
42 42 #-----------------------------------------------------------------------------
@@ -68,10 +68,21 b' class Kernel(Configurable):'
68 68 # Units are in seconds, kernel subclasses for GUI toolkits may need to
69 69 # adapt to milliseconds.
70 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 79 def __init__(self, **kwargs):
73 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 86 # Initialize the InteractiveShell subclass
76 87 self.shell = ZMQInteractiveShell.instance()
77 88 self.shell.displayhook.session = self.session
@@ -82,7 +93,8 b' class Kernel(Configurable):'
82 93
83 94 # Build dict of handlers for message types
84 95 msg_types = [ 'execute_request', 'complete_request',
85 'object_info_request', 'history_request' ]
96 'object_info_request', 'history_request',
97 'shutdown_request']
86 98 self.handlers = {}
87 99 for msg_type in msg_types:
88 100 self.handlers[msg_type] = getattr(self, msg_type)
@@ -271,6 +283,11 b' class Kernel(Configurable):'
271 283 content, parent, ident)
272 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 292 # Protected interface
276 293 #---------------------------------------------------------------------------
@@ -360,6 +377,17 b' class Kernel(Configurable):'
360 377
361 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 392 class QtKernel(Kernel):
365 393 """A Kernel subclass with Qt support."""
@@ -367,10 +395,9 b' class QtKernel(Kernel):'
367 395 def start(self):
368 396 """Start a kernel with QtPy4 event loop integration."""
369 397
370 from PyQt4 import QtGui, QtCore
371 from IPython.lib.guisupport import (
372 get_app_qt4, start_event_loop_qt4
373 )
398 from PyQt4 import QtCore
399 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
400
374 401 self.app = get_app_qt4([" "])
375 402 self.app.setQuitOnLastWindowClosed(False)
376 403 self.timer = QtCore.QTimer()
@@ -388,6 +415,7 b' class WxKernel(Kernel):'
388 415
389 416 import wx
390 417 from IPython.lib.guisupport import start_event_loop_wx
418
391 419 doi = self.do_one_iteration
392 420 # Wx uses milliseconds
393 421 poll_interval = int(1000*self._poll_interval)
@@ -304,6 +304,23 b' class XReqSocketChannel(ZmqSocketChannel):'
304 304 self._queue_request(msg)
305 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 324 def _handle_events(self, socket, events):
308 325 if events & POLLERR:
309 326 self._handle_err()
@@ -700,14 +717,11 b' class KernelManager(HasTraits):'
700 717 """ Attempts to the stop the kernel process cleanly. If the kernel
701 718 cannot be stopped, it is killed, if possible.
702 719 """
703 # Send quit message to kernel. Once we implement kernel-side setattr,
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
720 self.xreq_channel.shutdown()
707 721 # Don't send any additional kernel kill messages immediately, to give
708 722 # the kernel a chance to properly execute shutdown actions. Wait for at
709 # most 2s, checking every 0.1s.
710 for i in range(20):
723 # most 1s, checking every 0.1s.
724 for i in range(10):
711 725 if self.is_alive:
712 726 time.sleep(0.1)
713 727 else:
@@ -716,18 +730,31 b' class KernelManager(HasTraits):'
716 730 # OK, we've waited long enough.
717 731 if self.has_kernel:
718 732 self.kill_kernel()
719
720 def restart_kernel(self):
733
734 def restart_kernel(self, instant_death=False):
721 735 """Restarts a kernel with the same arguments that were used to launch
722 736 it. If the old kernel was launched with random ports, the same ports
723 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 749 if self._launch_args is None:
726 750 raise RuntimeError("Cannot restart the kernel. "
727 751 "No previous call to 'start_kernel'.")
728 752 else:
729 753 if self.has_kernel:
730 self.kill_kernel()
754 if instant_death:
755 self.kill_kernel()
756 else:
757 self.shutdown_kernel()
731 758 self.start_kernel(**self._launch_args)
732 759
733 760 @property
@@ -755,6 +782,8 b' class KernelManager(HasTraits):'
755 782 @property
756 783 def is_alive(self):
757 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 787 if self.kernel is not None:
759 788 if self.kernel.poll() is None:
760 789 return True
@@ -534,7 +534,49 b' Message type: ``history_reply``::'
534 534 # respectively.
535 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 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