From 1d8297744172e3da38940fa2d92dc266b4d89477 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 3 Jan 2026 21:42:38 +0100 Subject: [PATCH] Exit on SIGTERM --- CMakeLists.txt | 4 + src/core/mainwindow.h | 2 +- src/core/unixsignalwatcher.cpp | 168 +++++++++++++++++++++++++++++++++ src/core/unixsignalwatcher.h | 53 +++++++++++ src/main.cpp | 12 ++- 5 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/core/unixsignalwatcher.cpp create mode 100644 src/core/unixsignalwatcher.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4da7792c8..5f1f09b37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1218,6 +1218,10 @@ set(UI src/device/deviceviewcontainer.ui ) +if(UNIX) + optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h) +endif() + if(APPLE) optional_source(APPLE SOURCES diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 9fea5527e..e4a0791d8 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -245,7 +245,6 @@ class MainWindow : public QMainWindow, public PlatformInterface { void ToggleSearchCoverAuto(const bool checked); void SaveGeometry(); - void Exit(); void DoExit(); void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2); @@ -280,6 +279,7 @@ class MainWindow : public QMainWindow, public PlatformInterface { public Q_SLOTS: void CommandlineOptionsReceived(const QByteArray &string_options); void Raise(); + void Exit(); private: void SaveSettings(); diff --git a/src/core/unixsignalwatcher.cpp b/src/core/unixsignalwatcher.cpp new file mode 100644 index 000000000..8539fddc4 --- /dev/null +++ b/src/core/unixsignalwatcher.cpp @@ -0,0 +1,168 @@ +/* + * Strawberry Music Player + * Copyright 2026, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "core/logging.h" +#include "unixsignalwatcher.h" + +UnixSignalWatcher *UnixSignalWatcher::sInstance = nullptr; + +UnixSignalWatcher::UnixSignalWatcher(QObject *parent) + : QObject(parent), + signal_fd_{-1, -1}, + socket_notifier_(nullptr) { + + Q_ASSERT(!sInstance); + + // Create a socket pair for the self-pipe trick + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signal_fd_) != 0) { + qLog(Error) << "Failed to create socket pair for signal handling:" << ::strerror(errno); + return; + } + + Q_ASSERT(signal_fd_[0] != -1); + + // Set the read end to non-blocking mode + // Non-blocking mode is important to prevent HandleSignalNotification from blocking + int flags = ::fcntl(signal_fd_[0], F_GETFL, 0); + if (flags == -1) { + qLog(Error) << "Failed to get socket flags:" << ::strerror(errno); + } + else if (::fcntl(signal_fd_[0], F_SETFL, flags | O_NONBLOCK) == -1) { + qLog(Error) << "Failed to set socket to non-blocking:" << ::strerror(errno); + } + + // Set the write end to non-blocking mode as well (used in signal handler) + // Non-blocking mode prevents the signal handler from blocking if buffer is full + flags = ::fcntl(signal_fd_[1], F_GETFL, 0); + if (flags == -1) { + qLog(Error) << "Failed to get socket flags for write end:" << ::strerror(errno); + } + else if (::fcntl(signal_fd_[1], F_SETFL, flags | O_NONBLOCK) == -1) { + qLog(Error) << "Failed to set write end of socket to non-blocking:" << ::strerror(errno); + } + + // Set up QSocketNotifier to monitor the read end of the socket + socket_notifier_ = new QSocketNotifier(signal_fd_[0], QSocketNotifier::Read, this); + QObject::connect(socket_notifier_, &QSocketNotifier::activated, this, &UnixSignalWatcher::HandleSignalNotification); + + sInstance = this; + +} + +UnixSignalWatcher::~UnixSignalWatcher() { + + if (socket_notifier_) { + socket_notifier_->setEnabled(false); + } + + // Restore original signal handlers + for (int i = 0; i < watched_signals_.size(); ++i) { + if (::sigaction(watched_signals_[i], &original_signal_actions_[i], nullptr) != 0) { + qLog(Error) << "Failed to restore signal handler for signal" << watched_signals_[i] << ":" << ::strerror(errno); + } + } + + if (signal_fd_[0] != -1) { + ::close(signal_fd_[0]); + signal_fd_[0] = -1; + } + if (signal_fd_[1] != -1) { + ::close(signal_fd_[1]); + signal_fd_[1] = -1; + } + + sInstance = nullptr; + +} + +void UnixSignalWatcher::WatchForSignal(const int signal) { + + // Check if socket pair was created successfully + if (signal_fd_[0] == -1 || signal_fd_[1] == -1) { + qLog(Error) << "Cannot watch for signal: socket pair not initialized"; + return; + } + + if (watched_signals_.contains(signal)) { + qLog(Error) << "Already watching for signal" << signal; + return; + } + + struct sigaction signal_action{}; + ::memset(&signal_action, 0, sizeof(signal_action)); + sigemptyset(&signal_action.sa_mask); + signal_action.sa_handler = UnixSignalWatcher::SignalHandler; + signal_action.sa_flags = SA_RESTART; + + struct sigaction old_signal_action{}; + ::memset(&old_signal_action, 0, sizeof(old_signal_action)); + if (::sigaction(signal, &signal_action, &old_signal_action) != 0) { + qLog(Error) << "sigaction error:" << ::strerror(errno); + return; + } + + watched_signals_ << signal; + original_signal_actions_ << old_signal_action; + +} + +void UnixSignalWatcher::SignalHandler(const int signal) { + + if (!sInstance || sInstance->signal_fd_[1] == -1) { + return; + } + + // Write the signal number to the socket pair (async-signal-safe) + // This is the only operation we perform in the signal handler + // Ignore errors as there's nothing we can safely do about them in a signal handler + (void)::write(sInstance->signal_fd_[1], &signal, sizeof(signal)); + +} + +void UnixSignalWatcher::HandleSignalNotification() { + + // Read all pending signals from the socket + // Multiple signals could arrive before the notifier triggers + while (true) { + int signal = 0; + const ssize_t bytes_read = ::read(signal_fd_[0], &signal, sizeof(signal)); + if (bytes_read == sizeof(signal)) { + qLog(Debug) << "Caught signal:" << signal; + Q_EMIT UnixSignal(signal); + } + else if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + // No more data available (expected with non-blocking socket) + break; + } + else { + // Error occurred or partial read + break; + } + } + +} diff --git a/src/core/unixsignalwatcher.h b/src/core/unixsignalwatcher.h new file mode 100644 index 000000000..c7dea2bf9 --- /dev/null +++ b/src/core/unixsignalwatcher.h @@ -0,0 +1,53 @@ +/* + * Strawberry Music Player + * Copyright 2026, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef UNIXSIGNALWATCHER_H +#define UNIXSIGNALWATCHER_H + +#include + +#include +#include + +class QSocketNotifier; + +class UnixSignalWatcher : public QObject { + Q_OBJECT + + public: + explicit UnixSignalWatcher(QObject *parent = nullptr); + ~UnixSignalWatcher() override; + + void WatchForSignal(const int signal); + + Q_SIGNALS: + void UnixSignal(const int signal); + + private: + static void SignalHandler(const int signal); + void HandleSignalNotification(); + + static UnixSignalWatcher *sInstance; + int signal_fd_[2]; + QSocketNotifier *socket_notifier_; + QList watched_signals_; + QList original_signal_actions_; +}; + +#endif // UNIXSIGNALWATCHER_H diff --git a/src/main.cpp b/src/main.cpp index e13db3b62..9b20fbd7f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,7 @@ * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome - * Copyright 2018-2021, Jonas Kvinge + * Copyright 2018-2026, Jonas Kvinge * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -76,6 +76,10 @@ #include +#ifdef Q_OS_UNIX + #include "core/unixsignalwatcher.h" +#endif + #ifdef HAVE_QTSPARKLE # include #endif // HAVE_QTSPARKLE @@ -365,6 +369,12 @@ int main(int argc, char *argv[]) { #endif options); +#ifdef Q_OS_UNIX + UnixSignalWatcher unix_signal_watcher; + unix_signal_watcher.WatchForSignal(SIGTERM); + QObject::connect(&unix_signal_watcher, &UnixSignalWatcher::UnixSignal, &w, &MainWindow::Exit); +#endif + #ifdef Q_OS_MACOS mac::EnableFullScreen(w); #endif // Q_OS_MACOS