diff options
author | toofar <toofar@spalge.com> | 2024-02-25 16:56:15 +1300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-25 16:56:15 +1300 |
commit | 4f91fc402555e176e47081ac3772dd6d60930c6b (patch) | |
tree | c3467d4a97765ceb3c1b3294e2c911a510c13be9 | |
parent | 42cf53ae7cc71ef107db026aada1428b73327d1a (diff) | |
parent | 145bfe4de0802e7eb21ef902e8e53544e996d3a4 (diff) | |
download | qutebrowser-4f91fc402555e176e47081ac3772dd6d60930c6b.tar.gz qutebrowser-4f91fc402555e176e47081ac3772dd6d60930c6b.zip |
Merge pull request #8110 from tarneaux/main
Allow reloading config on SIGHUP
-rw-r--r-- | doc/changelog.asciidoc | 6 | ||||
-rw-r--r-- | qutebrowser/misc/crashsignal.py | 29 | ||||
-rw-r--r-- | tests/unit/misc/test_crashsignal.py | 104 |
3 files changed, 134 insertions, 5 deletions
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 91b02b0da..89e33679e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -19,6 +19,12 @@ breaking changes (such as renamed commands) can happen in minor releases. v3.2.0 (unreleased) ------------------- +Added +~~~~~ + +- When qutebrowser receives a SIGHUP it will now reload any config.py file + in use (same as the `:config-source` command does). (#8108) + Changed ~~~~~~~ diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index c69dcbe29..05e5806df 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -22,8 +22,9 @@ from qutebrowser.qt.core import (pyqtSlot, qInstallMessageHandler, QObject, from qutebrowser.qt.widgets import QApplication from qutebrowser.api import cmdutils +from qutebrowser.config import configfiles, configexc from qutebrowser.misc import earlyinit, crashdialog, ipc, objects -from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils +from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils, message from qutebrowser.qt import sip if TYPE_CHECKING: from qutebrowser.misc import quitter @@ -322,6 +323,17 @@ class SignalHandler(QObject): self._activated = False self._orig_wakeup_fd: Optional[int] = None + self._handlers = { + signal.SIGINT: self.interrupt, + signal.SIGTERM: self.interrupt, + } + platform_dependant_handlers = { + "SIGHUP": self.reload_config, + } + for sig_str, handler in platform_dependant_handlers.items(): + if hasattr(signal.Signals, sig_str): + self._handlers[signal.Signals[sig_str]] = handler + def activate(self): """Set up signal handlers. @@ -331,10 +343,8 @@ class SignalHandler(QObject): On Unix, it uses a QSocketNotifier with os.set_wakeup_fd to get notified. """ - self._orig_handlers[signal.SIGINT] = signal.signal( - signal.SIGINT, self.interrupt) - self._orig_handlers[signal.SIGTERM] = signal.signal( - signal.SIGTERM, self.interrupt) + for sig, handler in self._handlers.items(): + self._orig_handlers[sig] = signal.signal(sig, handler) if utils.is_posix and hasattr(signal, 'set_wakeup_fd'): # pylint: disable=import-error,no-member,useless-suppression @@ -430,6 +440,15 @@ class SignalHandler(QObject): print("WHY ARE YOU DOING THIS TO ME? :(") sys.exit(128 + signum) + def reload_config(self, _signum, _frame): + """Reload the config.""" + log.signals.info("SIGHUP received, reloading config.") + filename = standarddir.config_py() + try: + configfiles.read_config_py(filename) + except configexc.ConfigFileErrors as e: + message.error(str(e)) + def init(q_app: QApplication, args: argparse.Namespace, diff --git a/tests/unit/misc/test_crashsignal.py b/tests/unit/misc/test_crashsignal.py new file mode 100644 index 000000000..7019118e5 --- /dev/null +++ b/tests/unit/misc/test_crashsignal.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for qutebrowser.misc.crashsignal.""" + +import signal + +import pytest + +from qutebrowser.config import configexc +from qutebrowser.qt.widgets import QApplication +from qutebrowser.misc import crashsignal, quitter + + +@pytest.fixture +def read_config_mock(mocker): + # covers reload_config + mocker.patch.object( + crashsignal.standarddir, + "config_py", + return_value="config.py-unittest", + ) + return mocker.patch.object( + crashsignal.configfiles, + "read_config_py", + autospec=True, + ) + + +@pytest.fixture +def signal_handler(qtbot, mocker, read_config_mock): + """Signal handler instance with all external methods mocked out.""" + # covers init + mocker.patch.object(crashsignal.sys, "exit", autospec=True) + signal_handler = crashsignal.SignalHandler( + app=mocker.Mock(spec=QApplication), + quitter=mocker.Mock(spec=quitter.Quitter), + ) + + return signal_handler + + +def test_handlers_registered(signal_handler): + signal_handler.activate() + + for sig, handler in signal_handler._handlers.items(): + registered = signal.signal(sig, signal.SIG_DFL) + assert registered == handler + + +def test_handlers_deregistered(signal_handler): + known_handler = lambda *_args: None + for sig in signal_handler._handlers: + signal.signal(sig, known_handler) + + signal_handler.activate() + signal_handler.deactivate() + + for sig in signal_handler._handlers: + registered = signal.signal(sig, signal.SIG_DFL) + assert registered == known_handler + + +def test_interrupt_repeatedly(signal_handler): + signal_handler.activate() + test_signal = signal.SIGINT + + expected_handlers = [ + signal_handler.interrupt, + signal_handler.interrupt_forcefully, + signal_handler.interrupt_really_forcefully, + ] + + # Call the SIGINT handler multiple times and make sure it calls the + # expected sequence of functions. + for expected in expected_handlers: + registered = signal.signal(test_signal, signal.SIG_DFL) + assert registered == expected + expected(test_signal, None) + + +@pytest.mark.posix +def test_reload_config_call_on_hup(signal_handler, read_config_mock): + signal_handler._handlers[signal.SIGHUP](None, None) + + read_config_mock.assert_called_once_with("config.py-unittest") + + +@pytest.mark.posix +def test_reload_config_displays_errors(signal_handler, read_config_mock, mocker): + read_config_mock.side_effect = configexc.ConfigFileErrors( + "config.py", + [ + configexc.ConfigErrorDesc("no config.py", ValueError("asdf")) + ] + ) + message_mock = mocker.patch.object(crashsignal.message, "error") + + signal_handler._handlers[signal.SIGHUP](None, None) + + message_mock.assert_called_once_with( + "Errors occurred while reading config.py:\n no config.py: asdf" + ) |