From ca4a9975591c0f39d3f977bee5812563fbbe5885 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 Sep 2017 10:33:16 +0200 Subject: [PATCH 01/19] Update settings for QtWebEngine by default See #2335 --- doc/help/settings.asciidoc | 11 +++++------ qutebrowser/config/configdata.yml | 19 +++++++++---------- qutebrowser/config/configinit.py | 13 +------------ 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index e08869903..36159b2db 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -291,18 +291,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: <> 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 diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 2055e518d..c597fb188 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -105,23 +105,22 @@ 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 diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 3ec98a293..aa622850c 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -74,13 +74,6 @@ def early_init(args): 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 +81,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): From 093f34183c1b2a51c6c6e38d20c1c9e19a3ae909 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 Sep 2017 22:39:49 +0200 Subject: [PATCH 02/19] Add improved checks for Nouveau/Wayland for QtWebEngine Closes #2368 Closes #2932 See #2335 --- doc/help/settings.asciidoc | 17 ++ qutebrowser/app.py | 12 +- qutebrowser/browser/webengine/webenginetab.py | 12 +- qutebrowser/config/configdata.yml | 9 + qutebrowser/misc/backendproblem.py | 182 ++++++++++++++++++ 5 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 qutebrowser/misc/backendproblem.py diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 36159b2db..c2a7f55fb 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -179,6 +179,7 @@ |<>|The default font size for fixed-pitch text. |<>|The hard minimum font size. |<>|The minimum logical font size that is applied when zooming out. +|<>|Force software rendering for QtWebEngine. |<>|Controls when a hint can be automatically followed without pressing Enter. |<>|A timeout (in milliseconds) to ignore normal-mode key bindings after a successful auto-follow. |<>|CSS border value for hints. @@ -2262,6 +2263,22 @@ Type: <> 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: <> + +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. diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 85f0e9410..b8649b7fb 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -54,7 +54,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 @@ -389,14 +390,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() diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 5c2533422..707ea10c3 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -38,7 +38,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 +50,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) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index c597fb188..9a3574282 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -101,6 +101,15 @@ 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 diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py new file mode 100644 index 000000000..15d27212f --- /dev/null +++ b/qutebrowser/misc/backendproblem.py @@ -0,0 +1,182 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 Florian Bruhin (The Compiler) +# +# 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 . + +"""Dialogs shown when there was a problem with a backend choice.""" + +import os +import sys +import functools + +import attr +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout, + QVBoxLayout, QLabel) + +from qutebrowser.config import config +from qutebrowser.utils import usertypes, objreg, version +from qutebrowser.misc import objects + + +_Result = usertypes.enum('_Result', ['quit', 'restart'], 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( + "Failed to start with the {backend} backend!" + "

qutebrowser tried to start with the {backend} backend but " + "failed because {because}.

{text}" + "

Forcing the {other_backend.name} backend

" + "

This forces usage of the {other_backend.name} backend. " + "This sets the backend = '{other_setting}' setting " + "(if you have a config.py file, you'll need to set " + "this manually).

".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) + self.done(_Result.restart) + + +def _show_dialog(*args, **kwargs): + """Show a dialog for a backend problem.""" + dialog = _Dialog(*args, **kwargs) + + status = dialog.exec_() + + if status == _Result.quit: + sys.exit(usertypes.Exit.err_init) + elif status == _Result.restart: + # FIXME pass --backend webengine + quitter = objreg.get('quitter') + quitter.restart() + sys.exit(usertypes.Exit.err_init) + + +def _handle_nouveau_graphics(): + force_sw_var = 'QT_XCB_FORCE_SOFTWARE_OPENGL' + + if version.opengl_vendor() != 'nouveau': + return + + if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or + force_sw_var in os.environ): + return + + if config.force_software_rendering: + os.environ[force_sw_var] = '1' + return + + button = _Button("Force software rendering", 'force_software_rendering', + True) + _show_dialog( + backend=usertypes.Backend.QtWebEngine, + because="you're using Nouveau graphics", + text="

There are two ways to fix this:

" + "

Forcing software rendering

" + "

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 force_software_rendering = True setting " + "(if you have a config.py file, you'll need to set this " + "manually).

", + buttons=[button], + ) + + # Should never be reached + assert False + + +def _handle_wayland(): + if QApplication.instance().platformName() not in ['wayland', 'wayland-egl']: + return + if os.environ.get('DISPLAY'): + # When DISPLAY is set but with the wayland/wayland-egl platform plugin, + # QtWebEngine will do the right hting. + return + + _show_dialog( + backend=usertypes.Backend.QtWebEngine, + because="you're using Wayland", + text="

There are two ways to fix this:

" + "

Set up XWayland

" + "

This allows you to use the newer QtWebEngine backend (based " + "on Chromium). " + ) + + # Should never be reached + assert False + + +def init(): + if objects.backend == usertypes.Backend.QtWebEngine: + _handle_wayland() + _handle_nouveau_graphics() + else: + assert objects.backend == usertypes.Backend.QtWebKit, objects.backend From fa902c5d82fd6366289fe8e9d13d72ba4dfc8a85 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 Sep 2017 23:04:18 +0200 Subject: [PATCH 03/19] Improve error dialogs when QtWebKit/QtWebEngine was not found --- qutebrowser/misc/backendproblem.py | 113 +++++++++++++++++++++++++++-- qutebrowser/misc/earlyinit.py | 31 -------- qutebrowser/misc/savemanager.py | 5 ++ 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 15d27212f..caa21f97d 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -22,15 +22,16 @@ 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) + QVBoxLayout, QLabel, QMessageBox) from qutebrowser.config import config -from qutebrowser.utils import usertypes, objreg, version -from qutebrowser.misc import objects +from qutebrowser.utils import usertypes, objreg, version, qtutils +from qutebrowser.misc import objects, msgbox _Result = usertypes.enum('_Result', ['quit', 'restart'], is_int=True, @@ -67,8 +68,8 @@ class _Dialog(QDialog): "

qutebrowser tried to start with the {backend} backend but " "failed because {because}.

{text}" "

Forcing the {other_backend.name} backend

" - "

This forces usage of the {other_backend.name} backend. " - "This sets the backend = '{other_setting}' setting " + "

This forces usage of the {other_backend.name} backend by " + "setting the backend = '{other_setting}' option " "(if you have a config.py file, you'll need to set " "this manually).

".format( backend=backend.name, because=because, text=text, @@ -101,6 +102,8 @@ class _Dialog(QDialog): 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) self.done(_Result.restart) @@ -110,13 +113,15 @@ def _show_dialog(*args, **kwargs): status = dialog.exec_() - if status == _Result.quit: + if status in [_Result.quit, QDialog.Rejected]: sys.exit(usertypes.Exit.err_init) elif status == _Result.restart: # FIXME pass --backend webengine quitter = objreg.get('quitter') quitter.restart() sys.exit(usertypes.Exit.err_init) + else: + assert False, status def _handle_nouveau_graphics(): @@ -143,7 +148,7 @@ def _handle_nouveau_graphics(): "

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 force_software_rendering = True setting " + "This sets the force_software_rendering = True option " "(if you have a config.py file, you'll need to set this " "manually).

", buttons=[button], @@ -174,7 +179,101 @@ def _handle_wayland(): 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.""" + 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 _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 = ("

qutebrowser needs QtWebKit or QtWebEngine, but neither " + "could be imported!

" + "

The errors encountered were:

    " + "
  • QtWebKit: {webkit_error}" + "
  • QtWebEngine: {webengine_error}" + "

".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="

The error encountered was:
{}

".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="

The error encountered was:
{}

".format( + html.escape(imports.webengine_error)) + ) + + # Should never be reached + assert False + + def init(): + _check_backend_modules() if objects.backend == usertypes.Backend.QtWebEngine: _handle_wayland() _handle_nouveau_graphics() diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index c2ab454ac..f481f4dba 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -247,35 +247,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. @@ -338,6 +309,4 @@ def init_with_backend(backend): """ assert not isinstance(backend, str), backend assert backend is not None - check_backend_libraries(backend) check_backend_ssl_support(backend) - check_new_webkit(backend) diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index ddda5325b..02001902c 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -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.""" From defcf5394a035e8606359c8d6b6a8abc60014cd6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:11:26 +0200 Subject: [PATCH 04/19] Move SSL backend checking to backendproblem.py --- qutebrowser/config/configinit.py | 1 - qutebrowser/misc/backendproblem.py | 31 +++++++++++++++++++++++++++++- qutebrowser/misc/earlyinit.py | 28 --------------------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index aa622850c..1b36fd675 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -69,7 +69,6 @@ def early_init(args): configfiles.init() objects.backend = get_backend(args) - earlyinit.init_with_backend(objects.backend) def get_backend(args): diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index caa21f97d..fdd13346c 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -28,9 +28,10 @@ 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 +from qutebrowser.utils import usertypes, objreg, version, qtutils, log from qutebrowser.misc import objects, msgbox @@ -225,6 +226,32 @@ def _try_import_backends(): 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() @@ -275,7 +302,9 @@ def _check_backend_modules(): 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) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index f481f4dba..ca12cd901 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -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 @@ -299,14 +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_ssl_support(backend) From e5958e6061029eb4d357d3f013277764f500ffd7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:30:28 +0200 Subject: [PATCH 05/19] Override --backend argument from backend problem dialog --- qutebrowser/app.py | 11 ++++++++--- qutebrowser/misc/backendproblem.py | 26 +++++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index b8649b7fb..05420a9e4 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -513,12 +513,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. @@ -569,6 +570,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] @@ -593,7 +597,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 @@ -606,6 +610,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. @@ -621,7 +626,7 @@ class Quitter: session_manager.save(session, with_private=True) # 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: diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index fdd13346c..806c32a36 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -35,8 +35,10 @@ from qutebrowser.utils import usertypes, objreg, version, qtutils, log from qutebrowser.misc import objects, msgbox -_Result = usertypes.enum('_Result', ['quit', 'restart'], is_int=True, - start=QDialog.Accepted + 1) +_Result = usertypes.enum( + '_Result', + ['quit', 'restart', 'restart_webkit', 'restart_webengine'], + is_int=True, start=QDialog.Accepted + 1) @attr.s @@ -105,7 +107,13 @@ class _Dialog(QDialog): config.instance.set_obj(setting, value, save_yaml=True) save_manager = objreg.get('save-manager') save_manager.save_all(is_exit=True) - self.done(_Result.restart) + + 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): @@ -113,17 +121,21 @@ def _show_dialog(*args, **kwargs): dialog = _Dialog(*args, **kwargs) status = dialog.exec_() + quitter = objreg.get('quitter') if status in [_Result.quit, QDialog.Rejected]: - sys.exit(usertypes.Exit.err_init) + 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: - # FIXME pass --backend webengine - quitter = objreg.get('quitter') quitter.restart() - sys.exit(usertypes.Exit.err_init) else: assert False, status + sys.exit(usertypes.Exit.err_init) + def _handle_nouveau_graphics(): force_sw_var = 'QT_XCB_FORCE_SOFTWARE_OPENGL' From ce0622e38a544037507ddf8eddf2607bbc912f20 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:40:48 +0200 Subject: [PATCH 06/19] Document how initialization roughly works --- qutebrowser/app.py | 20 +++++++++++++++++++- qutebrowser/qutebrowser.py | 16 +++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 05420a9e4..c0dc80619 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -17,7 +17,25 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""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 diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index a2066acd3..8c80dfc22 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -17,7 +17,21 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""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 From b906c862bb56da81304d8afdff34feb49f254c3b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:52:32 +0200 Subject: [PATCH 07/19] Remove ipc-server from objreg --- qutebrowser/app.py | 8 +++++++- qutebrowser/misc/crashsignal.py | 4 ++-- qutebrowser/misc/ipc.py | 8 ++++++-- tests/unit/misc/test_ipc.py | 20 ++++---------------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index c0dc80619..00c8f98d8 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -638,10 +638,16 @@ 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, override_args) @@ -732,7 +738,7 @@ class Quitter: QApplication.closeAllWindows() # Shut down IPC try: - objreg.get('ipc-server').shutdown() + ipc.server.shutdown() except KeyError: pass # Save everything diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 186bd9103..9899cfcd3 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -39,7 +39,7 @@ from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, from PyQt5.QtWidgets import QApplication, QDialog 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") diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index e308ac8a0..c8c9d83b7 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -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()] @@ -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... diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 874419511..b515535bb 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -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) From c77cff3fcbd0f0f86c2cac3e81af111185d1a2c4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:56:05 +0200 Subject: [PATCH 08/19] Also fail with DISPLAY with wayland platform plugin QtWebEngine spews errors at us, and while it seems to work with Weston for some reason (despite errors logged), it doesn't with sway. --- qutebrowser/misc/backendproblem.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 806c32a36..7f3703be9 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -174,10 +174,6 @@ def _handle_nouveau_graphics(): def _handle_wayland(): if QApplication.instance().platformName() not in ['wayland', 'wayland-egl']: return - if os.environ.get('DISPLAY'): - # When DISPLAY is set but with the wayland/wayland-egl platform plugin, - # QtWebEngine will do the right hting. - return _show_dialog( backend=usertypes.Backend.QtWebEngine, From f077f52997702a64deba54abcbfbe193d5211959 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 08:57:30 +0200 Subject: [PATCH 09/19] Add asserts for the backend --- qutebrowser/misc/backendproblem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 7f3703be9..bfe2f7cb0 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -138,6 +138,7 @@ def _show_dialog(*args, **kwargs): def _handle_nouveau_graphics(): + assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend force_sw_var = 'QT_XCB_FORCE_SOFTWARE_OPENGL' if version.opengl_vendor() != 'nouveau': @@ -172,6 +173,8 @@ def _handle_nouveau_graphics(): def _handle_wayland(): + assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend + if QApplication.instance().platformName() not in ['wayland', 'wayland-egl']: return From 35beb84e856bacd9e16f8c0dd7d334bebfcd6a60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 09:32:23 +0200 Subject: [PATCH 10/19] Fix tests and lint --- qutebrowser/app.py | 4 ++-- qutebrowser/browser/webengine/webenginetab.py | 1 - qutebrowser/config/configinit.py | 4 ++-- qutebrowser/misc/backendproblem.py | 6 +++-- qutebrowser/misc/crashsignal.py | 2 +- qutebrowser/misc/ipc.py | 8 +++---- qutebrowser/qutebrowser.py | 4 ++-- tests/unit/config/test_configinit.py | 23 ++++--------------- 8 files changed, 20 insertions(+), 32 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 00c8f98d8..04eff1925 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -644,8 +644,8 @@ class Quitter: 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. + # 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 diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 707ea10c3..4de4bf26f 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -19,7 +19,6 @@ """Wrapper over a QWebEngineView.""" -import os import math import functools import html as html_utils diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 1b36fd675..a046395c3 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -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 +from qutebrowser.misc import msgbox, objects # Error which happened during init, so we can show a message box. diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index bfe2f7cb0..6bc10ba79 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -148,7 +148,7 @@ def _handle_nouveau_graphics(): force_sw_var in os.environ): return - if config.force_software_rendering: + if config.val.force_software_rendering: os.environ[force_sw_var] = '1' return @@ -175,7 +175,8 @@ def _handle_nouveau_graphics(): def _handle_wayland(): assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - if QApplication.instance().platformName() not in ['wayland', 'wayland-egl']: + platform = QApplication.instance().platformName() + if platform not in ['wayland', 'wayland-egl']: return _show_dialog( @@ -204,6 +205,7 @@ class BackendImports: def _try_import_backends(): """Check whether backends can be imported and return BackendImports.""" + # pylint: disable=unused-variable results = BackendImports() try: diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 9899cfcd3..b90eae829 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -36,7 +36,7 @@ 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, ipc diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index c8c9d83b7..c9f982365 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -113,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( diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 8c80dfc22..1b1cfb013 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -27,8 +27,8 @@ qutebrowser's initialization process roughly looks like this: 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. +- 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. """ diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 1332499de..02b87ebda 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -39,9 +39,6 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, 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') @@ -242,33 +239,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): + 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 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 From 6770a474c4801e7f9305903e49e650225ce971a1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 09:52:56 +0200 Subject: [PATCH 11/19] Force software rendering earlier We need to do this before a QApplication exists --- qutebrowser/config/configinit.py | 4 ++++ qutebrowser/misc/backendproblem.py | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index a046395c3..6b4553626 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -70,6 +70,10 @@ def early_init(args): objects.backend = get_backend(args) + 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.""" diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 6bc10ba79..3cd9e5abd 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -139,17 +139,12 @@ def _show_dialog(*args, **kwargs): def _handle_nouveau_graphics(): assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - force_sw_var = 'QT_XCB_FORCE_SOFTWARE_OPENGL' if version.opengl_vendor() != 'nouveau': return if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or - force_sw_var in os.environ): - return - - if config.val.force_software_rendering: - os.environ[force_sw_var] = '1' + 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ): return button = _Button("Force software rendering", 'force_software_rendering', From 45c6ffe9919748fcfd20f88d99a41b13d88ff149 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 10:04:47 +0200 Subject: [PATCH 12/19] Add a test for force_software_rendering --- tests/end2end/test_invocations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 03626dc14..aa28c91f7 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -319,3 +319,14 @@ 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 +def test_force_software_rendering(request, quteproc_new): + """Make sure we can force software rendering with -s.""" + 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() From 865fc2e0dee5ed8cf2df66a87ab8f3d068bd4dc0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 10:10:14 +0200 Subject: [PATCH 13/19] Handle -s argument earlier This makes sure we can e.g. set software_rendering via -s --- qutebrowser/app.py | 6 ------ qutebrowser/config/configinit.py | 8 +++++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 04eff1925..b3e299a80 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -207,12 +207,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') diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 6b4553626..2bbed56e5 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QMessageBox from qutebrowser.config import (config, configdata, configfiles, configtypes, configexc) -from qutebrowser.utils import objreg, usertypes, log, standarddir +from qutebrowser.utils import objreg, usertypes, log, standarddir, message from qutebrowser.misc import msgbox, objects @@ -70,6 +70,12 @@ def early_init(args): objects.backend = get_backend(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 (objects.backend == usertypes.Backend.QtWebEngine and config.val.force_software_rendering): os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1' From 3be0a78819cf358e932605e66be684162b4e2ec9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 11:05:53 +0200 Subject: [PATCH 14/19] Fix configinit tests --- tests/unit/config/test_configinit.py | 61 ++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 02b87ebda..811ef80b9 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -18,6 +18,7 @@ """Tests for qutebrowser.config.configinit.""" +import os import sys import logging import unittest.mock @@ -33,7 +34,6 @@ 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) @@ -46,10 +46,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' @@ -62,7 +69,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 = [] @@ -92,7 +99,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 @@ -119,7 +126,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 = [] @@ -158,16 +165,46 @@ 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')] + + 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]) @@ -245,7 +282,7 @@ class TestQtArgs: # set in config (None, 'webkit', usertypes.Backend.QtWebKit), ]) -def test_get_backend(monkeypatch, fake_args, config_stub, +def test_get_backend(monkeypatch, args, config_stub, arg, confval, used): real_import = __import__ @@ -254,8 +291,8 @@ def test_get_backend(monkeypatch, fake_args, config_stub, return real_import(name, *args, **kwargs) raise ImportError - fake_args.backend = arg + args.backend = arg config_stub.val.backend = confval monkeypatch.setattr('builtins.__import__', fake_import) - assert configinit.get_backend(fake_args) == used + assert configinit.get_backend(args) == used From 6c25e966214d2224fb7c8491ed2e50c3bf880175 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 11:38:52 +0200 Subject: [PATCH 15/19] Remove unused imports --- qutebrowser/app.py | 3 +-- tests/unit/config/test_configinit.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index b3e299a80..d2efb21e5 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -62,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 diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 811ef80b9..92a5308c6 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -26,8 +26,7 @@ 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 From 6496442503c07c10ba4a7c61094f4f2fd949aaeb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 11:42:02 +0200 Subject: [PATCH 16/19] Skip test_force_software_rendering on CI We can't be sure we have hardware acceleration there --- tests/end2end/test_invocations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index aa28c91f7..5ebd5568f 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -322,6 +322,7 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): @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.""" args = (_base_args(request.config) + From 45db0eaccb888108561d7943ebd9fa199211dee6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 11:44:21 +0200 Subject: [PATCH 17/19] Really force QtWebEngine for test_force_software_rendering init --- tests/unit/config/test_configinit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 92a5308c6..4a8c05dae 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -194,6 +194,7 @@ class TestEarlyInit: usertypes.Backend.QtWebEngine) monkeypatch.delenv(envvar, raising=False) args.temp_settings = [('force_software_rendering', 'true')] + args.backend = 'webengine' configinit.early_init(args) From 4b9bbaa04d528085aad43e3347a8d2405cdd1295 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 17:30:53 +0200 Subject: [PATCH 18/19] Skip test_force_software_rendering with QtWebKit --- tests/end2end/test_invocations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 5ebd5568f..ecb824f95 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -325,6 +325,9 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): @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) From 9d963d55f5dfc46a2a7f35c2922a46f8a16fee01 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Sep 2017 17:42:21 +0200 Subject: [PATCH 19/19] Fix :debug-cache-stats with QtWebEngine When we use --backend webengine, the QtWebKit stuff might be importable, but the history still isn't initialized because of that. --- qutebrowser/misc/utilcmds.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 4b6909344..5c85aae10 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -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))