diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index ffbf9ebfc..d30eb3ca8 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -117,6 +117,8 @@ Fixed - Crash when opening an invalid URL from an application on macOS. - Crash with an empty `completion.timestamp_format`. - Crash when `completion.min_chars` is set in some cases. +- HTML/JS resource files are now read into RAM on start to avoid crashes when + changing qutebrowser versions while it's open. Removed ~~~~~~~ diff --git a/qutebrowser/app.py b/qutebrowser/app.py index e9f9d2229..014e02c5f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -95,6 +95,7 @@ def run(args): log.init.debug("Initializing directories...") standarddir.init(args) + utils.preload_resources() log.init.debug("Initializing config...") configinit.early_init(args) diff --git a/qutebrowser/html/undef_error.html b/qutebrowser/html/undef_error.html deleted file mode 100644 index 55a47ca95..000000000 --- a/qutebrowser/html/undef_error.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - -
- -There was an error while rendering {pagename}.
- -This most likely happened because you updated qutebrowser but didn't restart yet.
- -If you believe this isn't the case and this is a bug, please do :report.
- -
{traceback}- - diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index d4ce3368f..b06444f93 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -22,12 +22,10 @@ import os import os.path import contextlib -import traceback import mimetypes import html import jinja2 -import jinja2.exceptions from PyQt5.QtCore import QUrl from qutebrowser.utils import utils, urlutils, log @@ -125,14 +123,7 @@ class Environment(jinja2.Environment): def render(template, **kwargs): """Render the given template and pass the given arguments to it.""" - try: - return environment.get_template(template).render(**kwargs) - except jinja2.exceptions.UndefinedError: - log.misc.exception("UndefinedError while rendering " + template) - err_path = os.path.join('html', 'undef_error.html') - err_template = utils.read_file(err_path) - tb = traceback.format_exc() - return err_template.format(pagename=template, traceback=tb) + return environment.get_template(template).render(**kwargs) environment = Environment() diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index f03d42844..9f3acde5a 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -32,6 +32,7 @@ import functools import contextlib import socket import shlex +import glob from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -51,6 +52,7 @@ from qutebrowser.utils import qtutils, log fake_clipboard = None log_clipboard = False +_resource_cache = {} is_mac = sys.platform.startswith('darwin') is_linux = sys.platform.startswith('linux') @@ -140,6 +142,15 @@ def compact_text(text, elidelength=None): return out +def preload_resources(): + """Load resource files into the cache.""" + for subdir, pattern in [('html', '*.html'), ('javascript', '*.js')]: + path = resource_filename(subdir) + for full_path in glob.glob(os.path.join(path, pattern)): + sub_path = os.path.join(subdir, os.path.basename(full_path)) + _resource_cache[sub_path] = read_file(sub_path) + + def read_file(filename, binary=False): """Get the contents of a file contained with qutebrowser. @@ -151,6 +162,9 @@ def read_file(filename, binary=False): Return: The file contents as string. """ + if not binary and filename in _resource_cache: + return _resource_cache[filename] + if hasattr(sys, 'frozen'): # PyInstaller doesn't support pkg_resources :( # https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index f47155f91..b1a50772e 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -23,6 +23,7 @@ import os import os.path import logging +import jinja2.exceptions import pytest from PyQt5.QtCore import QUrl @@ -32,7 +33,6 @@ from qutebrowser.utils import utils, jinja @pytest.fixture(autouse=True) def patch_read_file(monkeypatch): """pytest fixture to patch utils.read_file.""" - real_read_file = utils.read_file real_resource_filename = utils.resource_filename def _read_file(path, binary=False): @@ -52,9 +52,6 @@ def patch_read_file(monkeypatch): elif path == os.path.join('html', 'undef.html'): assert not binary return """{{ does_not_exist() }}""" - elif path == os.path.join('html', 'undef_error.html'): - assert not binary - return real_read_file(path) elif path == os.path.join('html', 'attributeerror.html'): assert not binary return """{{ obj.foobar }}""" @@ -129,15 +126,9 @@ def test_utf8(): def test_undefined_function(caplog): - """Make sure we don't crash if an undefined function is called.""" - with caplog.at_level(logging.ERROR): - data = jinja.render('undef.html') - assert 'There was an error while rendering undef.html' in data - assert "'does_not_exist' is undefined" in data - assert data.startswith('') - - assert len(caplog.records) == 1 - assert caplog.records[0].msg == "UndefinedError while rendering undef.html" + """Make sure undefined attributes crash since we preload resources..""" + with pytest.raises(jinja2.exceptions.UndefinedError): + jinja.render('undef.html') def test_attribute_error(): diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index b2eef0237..df0eb9ecb 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -133,6 +133,14 @@ class TestReadFile: content = utils.read_file(os.path.join('utils', 'testfile')) assert content.splitlines()[0] == "Hello World!" + @pytest.mark.parametrize('filename', ['javascript/scroll.js', + 'html/error.html']) + def test_read_cached_file(self, mocker, filename): + utils.preload_resources() + m = mocker.patch('pkg_resources.resource_string') + utils.read_file(filename) + m.assert_not_called() + def test_readfile_binary(self): """Read a test file in binary mode.""" content = utils.read_file(os.path.join('utils', 'testfile'),