consoleapp.py
352 lines
| 13.4 KiB
| text/x-python
|
PythonLexer
Paul Ivanov
|
r5603 | """ A minimal application base mixin for all ZMQ based IPython frontends. | ||
This is not a complete console app, as subprocess will not be able to receive | ||||
input, there is no real readline support, among other limitations. This is a | ||||
refactoring of what used to be the IPython/frontend/qt/console/qtconsoleapp.py | ||||
Authors: | ||||
* Evan Patterson | ||||
* Min RK | ||||
* Erik Tollerud | ||||
* Fernando Perez | ||||
* Bussonnier Matthias | ||||
* Thomas Kluyver | ||||
* Paul Ivanov | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
# stdlib imports | ||||
MinRK
|
r5609 | import atexit | ||
Paul Ivanov
|
r5603 | import json | ||
import os | ||||
import signal | ||||
import sys | ||||
import uuid | ||||
# Local imports | ||||
from IPython.config.application import boolean_flag | ||||
from IPython.config.configurable import Configurable | ||||
from IPython.core.profiledir import ProfileDir | ||||
MinRK
|
r5620 | from IPython.lib.kernel import tunnel_to_kernel, find_connection_file, swallow_argv | ||
Paul Ivanov
|
r5603 | from IPython.zmq.blockingkernelmanager import BlockingKernelManager | ||
from IPython.utils.path import filefind | ||||
from IPython.utils.py3compat import str_to_bytes | ||||
from IPython.utils.traitlets import ( | ||||
Dict, List, Unicode, CUnicode, Int, CBool, Any | ||||
) | ||||
from IPython.zmq.ipkernel import ( | ||||
flags as ipkernel_flags, | ||||
aliases as ipkernel_aliases, | ||||
IPKernelApp | ||||
) | ||||
from IPython.zmq.session import Session, default_secure | ||||
from IPython.zmq.zmqshell import ZMQInteractiveShell | ||||
#----------------------------------------------------------------------------- | ||||
# Network Constants | ||||
#----------------------------------------------------------------------------- | ||||
from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS | ||||
#----------------------------------------------------------------------------- | ||||
# Globals | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Aliases and Flags | ||||
#----------------------------------------------------------------------------- | ||||
flags = dict(ipkernel_flags) | ||||
Paul Ivanov
|
r5604 | |||
# the flags that are specific to the frontend | ||||
# these must be scrubbed before being passed to the kernel, | ||||
# or it will raise an error on unrecognized flags | ||||
Paul Ivanov
|
r5603 | app_flags = { | ||
MinRK
|
r5618 | 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}}, | ||
Paul Ivanov
|
r5603 | "Connect to an existing kernel. If no argument specified, guess most recent"), | ||
} | ||||
app_flags.update(boolean_flag( | ||||
MinRK
|
r5618 | 'confirm-exit', 'IPythonConsoleApp.confirm_exit', | ||
Paul Ivanov
|
r5603 | """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit', | ||
to force a direct exit without any confirmation. | ||||
""", | ||||
"""Don't prompt the user when exiting. This will terminate the kernel | ||||
if it is owned by the frontend, and leave it alive if it is external. | ||||
""" | ||||
)) | ||||
flags.update(app_flags) | ||||
aliases = dict(ipkernel_aliases) | ||||
Paul Ivanov
|
r5604 | # also scrub aliases from the frontend | ||
Paul Ivanov
|
r5603 | app_aliases = dict( | ||
MinRK
|
r5618 | hb = 'IPythonConsoleApp.hb_port', | ||
shell = 'IPythonConsoleApp.shell_port', | ||||
iopub = 'IPythonConsoleApp.iopub_port', | ||||
stdin = 'IPythonConsoleApp.stdin_port', | ||||
ip = 'IPythonConsoleApp.ip', | ||||
existing = 'IPythonConsoleApp.existing', | ||||
f = 'IPythonConsoleApp.connection_file', | ||||
Paul Ivanov
|
r5603 | |||
MinRK
|
r5618 | ssh = 'IPythonConsoleApp.sshserver', | ||
Paul Ivanov
|
r5603 | ) | ||
aliases.update(app_aliases) | ||||
#----------------------------------------------------------------------------- | ||||
# Classes | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r5618 | # IPythonConsole | ||
Paul Ivanov
|
r5603 | #----------------------------------------------------------------------------- | ||
MinRK
|
r5618 | class IPythonConsoleApp(Configurable): | ||
Paul Ivanov
|
r5603 | name = 'ipython-console-mixin' | ||
default_config_file_name='ipython_config.py' | ||||
description = """ | ||||
The IPython Mixin Console. | ||||
This class contains the common portions of console client (QtConsole, | ||||
ZMQ-based terminal console, etc). It is not a full console, in that | ||||
launched terminal subprocesses will not be able to accept input. | ||||
The Console using this mixing supports various extra features beyond | ||||
the single-process Terminal IPython shell, such as connecting to | ||||
existing kernel, via: | ||||
ipython <appname> --existing | ||||
as well as tunnel via SSH | ||||
""" | ||||
classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session] | ||||
flags = Dict(flags) | ||||
aliases = Dict(aliases) | ||||
kernel_manager_class = BlockingKernelManager | ||||
kernel_argv = List(Unicode) | ||||
MinRK
|
r5620 | # frontend flags&aliases to be stripped when building kernel_argv | ||
frontend_flags = Any(app_flags) | ||||
frontend_aliases = Any(app_aliases) | ||||
Paul Ivanov
|
r5603 | |||
Paul Ivanov
|
r5604 | pure = CBool(False, config=True, | ||
help="Use a pure Python kernel instead of an IPython kernel.") | ||||
Paul Ivanov
|
r5603 | # create requested profiles by default, if they don't exist: | ||
auto_create = CBool(True) | ||||
# connection info: | ||||
ip = Unicode(LOCALHOST, config=True, | ||||
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!""" | ||||
) | ||||
sshserver = Unicode('', config=True, | ||||
help="""The SSH server to use to connect to the kernel.""") | ||||
sshkey = Unicode('', config=True, | ||||
help="""Path to the ssh key to use for logging in to the ssh server.""") | ||||
hb_port = Int(0, config=True, | ||||
help="set the heartbeat port [default: random]") | ||||
shell_port = Int(0, config=True, | ||||
help="set the shell (XREP) port [default: random]") | ||||
iopub_port = Int(0, config=True, | ||||
help="set the iopub (PUB) port [default: random]") | ||||
stdin_port = Int(0, config=True, | ||||
help="set the stdin (XREQ) port [default: random]") | ||||
connection_file = Unicode('', config=True, | ||||
help="""JSON file in which to store connection info [default: kernel-<pid>.json] | ||||
This file will contain the IP, ports, and authentication key needed to connect | ||||
clients to this kernel. By default, this file will be created in the security-dir | ||||
of the current profile, but can be specified by absolute path. | ||||
""") | ||||
def _connection_file_default(self): | ||||
return 'kernel-%i.json' % os.getpid() | ||||
existing = CUnicode('', config=True, | ||||
help="""Connect to an already running kernel""") | ||||
confirm_exit = CBool(True, config=True, | ||||
help=""" | ||||
Set to display confirmation dialog on exit. You can always use 'exit' or 'quit', | ||||
to force a direct exit without any confirmation.""", | ||||
) | ||||
Paul Ivanov
|
r5604 | |||
Paul Ivanov
|
r5603 | |||
MinRK
|
r5620 | def build_kernel_argv(self, argv=None): | ||
"""build argv to be passed to kernel subprocess""" | ||||
Paul Ivanov
|
r5603 | if argv is None: | ||
argv = sys.argv[1:] | ||||
MinRK
|
r5620 | self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags) | ||
Paul Ivanov
|
r5603 | # kernel should inherit default config file from frontend | ||
self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name) | ||||
def init_connection_file(self): | ||||
"""find the connection file, and load the info if found. | ||||
The current working directory and the current profile's security | ||||
directory will be searched for the file if it is not given by | ||||
absolute path. | ||||
When attempting to connect to an existing kernel and the `--existing` | ||||
argument does not match an existing file, it will be interpreted as a | ||||
fileglob, and the matching file in the current profile's security dir | ||||
with the latest access time will be used. | ||||
MinRK
|
r5609 | |||
After this method is called, self.connection_file contains the *full path* | ||||
to the connection file, never just its name. | ||||
Paul Ivanov
|
r5603 | """ | ||
if self.existing: | ||||
try: | ||||
cf = find_connection_file(self.existing) | ||||
except Exception: | ||||
self.log.critical("Could not find existing kernel connection file %s", self.existing) | ||||
self.exit(1) | ||||
self.log.info("Connecting to existing kernel: %s" % cf) | ||||
self.connection_file = cf | ||||
MinRK
|
r5609 | else: | ||
# not existing, check if we are going to write the file | ||||
# and ensure that self.connection_file is a full path, not just the shortname | ||||
try: | ||||
cf = find_connection_file(self.connection_file) | ||||
except Exception: | ||||
# file might not exist | ||||
if self.connection_file == os.path.basename(self.connection_file): | ||||
# just shortname, put it in security dir | ||||
cf = os.path.join(self.profile_dir.security_dir, self.connection_file) | ||||
else: | ||||
cf = self.connection_file | ||||
self.connection_file = cf | ||||
Paul Ivanov
|
r5603 | # should load_connection_file only be used for existing? | ||
# as it is now, this allows reusing ports if an existing | ||||
# file is requested | ||||
try: | ||||
self.load_connection_file() | ||||
except Exception: | ||||
self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True) | ||||
self.exit(1) | ||||
def load_connection_file(self): | ||||
"""load ip/port/hmac config from JSON connection file""" | ||||
# this is identical to KernelApp.load_connection_file | ||||
# perhaps it can be centralized somewhere? | ||||
try: | ||||
fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir]) | ||||
except IOError: | ||||
self.log.debug("Connection File not found: %s", self.connection_file) | ||||
return | ||||
self.log.debug(u"Loading connection file %s", fname) | ||||
with open(fname) as f: | ||||
s = f.read() | ||||
cfg = json.loads(s) | ||||
if self.ip == LOCALHOST and 'ip' in cfg: | ||||
# not overridden by config or cl_args | ||||
self.ip = cfg['ip'] | ||||
for channel in ('hb', 'shell', 'iopub', 'stdin'): | ||||
name = channel + '_port' | ||||
if getattr(self, name) == 0 and name in cfg: | ||||
# not overridden by config or cl_args | ||||
setattr(self, name, cfg[name]) | ||||
if 'key' in cfg: | ||||
self.config.Session.key = str_to_bytes(cfg['key']) | ||||
def init_ssh(self): | ||||
"""set up ssh tunnels, if needed.""" | ||||
if not self.sshserver and not self.sshkey: | ||||
return | ||||
if self.sshkey and not self.sshserver: | ||||
# specifying just the key implies that we are connecting directly | ||||
self.sshserver = self.ip | ||||
self.ip = LOCALHOST | ||||
# build connection dict for tunnels: | ||||
info = dict(ip=self.ip, | ||||
shell_port=self.shell_port, | ||||
iopub_port=self.iopub_port, | ||||
stdin_port=self.stdin_port, | ||||
hb_port=self.hb_port | ||||
) | ||||
self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver)) | ||||
# tunnels return a new set of ports, which will be on localhost: | ||||
self.ip = LOCALHOST | ||||
try: | ||||
newports = tunnel_to_kernel(info, self.sshserver, self.sshkey) | ||||
except: | ||||
# even catch KeyboardInterrupt | ||||
self.log.error("Could not setup tunnels", exc_info=True) | ||||
self.exit(1) | ||||
self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports | ||||
cf = self.connection_file | ||||
base,ext = os.path.splitext(cf) | ||||
base = os.path.basename(base) | ||||
self.connection_file = os.path.basename(base)+'-ssh'+ext | ||||
self.log.critical("To connect another client via this tunnel, use:") | ||||
self.log.critical("--existing %s" % self.connection_file) | ||||
def _new_connection_file(self): | ||||
MinRK
|
r5609 | cf = '' | ||
while not cf: | ||||
# we don't need a 128b id to distinguish kernels, use more readable | ||||
# 48b node segment (12 hex chars). Users running more than 32k simultaneous | ||||
# kernels can subclass. | ||||
ident = str(uuid.uuid4()).split('-')[-1] | ||||
cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident) | ||||
# only keep if it's actually new. Protect against unlikely collision | ||||
# in 48b random search space | ||||
cf = cf if not os.path.exists(cf) else '' | ||||
return cf | ||||
Paul Ivanov
|
r5603 | |||
def init_kernel_manager(self): | ||||
# Don't let Qt or ZMQ swallow KeyboardInterupts. | ||||
signal.signal(signal.SIGINT, signal.SIG_DFL) | ||||
# Create a KernelManager and start a kernel. | ||||
self.kernel_manager = self.kernel_manager_class( | ||||
ip=self.ip, | ||||
shell_port=self.shell_port, | ||||
iopub_port=self.iopub_port, | ||||
stdin_port=self.stdin_port, | ||||
hb_port=self.hb_port, | ||||
MinRK
|
r5609 | connection_file=self.connection_file, | ||
Paul Ivanov
|
r5603 | config=self.config, | ||
) | ||||
# start the kernel | ||||
if not self.existing: | ||||
kwargs = dict(ipython=not self.pure) | ||||
kwargs['extra_arguments'] = self.kernel_argv | ||||
self.kernel_manager.start_kernel(**kwargs) | ||||
elif self.sshserver: | ||||
# ssh, write new connection file | ||||
self.kernel_manager.write_connection_file() | ||||
MinRK
|
r5609 | atexit.register(self.kernel_manager.cleanup_connection_file) | ||
Paul Ivanov
|
r5603 | self.kernel_manager.start_channels() | ||
def initialize(self, argv=None): | ||||
Paul Ivanov
|
r5604 | """ | ||
Classes which mix this class in should call: | ||||
MinRK
|
r5618 | IPythonConsoleApp.initialize(self,argv) | ||
Paul Ivanov
|
r5604 | """ | ||
Paul Ivanov
|
r5603 | self.init_connection_file() | ||
default_secure(self.config) | ||||
self.init_ssh() | ||||
self.init_kernel_manager() | ||||