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

//! 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>;
}