Merge pull request #8110 from tarneaux/main
Allow reloading config on SIGHUP
This commit is contained in:
commit
4f91fc4025
|
|
@ -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
|
||||
~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
Loading…
Reference in New Issue