handlers.py
907 lines
| 31.8 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r4609 | """Tornado handlers for the notebook. | ||
Authors: | ||||
* Brian Granger | ||||
""" | ||||
Brian E. Granger
|
r4346 | |||
#----------------------------------------------------------------------------- | ||||
Brian E. Granger
|
r4609 | # Copyright (C) 2008-2011 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. | ||||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4609 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r4297 | |||
MinRK
|
r4707 | import Cookie | ||
MinRK
|
r7922 | import datetime | ||
import email.utils | ||||
import hashlib | ||||
import logging | ||||
import mimetypes | ||||
import os | ||||
import stat | ||||
MinRK
|
r8016 | import threading | ||
MinRK
|
r5812 | import time | ||
MinRK
|
r5101 | import uuid | ||
Cameron Bates
|
r8350 | import os | ||
MinRK
|
r4707 | |||
Cameron Bates
|
r8907 | from tornado.escape import url_escape | ||
Brian Granger
|
r4297 | from tornado import web | ||
from tornado import websocket | ||||
Brian E. Granger
|
r4545 | from zmq.eventloop import ioloop | ||
from zmq.utils import jsonapi | ||||
MinRK
|
r5191 | from IPython.external.decorator import decorator | ||
Brian E. Granger
|
r4545 | from IPython.zmq.session import Session | ||
Stefan van der Walt
|
r5321 | from IPython.lib.security import passwd_check | ||
MinRK
|
r6870 | from IPython.utils.jsonutil import date_default | ||
MinRK
|
r7922 | from IPython.utils.path import filefind | ||
Brian E. Granger
|
r4545 | |||
Brian E. Granger
|
r4507 | try: | ||
from docutils.core import publish_string | ||||
except ImportError: | ||||
publish_string = None | ||||
Fernando Perez
|
r5240 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r5241 | # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! | ||
Fernando Perez
|
r5240 | #----------------------------------------------------------------------------- | ||
# Google Chrome, as of release 16, changed its websocket protocol number. The | ||||
# parts tornado cares about haven't really changed, so it's OK to continue | ||||
# accepting Chrome connections, but as of Tornado 2.1.1 (the currently released | ||||
# version as of Oct 30/2011) the version check fails, see the issue report: | ||||
# https://github.com/facebook/tornado/issues/385 | ||||
# This issue has been fixed in Tornado post 2.1.1: | ||||
# https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710 | ||||
# Here we manually apply the same patch as above so that users of IPython can | ||||
# continue to work with an officially released Tornado. We make the | ||||
# monkeypatch version check as narrow as possible to limit its effects; once | ||||
# Tornado 2.1.1 is no longer found in the wild we'll delete this code. | ||||
import tornado | ||||
Fernando Perez
|
r5241 | if tornado.version_info <= (2,1,1): | ||
Fernando Perez
|
r5240 | |||
def _execute(self, transforms, *args, **kwargs): | ||||
from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76 | ||||
self.open_args = args | ||||
self.open_kwargs = kwargs | ||||
# The difference between version 8 and 13 is that in 8 the | ||||
# client sends a "Sec-Websocket-Origin" header and in 13 it's | ||||
# simply "Origin". | ||||
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"): | ||||
self.ws_connection = WebSocketProtocol8(self) | ||||
self.ws_connection.accept_connection() | ||||
elif self.request.headers.get("Sec-WebSocket-Version"): | ||||
self.stream.write(tornado.escape.utf8( | ||||
"HTTP/1.1 426 Upgrade Required\r\n" | ||||
"Sec-WebSocket-Version: 8\r\n\r\n")) | ||||
self.stream.close() | ||||
else: | ||||
self.ws_connection = WebSocketProtocol76(self) | ||||
self.ws_connection.accept_connection() | ||||
Brian E. Granger
|
r4484 | |||
Fernando Perez
|
r5240 | websocket.WebSocketHandler._execute = _execute | ||
del _execute | ||||
MinRK
|
r5191 | #----------------------------------------------------------------------------- | ||
# Decorator for disabling read-only handlers | ||||
#----------------------------------------------------------------------------- | ||||
@decorator | ||||
def not_if_readonly(f, self, *args, **kwargs): | ||||
MinRK
|
r5213 | if self.application.read_only: | ||
MinRK
|
r5191 | raise web.HTTPError(403, "Notebook server is read-only") | ||
else: | ||||
return f(self, *args, **kwargs) | ||||
Brian E. Granger
|
r4545 | |||
MinRK
|
r5200 | @decorator | ||
def authenticate_unless_readonly(f, self, *args, **kwargs): | ||||
"""authenticate this page *unless* readonly view is active. | ||||
In read-only mode, the notebook list and print view should | ||||
be accessible without authentication. | ||||
""" | ||||
@web.authenticated | ||||
def auth_f(self, *args, **kwargs): | ||||
return f(self, *args, **kwargs) | ||||
MinRK
|
r5824 | |||
MinRK
|
r5213 | if self.application.read_only: | ||
MinRK
|
r5200 | return f(self, *args, **kwargs) | ||
else: | ||||
return auth_f(self, *args, **kwargs) | ||||
Matthias BUSSONNIER
|
r8097 | def urljoin(*pieces): | ||
"""Join componenet of url into a relative url | ||||
Use to prevent double slash when joining subpath | ||||
""" | ||||
striped = [s.strip('/') for s in pieces] | ||||
return '/'.join(s for s in striped if s) | ||||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian E. Granger
|
r4494 | # Top-level handlers | ||
Brian E. Granger
|
r4346 | #----------------------------------------------------------------------------- | ||
Brian Granger
|
r4292 | |||
Stefan van der Walt
|
r5324 | class RequestHandler(web.RequestHandler): | ||
"""RequestHandler with default variable setting.""" | ||||
def render(*args, **kwargs): | ||||
kwargs.setdefault('message', '') | ||||
return web.RequestHandler.render(*args, **kwargs) | ||||
class AuthenticatedHandler(RequestHandler): | ||||
MinRK
|
r4706 | """A RequestHandler with an authenticated user.""" | ||
Brian E. Granger
|
r5102 | |||
Satrajit Ghosh
|
r4690 | def get_current_user(self): | ||
Bradley M. Froehle
|
r8340 | user_id = self.get_secure_cookie(self.settings['cookie_name']) | ||
Brian E. Granger
|
r5102 | # For now the user_id should not return empty, but it could eventually | ||
MinRK
|
r4724 | if user_id == '': | ||
user_id = 'anonymous' | ||||
if user_id is None: | ||||
# prevent extra Invalid cookie sig warnings: | ||||
Bradley M. Froehle
|
r8340 | self.clear_cookie(self.settings['cookie_name']) | ||
MinRK
|
r5213 | if not self.application.password and not self.application.read_only: | ||
MinRK
|
r4724 | user_id = 'anonymous' | ||
return user_id | ||||
Stefan van der Walt
|
r5721 | |||
MinRK
|
r5213 | @property | ||
Stefan van der Walt
|
r5722 | def logged_in(self): | ||
"""Is a user currently logged in? | ||||
""" | ||||
user = self.get_current_user() | ||||
return (user and not user == 'anonymous') | ||||
@property | ||||
def login_available(self): | ||||
"""May a user proceed to log in? | ||||
This returns True if login capability is available, irrespective of | ||||
whether the user is already logged in or not. | ||||
""" | ||||
return bool(self.application.password) | ||||
@property | ||||
MinRK
|
r5213 | def read_only(self): | ||
Stefan van der Walt
|
r5721 | """Is the notebook read-only? | ||
""" | ||||
Stefan van der Walt
|
r5722 | return self.application.read_only | ||
Stefan van der Walt
|
r5721 | |||
MinRK
|
r5252 | @property | ||
def ws_url(self): | ||||
"""websocket url matching the current request | ||||
Andrew Straw
|
r6006 | |||
MinRK
|
r5300 | turns http[s]://host[:port] into | ||
ws[s]://host[:port] | ||||
MinRK
|
r5252 | """ | ||
MinRK
|
r5300 | proto = self.request.protocol.replace('http', 'ws') | ||
Andrew Straw
|
r6006 | host = self.application.ipython_app.websocket_host # default to config value | ||
if host == '': | ||||
host = self.request.host # get from request | ||||
return "%s://%s" % (proto, host) | ||||
Cameron Bates
|
r8792 | |||
Satrajit Ghosh
|
r4690 | |||
MinRK
|
r5824 | class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler): | ||
"""static files should only be accessible when logged in""" | ||||
@authenticate_unless_readonly | ||||
def get(self, path): | ||||
return web.StaticFileHandler.get(self, path) | ||||
Brian E. Granger
|
r5112 | class ProjectDashboardHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4488 | def get(self): | ||
nbm = self.application.notebook_manager | ||||
Cameron Bates
|
r8792 | project = nbm.notebook_dir | ||
Cameron Bates
|
r8847 | template = self.application.jinja2_env.get_template('projectdashboard.html') | ||
self.write( template.render(project=project, | ||||
Andrew Straw
|
r6004 | base_project_url=self.application.ipython_app.base_project_url, | ||
base_kernel_url=self.application.ipython_app.base_kernel_url, | ||||
MinRK
|
r5213 | read_only=self.read_only, | ||
Stefan van der Walt
|
r5722 | logged_in=self.logged_in, | ||
Cameron Bates
|
r8350 | login_available=self.login_available)) | ||
Brian E. Granger
|
r4488 | |||
Brian E. Granger
|
r5102 | |||
MinRK
|
r4706 | class LoginHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r5102 | |||
Cameron Bates
|
r8792 | def _render(self, message=None): | ||
Cameron Bates
|
r8847 | template = self.application.jinja2_env.get_template('login.html') | ||
self.write( template.render( | ||||
Cameron Bates
|
r8907 | next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)), | ||
MinRK
|
r5213 | read_only=self.read_only, | ||
Stefan van der Walt
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available, | ||||
Brian Granger
|
r6192 | base_project_url=self.application.ipython_app.base_project_url, | ||
Stefan van der Walt
|
r5323 | message=message | ||
Cameron Bates
|
r8350 | )) | ||
Satrajit Ghosh
|
r4690 | |||
Stefan van der Walt
|
r5323 | def get(self): | ||
Stefan van der Walt
|
r5327 | if self.current_user: | ||
Matthias BUSSONNIER
|
r7797 | self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url)) | ||
Stefan van der Walt
|
r5327 | else: | ||
self._render() | ||||
Stefan van der Walt
|
r5323 | |||
Satrajit Ghosh
|
r4690 | def post(self): | ||
Brian E. Granger
|
r5109 | pwd = self.get_argument('password', default=u'') | ||
Stefan van der Walt
|
r5323 | if self.application.password: | ||
if passwd_check(self.application.password, pwd): | ||||
Bradley M. Froehle
|
r8340 | self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4())) | ||
Stefan van der Walt
|
r5323 | else: | ||
Stefan van der Walt
|
r5326 | self._render(message={'error': 'Invalid password'}) | ||
Stefan van der Walt
|
r5323 | return | ||
Matthias BUSSONNIER
|
r7797 | self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url)) | ||
Brian E. Granger
|
r4488 | |||
Brian E. Granger
|
r5102 | |||
Stefan van der Walt
|
r5325 | class LogoutHandler(AuthenticatedHandler): | ||
def get(self): | ||||
Bradley M. Froehle
|
r8340 | self.clear_cookie(self.settings['cookie_name']) | ||
Stefan van der Walt
|
r5722 | if self.login_available: | ||
message = {'info': 'Successfully logged out.'} | ||||
else: | ||||
message = {'warning': 'Cannot log out. Notebook authentication ' | ||||
'is disabled.'} | ||||
Cameron Bates
|
r8847 | template = self.application.jinja2_env.get_template('logout.html') | ||
self.write( template.render( | ||||
Stefan van der Walt
|
r5722 | read_only=self.read_only, | ||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
Brian Granger
|
r6192 | base_project_url=self.application.ipython_app.base_project_url, | ||
Cameron Bates
|
r8350 | message=message)) | ||
Stefan van der Walt
|
r5325 | |||
MinRK
|
r4706 | class NewHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4292 | def get(self): | ||
Brian E. Granger
|
r5105 | nbm = self.application.notebook_manager | ||
project = nbm.notebook_dir | ||||
notebook_id = nbm.new_notebook() | ||||
Matthias BUSSONNIER
|
r8097 | self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id)) | ||
Brian E. Granger
|
r4484 | |||
MinRK
|
r4706 | class NamedNotebookHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r5102 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
Brian E. Granger
|
r5105 | project = nbm.notebook_dir | ||
Brian E. Granger
|
r4484 | if not nbm.notebook_exists(notebook_id): | ||
Cameron Bates
|
r8792 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Cameron Bates
|
r8847 | template = self.application.jinja2_env.get_template('notebook.html') | ||
self.write( template.render(project=project, | ||||
Brian E. Granger
|
r5105 | notebook_id=notebook_id, | ||
Andrew Straw
|
r6004 | base_project_url=self.application.ipython_app.base_project_url, | ||
base_kernel_url=self.application.ipython_app.base_kernel_url, | ||||
MinRK
|
r5213 | kill_kernel=False, | ||
read_only=self.read_only, | ||||
Stefan van der Walt
|
r5722 | logged_in=self.logged_in, | ||
login_available=self.login_available, | ||||
Cameron Bates
|
r8350 | mathjax_url=self.application.ipython_app.mathjax_url,)) | ||
Brian Granger
|
r4292 | |||
Brian Granger
|
r5875 | class PrintNotebookHandler(AuthenticatedHandler): | ||
@authenticate_unless_readonly | ||||
def get(self, notebook_id): | ||||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
if not nbm.notebook_exists(notebook_id): | ||||
Cameron Bates
|
r8792 | raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id) | ||
Cameron Bates
|
r8847 | template = self.application.jinja2_env.get_template('printnotebook.html') | ||
self.write( template.render( | ||||
Cameron Bates
|
r8350 | project=project, | ||
Brian Granger
|
r5875 | notebook_id=notebook_id, | ||
Andrew Straw
|
r6004 | base_project_url=self.application.ipython_app.base_project_url, | ||
base_kernel_url=self.application.ipython_app.base_kernel_url, | ||||
Brian Granger
|
r5875 | kill_kernel=False, | ||
read_only=self.read_only, | ||||
logged_in=self.logged_in, | ||||
login_available=self.login_available, | ||||
mathjax_url=self.application.ipython_app.mathjax_url, | ||||
Cameron Bates
|
r8350 | )) | ||
Brian Granger
|
r5875 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Kernel handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class MainKernelHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4297 | def get(self): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.finish(jsonapi.dumps(km.kernel_ids)) | ||||
Brian Granger
|
r4298 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4306 | def post(self): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
MinRK
|
r7558 | nbm = self.application.notebook_manager | ||
Brian E. Granger
|
r4494 | notebook_id = self.get_argument('notebook', default=None) | ||
MinRK
|
r7558 | kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir) | ||
MinRK
|
r5252 | data = {'ws_url':self.ws_url,'kernel_id':kernel_id} | ||
Brian E. Granger
|
r4484 | self.set_header('Location', '/'+kernel_id) | ||
Brian E. Granger
|
r4572 | self.finish(jsonapi.dumps(data)) | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4706 | class KernelHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r4494 | |||
SUPPORTED_METHODS = ('DELETE') | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4494 | def delete(self, kernel_id): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
MinRK
|
r7627 | km.shutdown_kernel(kernel_id) | ||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
self.finish() | ||||
MinRK
|
r4706 | class KernelActionHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4308 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian Granger
|
r4309 | def post(self, kernel_id, action): | ||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
Brian Granger
|
r4309 | if action == 'interrupt': | ||
Brian E. Granger
|
r4545 | km.interrupt_kernel(kernel_id) | ||
Brian E. Granger
|
r4494 | self.set_status(204) | ||
Brian Granger
|
r4309 | if action == 'restart': | ||
Brian E. Granger
|
r4545 | new_kernel_id = km.restart_kernel(kernel_id) | ||
MinRK
|
r5252 | data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id} | ||
Brian E. Granger
|
r4572 | self.set_header('Location', '/'+new_kernel_id) | ||
self.write(jsonapi.dumps(data)) | ||||
Brian E. Granger
|
r4493 | self.finish() | ||
Brian Granger
|
r4308 | |||
Brian Granger
|
r4306 | class ZMQStreamHandler(websocket.WebSocketHandler): | ||
Brian Granger
|
r4297 | |||
Brian E. Granger
|
r4545 | def _reserialize_reply(self, msg_list): | ||
"""Reserialize a reply message using JSON. | ||||
This takes the msg list from the ZMQ socket, unserializes it using | ||||
self.session and then serializes the result using JSON. This method | ||||
should be used by self._on_zmq_reply to build messages that can | ||||
be sent back to the browser. | ||||
""" | ||||
idents, msg_list = self.session.feed_identities(msg_list) | ||||
msg = self.session.unserialize(msg_list) | ||||
Brian E. Granger
|
r4561 | try: | ||
msg['header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
try: | ||||
msg['parent_header'].pop('date') | ||||
except KeyError: | ||||
pass | ||||
Brian E. Granger
|
r4545 | msg.pop('buffers') | ||
MinRK
|
r6870 | return jsonapi.dumps(msg, default=date_default) | ||
Brian E. Granger
|
r4545 | |||
Brian E. Granger
|
r4561 | def _on_zmq_reply(self, msg_list): | ||
try: | ||||
msg = self._reserialize_reply(msg_list) | ||||
MinRK
|
r6870 | except Exception: | ||
self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True) | ||||
Brian E. Granger
|
r4561 | else: | ||
self.write_message(msg) | ||||
MinRK
|
r6070 | def allow_draft76(self): | ||
"""Allow draft 76, until browsers such as Safari update to RFC 6455. | ||||
This has been disabled by default in tornado in release 2.2.0, and | ||||
support will be removed in later versions. | ||||
""" | ||||
return True | ||||
Brian E. Granger
|
r5114 | |||
MinRK
|
r4707 | class AuthenticatedZMQStreamHandler(ZMQStreamHandler): | ||
Brian E. Granger
|
r5114 | |||
MinRK
|
r4707 | def open(self, kernel_id): | ||
Thomas Kluyver
|
r4839 | self.kernel_id = kernel_id.decode('ascii') | ||
MinRK
|
r4963 | try: | ||
cfg = self.application.ipython_app.config | ||||
except AttributeError: | ||||
# protect from the case where this is run from something other than | ||||
# the notebook app: | ||||
cfg = None | ||||
self.session = Session(config=cfg) | ||||
MinRK
|
r4707 | self.save_on_message = self.on_message | ||
self.on_message = self.on_first_message | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def get_current_user(self): | ||
Bradley M. Froehle
|
r8340 | user_id = self.get_secure_cookie(self.settings['cookie_name']) | ||
MinRK
|
r4724 | if user_id == '' or (user_id is None and not self.application.password): | ||
user_id = 'anonymous' | ||||
return user_id | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def _inject_cookie_message(self, msg): | ||
"""Inject the first message, which is the document cookie, | ||||
for authentication.""" | ||||
if isinstance(msg, unicode): | ||||
# Cookie can't constructor doesn't accept unicode strings for some reason | ||||
msg = msg.encode('utf8', 'replace') | ||||
try: | ||||
Brian E. Granger
|
r5119 | self.request._cookies = Cookie.SimpleCookie(msg) | ||
MinRK
|
r4707 | except: | ||
logging.warn("couldn't parse cookie string: %s",msg, exc_info=True) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
self._inject_cookie_message(msg) | ||||
if self.get_current_user() is None: | ||||
logging.warn("Couldn't authenticate WebSocket connection") | ||||
raise web.HTTPError(403) | ||||
self.on_message = self.save_on_message | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | |||
class IOPubHandler(AuthenticatedZMQStreamHandler): | ||||
Brian E. Granger
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
self._kernel_alive = True | ||||
self._beating = False | ||||
Brian E. Granger
|
r4572 | self.iopub_stream = None | ||
self.hb_stream = None | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
super(IOPubHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.time_to_dead = km.time_to_dead | ||||
MinRK
|
r5812 | self.first_beat = km.first_beat | ||
MinRK
|
r4707 | kernel_id = self.kernel_id | ||
Brian E. Granger
|
r4572 | try: | ||
self.iopub_stream = km.create_iopub_stream(kernel_id) | ||||
self.hb_stream = km.create_hb_stream(kernel_id) | ||||
except web.HTTPError: | ||||
# WebSockets don't response to traditional error codes so we | ||||
# close the connection. | ||||
if not self.stream.closed(): | ||||
self.stream.close() | ||||
MinRK
|
r4707 | self.close() | ||
Brian E. Granger
|
r4572 | else: | ||
self.iopub_stream.on_recv(self._on_zmq_reply) | ||||
self.start_hb(self.kernel_died) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4707 | def on_message(self, msg): | ||
pass | ||||
Brian E. Granger
|
r4545 | |||
def on_close(self): | ||||
Bernardo B. Marques
|
r4872 | # This method can be called twice, once by self.kernel_died and once | ||
Brian E. Granger
|
r4572 | # from the WebSocket close event. If the WebSocket connection is | ||
# closed before the ZMQ streams are setup, they could be None. | ||||
Brian E. Granger
|
r4545 | self.stop_hb() | ||
Brian E. Granger
|
r4572 | if self.iopub_stream is not None and not self.iopub_stream.closed(): | ||
self.iopub_stream.on_recv(None) | ||||
self.iopub_stream.close() | ||||
if self.hb_stream is not None and not self.hb_stream.closed(): | ||||
self.hb_stream.close() | ||||
Bernardo B. Marques
|
r4872 | |||
Brian E. Granger
|
r4545 | def start_hb(self, callback): | ||
"""Start the heartbeating and call the callback if the kernel dies.""" | ||||
if not self._beating: | ||||
self._kernel_alive = True | ||||
def ping_or_dead(): | ||||
MinRK
|
r5812 | self.hb_stream.flush() | ||
Brian E. Granger
|
r4545 | if self._kernel_alive: | ||
self._kernel_alive = False | ||||
self.hb_stream.send(b'ping') | ||||
MinRK
|
r5952 | # flush stream to force immediate socket send | ||
self.hb_stream.flush() | ||||
Brian E. Granger
|
r4545 | else: | ||
try: | ||||
callback() | ||||
except: | ||||
pass | ||||
finally: | ||||
MinRK
|
r5835 | self.stop_hb() | ||
Brian E. Granger
|
r4545 | |||
def beat_received(msg): | ||||
self._kernel_alive = True | ||||
self.hb_stream.on_recv(beat_received) | ||||
MinRK
|
r5812 | loop = ioloop.IOLoop.instance() | ||
self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop) | ||||
MinRK
|
r5835 | loop.add_timeout(time.time()+self.first_beat, self._really_start_hb) | ||
Brian E. Granger
|
r4545 | self._beating= True | ||
MinRK
|
r5835 | |||
def _really_start_hb(self): | ||||
"""callback for delayed heartbeat start | ||||
Only start the hb loop if we haven't been closed during the wait. | ||||
""" | ||||
if self._beating and not self.hb_stream.closed(): | ||||
self._hb_periodic_callback.start() | ||||
Brian E. Granger
|
r4545 | |||
def stop_hb(self): | ||||
"""Stop the heartbeating and cancel all related callbacks.""" | ||||
if self._beating: | ||||
MinRK
|
r5835 | self._beating = False | ||
Brian E. Granger
|
r4545 | self._hb_periodic_callback.stop() | ||
if not self.hb_stream.closed(): | ||||
self.hb_stream.on_recv(None) | ||||
def kernel_died(self): | ||||
Brian E. Granger
|
r4563 | self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id) | ||
MinRK
|
r5836 | self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id) | ||
Brian E. Granger
|
r4545 | self.write_message( | ||
{'header': {'msg_type': 'status'}, | ||||
'parent_header': {}, | ||||
'content': {'execution_state':'dead'} | ||||
} | ||||
) | ||||
self.on_close() | ||||
MinRK
|
r4707 | class ShellHandler(AuthenticatedZMQStreamHandler): | ||
Brian E. Granger
|
r4545 | |||
def initialize(self, *args, **kwargs): | ||||
Brian E. Granger
|
r4572 | self.shell_stream = None | ||
Brian Granger
|
r4297 | |||
MinRK
|
r4707 | def on_first_message(self, msg): | ||
try: | ||||
super(ShellHandler, self).on_first_message(msg) | ||||
except web.HTTPError: | ||||
self.close() | ||||
return | ||||
Brian E. Granger
|
r4545 | km = self.application.kernel_manager | ||
self.max_msg_size = km.max_msg_size | ||||
MinRK
|
r4707 | kernel_id = self.kernel_id | ||
Brian E. Granger
|
r4572 | try: | ||
self.shell_stream = km.create_shell_stream(kernel_id) | ||||
except web.HTTPError: | ||||
# WebSockets don't response to traditional error codes so we | ||||
# close the connection. | ||||
if not self.stream.closed(): | ||||
self.stream.close() | ||||
MinRK
|
r4707 | self.close() | ||
Brian E. Granger
|
r4572 | else: | ||
self.shell_stream.on_recv(self._on_zmq_reply) | ||||
Brian E. Granger
|
r4545 | |||
Brian Granger
|
r4306 | def on_message(self, msg): | ||
Brian E. Granger
|
r4545 | if len(msg) < self.max_msg_size: | ||
msg = jsonapi.loads(msg) | ||||
self.session.send(self.shell_stream, msg) | ||||
Brian Granger
|
r4297 | |||
Brian Granger
|
r4306 | def on_close(self): | ||
Brian E. Granger
|
r4572 | # Make sure the stream exists and is not already closed. | ||
if self.shell_stream is not None and not self.shell_stream.closed(): | ||||
self.shell_stream.close() | ||||
Brian Granger
|
r4297 | |||
Brian E. Granger
|
r4494 | #----------------------------------------------------------------------------- | ||
# Notebook web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class NotebookRootHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4301 | |||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian Granger
|
r4301 | def get(self): | ||
Brian E. Granger
|
r4484 | nbm = self.application.notebook_manager | ||
Matthias BUSSONNIER
|
r6841 | km = self.application.kernel_manager | ||
Brian E. Granger
|
r4484 | files = nbm.list_notebooks() | ||
Matthias BUSSONNIER
|
r6841 | for f in files : | ||
Matthias BUSSONNIER
|
r6848 | f['kernel_id'] = km.kernel_for_notebook(f['notebook_id']) | ||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(files)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def post(self): | ||
nbm = self.application.notebook_manager | ||||
body = self.request.body.strip() | ||||
format = self.get_argument('format', default='json') | ||||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
Brian E. Granger
|
r4484 | if body: | ||
Brian E. Granger
|
r4491 | notebook_id = nbm.save_new_notebook(body, name=name, format=format) | ||
Brian E. Granger
|
r4484 | else: | ||
notebook_id = nbm.new_notebook() | ||||
self.set_header('Location', '/'+notebook_id) | ||||
Brian E. Granger
|
r4545 | self.finish(jsonapi.dumps(notebook_id)) | ||
Brian Granger
|
r4301 | |||
MinRK
|
r4706 | class NotebookHandler(AuthenticatedHandler): | ||
Brian Granger
|
r4301 | |||
Brian E. Granger
|
r4484 | SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE') | ||
MinRK
|
r5200 | @authenticate_unless_readonly | ||
Brian E. Granger
|
r4484 | def get(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
format = self.get_argument('format', default='json') | ||||
last_mod, name, data = nbm.get_notebook(notebook_id, format) | ||||
MinRK
|
r5200 | |||
Brian E. Granger
|
r4484 | if format == u'json': | ||
self.set_header('Content-Type', 'application/json') | ||||
Brian E. Granger
|
r4634 | self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name) | ||
Brian E. Granger
|
r4484 | elif format == u'py': | ||
Brian E. Granger
|
r4493 | self.set_header('Content-Type', 'application/x-python') | ||
Brian E. Granger
|
r4558 | self.set_header('Content-Disposition','attachment; filename="%s.py"' % name) | ||
Brian E. Granger
|
r4484 | self.set_header('Last-Modified', last_mod) | ||
self.finish(data) | ||||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def put(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
format = self.get_argument('format', default='json') | ||||
Brian E. Granger
|
r4491 | name = self.get_argument('name', default=None) | ||
nbm.save_notebook(notebook_id, self.request.body, name=name, format=format) | ||||
Brian E. Granger
|
r4484 | self.set_status(204) | ||
Brian Granger
|
r4301 | self.finish() | ||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4484 | def delete(self, notebook_id): | ||
nbm = self.application.notebook_manager | ||||
nbm.delete_notebook(notebook_id) | ||||
Brian Granger
|
r4301 | self.set_status(204) | ||
self.finish() | ||||
Brian Granger
|
r5860 | |||
class NotebookCopyHandler(AuthenticatedHandler): | ||||
@web.authenticated | ||||
def get(self, notebook_id): | ||||
nbm = self.application.notebook_manager | ||||
project = nbm.notebook_dir | ||||
notebook_id = nbm.copy_notebook(notebook_id) | ||||
Matthias BUSSONNIER
|
r8097 | self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id)) | ||
Brian Granger
|
r5860 | |||
Brian Granger
|
r6191 | |||
#----------------------------------------------------------------------------- | ||||
# Cluster handlers | ||||
#----------------------------------------------------------------------------- | ||||
class MainClusterHandler(AuthenticatedHandler): | ||||
@web.authenticated | ||||
def get(self): | ||||
cm = self.application.cluster_manager | ||||
self.finish(jsonapi.dumps(cm.list_profiles())) | ||||
class ClusterProfileHandler(AuthenticatedHandler): | ||||
@web.authenticated | ||||
def get(self, profile): | ||||
cm = self.application.cluster_manager | ||||
self.finish(jsonapi.dumps(cm.profile_info(profile))) | ||||
class ClusterActionHandler(AuthenticatedHandler): | ||||
@web.authenticated | ||||
def post(self, profile, action): | ||||
cm = self.application.cluster_manager | ||||
if action == 'start': | ||||
Brian Granger
|
r6199 | n = self.get_argument('n',default=None) | ||
if n is None: | ||||
data = cm.start_cluster(profile) | ||||
else: | ||||
data = cm.start_cluster(profile,int(n)) | ||||
Brian Granger
|
r6191 | if action == 'stop': | ||
Brian Granger
|
r6197 | data = cm.stop_cluster(profile) | ||
self.finish(jsonapi.dumps(data)) | ||||
Brian Granger
|
r6191 | |||
Brian E. Granger
|
r4507 | #----------------------------------------------------------------------------- | ||
# RST web service handlers | ||||
#----------------------------------------------------------------------------- | ||||
MinRK
|
r4706 | class RSTHandler(AuthenticatedHandler): | ||
Brian E. Granger
|
r4507 | |||
MinRK
|
r4706 | @web.authenticated | ||
Brian E. Granger
|
r4507 | def post(self): | ||
if publish_string is None: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(503, u'docutils not available') | ||
Brian E. Granger
|
r4507 | body = self.request.body.strip() | ||
Brian E. Granger
|
r4540 | source = body | ||
# template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html') | ||||
Brian E. Granger
|
r4507 | defaults = {'file_insertion_enabled': 0, | ||
'raw_enabled': 0, | ||||
'_disable_config': 1, | ||||
Brian E. Granger
|
r4545 | 'stylesheet_path': 0 | ||
Brian E. Granger
|
r4540 | # 'template': template_path | ||
Brian E. Granger
|
r4507 | } | ||
try: | ||||
html = publish_string(source, writer_name='html', | ||||
settings_overrides=defaults | ||||
) | ||||
except: | ||||
Brian E. Granger
|
r4676 | raise web.HTTPError(400, u'Invalid RST') | ||
Brian E. Granger
|
r4507 | print html | ||
self.set_header('Content-Type', 'text/html') | ||||
self.finish(html) | ||||
MinRK
|
r8016 | # to minimize subclass changes: | ||
HTTPError = web.HTTPError | ||||
Brian E. Granger
|
r4507 | |||
MinRK
|
r7922 | class FileFindHandler(web.StaticFileHandler): | ||
"""subclass of StaticFileHandler for serving files from a search path""" | ||||
_static_paths = {} | ||||
MinRK
|
r8016 | # _lock is needed for tornado < 2.2.0 compat | ||
_lock = threading.Lock() # protects _static_hashes | ||||
MinRK
|
r7922 | |||
def initialize(self, path, default_filename=None): | ||||
MinRK
|
r7930 | if isinstance(path, basestring): | ||
path = [path] | ||||
MinRK
|
r7922 | self.roots = tuple( | ||
os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path | ||||
) | ||||
self.default_filename = default_filename | ||||
@classmethod | ||||
def locate_file(cls, path, roots): | ||||
"""locate a file to serve on our static file search path""" | ||||
with cls._lock: | ||||
if path in cls._static_paths: | ||||
return cls._static_paths[path] | ||||
try: | ||||
abspath = os.path.abspath(filefind(path, roots)) | ||||
except IOError: | ||||
# empty string should always give exists=False | ||||
return '' | ||||
# os.path.abspath strips a trailing / | ||||
# it needs to be temporarily added back for requests to root/ | ||||
if not (abspath + os.path.sep).startswith(roots): | ||||
MinRK
|
r8016 | raise HTTPError(403, "%s is not in root static directory", path) | ||
MinRK
|
r7922 | |||
cls._static_paths[path] = abspath | ||||
return abspath | ||||
def get(self, path, include_body=True): | ||||
path = self.parse_url_path(path) | ||||
MinRK
|
r8016 | # begin subclass override | ||
abspath = self.locate_file(path, self.roots) | ||||
# end subclass override | ||||
MinRK
|
r7922 | |||
if os.path.isdir(abspath) and self.default_filename is not None: | ||||
# need to look at the request.path here for when path is empty | ||||
# but there is some prefix to the path that was already | ||||
# trimmed by the routing | ||||
if not self.request.path.endswith("/"): | ||||
self.redirect(self.request.path + "/") | ||||
return | ||||
abspath = os.path.join(abspath, self.default_filename) | ||||
if not os.path.exists(abspath): | ||||
MinRK
|
r8016 | raise HTTPError(404) | ||
MinRK
|
r7922 | if not os.path.isfile(abspath): | ||
MinRK
|
r8016 | raise HTTPError(403, "%s is not a file", path) | ||
MinRK
|
r7922 | |||
stat_result = os.stat(abspath) | ||||
modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME]) | ||||
self.set_header("Last-Modified", modified) | ||||
mime_type, encoding = mimetypes.guess_type(abspath) | ||||
if mime_type: | ||||
self.set_header("Content-Type", mime_type) | ||||
cache_time = self.get_cache_time(path, modified, mime_type) | ||||
if cache_time > 0: | ||||
self.set_header("Expires", datetime.datetime.utcnow() + \ | ||||
datetime.timedelta(seconds=cache_time)) | ||||
self.set_header("Cache-Control", "max-age=" + str(cache_time)) | ||||
else: | ||||
self.set_header("Cache-Control", "public") | ||||
self.set_extra_headers(path) | ||||
# Check the If-Modified-Since, and don't send the result if the | ||||
# content has not been modified | ||||
ims_value = self.request.headers.get("If-Modified-Since") | ||||
if ims_value is not None: | ||||
date_tuple = email.utils.parsedate(ims_value) | ||||
if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)) | ||||
if if_since >= modified: | ||||
self.set_status(304) | ||||
return | ||||
with open(abspath, "rb") as file: | ||||
data = file.read() | ||||
hasher = hashlib.sha1() | ||||
hasher.update(data) | ||||
self.set_header("Etag", '"%s"' % hasher.hexdigest()) | ||||
if include_body: | ||||
self.write(data) | ||||
else: | ||||
assert self.request.method == "HEAD" | ||||
self.set_header("Content-Length", len(data)) | ||||
@classmethod | ||||
def get_version(cls, settings, path): | ||||
"""Generate the version string to be used in static URLs. | ||||
This method may be overridden in subclasses (but note that it | ||||
is a class method rather than a static method). The default | ||||
implementation uses a hash of the file's contents. | ||||
``settings`` is the `Application.settings` dictionary and ``path`` | ||||
is the relative location of the requested asset on the filesystem. | ||||
The returned value should be a string, or ``None`` if no version | ||||
could be determined. | ||||
""" | ||||
# begin subclass override: | ||||
MinRK
|
r7930 | static_paths = settings['static_path'] | ||
if isinstance(static_paths, basestring): | ||||
static_paths = [static_paths] | ||||
MinRK
|
r7922 | roots = tuple( | ||
MinRK
|
r7930 | os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths | ||
MinRK
|
r7922 | ) | ||
try: | ||||
abs_path = filefind(path, roots) | ||||
MinRK
|
r7930 | except IOError: | ||
MinRK
|
r7922 | logging.error("Could not find static file %r", path) | ||
return None | ||||
# end subclass override | ||||
with cls._lock: | ||||
hashes = cls._static_hashes | ||||
if abs_path not in hashes: | ||||
try: | ||||
f = open(abs_path, "rb") | ||||
hashes[abs_path] = hashlib.md5(f.read()).hexdigest() | ||||
f.close() | ||||
except Exception: | ||||
logging.error("Could not open static file %r", path) | ||||
hashes[abs_path] = None | ||||
hsh = hashes.get(abs_path) | ||||
if hsh: | ||||
return hsh[:5] | ||||
return None | ||||
MinRK
|
r8016 | |||
# make_static_url and parse_url_path totally unchanged from tornado 2.2.0 | ||||
# but needed for tornado < 2.2.0 compat | ||||
@classmethod | ||||
def make_static_url(cls, settings, path): | ||||
"""Constructs a versioned url for the given path. | ||||
This method may be overridden in subclasses (but note that it is | ||||
a class method rather than an instance method). | ||||
``settings`` is the `Application.settings` dictionary. ``path`` | ||||
is the static path being requested. The url returned should be | ||||
relative to the current host. | ||||
""" | ||||
static_url_prefix = settings.get('static_url_prefix', '/static/') | ||||
version_hash = cls.get_version(settings, path) | ||||
if version_hash: | ||||
return static_url_prefix + path + "?v=" + version_hash | ||||
return static_url_prefix + path | ||||
def parse_url_path(self, url_path): | ||||
"""Converts a static URL path into a filesystem path. | ||||
``url_path`` is the path component of the URL with | ||||
``static_url_prefix`` removed. The return value should be | ||||
filesystem path relative to ``static_path``. | ||||
""" | ||||
if os.path.sep != "/": | ||||
url_path = url_path.replace("/", os.path.sep) | ||||
return url_path | ||||