From 6b80321083657e9e87b8780de578b409fd34408b Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 9 Mar 2025 16:38:05 +0100 Subject: [PATCH] feat: gracefully shut down on SIGINT/SIGTERM on Linux/mac (#6053) --- CHANGELOG.md | 1 + src/CMakeLists.txt | 2 + src/RunGui.cpp | 16 +++++++ src/util/UnixSignalHandler.cpp | 83 ++++++++++++++++++++++++++++++++++ src/util/UnixSignalHandler.hpp | 34 ++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 src/util/UnixSignalHandler.cpp create mode 100644 src/util/UnixSignalHandler.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 7efc2553..4c72631f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Dev: Subscriptions to PubSub channel points redemption topics now use no auth token, making it continue to work during PubSub shutdown. (#5947) - Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932, #5943, #5952, #5953, #5968, #5973, #5974, #5980, #5981, #5985, #5990, #5992, #5993, #5996, #5995, #6000, #6001, #6002, #6003, #6005, #6007, #6010, #6008, #6012, #6013, #6015, #6017, #6027, #6028, #6035, #6036, #6040, #6041, #6048) - Dev: Remove unneeded platform specifier for toasts. (#5914) +- Dev: Cleanly shutdown on `SIGINT`/`SIGTERM` on Linux & macOS. (#6053) - Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784) - Dev: Removed unused PubSub whisper code. (#5898) - Dev: Updated Conan dependencies. (#5776) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f666fc2..20757791 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -547,6 +547,8 @@ set(SOURCE_FILES util/Twitch.cpp util/Twitch.hpp util/TypeName.hpp + util/UnixSignalHandler.cpp + util/UnixSignalHandler.hpp util/Variant.hpp util/VectorMessageSink.cpp util/VectorMessageSink.hpp diff --git a/src/RunGui.cpp b/src/RunGui.cpp index ff77d6c4..3c820732 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -11,6 +11,7 @@ #include "singletons/Settings.hpp" #include "singletons/Updates.hpp" #include "util/CombinePath.hpp" +#include "util/UnixSignalHandler.hpp" #include "widgets/dialogs/LastRunCrashDialog.hpp" #include @@ -172,6 +173,21 @@ namespace { signal(SIGSEGV, handleSignal); #endif + +#if defined(Q_OS_UNIX) + auto *sigintHandler = new UnixSignalHandler(SIGINT); + QObject::connect(sigintHandler, &UnixSignalHandler::signalFired, [] { + qCInfo(chatterinoApp) + << "Received SIGINT, request application quit"; + QApplication::quit(); + }); + auto *sigtermHandler = new UnixSignalHandler(SIGTERM); + QObject::connect(sigtermHandler, &UnixSignalHandler::signalFired, [] { + qCInfo(chatterinoApp) + << "Received SIGTERM, request application quit"; + QApplication::quit(); + }); +#endif } // We delete cache files that haven't been modified in 14 days. This strategy may be diff --git a/src/util/UnixSignalHandler.cpp b/src/util/UnixSignalHandler.cpp new file mode 100644 index 00000000..3b7a2000 --- /dev/null +++ b/src/util/UnixSignalHandler.cpp @@ -0,0 +1,83 @@ +#include + +#if defined(Q_OS_UNIX) + +# include "common/QLogging.hpp" +# include "util/UnixSignalHandler.hpp" + +# include +# include +# include + +# include +# include + +namespace { + +using namespace chatterino; + +std::unordered_map> HANDLERS{}; + +} // namespace + +namespace chatterino { + +UnixSignalHandler::UnixSignalHandler(int signal) +{ + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, this->fd.data()) != 0) + { + qCWarning(chatterinoApp) + << "Failed to create UnixSignalHandler for" << signal; + return; + } + + this->socketNotifier = + new QSocketNotifier(this->fd[1], QSocketNotifier::Read, this); + + QObject::connect(this->socketNotifier, &QSocketNotifier::activated, this, + &UnixSignalHandler::handleSignal); + + struct sigaction action; + action.sa_handler = UnixSignalHandler::fired; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_flags |= SA_RESTART; + + if (sigaction(signal, &action, nullptr) != 0) + { + qCWarning(chatterinoApp) << "Failed to listen to signal for" << signal; + return; + } + + HANDLERS[signal] = QPointer(this); +} + +void UnixSignalHandler::fired(int signal) +{ + const auto &cls = HANDLERS[signal]; + assert(!cls.isNull()); + if (cls.isNull()) + { + qCWarning(chatterinoApp) + << "unix signal fired without a registered handler"; + return; + } + + char a = 1; + ::write(cls->fd[0], &a, sizeof(a)); +} + +void UnixSignalHandler::handleSignal() +{ + this->socketNotifier->setEnabled(false); + + char tmp{}; + ::read(this->fd[1], &tmp, sizeof(tmp)); + + this->socketNotifier->setEnabled(true); + + this->signalFired(); +} + +} // namespace chatterino +#endif diff --git a/src/util/UnixSignalHandler.hpp b/src/util/UnixSignalHandler.hpp new file mode 100644 index 00000000..96cd259b --- /dev/null +++ b/src/util/UnixSignalHandler.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#if defined(Q_OS_UNIX) +# include +# include + +# include + +namespace chatterino { + +class UnixSignalHandler : public QObject +{ + Q_OBJECT + +public: + UnixSignalHandler(int signal); + + static void fired(int signal); + +Q_SIGNALS: + void signalFired(); + +private: + void handleSignal(); + + std::array fd{}; + QSocketNotifier *socketNotifier; +}; + +} // namespace chatterino + +#endif