Merge branch 'webengine-by-default'

This commit is contained in:
Florian Bruhin 2017-09-28 18:41:41 +02:00
commit 4e57b79e91
15 changed files with 529 additions and 178 deletions

View File

@ -179,6 +179,7 @@
|<<fonts.web.size.default_fixed,fonts.web.size.default_fixed>>|The default font size for fixed-pitch text.
|<<fonts.web.size.minimum,fonts.web.size.minimum>>|The hard minimum font size.
|<<fonts.web.size.minimum_logical,fonts.web.size.minimum_logical>>|The minimum logical font size that is applied when zooming out.
|<<force_software_rendering,force_software_rendering>>|Force software rendering for QtWebEngine.
|<<hints.auto_follow,hints.auto_follow>>|Controls when a hint can be automatically followed without pressing Enter.
|<<hints.auto_follow_timeout,hints.auto_follow_timeout>>|A timeout (in milliseconds) to ignore normal-mode key bindings after a successful auto-follow.
|<<hints.border,hints.border>>|CSS border value for hints.
@ -291,18 +292,17 @@ Default: +pass:[false]+
=== backend
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium project. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
Type: <<types,String>>
Valid values:
* +auto+: Automatically select either QtWebEngine or QtWebKit
* +webkit+: Force QtWebKit
* +webengine+: Force QtWebEngine
* +webengine+: Use QtWebEngine (based on Chromium)
* +webkit+: Use QtWebKit (based on WebKit, similar to Safari)
Default: +pass:[auto]+
Default: +pass:[webengine]+
[[bindings.commands]]
=== bindings.commands
@ -2263,6 +2263,22 @@ Type: <<types,Int>>
Default: +pass:[6]+
[[force_software_rendering]]
=== force_software_rendering
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
[[hints.auto_follow]]
=== hints.auto_follow
Controls when a hint can be automatically followed without pressing Enter.

View File

@ -17,7 +17,25 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Initialization of qutebrowser and application-wide things."""
"""Initialization of qutebrowser and application-wide things.
The run() function will get called once early initialization (in
qutebrowser.py/earlyinit.py) is done. See the qutebrowser.py docstring for
details about early initialization.
As we need to access the config before the QApplication is created, we
initialize everything the config needs before the QApplication is created, and
then leave it in a partially initialized state (no saving, no config errors
shown yet).
We then set up the QApplication object and initialize a few more low-level
things.
After that, init() and _init_modules() take over and initialize the rest.
After all initialization is done, the qt_mainloop() function is called, which
blocks and spins the Qt mainloop.
"""
import os
import sys
@ -44,8 +62,7 @@ import qutebrowser.resources
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import (config, websettings, configexc, configfiles,
configinit)
from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.network import proxy
@ -54,7 +71,8 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, sql, cmdhistory)
crashsignal, earlyinit, sql, cmdhistory,
backendproblem)
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error)
# pylint: disable=unused-import
@ -188,12 +206,6 @@ def _init_icon():
def _process_args(args):
"""Open startpage etc. and process commandline args."""
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore:
_load_session(args.session)
session_manager = objreg.get('session-manager')
@ -389,14 +401,17 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance.
"""
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager)
configinit.late_init(save_manager)
log.init.debug("Checking backend requirements...")
backendproblem.init()
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing network...")
networkmanager.init()
@ -509,12 +524,13 @@ class Quitter:
with tokenize.open(os.path.join(dirpath, fn)) as f:
compile(f.read(), fn, 'exec')
def _get_restart_args(self, pages=(), session=None):
def _get_restart_args(self, pages=(), session=None, override_args=None):
"""Get the current working directory and args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
session: The session to load, or None.
override_args: Argument overrides as a dict.
Return:
An (args, cwd) tuple.
@ -565,6 +581,9 @@ class Quitter:
argdict['temp_basedir'] = False
argdict['temp_basedir_restarted'] = True
if override_args is not None:
argdict.update(override_args)
# Dump the data
data = json.dumps(argdict)
args += ['--json-args', data]
@ -589,7 +608,7 @@ class Quitter:
if ok:
self.shutdown(restart=True)
def restart(self, pages=(), session=None):
def restart(self, pages=(), session=None, override_args=None):
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
@ -602,6 +621,7 @@ class Quitter:
Args:
pages: A list of URLs to open.
session: The session to load, or None.
override_args: Argument overrides as a dict.
Return:
True if the restart succeeded, False otherwise.
@ -611,13 +631,19 @@ class Quitter:
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
# Save the session if one is given.
if session is not None:
session_manager = objreg.get('session-manager')
session_manager.save(session, with_private=True)
# Make sure we're not accepting a connection from the new process
# before we fully exited.
ipc.server.shutdown()
# Open a new process and immediately shutdown the existing one
try:
args, cwd = self._get_restart_args(pages, session)
args, cwd = self._get_restart_args(pages, session, override_args)
if cwd is None:
subprocess.Popen(args)
else:
@ -705,7 +731,7 @@ class Quitter:
QApplication.closeAllWindows()
# Shut down IPC
try:
objreg.get('ipc-server').shutdown()
ipc.server.shutdown()
except KeyError:
pass
# Save everything

View File

@ -19,7 +19,6 @@
"""Wrapper over a QWebEngineView."""
import os
import math
import functools
import html as html_utils
@ -38,7 +37,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webenginesettings)
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, objreg, jinja, debug, version)
message, objreg, jinja, debug)
_qute_scheme_handler = None
@ -50,16 +49,8 @@ def init():
# won't work...
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
global _qute_scheme_handler
app = QApplication.instance()
software_rendering = (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ)
if version.opengl_vendor() == 'nouveau' and not software_rendering:
# FIXME:qtwebengine display something more sophisticated here
raise browsertab.WebTabError(
"QtWebEngine is not supported with Nouveau graphics (unless "
"QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).")
log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
_qute_scheme_handler.install(webenginesettings.default_profile)

View File

@ -101,27 +101,35 @@ qt_args:
https://peter.sh/experiments/chromium-command-line-switches/ for a list)
will work.
force_software_rendering:
type: Bool
default: false
backend: QtWebEngine
desc: >-
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers.
backend:
type:
name: String
valid_values:
- auto: Automatically select either QtWebEngine or QtWebKit
- webkit: Force QtWebKit
- webengine: Force QtWebEngine
default: auto
- webengine: Use QtWebEngine (based on Chromium)
- webkit: Use QtWebKit (based on WebKit, similar to Safari)
default: webengine
desc: >-
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends,
QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the
Qt project with Qt 5.6, but picked up as a well maintained fork:
https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a
well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser
only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium
project. It's slightly more resource hungry that QtWebKit and has a couple
of missing features in qutebrowser, but is generally the preferred choice.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more
resource hungry that QtWebKit and has a couple of missing features in
qutebrowser, but is generally the preferred choice.
## auto_save

View File

@ -26,8 +26,8 @@ from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc)
from qutebrowser.utils import objreg, qtutils, usertypes, log, standarddir
from qutebrowser.misc import earlyinit, msgbox, objects
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
from qutebrowser.misc import msgbox, objects
# Error which happened during init, so we can show a message box.
@ -69,18 +69,20 @@ def early_init(args):
configfiles.init()
objects.backend = get_backend(args)
earlyinit.init_with_backend(objects.backend)
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if (objects.backend == usertypes.Backend.QtWebEngine and
config.val.force_software_rendering):
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
def get_backend(args):
"""Find out what backend to use based on available libraries."""
try:
import PyQt5.QtWebKit # pylint: disable=unused-variable
except ImportError:
webkit_available = False
else:
webkit_available = qtutils.is_new_qtwebkit()
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
@ -88,12 +90,8 @@ def get_backend(args):
if args.backend is not None:
return str_to_backend[args.backend]
elif config.val.backend != 'auto':
return str_to_backend[config.val.backend]
elif webkit_available:
return usertypes.Backend.QtWebKit
else:
return usertypes.Backend.QtWebEngine
return str_to_backend[config.val.backend]
def late_init(save_manager):

View File

@ -0,0 +1,318 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Dialogs shown when there was a problem with a backend choice."""
import os
import sys
import functools
import html
import attr
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout,
QVBoxLayout, QLabel, QMessageBox)
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.config import config
from qutebrowser.utils import usertypes, objreg, version, qtutils, log
from qutebrowser.misc import objects, msgbox
_Result = usertypes.enum(
'_Result',
['quit', 'restart', 'restart_webkit', 'restart_webengine'],
is_int=True, start=QDialog.Accepted + 1)
@attr.s
class _Button:
"""A button passed to BackendProblemDialog."""
text = attr.ib()
setting = attr.ib()
value = attr.ib()
default = attr.ib(default=False)
class _Dialog(QDialog):
"""A dialog which gets shown if there are issues with the backend."""
def __init__(self, because, text, backend, buttons=None, parent=None):
super().__init__(parent)
vbox = QVBoxLayout(self)
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
usertypes.Backend.QtWebEngine: usertypes.Backend.QtWebKit,
}[backend]
other_setting = other_backend.name.lower()[2:]
label = QLabel(
"<b>Failed to start with the {backend} backend!</b>"
"<p>qutebrowser tried to start with the {backend} backend but "
"failed because {because}.</p>{text}"
"<p><b>Forcing the {other_backend.name} backend</b></p>"
"<p>This forces usage of the {other_backend.name} backend by "
"setting the <i>backend = '{other_setting}'</i> option "
"(if you have a <i>config.py</i> file, you'll need to set "
"this manually).</p>".format(
backend=backend.name, because=because, text=text,
other_backend=other_backend, other_setting=other_setting),
wordWrap=True)
label.setTextFormat(Qt.RichText)
vbox.addWidget(label)
hbox = QHBoxLayout()
buttons = [] if buttons is None else buttons
quit_button = QPushButton("Quit")
quit_button.clicked.connect(lambda: self.done(_Result.quit))
hbox.addWidget(quit_button)
backend_button = QPushButton("Force {} backend".format(
other_backend.name))
backend_button.clicked.connect(functools.partial(
self._change_setting, 'backend', other_setting))
hbox.addWidget(backend_button)
for button in buttons:
btn = QPushButton(button.text, default=button.default)
btn.clicked.connect(functools.partial(
self._change_setting, button.setting, button.value))
hbox.addWidget(btn)
vbox.addLayout(hbox)
def _change_setting(self, setting, value):
"""Change the given setting and restart."""
config.instance.set_obj(setting, value, save_yaml=True)
save_manager = objreg.get('save-manager')
save_manager.save_all(is_exit=True)
if setting == 'backend' and value == 'webkit':
self.done(_Result.restart_webkit)
elif setting == 'backend' and value == 'webengine':
self.done(_Result.restart_webengine)
else:
self.done(_Result.restart)
def _show_dialog(*args, **kwargs):
"""Show a dialog for a backend problem."""
dialog = _Dialog(*args, **kwargs)
status = dialog.exec_()
quitter = objreg.get('quitter')
if status in [_Result.quit, QDialog.Rejected]:
pass
elif status == _Result.restart_webkit:
quitter.restart(override_args={'backend': 'webkit'})
elif status == _Result.restart_webengine:
quitter.restart(override_args={'backend': 'webengine'})
elif status == _Result.restart:
quitter.restart()
else:
assert False, status
sys.exit(usertypes.Exit.err_init)
def _handle_nouveau_graphics():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
if version.opengl_vendor() != 'nouveau':
return
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ):
return
button = _Button("Force software rendering", 'force_software_rendering',
True)
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Nouveau graphics",
text="<p>There are two ways to fix this:</p>"
"<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium) but could have noticable performance impact "
"(depending on your hardware). "
"This sets the <i>force_software_rendering = True</i> option "
"(if you have a <i>config.py</i> file, you'll need to set this "
"manually).</p>",
buttons=[button],
)
# Should never be reached
assert False
def _handle_wayland():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Wayland",
text="<p>There are two ways to fix this:</p>"
"<p><b>Set up XWayland</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium). "
)
# Should never be reached
assert False
@attr.s
class BackendImports:
"""Whether backend modules could be imported."""
webkit_available = attr.ib(default=None)
webengine_available = attr.ib(default=None)
webkit_error = attr.ib(default=None)
webengine_error = attr.ib(default=None)
def _try_import_backends():
"""Check whether backends can be imported and return BackendImports."""
# pylint: disable=unused-variable
results = BackendImports()
try:
from PyQt5 import QtWebKit
from PyQt5 import QtWebKitWidgets
except ImportError as e:
results.webkit_available = False
results.webkit_error = str(e)
else:
if qtutils.is_new_qtwebkit():
results.webkit_available = True
else:
results.webkit_available = False
results.webkit_error = "Unsupported legacy QtWebKit found"
try:
from PyQt5 import QtWebEngineWidgets
except ImportError as e:
results.webengine_available = False
results.webengine_error = str(e)
else:
results.webengine_available = True
assert results.webkit_available is not None
assert results.webengine_available is not None
if not results.webkit_available:
assert results.webkit_error is not None
if not results.webengine_available:
assert results.webengine_error is not None
return results
def _handle_ssl_support(fatal=False):
"""Check for full SSL availability.
If "fatal" is given, show an error and exit.
"""
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if QSslSocket.supportsSsl():
return
if fatal:
errbox = msgbox.msgbox(parent=None,
title="SSL error",
text="Could not initialize SSL support.",
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
assert not fatal
log.init.warning(text)
def _check_backend_modules():
"""Check for the modules needed for QtWebKit/QtWebEngine."""
imports = _try_import_backends()
if imports.webkit_available and imports.webengine_available:
return
elif not imports.webkit_available and not imports.webengine_available:
text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but neither "
"could be imported!</p>"
"<p>The errors encountered were:<ul>"
"<li><b>QtWebKit:</b> {webkit_error}"
"<li><b>QtWebEngine:</b> {webengine_error}"
"</ul></p>".format(
webkit_error=html.escape(imports.webkit_error),
webengine_error=html.escape(imports.webengine_error)))
errbox = msgbox.msgbox(parent=None,
title="No backend library found!",
text=text,
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
elif objects.backend == usertypes.Backend.QtWebKit:
if imports.webkit_available:
return
assert imports.webengine_available
_show_dialog(
backend=usertypes.Backend.QtWebKit,
because="QtWebKit could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webkit_error))
)
elif objects.backend == usertypes.Backend.QtWebEngine:
if imports.webengine_available:
return
assert imports.webkit_available
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="QtWebEngine could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webengine_error))
)
# Should never be reached
assert False
def init():
_check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
_handle_ssl_support()
_handle_wayland()
_handle_nouveau_graphics()
else:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
_handle_ssl_support(fatal=True)

View File

@ -36,10 +36,10 @@ except ImportError:
import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog
from PyQt5.QtWidgets import QApplication
from qutebrowser.commands import cmdutils
from qutebrowser.misc import earlyinit, crashdialog
from qutebrowser.misc import earlyinit, crashdialog, ipc
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
@ -236,7 +236,7 @@ class CrashHandler(QObject):
info = self._get_exception_info()
try:
objreg.get('ipc-server').ignored = True
ipc.server.ignored = True
except Exception:
log.destroy.exception("Error while ignoring ipc")

View File

@ -187,23 +187,6 @@ def check_ssl_support():
_die("Fatal error: Your Qt is built without SSL support.")
def check_backend_ssl_support(backend):
"""Check for full SSL availability when we know the backend."""
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.utils import log, usertypes
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if not QSslSocket.supportsSsl():
if backend == usertypes.Backend.QtWebKit:
_die("Could not initialize SSL support.")
else:
assert backend == usertypes.Backend.QtWebEngine
log.init.warning(text)
def _check_modules(modules):
"""Make sure the given modules are available."""
from qutebrowser.utils import log
@ -247,35 +230,6 @@ def check_libraries():
_check_modules(modules)
def check_backend_libraries(backend):
"""Make sure the libraries needed by the given backend are available.
Args:
backend: The backend as usertypes.Backend member.
"""
from qutebrowser.utils import usertypes
if backend == usertypes.Backend.QtWebEngine:
modules = {
'PyQt5.QtWebEngineWidgets':
_missing_str("QtWebEngine", webengine=True),
}
else:
assert backend == usertypes.Backend.QtWebKit, backend
modules = {
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
'PyQt5.QtWebKitWidgets': _missing_str("PyQt5.QtWebKitWidgets"),
}
_check_modules(modules)
def check_new_webkit(backend):
"""Make sure we use QtWebEngine or a new QtWebKit."""
from qutebrowser.utils import usertypes, qtutils
if backend == usertypes.Backend.QtWebKit and not qtutils.is_new_qtwebkit():
_die("qutebrowser does not support legacy QtWebKit versions anymore, "
"see the installation docs for details.")
def remove_inputhook():
"""Remove the PyQt input hook.
@ -328,16 +282,3 @@ def early_init(args):
remove_inputhook()
check_ssl_support()
check_optimize_flag()
def init_with_backend(backend):
"""Do later stages of init when we know the backend.
Args:
backend: The backend as usertypes.Backend member.
"""
assert not isinstance(backend, str), backend
assert backend is not None
check_backend_libraries(backend)
check_backend_ssl_support(backend)
check_new_webkit(backend)

View File

@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
import qutebrowser
from qutebrowser.utils import log, usertypes, error, objreg, standarddir, utils
from qutebrowser.utils import log, usertypes, error, standarddir, utils
CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting
@ -40,6 +40,10 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
PROTOCOL_VERSION = 1
# The ipc server instance
server = None
def _get_socketname_windows(basedir):
"""Get a socketname to use for Windows."""
parts = ['qutebrowser', getpass.getuser()]
@ -109,15 +113,15 @@ class ListenError(Error):
message: The error message.
"""
def __init__(self, server):
def __init__(self, local_server):
"""Constructor.
Args:
server: The QLocalServer which has the error set.
local_server: The QLocalServer which has the error set.
"""
super().__init__()
self.code = server.serverError()
self.message = server.errorString()
self.code = local_server.serverError()
self.message = local_server.errorString()
def __str__(self):
return "Error while listening to IPC server: {} (error {})".format(
@ -482,6 +486,7 @@ def send_or_listen(args):
The IPCServer instance if no running instance was detected.
None if an instance was running and received our request.
"""
global server
socketname = _get_socketname(args.basedir)
try:
try:
@ -492,7 +497,6 @@ def send_or_listen(args):
log.init.debug("Starting IPC server...")
server = IPCServer(socketname)
server.listen()
objreg.register('ipc-server', server)
return server
except AddressInUseError as e:
# This could be a race condition...

View File

@ -164,6 +164,11 @@ class SaveManager(QObject):
self.saveables[name].save(is_exit=is_exit, explicit=explicit,
silent=silent, force=force)
def save_all(self, *args, **kwargs):
"""Save all saveables."""
for saveable in self.saveables:
self.save(saveable, *args, **kwargs)
@pyqtSlot()
def autosave(self):
"""Slot used when the configs are auto-saved."""

View File

@ -171,12 +171,15 @@ def debug_cache_stats():
prefix_info = configdata.is_valid_prefix.cache_info()
# pylint: disable=protected-access
render_stylesheet_info = config._render_stylesheet.cache_info()
history_info = None
try:
from PyQt5.QtWebKit import QWebHistoryInterface
interface = QWebHistoryInterface.defaultInterface()
history_info = interface.historyContains.cache_info()
if interface is not None:
history_info = interface.historyContains.cache_info()
except ImportError:
history_info = None
pass
log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))

View File

@ -17,7 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Early initialization and main entry point."""
"""Early initialization and main entry point.
qutebrowser's initialization process roughly looks like this:
- This file gets imported, either via the setuptools entry point or
__main__.py.
- At import time, we check for the correct Python version and show an error if
it's too old.
- The main() function in this file gets invoked
- Argument parsing takes place
- earlyinit.early_init() gets invoked to do various low-level initialization
and checks whether all dependencies are met.
- app.run() gets called, which takes over.
See the docstring of app.py for details.
"""
import sys
import json

View File

@ -319,3 +319,18 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
quteproc_new.start(args)
assert quteproc_new.get_setting('ignore_case') == 'always'
@pytest.mark.no_xvfb
@pytest.mark.no_ci
def test_force_software_rendering(request, quteproc_new):
"""Make sure we can force software rendering with -s."""
if not request.config.webengine:
pytest.skip("Only runs with QtWebEngine")
args = (_base_args(request.config) +
['--temp-basedir', '-s', 'force_software_rendering', 'true'])
quteproc_new.start(args)
quteproc_new.open_path('chrome://gpu')
message = 'Canvas: Software only, hardware acceleration unavailable'
assert message in quteproc_new.get_content()

View File

@ -18,6 +18,7 @@
"""Tests for qutebrowser.config.configinit."""
import os
import sys
import logging
import unittest.mock
@ -25,23 +26,18 @@ import unittest.mock
import pytest
from qutebrowser import qutebrowser
from qutebrowser.config import (config, configdata, configexc, configfiles,
configinit)
from qutebrowser.config import config, configexc, configfiles, configinit
from qutebrowser.utils import objreg, usertypes
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
monkeypatch.setattr(configdata, 'DATA', None)
monkeypatch.setattr(configfiles, 'state', None)
monkeypatch.setattr(config, 'instance', None)
monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, 'change_filters', [])
monkeypatch.setattr(configinit, '_init_errors', None)
# Make sure we get no SSL warning
monkeypatch.setattr(configinit.earlyinit, 'check_backend_ssl_support',
lambda _backend: None)
yield
try:
objreg.delete('config-commands')
@ -49,10 +45,17 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
pass
@pytest.fixture
def args(fake_args):
"""Arguments needed for the config to init."""
fake_args.temp_settings = []
return fake_args
class TestEarlyInit:
@pytest.mark.parametrize('config_py', [True, 'error', False])
def test_config_py(self, init_patch, config_tmpdir, caplog, fake_args,
def test_config_py(self, init_patch, config_tmpdir, caplog, args,
config_py):
"""Test loading with only a config.py."""
config_py_file = config_tmpdir / 'config.py'
@ -65,7 +68,7 @@ class TestEarlyInit:
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(fake_args)
configinit.early_init(args)
# Check error messages
expected_errors = []
@ -95,7 +98,7 @@ class TestEarlyInit:
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', 'wrong-type',
False])
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, fake_args,
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
load_autoconfig, config_py, invalid_yaml):
"""Test interaction between config.py and autoconfig.yml."""
# pylint: disable=too-many-locals,too-many-branches
@ -122,7 +125,7 @@ class TestEarlyInit:
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(fake_args)
configinit.early_init(args)
# Check error messages
expected_errors = []
@ -161,16 +164,47 @@ class TestEarlyInit:
else:
assert config.instance._values == {'colors.hints.fg': 'magenta'}
def test_invalid_change_filter(self, init_patch, fake_args):
def test_invalid_change_filter(self, init_patch, args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
configinit.early_init(fake_args)
configinit.early_init(args)
def test_temp_settings_valid(self, init_patch, args):
args.temp_settings = [('colors.completion.fg', 'magenta')]
configinit.early_init(args)
assert config.instance._values['colors.completion.fg'] == 'magenta'
def test_temp_settings_invalid(self, caplog, init_patch, message_mock,
args):
"""Invalid temp settings should show an error."""
args.temp_settings = [('foo', 'bar')]
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
msg = message_mock.getmsg()
assert msg.level == usertypes.MessageLevel.error
assert msg.text == "set: NoOptionError - No option 'foo'"
assert 'colors.completion.fg' not in config.instance._values
def test_force_software_rendering(self, monkeypatch, init_patch, args):
"""Setting force_software_rendering should set the environment var."""
envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL'
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
monkeypatch.delenv(envvar, raising=False)
args.temp_settings = [('force_software_rendering', 'true')]
args.backend = 'webengine'
configinit.early_init(args)
assert os.environ[envvar] == '1'
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args,
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
mocker, errors):
configinit.early_init(fake_args)
configinit.early_init(args)
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
@ -242,33 +276,23 @@ class TestQtArgs:
assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
@pytest.mark.parametrize('arg, confval, can_import, is_new_webkit, used', [
@pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg
('webkit', 'auto', False, False, usertypes.Backend.QtWebKit),
# overridden by config
(None, 'webkit', False, False, usertypes.Backend.QtWebKit),
# WebKit available but too old
(None, 'auto', True, False, usertypes.Backend.QtWebEngine),
# WebKit available and new
(None, 'auto', True, True, usertypes.Backend.QtWebKit),
# WebKit unavailable
(None, 'auto', False, False, usertypes.Backend.QtWebEngine),
('webkit', 'webengine', usertypes.Backend.QtWebKit),
# set in config
(None, 'webkit', usertypes.Backend.QtWebKit),
])
def test_get_backend(monkeypatch, fake_args, config_stub,
arg, confval, can_import, is_new_webkit, used):
def test_get_backend(monkeypatch, args, config_stub,
arg, confval, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
if can_import:
return None
raise ImportError
fake_args.backend = arg
args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr(configinit.qtutils, 'is_new_qtwebkit',
lambda: is_new_webkit)
monkeypatch.setattr('builtins.__import__', fake_import)
assert configinit.get_backend(fake_args) == used
assert configinit.get_backend(args) == used

View File

@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy
import qutebrowser
from qutebrowser.misc import ipc
from qutebrowser.utils import objreg, standarddir, utils
from qutebrowser.utils import standarddir, utils
from helpers import stubs
@ -45,12 +45,8 @@ pytestmark = pytest.mark.usefixtures('qapp')
def shutdown_server():
"""If ipc.send_or_listen was called, make sure to shut server down."""
yield
try:
server = objreg.get('ipc-server')
except KeyError:
pass
else:
server.shutdown()
if ipc.server is not None:
ipc.server.shutdown()
@pytest.fixture
@ -609,13 +605,6 @@ class TestSendOrListen:
return self.Args(no_err_windows=True, basedir='/basedir/for/testing',
command=['test'], target=None)
@pytest.fixture(autouse=True)
def cleanup(self):
try:
objreg.delete('ipc-server')
except KeyError:
pass
@pytest.fixture
def qlocalserver_mock(self, mocker):
m = mocker.patch('qutebrowser.misc.ipc.QLocalServer', autospec=True)
@ -639,8 +628,7 @@ class TestSendOrListen:
assert isinstance(ret_server, ipc.IPCServer)
msgs = [e.message for e in caplog.records]
assert "Starting IPC server..." in msgs
objreg_server = objreg.get('ipc-server')
assert objreg_server is ret_server
assert ret_server is ipc.server
with qtbot.waitSignal(ret_server.got_args):
ret_client = ipc.send_or_listen(args)