Skip to content

Commit

Permalink
Merge branch 'minrk-keepkernel' into trunk
Browse files Browse the repository at this point in the history
Improvements to the closing behavior of a Qt console regarding what
happens to the kernel and other consoles that may be active.  Now,
there is fine control on who is closed and who remains active.

Closes ipythongh-164 (pull request).
  • Loading branch information
fperez committed Oct 21, 2010
2 parents 147b245 + ac47604 commit dc30bee
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 54 deletions.
38 changes: 30 additions & 8 deletions IPython/frontend/qt/console/frontend_widget.py
Expand Up @@ -101,6 +101,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
_CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
_ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
_input_splitter_class = InputSplitter
_local_kernel = False

#---------------------------------------------------------------------------
# 'object' interface
Expand Down Expand Up @@ -141,6 +142,9 @@ def __init__(self, *args, **kw):
# Connect signal handlers.
document = self._control.document()
document.contentsChange.connect(self._document_contents_change)

# set flag for whether we are connected via localhost
self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)

#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface
Expand Down Expand Up @@ -366,14 +370,32 @@ 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()
if self._local_kernel:
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()
else: # remote kernel, prompt on Kernel shutdown/reset
title = self.window().windowTitle()
if not msg['content']['restart']:
reply = QtGui.QMessageBox.question(self, title,
"Kernel has been shutdown permanently. Close the Console?",
QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
sys.exit(0)
else:
reply = QtGui.QMessageBox.question(self, title,
"Kernel has been reset. Clear the Console?",
QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
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
Expand Down
97 changes: 69 additions & 28 deletions IPython/frontend/qt/console/ipythonqt.py
Expand Up @@ -16,10 +16,10 @@
from IPython.frontend.qt.kernelmanager import QtKernelManager

#-----------------------------------------------------------------------------
# Constants
# Network Constants
#-----------------------------------------------------------------------------

LOCALHOST = '127.0.0.1'
from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS

#-----------------------------------------------------------------------------
# Classes
Expand All @@ -31,18 +31,24 @@ class MainWindow(QtGui.QMainWindow):
# 'object' interface
#---------------------------------------------------------------------------

def __init__(self, app, frontend, existing=False):
def __init__(self, app, frontend, existing=False, may_close=True):
""" 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.
If existing is True, then this Console does not own the Kernel.
If may_close is True, then this Console is permitted to close the kernel
"""
super(MainWindow, self).__init__()
self._app = app
self._frontend = frontend
self._existing = existing
if existing:
self._may_close = may_close
else:
self._may_close = True
self._frontend.exit_requested.connect(self.close)
self.setCentralWidget(frontend)

Expand All @@ -56,21 +62,47 @@ def closeEvent(self, event):
kernel_manager = self._frontend.kernel_manager
if kernel_manager and kernel_manager.channels_running:
title = self.window().windowTitle()
reply = QtGui.QMessageBox.question(self, title,
"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()
cancel = QtGui.QMessageBox.Cancel
okay = QtGui.QMessageBox.Ok
if self._may_close:
msg = "You are closing this Console window."
info = "Would you like to quit the Kernel and all attached Consoles as well?"
justthis = QtGui.QPushButton("&No, just this Console", self)
justthis.setShortcut('N')
closeall = QtGui.QPushButton("&Yes, quit everything", self)
closeall.setShortcut('Y')
box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
box.setInformativeText(info)
box.addButton(cancel)
box.addButton(justthis, QtGui.QMessageBox.NoRole)
box.addButton(closeall, QtGui.QMessageBox.YesRole)
box.setDefaultButton(closeall)
box.setEscapeButton(cancel)
reply = box.exec_()
if reply == 1: # close All
kernel_manager.shutdown_kernel()
#kernel_manager.stop_channels()
event.accept()
elif reply == 0: # close Console
if not self._existing:
# I have the kernel: don't quit, just close the window
self._app.setQuitOnLastWindowClosed(False)
self.deleteLater()
event.accept()
else:
event.ignore()
else:
event.ignore()
reply = QtGui.QMessageBox.question(self, title,
"Are you sure you want to close this Console?"+
"\nThe Kernel and other Consoles will remain active.",
okay|cancel,
defaultButton=okay
)
if reply == okay:
event.accept()
else:
event.ignore()


#-----------------------------------------------------------------------------
# Main entry point
Expand All @@ -85,7 +117,11 @@ def main():
kgroup.add_argument('-e', '--existing', action='store_true',
help='connect to an existing kernel')
kgroup.add_argument('--ip', type=str, default=LOCALHOST,
help='set the kernel\'s IP address [default localhost]')
help=\
"set the kernel\'s IP address [default localhost].\
If the IP address is something other than localhost, then \
Consoles on other machines will be able to connect\
to the Kernel, so be careful!")
kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
help='set the XREQ channel port [default random]')
kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
Expand Down Expand Up @@ -124,29 +160,34 @@ def main():
sub_address=(args.ip, args.sub),
rep_address=(args.ip, args.rep),
hb_address=(args.ip, args.hb))
if args.ip == LOCALHOST and not args.existing:
if not args.existing:
# if not args.ip in LOCAL_IPS+ALL_ALIAS:
# raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)

kwargs = dict(ip=args.ip)
if args.pure:
kernel_manager.start_kernel(ipython=False)
kwargs['ipython']=False
elif args.pylab:
kernel_manager.start_kernel(pylab=args.pylab)
else:
kernel_manager.start_kernel()
kwargs['pylab']=args.pylab
kernel_manager.start_kernel(**kwargs)
kernel_manager.start_channels()

local_kernel = (not args.existing) or args.ip in LOCAL_IPS
# Create the widget.
app = QtGui.QApplication([])
if args.pure:
kind = 'rich' if args.rich else 'plain'
widget = FrontendWidget(kind=kind, paging=args.paging)
widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
elif args.rich or args.pylab:
widget = RichIPythonWidget(paging=args.paging)
widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
else:
widget = IPythonWidget(paging=args.paging)
widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
widget.gui_completion = args.gui_completion
widget.kernel_manager = kernel_manager

# Create the main window.
window = MainWindow(app, widget, args.existing)
window = MainWindow(app, widget, args.existing, may_close=local_kernel)
window.setWindowTitle('Python' if args.pure else 'IPython')
window.show()

Expand Down
40 changes: 40 additions & 0 deletions IPython/utils/localinterfaces.py
@@ -0,0 +1,40 @@
"""Simple utility for building a list of local IPs using the socket module.
This module defines two constants:
LOCALHOST : The loopback interface, or the first interface that points to this
machine. It will *almost* always be '127.0.0.1'
LOCAL_IPS : A list of IP addresses, loopback first, that point to this machine.
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2010 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

import socket

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------

LOCAL_IPS = []
try:
LOCAL_IPS = socket.gethostbyname_ex('localhost')[2]
except socket.gaierror:
pass

try:
LOCAL_IPS.extend(socket.gethostbyname_ex(socket.gethostname())[2])
except socket.gaierror:
pass

# include all-interface aliases: 0.0.0.0 and ''
LOCAL_IPS.extend(['0.0.0.0', ''])

LOCALHOST = LOCAL_IPS[0]
3 changes: 2 additions & 1 deletion IPython/zmq/entry_point.py
Expand Up @@ -16,6 +16,7 @@
from IPython.core.ultratb import FormattedTB
from IPython.external.argparse import ArgumentParser
from IPython.utils import io
from IPython.utils.localinterfaces import LOCALHOST
from displayhook import DisplayHook
from heartbeat import Heartbeat
from iostream import OutStream
Expand All @@ -40,7 +41,7 @@ def make_argument_parser():
kernel entry points.
"""
parser = ArgumentParser()
parser.add_argument('--ip', type=str, default='127.0.0.1',
parser.add_argument('--ip', type=str, default=LOCALHOST,
help='set the kernel\'s IP address [default: local]')
parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
help='set the XREP channel port [default: random]')
Expand Down
3 changes: 2 additions & 1 deletion IPython/zmq/frontend.py
Expand Up @@ -17,6 +17,7 @@
import zmq
import session
import completer
from IPython.utils.localinterfaces import LOCALHOST

#-----------------------------------------------------------------------------
# Classes and functions
Expand Down Expand Up @@ -168,7 +169,7 @@ def interact(self):
def main():
# Defaults
#ip = '192.168.2.109'
ip = '127.0.0.1'
ip = LOCALHOST
#ip = '99.146.222.252'
port_base = 5575
connection = ('tcp://%s' % ip) + ':%i'
Expand Down
4 changes: 3 additions & 1 deletion IPython/zmq/heartbeat.py
Expand Up @@ -17,6 +17,8 @@

import zmq

from IPython.utils.localinterfaces import LOCALHOST

#-----------------------------------------------------------------------------
# Code
#-----------------------------------------------------------------------------
Expand All @@ -25,7 +27,7 @@
class Heartbeat(Thread):
"A simple ping-pong style heartbeat that runs in a thread."

def __init__(self, context, addr=('127.0.0.1', 0)):
def __init__(self, context, addr=(LOCALHOST, 0)):
Thread.__init__(self)
self.context = context
self.addr = addr
Expand Down
9 changes: 8 additions & 1 deletion IPython/zmq/ipkernel.py
Expand Up @@ -534,12 +534,15 @@ def start(self):
# Kernel main and launch functions
#-----------------------------------------------------------------------------

def launch_kernel(xrep_port=0, pub_port=0, req_port=0, hb_port=0,
def launch_kernel(ip=None, xrep_port=0, pub_port=0, req_port=0, hb_port=0,
independent=False, pylab=False):
"""Launches a localhost kernel, binding to the specified ports.
Parameters
----------
ip : str, optional
The ip address the kernel will bind to.
xrep_port : int, optional
The port to use for XREP channel.
Expand Down Expand Up @@ -574,6 +577,10 @@ def launch_kernel(xrep_port=0, pub_port=0, req_port=0, hb_port=0,
extra_arguments.append('--pylab')
if isinstance(pylab, basestring):
extra_arguments.append(pylab)
if ip is not None:
extra_arguments.append('--ip')
if isinstance(ip, basestring):
extra_arguments.append(ip)
return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
xrep_port, pub_port, req_port, hb_port,
independent, extra_arguments)
Expand Down
25 changes: 13 additions & 12 deletions IPython/zmq/kernelmanager.py
Expand Up @@ -31,15 +31,14 @@

# Local imports.
from IPython.utils import io
from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
from session import Session

#-----------------------------------------------------------------------------
# Constants and exceptions
#-----------------------------------------------------------------------------

LOCALHOST = '127.0.0.1'

class InvalidPortNumber(Exception):
pass

Expand Down Expand Up @@ -724,24 +723,26 @@ def start_kernel(self, **kw):
"""
xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
self.rep_address, self.hb_address
if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
rep[0] != LOCALHOST or hb[0] != LOCALHOST:
raise RuntimeError("Can only launch a kernel on localhost."
if xreq[0] not in LOCAL_IPS or sub[0] not in LOCAL_IPS or \
rep[0] not in LOCAL_IPS or hb[0] not in LOCAL_IPS:
raise RuntimeError("Can only launch a kernel on a local interface. "
"Make sure that the '*_address' attributes are "
"configured properly.")

"configured properly. "
"Currently valid addresses are: %s"%LOCAL_IPS
)

self._launch_args = kw.copy()
if kw.pop('ipython', True):
from ipkernel import launch_kernel
else:
from pykernel import launch_kernel
self.kernel, xrep, pub, req, hb = launch_kernel(
self.kernel, xrep, pub, req, _hb = launch_kernel(
xrep_port=xreq[1], pub_port=sub[1],
req_port=rep[1], hb_port=hb[1], **kw)
self.xreq_address = (LOCALHOST, xrep)
self.sub_address = (LOCALHOST, pub)
self.rep_address = (LOCALHOST, req)
self.hb_address = (LOCALHOST, hb)
self.xreq_address = (xreq[0], xrep)
self.sub_address = (sub[0], pub)
self.rep_address = (rep[0], req)
self.hb_address = (hb[0], _hb)

def shutdown_kernel(self, restart=False):
""" Attempts to the stop the kernel process cleanly. If the kernel
Expand Down

0 comments on commit dc30bee

Please sign in to comment.