You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
13 KiB
322 lines
13 KiB
//! Multi-threaded signal handling.
|
|
#pragma once
|
|
#include <utility>
|
|
#include <thread>
|
|
#include <functional> // needed for std::bind().... (TODO: How to remove use of bind...?)
|
|
#include <array>
|
|
|
|
#include <signal.h> // Needed for `sigset_t`
|
|
|
|
namespace fx::signal {
|
|
namespace details [[gnu::visibility("hidden")]] {
|
|
|
|
}
|
|
|
|
constexpr static inline struct wrap_callback_t {} wrap_callback {};
|
|
constexpr static inline struct with_add_t {} add_signals {};
|
|
|
|
template<typename F>
|
|
class wait_for_signals;
|
|
|
|
/// Create a function-object that, when invoked, blocks the thread until it receives one of the signals specified in construction.
|
|
///
|
|
/// On destruction, the signal mask is reset to what it was before the object was constructed (unless disabled entirely.)
|
|
template<>
|
|
class wait_for_signals<void> {
|
|
template<typename> friend class wait_for_signals;
|
|
|
|
sigset_t oldset;
|
|
sigset_t sigset = {0};
|
|
|
|
// Impl for add w/o arg-pack
|
|
explicit wait_for_signals(std::initializer_list<int> sigs, with_add_t const&, std::initializer_list<int> add) noexcept;
|
|
|
|
[[gnu::artificial]]
|
|
inline
|
|
void _do_signal_reached(bool move, int signum) {
|
|
if(move) std::move(*this)
|
|
.do_signal_reached(signum);
|
|
else do_signal_reached(signum);
|
|
}
|
|
|
|
static int wait_for_signal(sigset_t const& sigset);
|
|
protected:
|
|
//inline int wait_for_signal() { return wait_for_signal(sigset); }
|
|
|
|
virtual void do_signal_reached(int signum) &;
|
|
virtual void do_signal_reached(int signum) &&;
|
|
|
|
inline wait_for_signals(std::initializer_list<int> sigs, std::convertible_to<int> auto const&... add) noexcept
|
|
requires(sizeof...(add) > 0)
|
|
: wait_for_signals(sigs, add_signals, { int(add)... }) {}
|
|
|
|
virtual bool do_reset_mask() noexcept;
|
|
|
|
//virtual
|
|
int operator()(sigset_t const& sigset) &;
|
|
//virtual
|
|
int operator()(sigset_t const& sigset) &&;
|
|
|
|
template<typename W>
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) &
|
|
{
|
|
int signum = wait_for_signal(sigset);
|
|
|
|
return std::forward<W>(wrapper)(std::bind(&wait_for_signals<void>::_do_signal_reached, this, false, std::placeholders::_1), signum);
|
|
}
|
|
|
|
template<typename W>
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) &&
|
|
{
|
|
int signum = wait_for_signal(sigset);
|
|
|
|
return std::forward<W>(wrapper)(std::bind(&wait_for_signals<void>::_do_signal_reached, this, true, std::placeholders::_1), signum);
|
|
}
|
|
public:
|
|
wait_for_signals(std::initializer_list<int> sigs) noexcept;
|
|
|
|
inline explicit wait_for_signals(std::convertible_to<int> auto const&... sigs) noexcept
|
|
requires(sizeof...(sigs) > 0)
|
|
: wait_for_signals({ int(sigs)... }) {}
|
|
|
|
wait_for_signals(wait_for_signals const&) = delete;
|
|
wait_for_signals& operator=(wait_for_signals const&) = delete;
|
|
|
|
wait_for_signals(wait_for_signals&& m) noexcept;
|
|
wait_for_signals& operator=(wait_for_signals&& m) noexcept = delete; // NOTE: We can't have multiple sigmasks so there's no reason for this to exist (XXX: Is it actually okay if the previous mask is reset first...? I don't know...)
|
|
|
|
/// Do not reset the signal mask on destruction.
|
|
///
|
|
/// This will leave the blocked signals handled by the invocation operator blocked and `sigwait()` can still be used on them.
|
|
virtual void detach() && noexcept;
|
|
|
|
/// Reset the signal mask right now.
|
|
///
|
|
/// This also detaches the handle from doing it on destruction (see `detach()`.)
|
|
bool reset_mask() noexcept;
|
|
|
|
virtual ~wait_for_signals() noexcept;
|
|
|
|
int operator()() &;
|
|
int operator()() &&;
|
|
|
|
|
|
/// W is a function that takes a function-object with the signature `void(int)&&` bound to `this`.
|
|
template<typename W>
|
|
inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) &&
|
|
{
|
|
return std::move(*this)(w, std::forward<W>(wrapper), this->sigset);
|
|
}
|
|
|
|
/// W is a function that takes a function-object with the signature `void(int)&` bound to `this`.
|
|
template<typename W>
|
|
inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) &
|
|
{
|
|
return this->operator()<W>(w, std::forward<W>(wrapper), this->sigset);
|
|
}
|
|
};
|
|
|
|
//#error "TODO: XXX: Eh, this impl doesn't work... Why does previous `int operator()` retain precedence when we re-make them here...???"
|
|
// XXX: Remember the virtual functions always polute the namespace for some fucking reason...
|
|
|
|
template<typename F>
|
|
class wait_for_signals : private wait_for_signals<void> {
|
|
using base_t = wait_for_signals<void>;
|
|
F on_sig;
|
|
|
|
inline
|
|
int do_wait_now(sigset_t const& sigset, bool move) noexcept
|
|
{
|
|
if(move) return static_cast<base_t &&>(*this)(sigset);
|
|
else return static_cast<base_t & >(*this)(sigset);
|
|
}
|
|
inline
|
|
int do_wait_now(bool move) noexcept
|
|
{
|
|
if(move) return static_cast<base_t &&>(*this)();
|
|
else return static_cast<base_t & >(*this)();
|
|
}
|
|
protected:
|
|
[[gnu::artificial]]
|
|
inline virtual void do_signal_reached(int) & override {}
|
|
[[gnu::artificial]]
|
|
inline virtual void do_signal_reached(int) && override {}
|
|
|
|
using base_t::do_reset_mask;
|
|
|
|
template<std::convertible_to<F> U = F>
|
|
inline wait_for_signals(U&& on_sig, std::initializer_list<int> sigs, std::convertible_to<int> auto const&... add) noexcept(std::is_nothrow_convertible_v<U, F>)
|
|
requires(sizeof...(add) > 0)
|
|
: base_t(sigs, std::forward<decltype(add)>(add)...)
|
|
, on_sig(std::forward<U>(on_sig)) {}
|
|
|
|
|
|
inline decltype(auto) operator()(sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
|
{
|
|
return std::invoke(on_sig, do_wait_now(sigset, false));
|
|
}
|
|
inline decltype(auto) operator()(sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
|
{
|
|
return std::invoke(std::move(on_sig), do_wait_now(sigset, true));
|
|
}
|
|
|
|
template<typename W>
|
|
requires(std::is_invocable_v<W, F&, int const&>)
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v<W, F&, const int&>)
|
|
{
|
|
return std::invoke(std::forward<W>(wrapper), on_sig, do_wait_now(sigset, false));
|
|
}
|
|
|
|
template<typename W>
|
|
requires(std::is_invocable_v<W, F&&, int const&>)
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v<W, F&&, const int&>)
|
|
{
|
|
return std::invoke(std::forward<W>(wrapper), std::move(on_sig), do_wait_now(sigset, true));
|
|
}
|
|
public:
|
|
template<std::convertible_to<F> U = F>
|
|
inline wait_for_signals(U&& on_sig, std::initializer_list<int> sigs) noexcept(std::is_nothrow_convertible_v<U, F>)
|
|
requires(std::is_invocable_v<F, int const&>)
|
|
: base_t(sigs)
|
|
, on_sig(std::forward<U>(on_sig)){}
|
|
template<std::convertible_to<F> U = F>
|
|
inline wait_for_signals(U&& on_sig, std::convertible_to<int> auto const&... sigs) noexcept(std::is_nothrow_convertible_v<U, F>)
|
|
requires(sizeof...(sigs) > 0)
|
|
: wait_for_signals(std::forward<U>(on_sig), { int(sigs)... }) {}
|
|
|
|
wait_for_signals(wait_for_signals const&) = delete;
|
|
wait_for_signals& operator=(wait_for_signals const&) = delete;
|
|
|
|
wait_for_signals(wait_for_signals&& m) noexcept(std::is_nothrow_move_constructible_v<F>) requires(std::is_move_constructible_v<F>) = default;
|
|
wait_for_signals& operator=(wait_for_signals&&) = delete; //TODO: Once <void> spec has been complete as baseclassi (XXX: **AND** we decide to make this implementable... see base for reason why not.), this can be defaulted instead.
|
|
|
|
|
|
inline decltype(auto) operator()() & noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
|
{
|
|
return std::invoke(on_sig, do_wait_now(false));
|
|
}
|
|
inline decltype(auto) operator()() && noexcept(std::is_nothrow_invocable_v<F, const int&>)
|
|
{
|
|
return std::invoke(std::move(on_sig), do_wait_now(true));
|
|
}
|
|
|
|
using base_t::detach;
|
|
using base_t::reset_mask;
|
|
|
|
template<typename W>
|
|
requires(std::is_invocable_v<W, F&, int const&>)
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) & noexcept(std::is_nothrow_invocable_v<W, F&, const int&>)
|
|
{
|
|
return std::invoke(std::forward<W>(wrapper), on_sig, do_wait_now(false));
|
|
}
|
|
|
|
template<typename W>
|
|
requires(std::is_invocable_v<W, F&&, int const&>)
|
|
inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) && noexcept(std::is_nothrow_invocable_v<W, F&&, const int&>)
|
|
{
|
|
return std::invoke(std::forward<W>(wrapper), std::move(on_sig), do_wait_now(true));
|
|
}
|
|
|
|
virtual ~wait_for_signals() noexcept = default;
|
|
};
|
|
|
|
|
|
template<typename R, typename... Args>
|
|
struct wait_for_signals< R (Args...) > : public wait_for_signals< std::function< R(Args...) > > {
|
|
using wait_for_signals< std::function< R(Args...) > >::wait_for_signals;
|
|
|
|
using wait_for_signals< std::function< R(Args...) > >::operator();
|
|
|
|
virtual ~wait_for_signals() noexcept = default;
|
|
};
|
|
|
|
|
|
extern template class wait_for_signals<void>;
|
|
extern template class wait_for_signals<void(*)(int)>;
|
|
extern template class wait_for_signals< std::function< void(int) > >;
|
|
|
|
template<typename F>
|
|
requires(std::is_invocable_v<F, int const&> or std::is_invocable_v<F, int const&, std::stop_token>)
|
|
class thread_wait_for_signals : wait_for_signals< F > {
|
|
using base_t = wait_for_signals<F>;
|
|
constexpr static inline bool TAKES_TOKEN = std::is_invocable_v<F, int const&, std::stop_token>;
|
|
|
|
int m_exit_signal;
|
|
std::jthread m_thread{};
|
|
public:
|
|
//TODO: Port this...
|
|
inline int exit_signal() const noexcept { return m_exit_signal; }
|
|
|
|
inline std::jthread& thread_handle() noexcept { return m_thread; }
|
|
inline std::jthread const& thread_handle() const noexcept { return m_thread; }
|
|
|
|
template<std::convertible_to<F> U = F>
|
|
inline thread_wait_for_signals(U&& on_sig, std::initializer_list<int> sigs, int exit = SIGTERM) noexcept(std::is_nothrow_convertible_v<U, F>)
|
|
: base_t(std::forward<U>(on_sig), sigs, exit)
|
|
, m_exit_signal(exit)
|
|
{
|
|
m_thread = std::jthread([this](std::stop_token st) { // XXX: This means the handle **must be pinned** and cannot be moved, see below.
|
|
if constexpr(TAKES_TOKEN) {
|
|
std::move(*this)(wrap_callback, [st] (F&& func, int const& signum) noexcept(std::is_nothrow_invocable_v<F, int const&>) -> decltype(auto) {
|
|
return std::invoke(std::forward<F>(func), signum, st);
|
|
});
|
|
} else {
|
|
std::move(*this)();
|
|
}
|
|
});
|
|
}
|
|
|
|
[[gnu::returns_nonnull, gnu::pure]]
|
|
inline std::jthread* operator->() noexcept { return &thread_handle(); }
|
|
[[gnu::returns_nonnull, gnu::pure]]
|
|
inline std::jthread const* operator->() const noexcept { return &thread_handle(); }
|
|
|
|
thread_wait_for_signals(thread_wait_for_signals&&) = delete; // = default; XXX: This is wrong! Sigset is not captured by value in the thread callback, it refers to `base::sigset&`. In order to make this move allowed, we must expose sigset **and** oldset to the ctor... Another option would be capturing *this as `base_t` by *move* *in* the constructor itself, but idk if that is legal tbh... It'd be better to just copy over the signal sets and give them to the callback before invoking it somehow i think... (XXX: This is actually not possible as the thread ctor also captures ref-access to the callback function as well, so...
|
|
|
|
// Prevent the exit-wake from happening.
|
|
//
|
|
// The thread handle is returned. To detach the handle entirely, use `std::move(self).release().detach()`.
|
|
// NOTE: Once this is called, to wake-kill the thread, use `pthread_kill(native_handle(), self.exit_signal())`.
|
|
// NOTE: The signal mask is **NOT** reset when this is called, you must also call `.reset_mask()` after the thread completes.
|
|
[[nodiscard]]
|
|
inline std::jthread release() && noexcept {
|
|
return std::move(m_thread);
|
|
}
|
|
|
|
|
|
// Both detach the thread from the handle and prevent the signal mask from being reset.
|
|
inline virtual void detach() && noexcept override {
|
|
// Detach the thread management.
|
|
m_thread.detach();
|
|
// Ensure the sigmask-reset is disabled *as well*.
|
|
static_cast<base_t&&>(*this).detach();
|
|
}
|
|
|
|
inline virtual ~thread_wait_for_signals() noexcept {
|
|
m_thread.request_stop();
|
|
|
|
//TODO: Instead, we could have the internal spawned thread issue a stop_callback that calls `raise(SIGTERM)`?
|
|
// That might make the whole handle-move-semantic thingy easier? But would also force the wake on any and all stop request, which... (XXX: That might be more desireable behaviour tbh...)
|
|
if(m_thread.joinable()) {
|
|
pthread_kill(m_thread.native_handle(), m_exit_signal);
|
|
m_thread.join();
|
|
}
|
|
|
|
// Parent ctor (resets signal mask.)
|
|
}
|
|
|
|
};
|
|
|
|
extern template class thread_wait_for_signals< void(*)(int) >;
|
|
extern template class thread_wait_for_signals< void(*)(int, std::stop_token) >;
|
|
|
|
extern template class thread_wait_for_signals<std::function<void(int)> >;
|
|
extern template class thread_wait_for_signals<std::move_only_function<void(int)> >;
|
|
|
|
extern template class thread_wait_for_signals<std::function<void(int, std::stop_token)> >;
|
|
extern template class thread_wait_for_signals<std::move_only_function<void(int, std::stop_token)> >;
|
|
|
|
template<typename F>
|
|
thread_wait_for_signals(F&& func, std::initializer_list<int>, int) -> thread_wait_for_signals<F>;
|
|
}
|