Merge pull request #8110 from tarneaux/main

Allow reloading config on SIGHUP
This commit is contained in:
toofar 2024-02-25 16:56:15 +13:00 committed by GitHub
commit 4f91fc4025
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 5 deletions

View File

@ -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
~~~~~~~

View File

@ -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,

View File

@ -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"
)