handlers.py
288 lines
| 10.6 KiB
| text/x-python
|
PythonLexer
|
r16697 | """Tornado handlers for kernels.""" | ||
|
r10641 | |||
|
r16697 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r10641 | |||
|
r17021 | import json | ||
|
r10641 | import logging | ||
|
r18497 | from tornado import gen, web | ||
from tornado.concurrent import Future | ||||
|
r18522 | from tornado.ioloop import IOLoop | ||
|
r10641 | |||
from IPython.utils.jsonutil import date_default | ||||
|
r18497 | from IPython.utils.py3compat import cast_unicode | ||
|
r13132 | from IPython.html.utils import url_path_join, url_escape | ||
|
r10641 | |||
|
r21469 | from ...base.handlers import IPythonHandler, APIHandler, json_errors | ||
|
r18332 | from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message | ||
|
r10641 | |||
|
r16697 | from IPython.core.release import kernel_protocol_version | ||
|
r10641 | |||
|
r21469 | class MainKernelHandler(APIHandler): | ||
|
r10641 | |||
@web.authenticated | ||||
|
r13052 | @json_errors | ||
|
r10641 | def get(self): | ||
km = self.kernel_manager | ||||
|
r17021 | self.finish(json.dumps(km.list_kernels())) | ||
|
r10641 | |||
@web.authenticated | ||||
|
r13052 | @json_errors | ||
|
r10641 | def post(self): | ||
|
r18308 | km = self.kernel_manager | ||
|
r17221 | model = self.get_json_body() | ||
if model is None: | ||||
|
r18308 | model = { | ||
'name': km.default_kernel_name | ||||
} | ||||
else: | ||||
model.setdefault('name', km.default_kernel_name) | ||||
|
r17221 | |||
|
r18308 | kernel_id = km.start_kernel(kernel_name=model['name']) | ||
|
r15400 | model = km.kernel_model(kernel_id) | ||
|
r15310 | location = url_path_join(self.base_url, 'api', 'kernels', kernel_id) | ||
|
r13132 | self.set_header('Location', url_escape(location)) | ||
|
r13058 | self.set_status(201) | ||
|
r17021 | self.finish(json.dumps(model)) | ||
|
r10641 | |||
|
r21469 | class KernelHandler(APIHandler): | ||
|
r10641 | |||
|
r12983 | SUPPORTED_METHODS = ('DELETE', 'GET') | ||
@web.authenticated | ||||
|
r13052 | @json_errors | ||
|
r12983 | def get(self, kernel_id): | ||
km = self.kernel_manager | ||||
|
r13058 | km._check_kernel_id(kernel_id) | ||
|
r15400 | model = km.kernel_model(kernel_id) | ||
|
r17021 | self.finish(json.dumps(model)) | ||
|
r10641 | |||
@web.authenticated | ||||
|
r13052 | @json_errors | ||
|
r10641 | def delete(self, kernel_id): | ||
km = self.kernel_manager | ||||
km.shutdown_kernel(kernel_id) | ||||
self.set_status(204) | ||||
self.finish() | ||||
|
r21469 | class KernelActionHandler(APIHandler): | ||
|
r10641 | |||
@web.authenticated | ||||
|
r13052 | @json_errors | ||
|
r10641 | def post(self, kernel_id, action): | ||
km = self.kernel_manager | ||||
if action == 'interrupt': | ||||
km.interrupt_kernel(kernel_id) | ||||
self.set_status(204) | ||||
if action == 'restart': | ||||
km.restart_kernel(kernel_id) | ||||
|
r15400 | model = km.kernel_model(kernel_id) | ||
|
r15310 | self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id)) | ||
|
r17021 | self.write(json.dumps(model)) | ||
|
r10641 | self.finish() | ||
|
r19824 | class ZMQChannelsHandler(AuthenticatedZMQStreamHandler): | ||
|
r10641 | |||
|
r18522 | @property | ||
def kernel_info_timeout(self): | ||||
return self.settings.get('kernel_info_timeout', 10) | ||||
|
r17647 | def __repr__(self): | ||
return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized')) | ||||
|
r10641 | def create_stream(self): | ||
km = self.kernel_manager | ||||
|
r19824 | identity = self.session.bsession | ||
for channel in ('shell', 'iopub', 'stdin'): | ||||
meth = getattr(km, 'connect_' + channel) | ||||
self.channels[channel] = stream = meth(self.kernel_id, identity=identity) | ||||
stream.channel = channel | ||||
km.add_restart_callback(self.kernel_id, self.on_kernel_restarted) | ||||
km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead') | ||||
|
r16697 | |||
|
r18497 | def request_kernel_info(self): | ||
|
r16698 | """send a request for kernel_info""" | ||
|
r18497 | km = self.kernel_manager | ||
kernel = km.get_kernel(self.kernel_id) | ||||
try: | ||||
|
r18563 | # check for previous request | ||
future = kernel._kernel_info_future | ||||
|
r18497 | except AttributeError: | ||
self.log.debug("Requesting kernel info from %s", self.kernel_id) | ||||
# Create a kernel_info channel to query the kernel protocol version. | ||||
# This channel will be closed after the kernel_info reply is received. | ||||
if self.kernel_info_channel is None: | ||||
self.kernel_info_channel = km.connect_shell(self.kernel_id) | ||||
self.kernel_info_channel.on_recv(self._handle_kernel_info_reply) | ||||
self.session.send(self.kernel_info_channel, "kernel_info_request") | ||||
|
r18563 | # store the future on the kernel, so only one request is sent | ||
kernel._kernel_info_future = self._kernel_info_future | ||||
|
r18497 | else: | ||
|
r18563 | if not future.done(): | ||
self.log.debug("Waiting for pending kernel_info request") | ||||
future.add_done_callback(lambda f: self._finish_kernel_info(f.result())) | ||||
|
r18497 | return self._kernel_info_future | ||
|
r16697 | |||
|
r16698 | def _handle_kernel_info_reply(self, msg): | ||
"""process the kernel_info_reply | ||||
enabling msg spec adaptation, if necessary | ||||
""" | ||||
|
r16697 | idents,msg = self.session.feed_identities(msg) | ||
try: | ||||
|
r18330 | msg = self.session.deserialize(msg) | ||
|
r16697 | except: | ||
self.log.error("Bad kernel_info reply", exc_info=True) | ||||
|
r18563 | self._kernel_info_future.set_result({}) | ||
|
r16697 | return | ||
else: | ||||
|
r18497 | info = msg['content'] | ||
self.log.debug("Received kernel info: %s", info) | ||||
if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info: | ||||
self.log.error("Kernel info request failed, assuming current %s", info) | ||||
|
r18563 | info = {} | ||
|
r18497 | self._finish_kernel_info(info) | ||
# close the kernel_info channel, we don't need it anymore | ||||
if self.kernel_info_channel: | ||||
self.kernel_info_channel.close() | ||||
|
r16697 | self.kernel_info_channel = None | ||
|
r18497 | def _finish_kernel_info(self, info): | ||
"""Finish handling kernel_info reply | ||||
Set up protocol adaptation, if needed, | ||||
and signal that connection can continue. | ||||
""" | ||||
protocol_version = info.get('protocol_version', kernel_protocol_version) | ||||
if protocol_version != kernel_protocol_version: | ||||
self.session.adapt_version = int(protocol_version.split('.')[0]) | ||||
|
r20573 | self.log.info("Adapting to protocol v%s for kernel %s", protocol_version, self.kernel_id) | ||
|
r18522 | if not self._kernel_info_future.done(): | ||
self._kernel_info_future.set_result(info) | ||||
|
r18497 | |||
|
r18277 | def initialize(self): | ||
|
r19824 | super(ZMQChannelsHandler, self).initialize() | ||
|
r10641 | self.zmq_stream = None | ||
|
r19824 | self.channels = {} | ||
|
r18498 | self.kernel_id = None | ||
|
r18497 | self.kernel_info_channel = None | ||
self._kernel_info_future = Future() | ||||
@gen.coroutine | ||||
|
r18522 | def pre_get(self): | ||
# authenticate first | ||||
|
r19824 | super(ZMQChannelsHandler, self).pre_get() | ||
|
r18522 | # then request kernel info, waiting up to a certain time before giving up. | ||
# We don't want to wait forever, because browsers don't take it well when | ||||
# servers never respond to websocket connection requests. | ||||
|
r20571 | kernel = self.kernel_manager.get_kernel(self.kernel_id) | ||
self.session.key = kernel.session.key | ||||
|
r18522 | future = self.request_kernel_info() | ||
def give_up(): | ||||
"""Don't wait forever for the kernel to reply""" | ||||
if future.done(): | ||||
return | ||||
self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id) | ||||
|
r18563 | future.set_result({}) | ||
|
r18522 | loop = IOLoop.current() | ||
loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up) | ||||
# actually wait for it | ||||
yield future | ||||
@gen.coroutine | ||||
|
r18497 | def get(self, kernel_id): | ||
self.kernel_id = cast_unicode(kernel_id, 'ascii') | ||||
|
r19824 | yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id) | ||
|
r10641 | |||
|
r18277 | def open(self, kernel_id): | ||
|
r19824 | super(ZMQChannelsHandler, self).open() | ||
|
r10641 | try: | ||
self.create_stream() | ||||
|
r18498 | except web.HTTPError as e: | ||
self.log.error("Error opening stream: %s", e) | ||||
|
r10641 | # WebSockets don't response to traditional error codes so we | ||
# close the connection. | ||||
|
r19824 | for channel, stream in self.channels.items(): | ||
if not stream.closed(): | ||||
stream.close() | ||||
|
r10641 | self.close() | ||
else: | ||||
|
r19824 | for channel, stream in self.channels.items(): | ||
stream.on_recv_stream(self._on_zmq_reply) | ||||
|
r10641 | |||
def on_message(self, msg): | ||||
|
r20183 | if not self.channels: | ||
# already closed, ignore the message | ||||
self.log.debug("Received message on closed websocket %r", msg) | ||||
return | ||||
|
r18332 | if isinstance(msg, bytes): | ||
msg = deserialize_binary_message(msg) | ||||
else: | ||||
msg = json.loads(msg) | ||||
|
r19824 | channel = msg.pop('channel', None) | ||
if channel is None: | ||||
self.log.warn("No channel specified, assuming shell: %s", msg) | ||||
channel = 'shell' | ||||
|
r20183 | if channel not in self.channels: | ||
self.log.warn("No such channel: %r", channel) | ||||
return | ||||
|
r19824 | stream = self.channels[channel] | ||
self.session.send(stream, msg) | ||||
|
r10641 | |||
def on_close(self): | ||||
km = self.kernel_manager | ||||
if self.kernel_id in km: | ||||
km.remove_restart_callback( | ||||
self.kernel_id, self.on_kernel_restarted, | ||||
) | ||||
km.remove_restart_callback( | ||||
self.kernel_id, self.on_restart_failed, 'dead', | ||||
) | ||||
|
r19824 | # This method can be called twice, once by self.kernel_died and once | ||
# from the WebSocket close event. If the WebSocket connection is | ||||
# closed before the ZMQ streams are setup, they could be None. | ||||
for channel, stream in self.channels.items(): | ||||
if stream is not None and not stream.closed(): | ||||
stream.on_recv(None) | ||||
# close the socket directly, don't wait for the stream | ||||
socket = stream.socket | ||||
stream.close() | ||||
socket.close() | ||||
self.channels = {} | ||||
|
r10641 | def _send_status_message(self, status): | ||
msg = self.session.msg("status", | ||||
{'execution_state': status} | ||||
) | ||||
|
r19824 | msg['channel'] = 'iopub' | ||
|
r17021 | self.write_message(json.dumps(msg, default=date_default)) | ||
|
r10641 | |||
def on_kernel_restarted(self): | ||||
logging.warn("kernel %s restarted", self.kernel_id) | ||||
self._send_status_message('restarting') | ||||
def on_restart_failed(self): | ||||
logging.error("kernel %s restarted failed!", self.kernel_id) | ||||
self._send_status_message('dead') | ||||
|
r10647 | |||
#----------------------------------------------------------------------------- | ||||
# URL to handler mappings | ||||
#----------------------------------------------------------------------------- | ||||
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)" | ||||
_kernel_action_regex = r"(?P<action>restart|interrupt)" | ||||
default_handlers = [ | ||||
|
r12983 | (r"/api/kernels", MainKernelHandler), | ||
(r"/api/kernels/%s" % _kernel_id_regex, KernelHandler), | ||||
(r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler), | ||||
|
r19824 | (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler), | ||
|
r10647 | ] | ||