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 |
|
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 |
|
723 | # most 1s, checking every 0.1s. | |
710 |
for i in range( |
|
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