From 2bb8735fcb66fb453c79351e9809afb2408c487f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 13 Oct 2019 14:58:02 +0200 Subject: [PATCH] Rewrite misc.backendproblem to not use objreg This means 'quitter' can now use command_only=True See #640 --- mypy.ini | 4 + qutebrowser/app.py | 14 +- qutebrowser/misc/backendproblem.py | 568 +++++++++++++++-------------- 3 files changed, 308 insertions(+), 278 deletions(-) diff --git a/mypy.ini b/mypy.ini index 1397a7617..8399e39fe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -117,3 +117,7 @@ disallow_incomplete_defs = True [mypy-qutebrowser.utils.*] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.misc.backendproblem] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 781d584ff..c8d40f96a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -92,7 +92,7 @@ def run(args): args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-') quitter = Quitter(args) - objreg.register('quitter', quitter) + objreg.register('quitter', quitter, command_only=True) log.init.debug("Initializing directories...") standarddir.init(args) @@ -140,7 +140,7 @@ def run(args): process_pos_args(args, cwd=cwd, via_ipc=True, target_arg=target_arg)) - init(args, crash_handler) + init(args=args, crash_handler=crash_handler, quitter=quitter) ret = qt_mainloop() return ret @@ -154,12 +154,13 @@ def qt_mainloop(): return q_app.exec_() -def init(args, crash_handler): +def init(*, args, crash_handler, quitter): """Initialize everything. Args: args: The argparse namespace. crash_handler: The CrashHandler instance. + quitter: The Quitter instance. """ log.init.debug("Starting init...") @@ -171,7 +172,7 @@ def init(args, crash_handler): loader.init() loader.load_components() try: - _init_modules(args, crash_handler) + _init_modules(args=args, crash_handler=crash_handler, quitter=quitter) except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: error.handle_fatal_exc(e, args, "Error while initializing!", pre_text="Error while initializing") @@ -425,12 +426,13 @@ def open_desktopservices_url(url): tabbed_browser.tabopen(url) -def _init_modules(args, crash_handler): +def _init_modules(*, args, crash_handler, quitter): """Initialize all 'modules' which need to be initialized. Args: args: The argparse namespace. crash_handler: The CrashHandler instance. + quitter: The Quitter instance. """ log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(q_app) @@ -438,7 +440,7 @@ def _init_modules(args, crash_handler): configinit.late_init(save_manager) log.init.debug("Checking backend requirements...") - backendproblem.init() + backendproblem.init(quitter=quitter, args=args, save_manager=save_manager) log.init.debug("Initializing prompts...") prompt.init() diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 812c058ff..53f6a397e 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -27,6 +27,8 @@ import ctypes import ctypes.util import enum import shutil +import typing +import argparse import attr from PyQt5.QtCore import Qt @@ -35,9 +37,11 @@ from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout, from PyQt5.QtNetwork import QSslSocket from qutebrowser.config import config, configfiles -from qutebrowser.utils import (usertypes, objreg, version, qtutils, log, utils, +from qutebrowser.utils import (usertypes, version, qtutils, log, utils, standarddir) -from qutebrowser.misc import objects, msgbox +from qutebrowser.misc import objects, msgbox, savemanager +if typing.TYPE_CHECKING: + from qutebrowser import app class _Result(enum.IntEnum): @@ -133,8 +137,6 @@ 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) if setting == 'backend' and value == 'webkit': self.done(_Result.restart_webkit) @@ -144,147 +146,8 @@ class _Dialog(QDialog): self.done(_Result.restart) -def _show_dialog(*args, **kwargs): - """Show a dialog for a backend problem.""" - cmd_args = objreg.get('args') - if cmd_args.no_err_windows: - text = _error_text(*args, **kwargs) - print(text, file=sys.stderr) - sys.exit(usertypes.Exit.err_init) - - 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: - raise utils.Unreachable(status) - - sys.exit(usertypes.Exit.err_init) - - -def _nvidia_shader_workaround(): - """Work around QOpenGLShaderProgram issues. - - NOTE: This needs to be called before _handle_nouveau_graphics, or some - setups will segfault in version.opengl_vendor(). - - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 - """ - assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - - if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): - return - - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) - - -def _handle_nouveau_graphics(): - """Force software rendering when using the Nouveau driver. - - WORKAROUND for https://bugreports.qt.io/browse/QTBUG-41242 - Should be fixed in Qt 5.10 via https://codereview.qt-project.org/#/c/208664/ - """ - assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - - if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'): - return - - if qtutils.version_check('5.10', compiled=False): - return - - if version.opengl_vendor() != 'nouveau': - return - - if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or - # qt.force_software_rendering = 'software-opengl' - 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or - # qt.force_software_rendering = 'chromium', also see: - # https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1 - 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ): - return - - button = _Button("Force software rendering", 'qt.force_software_rendering', - 'chromium') - _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 noticeable performance impact " - "(depending on your hardware). " - "This sets the qt.force_software_rendering = 'chromium' " - "option (if you have a config.py file, you'll need to set " - "this manually).

", - buttons=[button], - ) - - raise utils.Unreachable - - -def _handle_wayland(): - assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - - if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'): - return - - platform = QApplication.instance().platformName() - if platform not in ['wayland', 'wayland-egl']: - return - - has_qt511 = qtutils.version_check('5.11', compiled=False) - if has_qt511 and config.val.qt.force_software_rendering == 'chromium': - return - - if qtutils.version_check('5.11.2', compiled=False): - return - - buttons = [] - text = "

You can work around this in one of the following ways:

" - - if 'DISPLAY' in os.environ: - # XWayland is available, but QT_QPA_PLATFORM=wayland is set - buttons.append(_Button("Force XWayland", 'qt.force_platform', 'xcb')) - text += ("

Force Qt to use XWayland

" - "

This allows you to use the newer QtWebEngine backend " - "(based on Chromium). " - "This sets the qt.force_platform = 'xcb' option " - "(if you have a config.py file, you'll need to set " - "this manually).

") - else: - text += ("

Set up XWayland

" - "

This allows you to use the newer QtWebEngine backend " - "(based on Chromium). ") - - if has_qt511: - buttons.append(_Button("Force software rendering", - 'qt.force_software_rendering', - 'chromium')) - text += ("

Forcing software rendering

" - "

This allows you to use the newer QtWebEngine backend " - "(based on Chromium) but could have noticeable performance " - "impact (depending on your hardware). This sets the " - "qt.force_software_rendering = 'chromium' option " - "(if you have a config.py file, you'll need to set " - "this manually).

") - - _show_dialog(backend=usertypes.Backend.QtWebEngine, - because="you're using Wayland", text=text, buttons=buttons) - - @attr.s -class BackendImports: +class _BackendImports: """Whether backend modules could be imported.""" @@ -294,146 +157,307 @@ class BackendImports: webengine_error = attr.ib(default=None) -def _try_import_backends(): - """Check whether backends can be imported and return BackendImports.""" - # pylint: disable=unused-import - results = BackendImports() +class _BackendProblemChecker: - 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 + """Check for various backend-specific issues.""" + + def __init__(self, *, + quitter: 'app.Quitter', + no_err_windows: bool, + save_manager: savemanager.SaveManager) -> None: + self._quitter = quitter + self._save_manager = save_manager + self._no_err_windows = no_err_windows + + def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None: + """Show a dialog for a backend problem.""" + if self._no_err_windows: + text = _error_text(*args, **kwargs) + print(text, file=sys.stderr) + sys.exit(usertypes.Exit.err_init) + + dialog = _Dialog(*args, **kwargs) + + status = dialog.exec_() + self._save_manager.save_all(is_exit=True) + + if status in [_Result.quit, QDialog.Rejected]: + pass + elif status == _Result.restart_webkit: + self._quitter.restart(override_args={'backend': 'webkit'}) + elif status == _Result.restart_webengine: + self._quitter.restart(override_args={'backend': 'webengine'}) + elif status == _Result.restart: + self._quitter.restart() else: - results.webkit_available = False - results.webkit_error = "Unsupported legacy QtWebKit found" + raise utils.Unreachable(status) - 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 _nvidia_shader_workaround(self) -> None: + """Work around QOpenGLShaderProgram issues. + NOTE: This needs to be called before _handle_nouveau_graphics, or some + setups will segfault in version.opengl_vendor(). -def _check_backend_modules(): - """Check for the modules needed for QtWebKit/QtWebEngine.""" - imports = _try_import_backends() + See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 + """ + self._assert_backend(usertypes.Backend.QtWebEngine) - 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:

".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: + if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): 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: + + libgl = ctypes.util.find_library("GL") + if libgl is not None: + ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + + def _handle_nouveau_graphics(self) -> None: + """Force software rendering when using the Nouveau driver. + + WORKAROUND for + https://bugreports.qt.io/browse/QTBUG-41242 + Should be fixed in Qt 5.10 via + https://codereview.qt-project.org/#/c/208664/ + """ + self._assert_backend(usertypes.Backend.QtWebEngine) + + if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'): return - assert imports.webkit_available - _show_dialog( + + if qtutils.version_check('5.10', compiled=False): + return + + if version.opengl_vendor() != 'nouveau': + return + + if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or + # qt.force_software_rendering = 'software-opengl' + 'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or + # qt.force_software_rendering = 'chromium', also see: + # https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1 + 'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ): + return + + button = _Button("Force software rendering", + 'qt.force_software_rendering', + 'chromium') + self._show_dialog( backend=usertypes.Backend.QtWebEngine, - because="QtWebEngine could not be imported", - text="

The error encountered was:
{}

".format( - html.escape(imports.webengine_error)) + 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 noticeable performance " + "impact (depending on your hardware). This sets the " + "qt.force_software_rendering = 'chromium' option " + "(if you have a config.py file, you'll need to set " + "this manually).

"), + buttons=[button], ) - raise utils.Unreachable + raise utils.Unreachable + + def _handle_wayland(self) -> None: + self._assert_backend(usertypes.Backend.QtWebEngine) + + if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'): + return + + platform = QApplication.instance().platformName() + if platform not in ['wayland', 'wayland-egl']: + return + + has_qt511 = qtutils.version_check('5.11', compiled=False) + if has_qt511 and config.val.qt.force_software_rendering == 'chromium': + return + + if qtutils.version_check('5.11.2', compiled=False): + return + + buttons = [] + text = "

You can work around this in one of the following ways:

" + + if 'DISPLAY' in os.environ: + # XWayland is available, but QT_QPA_PLATFORM=wayland is set + buttons.append( + _Button("Force XWayland", 'qt.force_platform', 'xcb')) + text += ("

Force Qt to use XWayland

" + "

This allows you to use the newer QtWebEngine backend " + "(based on Chromium). " + "This sets the qt.force_platform = 'xcb' option " + "(if you have a config.py file, you'll need to " + "set this manually).

") + else: + text += ("

Set up XWayland

" + "

This allows you to use the newer QtWebEngine backend " + "(based on Chromium). ") + + if has_qt511: + buttons.append(_Button("Force software rendering", + 'qt.force_software_rendering', + 'chromium')) + text += ("

Forcing software rendering

" + "

This allows you to use the newer QtWebEngine backend " + "(based on Chromium) but could have noticeable " + "performance impact (depending on your hardware). This " + "sets the qt.force_software_rendering = " + "'chromium' option (if you have a config.py " + "file, you'll need to set this manually).

") + + self._show_dialog(backend=usertypes.Backend.QtWebEngine, + because="you're using Wayland", + text=text, + buttons=buttons) + + def _try_import_backends(self) -> _BackendImports: + """Check whether backends can be imported and return BackendImports.""" + # pylint: disable=unused-import + 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(self, fatal: bool = False) -> None: + """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(self) -> None: + """Check for the modules needed for QtWebKit/QtWebEngine.""" + imports = self._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:

".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 + self._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 + self._show_dialog( + backend=usertypes.Backend.QtWebEngine, + because="QtWebEngine could not be imported", + text="

The error encountered was:
{}

".format( + html.escape(imports.webengine_error)) + ) + + raise utils.Unreachable + + def _handle_cache_nuking(self) -> None: + """Nuke the QtWebEngine cache if the Qt version changed. + + WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532 + """ + if not configfiles.state.qt_version_changed: + return + + # Only nuke the cache in cases where we know there are problems. + # It seems these issues started with Qt 5.12. + # They should be fixed with Qt 5.12.5: + # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408 + affected = (qtutils.version_check('5.12', compiled=False) and not + qtutils.version_check('5.12.5', compiled=False)) + if not affected: + return + + log.init.info("Qt version changed, nuking QtWebEngine cache") + cache_dir = os.path.join(standarddir.cache(), 'webengine') + if os.path.exists(cache_dir): + shutil.rmtree(cache_dir) + + def _assert_backend(self, backend: usertypes.Backend) -> None: + assert objects.backend == backend, objects.backend + + def check(self): + """Run all checks.""" + self._check_backend_modules() + if objects.backend == usertypes.Backend.QtWebEngine: + self._handle_ssl_support() + self._handle_wayland() + self._nvidia_shader_workaround() + self._handle_nouveau_graphics() + self._handle_cache_nuking() + else: + self._assert_backend(usertypes.Backend.QtWebKit) + self._handle_ssl_support(fatal=True) -def _handle_cache_nuking(): - """Nuke the QtWebEngine cache if the Qt version changed. - - WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532 - """ - if not configfiles.state.qt_version_changed: - return - - # Only nuke the cache in cases where we know there are problems. - # It seems these issues started with Qt 5.12. - # They should be fixed with Qt 5.12.5: - # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408 - affected = (qtutils.version_check('5.12', compiled=False) and not - qtutils.version_check('5.12.5', compiled=False)) - if not affected: - return - - log.init.info("Qt version changed, nuking QtWebEngine cache") - cache_dir = os.path.join(standarddir.cache(), 'webengine') - if os.path.exists(cache_dir): - shutil.rmtree(cache_dir) - - -def init(): - """Check for various issues related to QtWebKit/QtWebEngine.""" - _check_backend_modules() - if objects.backend == usertypes.Backend.QtWebEngine: - _handle_ssl_support() - _handle_wayland() - _nvidia_shader_workaround() - _handle_nouveau_graphics() - _handle_cache_nuking() - else: - assert objects.backend == usertypes.Backend.QtWebKit, objects.backend - _handle_ssl_support(fatal=True) +def init(*, quitter: 'app.Quitter', + args: argparse.Namespace, + save_manager: savemanager.SaveManager) -> None: + """Run all checks.""" + checker = _BackendProblemChecker(quitter=quitter, + no_err_windows=args.no_err_windows, + save_manager=save_manager) + checker.check()