diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 2afb8e4..e732d42 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -3,6 +3,7 @@ from __future__ import print_function # Standard library imports from collections import namedtuple import sys +import time # System library imports from pygments.lexers import PythonLexer @@ -361,6 +362,19 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): self._append_plain_text(text) self._control.moveCursor(QtGui.QTextCursor.End) + def _handle_shutdown_reply(self, msg): + """ Handle shutdown signal, only if from other console. + """ + if not self._hidden and not self._is_from_this_session(msg): + if not msg['content']['restart']: + sys.exit(0) + else: + # we just got notified of a restart! + time.sleep(0.25) # wait 1/4 sec to reset + # lest the request for a new prompt + # goes to the old kernel + self.reset() + def _started_channels(self): """ Called when the KernelManager channels have started listening or when the frontend is assigned an already listening KernelManager. diff --git a/IPython/frontend/qt/console/ipythonqt.py b/IPython/frontend/qt/console/ipythonqt.py index c720c88..ad5415d 100644 --- a/IPython/frontend/qt/console/ipythonqt.py +++ b/IPython/frontend/qt/console/ipythonqt.py @@ -31,11 +31,18 @@ class MainWindow(QtGui.QMainWindow): # 'object' interface #--------------------------------------------------------------------------- - def __init__(self, frontend): + def __init__(self, app, frontend, existing=False): """ Create a MainWindow for the specified FrontendWidget. + + The app is passed as an argument to allow for different + closing behavior depending on whether we are the Kernel's parent. + + If existing is True, then this Window does not own the Kernel. """ super(MainWindow, self).__init__() + self._app = app self._frontend = frontend + self._existing = existing self._frontend.exit_requested.connect(self.close) self.setCentralWidget(frontend) @@ -50,11 +57,18 @@ class MainWindow(QtGui.QMainWindow): if kernel_manager and kernel_manager.channels_running: title = self.window().windowTitle() reply = QtGui.QMessageBox.question(self, title, - 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) - if reply == QtGui.QMessageBox.Yes: + "Close just this console, or shutdown the kernel and close "+ + "all windows attached to it?", + 'Cancel', 'Close Console', 'Close All') + if reply == 2: # close All kernel_manager.shutdown_kernel() #kernel_manager.stop_channels() event.accept() + elif reply == 1: # close Console + if not self._existing: + # I have the kernel: don't quit, just close the window + self._app.setQuitOnLastWindowClosed(False) + event.accept() else: event.ignore() @@ -132,7 +146,7 @@ def main(): widget.kernel_manager = kernel_manager # Create the main window. - window = MainWindow(widget) + window = MainWindow(app, widget, args.existing) window.setWindowTitle('Python' if args.pure else 'IPython') window.show() diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py index 09e7fa6..98df284 100644 --- a/IPython/frontend/qt/kernelmanager.py +++ b/IPython/frontend/qt/kernelmanager.py @@ -105,6 +105,9 @@ class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel): # last-resort sys.excepthook. crash_received = QtCore.pyqtSignal(object) + # Emitted when a shutdown is noticed. + shutdown_reply_received = QtCore.pyqtSignal(object) + #--------------------------------------------------------------------------- # 'SubSocketChannel' interface #--------------------------------------------------------------------------- diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 0966e7d..ca37c3f 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -330,7 +330,7 @@ class Kernel(Configurable): def shutdown_request(self, ident, parent): self.shell.exit_now = True - self._shutdown_message = self.session.msg(u'shutdown_reply', {}, parent) + self._shutdown_message = self.session.msg(u'shutdown_reply', parent['content'], parent) sys.exit(0) #--------------------------------------------------------------------------- @@ -428,6 +428,7 @@ class Kernel(Configurable): # io.rprint("Kernel at_shutdown") # dbg if self._shutdown_message is not None: self.reply_socket.send_json(self._shutdown_message) + self.pub_socket.send_json(self._shutdown_message) io.raw_print(self._shutdown_message) # A very short sleep to give zmq time to flush its message buffers # before Python truly shuts down. diff --git a/IPython/zmq/kernelmanager.py b/IPython/zmq/kernelmanager.py index 3794574..46e70cd 100644 --- a/IPython/zmq/kernelmanager.py +++ b/IPython/zmq/kernelmanager.py @@ -305,7 +305,7 @@ class XReqSocketChannel(ZmqSocketChannel): self._queue_request(msg) return msg['header']['msg_id'] - def shutdown(self): + def shutdown(self, restart=False): """Request an immediate kernel shutdown. Upon receipt of the (empty) reply, client code can safely assume that @@ -318,7 +318,7 @@ class XReqSocketChannel(ZmqSocketChannel): """ # Send quit message to kernel. Once we implement kernel-side setattr, # this should probably be done that way, but for now this will do. - msg = self.session.msg('shutdown_request', {}) + msg = self.session.msg('shutdown_request', {'restart':restart}) self._queue_request(msg) return msg['header']['msg_id'] @@ -743,7 +743,7 @@ class KernelManager(HasTraits): self.rep_address = (LOCALHOST, req) self.hb_address = (LOCALHOST, hb) - def shutdown_kernel(self): + def shutdown_kernel(self, restart=False): """ Attempts to the stop the kernel process cleanly. If the kernel cannot be stopped, it is killed, if possible. """ @@ -759,7 +759,7 @@ class KernelManager(HasTraits): # Don't send any additional kernel kill messages immediately, to give # the kernel a chance to properly execute shutdown actions. Wait for at # most 1s, checking every 0.1s. - self.xreq_channel.shutdown() + self.xreq_channel.shutdown(restart=restart) for i in range(10): if self.is_alive: time.sleep(0.1) @@ -793,7 +793,7 @@ class KernelManager(HasTraits): if now: self.kill_kernel() else: - self.shutdown_kernel() + self.shutdown_kernel(restart=True) self.start_kernel(**self._launch_args) # FIXME: Messages get dropped in Windows due to probable ZMQ bug diff --git a/IPython/zmq/pykernel.py b/IPython/zmq/pykernel.py index 30d79eb..4938a2a 100755 --- a/IPython/zmq/pykernel.py +++ b/IPython/zmq/pykernel.py @@ -60,7 +60,7 @@ class Kernel(HasTraits): # Build dict of handlers for message types msg_types = [ 'execute_request', 'complete_request', - 'object_info_request' ] + 'object_info_request', 'shutdown_request' ] self.handlers = {} for msg_type in msg_types: self.handlers[msg_type] = getattr(self, msg_type) @@ -163,6 +163,16 @@ class Kernel(HasTraits): object_info, parent, ident) print >> sys.__stdout__, msg + def shutdown_request(self, ident, parent): + content = dict(parent['content']) + msg = self.session.send(self.reply_socket, 'shutdown_reply', + content, parent, ident) + msg = self.session.send(self.pub_socket, 'shutdown_reply', + content, parent, ident) + print >> sys.__stdout__, msg + time.sleep(0.1) + sys.exit(0) + #--------------------------------------------------------------------------- # Protected interface #--------------------------------------------------------------------------- diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index fdb4ec3..230880c 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -663,11 +663,13 @@ be sent, so the content dict is empty. Message type: ``shutdown_request``:: content = { + 'restart' : bool # whether the shutdown is final, or precedes a restart } Message type: ``shutdown_reply``:: content = { + 'restart' : bool # whether the shutdown is final, or precedes a restart } .. Note::