From 79a7b1e42186e0489671042928f73301a9e2db90 Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 23 Oct 2013 12:01:38 -0700 Subject: [PATCH 1/2] fix tornado log propagation in the notebook --- IPython/html/notebookapp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index d6ec35829..539a64fba 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -542,9 +542,8 @@ class NotebookApp(BaseIPythonApplication): # hook up tornado 3's loggers to our app handlers for name in ('access', 'application', 'general'): logger = logging.getLogger('tornado.%s' % name) - logger.propagate = False + logger.parent = self.log logger.setLevel(self.log.level) - logger.handlers = self.log.handlers def init_webapp(self): """initialize tornado webapp and httpserver""" @@ -692,8 +691,8 @@ class NotebookApp(BaseIPythonApplication): @catch_config_error def initialize(self, argv=None): - self.init_logging() super(NotebookApp, self).initialize(argv) + self.init_logging() self.init_kernel_argv() self.init_configurables() self.init_components() From 6fa13ebb983b606a2a976da6c7b5b38cbf52e426 Mon Sep 17 00:00:00 2001 From: MinRK Date: Wed, 23 Oct 2013 12:09:42 -0700 Subject: [PATCH 2/2] bump minimum tornado version to 3.1.0 The no-hidden behavior of the files handler already requires this, but instead of raising, it just doesn't hide hidden files. Bumping the minimum version also allows much cleaner static file handler subclasses. --- IPython/html/base/handlers.py | 194 ++-------------------------------- IPython/html/notebookapp.py | 6 +- setup.py | 2 +- 3 files changed, 15 insertions(+), 187 deletions(-) diff --git a/IPython/html/base/handlers.py b/IPython/html/base/handlers.py index 9ca0d25ff..42a5826c2 100644 --- a/IPython/html/base/handlers.py +++ b/IPython/html/base/handlers.py @@ -17,21 +17,15 @@ Authors: #----------------------------------------------------------------------------- -import datetime -import email.utils import functools -import hashlib import json import logging -import mimetypes import os import stat import sys -import threading import traceback from tornado import web -from tornado import websocket try: from tornado.log import app_log @@ -39,65 +33,12 @@ except ImportError: app_log = logging.getLogger() from IPython.config import Application -from IPython.external.decorator import decorator from IPython.utils.path import filefind -from IPython.utils.jsonutil import date_default # UF_HIDDEN is a stat flag not defined in the stat module. # It is used by BSD to indicate hidden files. UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768) -#----------------------------------------------------------------------------- -# Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary! -#----------------------------------------------------------------------------- - -# 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 - -if tornado.version_info <= (2,1,1): - - 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() - - websocket.WebSocketHandler._execute = _execute - del _execute - - #----------------------------------------------------------------------------- # Top-level handlers #----------------------------------------------------------------------------- @@ -359,20 +300,20 @@ HTTPError = web.HTTPError class FileFindHandler(web.StaticFileHandler): """subclass of StaticFileHandler for serving files from a search path""" + # cache search results, don't search for files more than once _static_paths = {} - # _lock is needed for tornado < 2.2.0 compat - _lock = threading.Lock() # protects _static_hashes def initialize(self, path, default_filename=None): if isinstance(path, basestring): path = [path] - self.roots = tuple( + + self.root = tuple( os.path.abspath(os.path.expanduser(p)) + os.sep for p in path ) self.default_filename = default_filename @classmethod - def locate_file(cls, path, roots): + def get_absolute_path(cls, roots, path): """locate a file to serve on our static file search path""" with cls._lock: if path in cls._static_paths: @@ -382,131 +323,18 @@ class FileFindHandler(web.StaticFileHandler): 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.sep).startswith(roots): - raise HTTPError(403, "%s is not in root static directory", path) - + cls._static_paths[path] = abspath return abspath - def get(self, path, include_body=True): - path = self.parse_url_path(path) - - # begin subclass override - abspath = self.locate_file(path, self.roots) - # end subclass override - - 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): - raise HTTPError(404) - if not os.path.isfile(abspath): - raise HTTPError(403, "%s is not a file", path) - - stat_result = os.stat(abspath) - modified = datetime.datetime.utcfromtimestamp(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(*date_tuple[:6]) - if if_since >= modified: - self.set_status(304) - return + def validate_absolute_path(self, root, absolute_path): + """check if the file should be served (raises 404, 403, etc.)""" + for root in self.root: + if (absolute_path + os.sep).startswith(root): + break - 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. + return super(FileFindHandler, self).validate_absolute_path(root, absolute_path) - ``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: - static_paths = settings['static_path'] - if isinstance(static_paths, basestring): - static_paths = [static_paths] - roots = tuple( - os.path.abspath(os.path.expanduser(p)) + os.sep for p in static_paths - ) - - try: - abs_path = filefind(path, roots) - except IOError: - app_log.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: - app_log.error("Could not open static file %r", path) - hashes[abs_path] = None - hsh = hashes.get(abs_path) - if hsh: - return hsh[:5] - return None - - - 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.sep != "/": - url_path = url_path.replace("/", os.sep) - return url_path class TrailingSlashHandler(web.RequestHandler): """Simple redirect handler that strips trailing slashes diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 539a64fba..7b8f118b1 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -42,8 +42,8 @@ from jinja2 import Environment, FileSystemLoader from zmq.eventloop import ioloop ioloop.install() -# check for tornado 2.1.0 -msg = "The IPython Notebook requires tornado >= 2.1.0" +# check for tornado 3.1.0 +msg = "The IPython Notebook requires tornado >= 3.1.0" try: import tornado except ImportError: @@ -52,7 +52,7 @@ try: version_info = tornado.version_info except AttributeError: raise ImportError(msg + ", but you have < 1.1.0") -if version_info < (2,1,0): +if version_info < (3,1,0): raise ImportError(msg + ", but you have %s" % tornado.version) from tornado import httpserver diff --git a/setup.py b/setup.py index 6498bff88..204804785 100755 --- a/setup.py +++ b/setup.py @@ -273,7 +273,7 @@ if 'setuptools' in sys.modules: zmq = 'pyzmq>=2.1.11', doc = 'Sphinx>=0.3', test = 'nose>=0.10.1', - notebook = ['tornado>=2.0', 'pyzmq>=2.1.11', 'jinja2'], + notebook = ['tornado>=3.1', 'pyzmq>=2.1.11', 'jinja2'], nbconvert = ['pygments', 'jinja2', 'Sphinx>=0.3'] ) everything = set()