Cache HTML/JS resource files when starting

This mostly reverts 9edc5a665e (see #1362).
Fixes #1943
This commit is contained in:
Florian Bruhin 2018-03-05 09:03:58 +01:00
parent 78623f4ec8
commit b4a2352833
7 changed files with 30 additions and 45 deletions

View File

@ -117,6 +117,8 @@ Fixed
- Crash when opening an invalid URL from an application on macOS. - Crash when opening an invalid URL from an application on macOS.
- Crash with an empty `completion.timestamp_format`. - Crash with an empty `completion.timestamp_format`.
- Crash when `completion.min_chars` is set in some cases. - 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 Removed
~~~~~~~ ~~~~~~~

View File

@ -95,6 +95,7 @@ def run(args):
log.init.debug("Initializing directories...") log.init.debug("Initializing directories...")
standarddir.init(args) standarddir.init(args)
utils.preload_resources()
log.init.debug("Initializing config...") log.init.debug("Initializing config...")
configinit.early_init(args) configinit.early_init(args)

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<!--
vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
-->
<html>
<head>
<meta charset="utf-8">
<title>Error while rendering HTML</title>
</head>
<body>
<h1>Error while rendering internal qutebrowser page</h1>
<p>There was an error while rendering {pagename}.</p>
<p>This most likely happened because you updated qutebrowser but didn't restart yet.</p>
<p>If you believe this isn't the case and this is a bug, please do :report.<p>
<h2>Traceback</h2>
<pre>{traceback}</pre>
</body>
</html>

View File

@ -22,12 +22,10 @@
import os import os
import os.path import os.path
import contextlib import contextlib
import traceback
import mimetypes import mimetypes
import html import html
import jinja2 import jinja2
import jinja2.exceptions
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.utils import utils, urlutils, log from qutebrowser.utils import utils, urlutils, log
@ -125,14 +123,7 @@ class Environment(jinja2.Environment):
def render(template, **kwargs): def render(template, **kwargs):
"""Render the given template and pass the given arguments to it.""" """Render the given template and pass the given arguments to it."""
try:
return environment.get_template(template).render(**kwargs) 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)
environment = Environment() environment = Environment()

View File

@ -32,6 +32,7 @@ import functools
import contextlib import contextlib
import socket import socket
import shlex import shlex
import glob
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
@ -51,6 +52,7 @@ from qutebrowser.utils import qtutils, log
fake_clipboard = None fake_clipboard = None
log_clipboard = False log_clipboard = False
_resource_cache = {}
is_mac = sys.platform.startswith('darwin') is_mac = sys.platform.startswith('darwin')
is_linux = sys.platform.startswith('linux') is_linux = sys.platform.startswith('linux')
@ -140,6 +142,15 @@ def compact_text(text, elidelength=None):
return out 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): def read_file(filename, binary=False):
"""Get the contents of a file contained with qutebrowser. """Get the contents of a file contained with qutebrowser.
@ -151,6 +162,9 @@ def read_file(filename, binary=False):
Return: Return:
The file contents as string. The file contents as string.
""" """
if not binary and filename in _resource_cache:
return _resource_cache[filename]
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen'):
# PyInstaller doesn't support pkg_resources :( # PyInstaller doesn't support pkg_resources :(
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc # https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc

View File

@ -23,6 +23,7 @@ import os
import os.path import os.path
import logging import logging
import jinja2.exceptions
import pytest import pytest
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -32,7 +33,6 @@ from qutebrowser.utils import utils, jinja
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_read_file(monkeypatch): def patch_read_file(monkeypatch):
"""pytest fixture to patch utils.read_file.""" """pytest fixture to patch utils.read_file."""
real_read_file = utils.read_file
real_resource_filename = utils.resource_filename real_resource_filename = utils.resource_filename
def _read_file(path, binary=False): def _read_file(path, binary=False):
@ -52,9 +52,6 @@ def patch_read_file(monkeypatch):
elif path == os.path.join('html', 'undef.html'): elif path == os.path.join('html', 'undef.html'):
assert not binary assert not binary
return """{{ does_not_exist() }}""" 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'): elif path == os.path.join('html', 'attributeerror.html'):
assert not binary assert not binary
return """{{ obj.foobar }}""" return """{{ obj.foobar }}"""
@ -129,15 +126,9 @@ def test_utf8():
def test_undefined_function(caplog): def test_undefined_function(caplog):
"""Make sure we don't crash if an undefined function is called.""" """Make sure undefined attributes crash since we preload resources.."""
with caplog.at_level(logging.ERROR): with pytest.raises(jinja2.exceptions.UndefinedError):
data = jinja.render('undef.html') 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('<!DOCTYPE html>')
assert len(caplog.records) == 1
assert caplog.records[0].msg == "UndefinedError while rendering undef.html"
def test_attribute_error(): def test_attribute_error():

View File

@ -133,6 +133,14 @@ class TestReadFile:
content = utils.read_file(os.path.join('utils', 'testfile')) content = utils.read_file(os.path.join('utils', 'testfile'))
assert content.splitlines()[0] == "Hello World!" 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): def test_readfile_binary(self):
"""Read a test file in binary mode.""" """Read a test file in binary mode."""
content = utils.read_file(os.path.join('utils', 'testfile'), content = utils.read_file(os.path.join('utils', 'testfile'),