diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fa62573..91facf6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ project(Qx # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("19d33b5bb1752b50767f78ca3e17796868354ac3") +fetch_ob_cmake("3336ff1e397faab10021890db2151265ee3e8c04") # Initialize project according to standard rules include(OB/Project) diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index 29cc548b..0e935ece 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -33,6 +33,7 @@ qx_add_component("Core" qx-string.h qx-system.h qx-systemerror.h + qx-systemsignalwatcher.h qx-traverser.h __private/qx-internalerror.h IMPLEMENTATION @@ -68,6 +69,8 @@ qx_add_component("Core" qx-systemerror.cpp qx-systemerror_linux.cpp qx-systemerror_win.cpp + qx-systemsignalwatcher.cpp + qx-systemsignalwatcher_p.h __private/qx-generalworkerthread.h __private/qx-generalworkerthread.cpp __private/qx-internalerror.cpp @@ -77,6 +80,11 @@ qx_add_component("Core" __private/qx-processwaiter_win.cpp __private/qx-processwaiter_linux.h __private/qx-processwaiter_linux.cpp + __private/qx-signaldaemon.h + __private/qx-signaldaemon_win.h + __private/qx-signaldaemon_win.cpp + __private/qx-signaldaemon_linux.h + __private/qx-signaldaemon_linux.cpp DOC_ONLY qx-bimap.dox qx-regularexpression.dox diff --git a/lib/core/include/qx/core/qx-systemsignalwatcher.h b/lib/core/include/qx/core/qx-systemsignalwatcher.h new file mode 100644 index 00000000..90382123 --- /dev/null +++ b/lib/core/include/qx/core/qx-systemsignalwatcher.h @@ -0,0 +1,64 @@ +#ifndef QX_SYSTEMSIGNALWATCHER_H +#define QX_SYSTEMSIGNALWATCHER_H + +// Qt Includes +#include + +namespace Qx +{ + +class SystemSignalWatcherPrivate; + +class SystemSignalWatcher : public QObject +{ + Q_OBJECT; + Q_DECLARE_PRIVATE(SystemSignalWatcher); + +//-Class Enums------------------------------------------------------------------------------------------------- +public: + /* For now we only support signals that can be mapped cross-platform. In the future we could support more + * with a doc note that the additional signals will never be received outside of Linux + */ + enum Signal + { + None = 0x0, + Interrupt = 0x1, + HangUp = 0x2, + Quit = 0x4, + Terminate = 0x8, + Abort = 0x10, + }; + Q_DECLARE_FLAGS(Signals, Signal); + + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + std::unique_ptr d_ptr; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + SystemSignalWatcher(); + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SystemSignalWatcher(); // Required for d_ptr destructor to compile + +//-Instance Functions-------------------------------------------------------------------------------------------- +public: + void watch(Signals s); + void stop(); + void yield(); + + Signals watching() const; + bool isWatching() const; + bool isRegistered() const; + +//-Signals & Slots------------------------------------------------------------------------------------------------ +signals: + void signaled(Signal s, bool* handled); +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(SystemSignalWatcher::Signals); + +} + +#endif // QX_SYSTEMSIGNALWATCHER_H diff --git a/lib/core/src/__private/qx-signaldaemon.h b/lib/core/src/__private/qx-signaldaemon.h new file mode 100644 index 00000000..337340ad --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon.h @@ -0,0 +1,30 @@ +#ifndef QX_SIGNALDAEMON_H +#define QX_SIGNALDAEMON_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/core/qx-systemsignalwatcher.h" + +/*! @cond */ +namespace Qx +{ + +class AbstractSignalDaemon +{ +//-Aliases-------------------------------------------------------------------------------------------------- +protected: + using Signal = SystemSignalWatcher::Signal; + +//-Instance Functions-------------------------------------------------------------------------------------------- +public: + virtual void addSignal(Signal signal) = 0; + virtual void removeSignal(Signal signal) = 0; + virtual void callDefaultHandler(Signal signal) = 0; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_H diff --git a/lib/core/src/__private/qx-signaldaemon_linux.cpp b/lib/core/src/__private/qx-signaldaemon_linux.cpp new file mode 100644 index 00000000..0a930fc8 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_linux.cpp @@ -0,0 +1,176 @@ +// Unit Include +#include "qx-signaldaemon_linux.h" + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-systemsignalwatcher_p.h" +#include "qx-generalworkerthread.h" + +// System Includes +#include +#include + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// SignalDaemon +//=============================================================================================================== + +//-Destructor------------------------------------------------------------------------------------------------- +//Public: +SignalDaemon::~SignalDaemon() { if(mNotifier) shutdownNotifier(); } + +//-Class Functions---------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::handler(int signal) +{ + /* This will be called by the system in a random thread of its choice (which is bad since it could + * block an important thread). We write to the "write end" of a socket pair (a cheap operation) to wake + * our notifier in a dedicated thread (which listens to the "read end") in order to quickly escape this one. + * + * There are also severe limits to what functions you can call in POSIX signal handlers: + * https://doc.qt.io/qt-6/unix-signals.html + */ + Q_ASSERT(smHandlerFds[0] && smHandlerFds[1]); + ssize_t bytes = sizeof(signal); + ssize_t bytesW = write(smHandlerFds[0], &signal, bytes); + Q_ASSERT(bytesW == bytes); +} + +SignalDaemon* SignalDaemon::instance() { static SignalDaemon d; return &d; } + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::installHandler(int sig) +{ + struct sigaction sigact = {}; + sigact.sa_handler = SignalDaemon::handler; + sigemptyset(&sigact.sa_mask); // Might not be needed since struct is value-initialized + sigact.sa_flags |= SA_RESTART; + if(sigaction(sig, &sigact, NULL) != 0) + qWarning("SignalDaemon: Failed to install sigaction! System signal monitoring will not function. %s", strerror(errno)); +} + +void SignalDaemon::restoreDefaultHandler(int sig) +{ + struct sigaction sigact = {}; + sigact.sa_handler = SIG_DFL; + sigemptyset(&sigact.sa_mask); // Might not be needed since struct is value-initialized + if(sigaction(sig, &sigact, NULL) != 0) + qWarning("SignalDaemon: Failed to restore default signal handler!. %s", strerror(errno)); +} + +void SignalDaemon::startupNotifier() +{ + Q_ASSERT(!mNotifier); + + // Create local socket pair + if(socketpair(AF_UNIX, SOCK_STREAM, 0, smHandlerFds) != 0) + { + qWarning("SignalDaemon: Failed to create socket pair! System signal monitoring will not function. %s", strerror(errno)); + return; + } + + // Setup notifier to watch read end of pair + mNotifier = new QSocketNotifier(smHandlerFds[1], QSocketNotifier::Read); + QObject::connect(mNotifier, &QSocketNotifier::activated, mNotifier, [](QSocketDescriptor socket, QSocketNotifier::Type type){ + // This all occurs within a dedicated thread + Q_ASSERT(type == QSocketNotifier::Read); + + // Read signal from fd + int signal; + ssize_t bytes = sizeof(signal); + ssize_t bytesR = read(socket, &signal, sizeof(signal)); + Q_ASSERT(bytesR == bytes); + + // Trigger daemon + auto daemon = SignalDaemon::instance(); + daemon->processNativeSignal(signal); + }); + mNotifier->setEnabled(true); + + // Move notifier to dedicated thread + auto gwt = GeneralWorkerThread::instance(); + gwt->moveTo(mNotifier); +} +void SignalDaemon::shutdownNotifier() +{ + Q_ASSERT(mNotifier); + + /* Closing the "write end" of the socketpair will cause EOF to be sent to the "read end", and therefore trigger + * our socket notifier, which we don't want, so we have to disable it first. Since the notifier lives in another + * thread we can't cause the change directly and instead have to invoke the slot via an event. We don't need to + * block here for that (Qt::BlockingQueuedConnection), but just make sure that the invocation of the setEnabled() + * slot is queued up before the socket is closed (so that the EOF is ignored when that event is processed). + * + * Lambda is used because arguments with invokeMethod() weren't added until Qt 6.7. + */ + QMetaObject::invokeMethod(mNotifier, [noti = mNotifier]{ noti->setEnabled(false); }); + + // Close sockets and zero out + if(close(smHandlerFds[0]) != 0) + qWarning("SignalDaemon: Failed to close write-end of socket. %s", strerror(errno)); + if(close(smHandlerFds[1]) != 0) + qWarning("SignalDaemon: Failed to close read-end of socket. %s", strerror(errno)); + smHandlerFds[0] = 0; + smHandlerFds[1] = 0; + + // Kill notifier + mNotifier->deleteLater(); + mNotifier = nullptr; +} + +//Public: +void SignalDaemon::addSignal(Signal signal) +{ + int nativeSignal = SIGNAL_MAP.from(signal); + Q_ASSERT(!mActiveSigs.contains(nativeSignal)); + mActiveSigs.insert(nativeSignal); + if(mActiveSigs.size() == 1) + startupNotifier(); + + installHandler(nativeSignal); +} + +void SignalDaemon::removeSignal(Signal signal) +{ + int nativeSignal = SIGNAL_MAP.from(signal); + Q_ASSERT(mActiveSigs.contains(nativeSignal)); + restoreDefaultHandler(nativeSignal); + mActiveSigs.remove(nativeSignal); + if(mActiveSigs.isEmpty()) + shutdownNotifier(); +} + +void SignalDaemon::callDefaultHandler(Signal signal) +{ + int nativeSig = SIGNAL_MAP.from(signal); + bool active = mActiveSigs.contains(nativeSig); + if(active) + restoreDefaultHandler(nativeSig); + if(raise(nativeSig) != 0) // Triggers default action, doesn't return until it's finished + qWarning("SignalDaemon: Failed to raise signal for default signal handler!"); + if(active) + installHandler(nativeSig); // If for some reason we're still alive, put back the custom handler +} + +void SignalDaemon::processNativeSignal(int sig) const +{ + /* Acquiring a lock on manager also acts like a mutex for this class as the dedicated notifier thread + * will block when it hits this if the manager is modifying this singleton + */ + auto manager = SswManager::instance(); + + /* Always forward, we should only ever get signals we're watching. Technically, one could have been + * removed at the last minute while trying to get the above lock, but we send the signal forward anyway + * as otherwise the signal would go entirely unhandled + */ + manager->processSignal(SIGNAL_MAP.from(sig)); +} + +} +/*! @endcond */ diff --git a/lib/core/src/__private/qx-signaldaemon_linux.h b/lib/core/src/__private/qx-signaldaemon_linux.h new file mode 100644 index 00000000..a144d3c4 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_linux.h @@ -0,0 +1,73 @@ +#ifndef QX_SIGNALDAEMON_LINUX_H +#define QX_SIGNALDAEMON_LINUX_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-signaldaemon.h" +#include "qx/core/qx-bimap.h" + +// System Includes +#include + +/*! @cond */ +class QSocketNotifier; + +namespace Qx +{ + +/* Can't use thread-safe singleton with this without resorting to the somewhat costly QRecursiveMutex as + * the class gets reentered by the same thread while locked; however we get around that and the need for + * manual mutex since with a trick in processNativeSignal. + */ +class SignalDaemon : public AbstractSignalDaemon +{ +//-Class Variables------------------------------------------------------------------------------------------------- +private: + static inline const Bimap SIGNAL_MAP{ + {Signal::HangUp, SIGHUP}, + {Signal::Interrupt, SIGINT}, + {Signal::Terminate, SIGTERM}, + {Signal::Quit, SIGQUIT}, + {Signal::Abort, SIGABRT}, + }; + + // Process local sockets for escaping the signal handler + static inline int smHandlerFds[2] = {0, 0}; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + QSocketNotifier* mNotifier; + QSet mActiveSigs; + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SignalDaemon(); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static void handler(int signal); + +public: + static SignalDaemon* instance(); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + void installHandler(int sig); + void restoreDefaultHandler(int sig); + void startupNotifier(); + void shutdownNotifier(); + +public: + void addSignal(Signal signal) override; + void removeSignal(Signal signal) override; + void callDefaultHandler(Signal signal) override; + + void processNativeSignal(int sig) const; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_LINUX_H diff --git a/lib/core/src/__private/qx-signaldaemon_win.cpp b/lib/core/src/__private/qx-signaldaemon_win.cpp new file mode 100644 index 00000000..1d134327 --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_win.cpp @@ -0,0 +1,103 @@ +// Unit Include +#include "qx-signaldaemon_win.h" + +// Inter-component Includes +#include "qx-systemsignalwatcher_p.h" + +/*! @cond */ +namespace Qx +{ + +//=============================================================================================================== +// SignalDaemon +//=============================================================================================================== + +//-Destructor------------------------------------------------------------------------------------------------- +//Public: +SignalDaemon::~SignalDaemon() { if(!mActiveCtrlTypes.isEmpty()) removeHandler(); } + +//-Class Functions---------------------------------------------------------------------------------------------- +//Private: +BOOL SignalDaemon::handler(DWORD dwCtrlType) +{ + // Everything within this function (and what it calls) occurs in a separate system thread + auto daemon = SignalDaemon::instance(); + return daemon->processNativeSignal(dwCtrlType); +} + +SignalDaemon* SignalDaemon::instance() { static SignalDaemon d; return &d; } + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +void SignalDaemon::installHandler() +{ + if(!SetConsoleCtrlHandler(&SignalDaemon::handler, TRUE)) + { + DWORD err = GetLastError(); + qWarning("Failed to install SignalWatcher native handler! 0x%X", err); + } + +} +void SignalDaemon::removeHandler() +{ + if(!SetConsoleCtrlHandler(&SignalDaemon::handler, FALSE)) + { + DWORD err = GetLastError(); + qWarning("Failed to uninstall SignalWatcher native handler! 0x%X", err); + } +} + +//Public: +void SignalDaemon::addSignal(Signal signal) +{ + DWORD ctrlType = SIGNAL_MAP.from(signal); + Q_ASSERT(!mActiveCtrlTypes.contains(ctrlType)); + mActiveCtrlTypes.insert(ctrlType); + if(mActiveCtrlTypes.size() == 1) + installHandler(); +} + +void SignalDaemon::removeSignal(Signal signal) +{ + DWORD ctrlType = SIGNAL_MAP.from(signal); + Q_ASSERT(mActiveCtrlTypes.contains(ctrlType)); + mActiveCtrlTypes.remove(ctrlType); + if(mActiveCtrlTypes.isEmpty()) + removeHandler(); +} + +void SignalDaemon::callDefaultHandler(Signal signal) +{ + Q_UNUSED(signal); + /* Unfortunately there is no way to get the address of the default handler, nor a way + * to proc it for any signal, so we have to repeat the behavior here. + * + * This is what it seems to do for every signal. + */ + ExitProcess(STATUS_CONTROL_C_EXIT); +} + +bool SignalDaemon::processNativeSignal(DWORD dwCtrlType) const +{ + /* We won't always use this (see next check), but this indirectly acts like a mutex for this class. + * SswManager is the only class that accesses this one other than its own handler. So, if we make sure + * to get a lock here immediately, we guarantee that this classes data members are in a valid state + * for the handler thread to read. It technically causes a touch of slow down in the case where the + * false branch of the below 'if' is taken but that likely means the program is exiting anyway so + * it's really no problem. + */ + auto manager = SswManager::instance(); + + if(mActiveCtrlTypes.contains(dwCtrlType)) + { + manager->processSignal(SIGNAL_MAP.from(dwCtrlType)); + return true; + } + + // Ctrl type we aren't handling + return false; +} + + +} +/*! @endcond */ diff --git a/lib/core/src/__private/qx-signaldaemon_win.h b/lib/core/src/__private/qx-signaldaemon_win.h new file mode 100644 index 00000000..8a60674f --- /dev/null +++ b/lib/core/src/__private/qx-signaldaemon_win.h @@ -0,0 +1,66 @@ +#ifndef QX_SIGNALDAEMON_WIN_H +#define QX_SIGNALDAEMON_WIN_H + +// Qt Includes +#include + +// Inter-component Includes +#include "__private/qx-signaldaemon.h" +#include "qx/core/qx-bimap.h" + +// Windows Includes +#define WIN32_LEAN_AND_MEAN +#include "windows.h" + +/*! @cond */ +namespace Qx +{ + +/* Can't use thread-safe singleton with this without resorting to the somewhat costly QRecursiveMutex as + * the class gets reentered by the same thread while locked; however we get around that and the need for + * manual mutex since with a trick in processNativeSignal. + */ +class SignalDaemon : public AbstractSignalDaemon +{ +//-Class Variables------------------------------------------------------------------------------------------------- +private: + static inline const Bimap SIGNAL_MAP{ + {Signal::HangUp, CTRL_CLOSE_EVENT}, + {Signal::Interrupt, CTRL_C_EVENT}, + {Signal::Terminate, CTRL_SHUTDOWN_EVENT}, + {Signal::Quit, CTRL_BREAK_EVENT}, + {Signal::Abort, CTRL_LOGOFF_EVENT}, + }; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + QSet mActiveCtrlTypes; + +//-Destructor------------------------------------------------------------------------------------------------- +public: + ~SignalDaemon(); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static BOOL handler(DWORD dwCtrlType); + +public: + static SignalDaemon* instance(); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + void installHandler(); + void removeHandler(); + +public: + void addSignal(Signal signal) override; + void removeSignal(Signal signal) override; + void callDefaultHandler(Signal signal) override; + + bool processNativeSignal(DWORD dwCtrlType) const; +}; + +} +/*! @endcond */ + +#endif // QX_SIGNALDAEMON_WIN_H diff --git a/lib/core/src/qx-systemsignalwatcher.cpp b/lib/core/src/qx-systemsignalwatcher.cpp new file mode 100644 index 00000000..ed6edea9 --- /dev/null +++ b/lib/core/src/qx-systemsignalwatcher.cpp @@ -0,0 +1,424 @@ +// Unit Include +#include "qx/core/qx-systemsignalwatcher.h" +#include "qx-systemsignalwatcher_p.h" + +// Inter-component Includes +#ifdef _WIN32 +#include "__private/qx-signaldaemon_win.h" +#endif +#ifdef __linux__ +#include "__private/qx-signaldaemon_linux.h" +#endif + +namespace Qx +{ +/*! @cond */ + +//=============================================================================================================== +// SystemSignalWatcherPrivate +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Public: +SystemSignalWatcherPrivate::SystemSignalWatcherPrivate(SystemSignalWatcher* q) : + q_ptr(q), + mWatching(Signal::None) +{} + +//-Instance Functions-------------------------------------------------------------------------------------------- +//Private: +bool SystemSignalWatcherPrivate::isRegistered() const { return mRepresentative.isValid(); } + +//Public: +void SystemSignalWatcherPrivate::watch(Signals s) +{ + if(s == mWatching) + return; + + /* We might not need the manager here, but we need to ensure we lock it before updating mWatching since + * the manager can read that value (could use our own mutex for that instead, but avoiding that for now + * for simplicity). + * + * Additionally, if registering, the lock prevents smRollingId from being corrupted by other instances + */ + auto man = SswManager::instance(); + mWatching = s; + + // Going to None doesn't require extra work since we stay registered + if(s == Signal::None) + return; + + // Register if not already + if(!isRegistered()) + { + mRepresentative = {.ptr = this, .id = smRollingId++}; + man->registerWatcher(mRepresentative); + } +} + +void SystemSignalWatcherPrivate::yield() +{ + if(!isRegistered()) + return; + + auto man = SswManager::instance(); + man->sendToBack(mRepresentative); +} + +SystemSignalWatcherPrivate::Signals SystemSignalWatcherPrivate::watching() const { return mWatching; } + +void SystemSignalWatcherPrivate::notify(Signal s) +{ + /* QMetaObject::invokeMethod() is thread-safe, so it's OK that the manager calls this without a lock. + * + * This function should always be invoked by a thread other than the one that the watcher lives in, + * but since we need to avoid deadlocks due to a signal coming in while the mutex to the manager + * is already locked, (should slots connected to `signaled` use the manager before the previous + * lock is released), and other unexpected processing order shenanigans, we explicitly queue up the + * signal emission here to be safe. + */ + Q_Q(SystemSignalWatcher); + + // Run this in the thread that the watcher lives + QMetaObject::invokeMethod(q, [q, s, this]{ + // Ignore signals that arrive late after a watch change + if(mWatching == Signal::None) + return; + + // Emit signal and handle result + bool handled = false; + emit q->signaled(s, &handled); + auto man = SswManager::instance(); + man->processResponse(mRepresentative, s, handled); + }, + Qt::QueuedConnection); +} + +//=============================================================================================================== +// SswManager +//=============================================================================================================== + +//-Constructor-------------------------------------------------------------------- +//Private: +SswManager::SswManager() = default; + +//-Instance Functions---------------------------------------------------------------------------------------------- +//Private: +void SswManager::dispatch(Signal signal, const SswRep& disp) +{ + Q_ASSERT(mDispatchTracker.contains(signal)); + + if(disp.isValid()) // Watcher handler type + { + if(!mWatcherRegistry.contains(disp)) // Watcher was unregistered, sim response + processResponse(disp, signal, false); + else + disp.ptr->notify(signal); + } + else // Default handler type + { + auto daemon = SignalDaemon::instance(); + daemon->callDefaultHandler(signal); + + /* The above likely kills the program, but in case the default was replaced with + * something else before we installed our signal handlers, and we're still alive, + * keep going. + */ + processResponse(disp, signal, true); + } +} + +//Public: +void SswManager::registerWatcher(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.contains(rep) && rep.isValid()); + mWatcherRegistry.append(rep); // Registry is checked backwards, so this is first + + // Update signal tracker + Signals watched = rep.ptr->watching(); + for(Signal s : ALL_SIGNALS) + { + if(watched.testFlag(s) && mSignalTracker.push(s)) + { + auto daemon = SignalDaemon::instance(); + daemon->addSignal(s); + } + } +} + +void SswManager::unregisterWatcher(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.isEmpty() && rep.isValid()); + + // Remove from registry + qsizetype rem = mWatcherRegistry.removeIf([rep](const SswRep& registered) { return registered == rep; }); + Q_ASSERT(rem == 1); + + // Update signal tracker + Signals watched = rep.ptr->watching(); + for(Signal s : ALL_SIGNALS) + { + if(watched.testFlag(s) && mSignalTracker.pop(s)) + { + auto daemon = SignalDaemon::instance(); + daemon->removeSignal(s); + } + } + + // Advance any queues that are on the watcher + for(auto [sig, dSet] : mDispatchTracker.asKeyValueRange()) + { + if(dSet.empty()) + continue; + + // Safe to double dip queues, as there should be no empty dispatch groups in dispatch sets + SswRep disp = dSet.front().front(); + if(disp.isValid() && disp == rep) + processResponse(disp, sig, false); // Manually fire reponse since it won't be received now + } +} + +void SswManager::sendToBack(const SswRep& rep) +{ + Q_ASSERT(!mWatcherRegistry.isEmpty()); + if(mWatcherRegistry.size() < 2) + return; + + // Remove from registry + qsizetype rem = mWatcherRegistry.removeIf([rep](const SswRep& registered) { return registered == rep; }); + Q_ASSERT(rem == 1); + + // Re-insert at start (effectively end) + mWatcherRegistry.prepend(rep); +} + +void SswManager::processResponse(const SswRep& rep, Signal signal, bool handled) +{ + // Get current dispatch for signal + Q_ASSERT(mDispatchTracker.contains(signal)); + DispatchSet& ds = mDispatchTracker[signal]; Q_ASSERT(!ds.empty()); + DispatchGroup& dg = ds.front(); Q_ASSERT(!dg.empty()); + SswRep& curDispatch = dg.front(); Q_ASSERT(curDispatch == rep); + + if(!handled) + dg.pop(); // Remove individual dispatch + + if(handled || dg.empty()) + ds.pop(); // Remove whole group + + // Go next if not done + if(!ds.empty()) + { + dg = ds.front(); Q_ASSERT(!dg.empty()); // Never should have an empty group outside of modification + dispatch(signal, dg.front()); + } +} + +void SswManager::processSignal(Signal signal) +{ + DispatchSet& ds = mDispatchTracker[signal]; + bool processing = !ds.empty(); // Will be processing if not empty + + // Add new group + DispatchGroup& dg = ds.emplace(); + + // Fill group queue (last registered goes first) + for(auto itr = mWatcherRegistry.crbegin(); itr != mWatcherRegistry.crend(); ++itr) + if(itr->ptr->watching().testFlag(signal)) + dg.push(*itr); + + // Add default implementation dispatcher (i.e. invalid SswRep) last + dg.emplace(); + + // Start processing signals if not already + if(!processing) + dispatch(signal, dg.front()); +} + +/*! @endcond */ + +//=============================================================================================================== +// SystemSignalWatcher +//=============================================================================================================== + +/*! + * @class SystemSignalWatcher qx/core/qx-systemsignalwatcher.h + * @ingroup qx-core + * + * @brief The SystemSignalWatcher class provides a convenient and comprehensive way to react to system + * defined signals. + * + * SystemSignalWatcher acts as user friendly replacement for the troubled std::signal, in a similar vein to + * sigaction(), but is cross-platform and features a Qt-oriented interface, unlike the latter. + * + * Any number of watchers can be created in any thread and are initially inactive, with the first call to + * watch() with a value other than Signal::None registering the watcher to listen for system signals. Signals + * are delivered to watchers on a last-registered, first-served basis, where each watcher can choose whether + * to block the signal from further handling, or allow it to proceed to the next watcher in the list in + * a similar fashion to event filters. If no watcher marks the signal as handled, the default system handler + * for the signal is called. + * + * Which system signals a given watcher is waiting for can be changed at any time, but its position in the + * delivery priority list is maintained until it's destroyed or yield() is called. + * + * @sa watch() and signaled(). + */ + +//-Class Enums----------------------------------------------------------------------------------------------- +//Public: +/*! + * @enum SystemSignalWatcher::Signal + * + * This enum specifies the system signal that was received by the watcher. + * + * Each system signal is based on a standard POSIX signal, though only a subset are supported. On Windows, + * native signals are mapped to their nearest POSIX equivalents, or a reasonable alternative if there is + * none, as shown below: + * + * + * + *
Mapping of native signals to SystemSignalWatcher::Signal
Signal Linux Windows + *
Interrupt SIGINT CTRL_C_EVENT + *
HangUp SIGHUP CTRL_CLOSE_EVENT + *
Quit SIGQUIT CTRL_BREAK_EVENT + *
Terminate SIGTERM CTRL_SHUTDOWN_EVENT + *
Abort SIGABRT CTRL_LOGOFF_EVENT + *
+ * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::None + * No signals to watch for. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Interrupt + * Terminal interrupt signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::HangUp + * Hangup signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Quit + * Terminal quit signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Terminate + * Termination signal. + * + * @var SystemSignalWatcher::Signal SystemSignalWatcher::Abort + * Process abort signal. + * + * @qflag{SystemSignalWatcher::Signals, SystemSignalWatcher::Signal} + * + * @sa signaled(). + */ + +//-Constructor-------------------------------------------------------------------- +//Public: +/*! + * Creates an inactive SystemSignalWatcher without a dispatch priority. + */ +SystemSignalWatcher::SystemSignalWatcher() : d_ptr(std::make_unique(this)) {} + +//-Destructor-------------------------------------------------------------------- +//Public: +/*! + * Deactivates and deletes the SystemSignalWatcher. + */ +SystemSignalWatcher::~SystemSignalWatcher() +{ + /* Here instead of private in order to unregister as soon as possible. + * + * NOTE: Current implementation somewhat requires the watcher to stay registered until its destroyed, as + * if it could be unregistered at any time, it does not account for the case where the user causes it to + * be unregistered while in a slot connected to the notify signal. Right now, that would cause its queue + * item to be prematurely canceled and then its actual response given after (failed the assert). Working + * around this would likely require a flag to be set during signal emissions such that if the watcher + * were to be unregistered while high, block the unregistration until the slot has returned (i.e. post + * emit) and then unregister (or have an optional parameter for processResponse() in manager that causes + * unregistration right after the response is handled. The latter would avoid the manager dispatching + * to the watcher again if it happens to be next in queue (after a default handler). + */ + Q_D(SystemSignalWatcher); + if(d->isRegistered()) + { + auto man = SswManager::instance(); + man->unregisterWatcher(d->mRepresentative); + } +} + +/*! + * Starts watching for any of the system signals that make up @a s, or stops the watcher if @a s is only + * Signal::None, in which case the watch is stopped immediately and any "in-flight" signals are + * ignored. + * + * @note + * The first time this function is called with any signal flags set in @a s, the watcher is registered as + * the first to receive any applicable signals, and will remain so until additional watchers are registered, + * this watcher is destroyed, or yield() is called. The dispatch order is independent of signals watched, so + * once a watcher has been registered, calling this function again will only change which signals it pays + * attention to, but will not change its dispatch priority. + * + * @sa stop(), isRegistered(), and isWatching(). + */ +void SystemSignalWatcher::watch(Signals s) { Q_D(SystemSignalWatcher); d->watch(s); } + +/*! + * Stops the watcher from responding to any system signal. This is equivalent to: + * + * `myWatcher.watch(Signal::None)` + * + * @note + * Although this effectively disables the watcher, its position in the dispatch queue is not changed, + * should it start watching for system signals again later. + * + * @sa watch() and isWatching(). + */ +void SystemSignalWatcher::stop() { Q_D(SystemSignalWatcher); d->watch(Signal::None); } + +/*! + * If the watcher has been registered, it's moved to the back of dispatch priority list so that it is + * the last to be able to handle system signals; otherwise, this function does nothing. + * + * @sa stop(). + */ +void SystemSignalWatcher::yield() { Q_D(SystemSignalWatcher); d->yield(); } + +/*! + * Returns the system signals that the watcher is waiting for, or Signal::None if the watcher is not currently + * waiting for any. + * + * @sa isWatching(). + */ +SystemSignalWatcher::Signals SystemSignalWatcher::watching() const { Q_D(const SystemSignalWatcher); return d->watching(); } + +/*! + * Returns @c true if the watcher is waiting for at least one system signal; otherwise, returns @c false. + * + * @sa watching(). + */ +bool SystemSignalWatcher::isWatching() const { Q_D(const SystemSignalWatcher); return d->mWatching != Signal::None; } + +/*! + * Returns @c true if the watcher has been used before at any point (that is, watch() has been called with a + * value other than Signal::None), and has a position in the dispatch priority list; otherwise, returns @c + * false. + * + * @sa isWatching(). + */ +bool SystemSignalWatcher::isRegistered() const { Q_D(const SystemSignalWatcher); return d->isRegistered(); } + +/*! + * @fn void SystemSignalWatcher::signaled(Signal s, bool* handled) + * + * This signal is emitted when the watcher has received a system signal that it's waiting for, with @a s + * containing the signal. + * + * The slot connected to this signal should set @a handled to @c true in order to indicate that the system + * signal has been fully handled and it should be discarded, or @c false to allow the signal to pass on to + * the next SystemSignalWatcher in the dispatch list. If all applicable watchers set @a handled to @c false, + * the system default handler for @a s is triggered. + * + * @warning + * It is not possible to use a QueuedConnection to connect to this signal and control further handling of + * @a s, as the signal will return immediately and use the default value of @a handled (@c false). If you + * do connect to this signal with a QueuedConnection just to be notified of when @a s has been received, + * but not to have influence on whether or not further watchers should be notified, do not deference + * the @a handled pointer as it will no longer be valid. + */ +} diff --git a/lib/core/src/qx-systemsignalwatcher_p.h b/lib/core/src/qx-systemsignalwatcher_p.h new file mode 100644 index 00000000..865f641c --- /dev/null +++ b/lib/core/src/qx-systemsignalwatcher_p.h @@ -0,0 +1,142 @@ +#ifndef QX_SYSTEMSIGNALWATCHER_P_H +#define QX_SYSTEMSIGNALWATCHER_P_H + +// Unit Includes +#include "qx/core/qx-systemsignalwatcher.h" + +// Standard Library Includes +#include + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/core/qx-threadsafesingleton.h" + +/*! @cond */ +namespace Qx +{ + +class SystemSignalWatcherPrivate +{ + Q_DECLARE_PUBLIC(SystemSignalWatcher); + +//-Class Types--------------------------------------------------------------------------------------------- +private: + using Signals = SystemSignalWatcher::Signals; + using Signal = SystemSignalWatcher::Signal; + +public: + struct Representative + { + SystemSignalWatcherPrivate* ptr = nullptr; + uint id = 0; + + inline bool isValid() const { return ptr != nullptr && id != 0; } + inline bool operator==(const Representative& other) const = default; + }; + +//-Class Variables---------------------------------------------------------------------------------------------------- +private: + static inline uint smRollingId = 1; + +//-Instance Variables------------------------------------------------------------------------------------------------- +private: + SystemSignalWatcher* const q_ptr; + Signals mWatching; + Representative mRepresentative; + +//-Constructor------------------------------------------------------------------------------------------------- +public: + SystemSignalWatcherPrivate(SystemSignalWatcher* q); + +//-Instance Functions-------------------------------------------------------------------------------------------- +private: + bool isRegistered() const; + +public: + void watch(Signals s); + void yield(); + + Signals watching() const; + + void notify(Signal s); +}; + +class SswManager : public ThreadSafeSingleton +{ + QX_THREAD_SAFE_SINGLETON(SswManager); +//-Class Types--------------------------------------------------------------------------------------------- +private: + using Ssw = SystemSignalWatcherPrivate; + using Signal = SystemSignalWatcher::Signal; + using Signals = SystemSignalWatcher::Signals; + using SswRep = Ssw::Representative; + + /* TODO: The registry being a list means that although it has good iteration speed, it has worse search speed. + * The only way to remedy this we have is to replace the list with a new template that combines a QSet and + * QList for ordering with good "contains" speed checking; however, this is hard to do without using a linked-list, + * which suffers from slower iteration speed (which might occur more than modification here). + * + * A queue of iterators and list queue using wrappers around shared_ptr and weak_ptr were initially tried instead of + * the current approach as alternatives for making sure that queue ptrs are immediately invalidated and to avoid + * pointer address reuse issues, but both were overly complex. + */ + using Registry = QList; + + class SignalTracker + { + private: + QHash mCounts; + + public: + bool push(Signal s) { return mCounts[s]++ == 0; } + bool pop(Signal s) { Q_ASSERT(mCounts[s] != 0); return --mCounts[s] == 0; } + }; + + using DispatchGroup = std::queue; + using DispatchSet = std::queue; + using DispatchTracker = QHash; // Sets added by signal on the fly + +//-Class Variables--------------------------------------------------------------------------------------------- +private: + // NOTE: ADD NEW SIGNALS HERE + // TODO: Don't feel like introducing magic_enum as a dep. here, so when on C++26 use reflection for this list + static constexpr std::array ALL_SIGNALS{ + Signal::Interrupt, + Signal::HangUp, + Signal::Quit, + Signal::Terminate, + Signal::Abort + }; + +//-Instance Variables------------------------------------------------------------------------------------------ +private: + Registry mWatcherRegistry; + DispatchTracker mDispatchTracker; + SignalTracker mSignalTracker; + +//-Constructor---------------------------------------------------------------------------------------------- +private: + explicit SswManager(); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void dispatch(Signal signal, const SswRep& disp); + +public: + // Watcher -> Manager + void registerWatcher(const SswRep& rep); + void unregisterWatcher(const SswRep& rep); + void sendToBack(const SswRep& rep); + void processResponse(const SswRep& rep, Signal signal, bool handled); + + // Signaler -> Manager + void processSignal(Signal signal); + +}; + +} +/*! @endcond */ + +#endif // QX_SYSTEMSIGNALWATCHER_P_H