Skip to content

Commit

Permalink
Add SystemSignalWatcher, respond to system signals in a Qt fashion
Browse files Browse the repository at this point in the history
  • Loading branch information
oblivioncth committed Nov 12, 2024
1 parent 1e3de4d commit a2c4331
Show file tree
Hide file tree
Showing 10 changed files with 1,087 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions lib/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
64 changes: 64 additions & 0 deletions lib/core/include/qx/core/qx-systemsignalwatcher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#ifndef QX_SYSTEMSIGNALWATCHER_H
#define QX_SYSTEMSIGNALWATCHER_H

// Qt Includes
#include <QObject>

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<SystemSignalWatcherPrivate> 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
30 changes: 30 additions & 0 deletions lib/core/src/__private/qx-signaldaemon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef QX_SIGNALDAEMON_H
#define QX_SIGNALDAEMON_H

// Qt Includes
#include <QList>

// 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
176 changes: 176 additions & 0 deletions lib/core/src/__private/qx-signaldaemon_linux.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Unit Include
#include "qx-signaldaemon_linux.h"

// Qt Includes
#include <QSocketNotifier>

// Inter-component Includes
#include "qx-systemsignalwatcher_p.h"
#include "qx-generalworkerthread.h"

// System Includes
#include <sys/socket.h>
#include <unistd.h>

/*! @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 */
73 changes: 73 additions & 0 deletions lib/core/src/__private/qx-signaldaemon_linux.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#ifndef QX_SIGNALDAEMON_LINUX_H
#define QX_SIGNALDAEMON_LINUX_H

// Qt Includes
#include <QSet>

// Inter-component Includes
#include "qx-signaldaemon.h"
#include "qx/core/qx-bimap.h"

// System Includes
#include <signal.h>

/*! @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, int> 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<int> 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
Loading

0 comments on commit a2c4331

Please sign in to comment.