//! Multi-threaded signal handling. #pragma once #include #include #include // needed for std::bind().... (TODO: How to remove use of bind...?) #include #include // 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 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 { template 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 sigs, with_add_t const&, std::initializer_list 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 sigs, std::convertible_to 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 inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) & { int signum = wait_for_signal(sigset); return std::forward(wrapper)(std::bind(&wait_for_signals::_do_signal_reached, this, false, std::placeholders::_1), signum); } template inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) && { int signum = wait_for_signal(sigset); return std::forward(wrapper)(std::bind(&wait_for_signals::_do_signal_reached, this, true, std::placeholders::_1), signum); } public: wait_for_signals(std::initializer_list sigs) noexcept; inline explicit wait_for_signals(std::convertible_to 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 inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) && { return std::move(*this)(w, std::forward(wrapper), this->sigset); } /// W is a function that takes a function-object with the signature `void(int)&` bound to `this`. template inline decltype(auto) operator()(wrap_callback_t const& w, W&& wrapper) & { return this->operator()(w, std::forward(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 class wait_for_signals : private wait_for_signals { using base_t = wait_for_signals; F on_sig; inline int do_wait_now(sigset_t const& sigset, bool move) noexcept { if(move) return static_cast(*this)(sigset); else return static_cast(*this)(sigset); } inline int do_wait_now(bool move) noexcept { if(move) return static_cast(*this)(); else return static_cast(*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 U = F> inline wait_for_signals(U&& on_sig, std::initializer_list sigs, std::convertible_to auto const&... add) noexcept(std::is_nothrow_convertible_v) requires(sizeof...(add) > 0) : base_t(sigs, std::forward(add)...) , on_sig(std::forward(on_sig)) {} inline decltype(auto) operator()(sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v) { return std::invoke(on_sig, do_wait_now(sigset, false)); } inline decltype(auto) operator()(sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::move(on_sig), do_wait_now(sigset, true)); } template requires(std::is_invocable_v) inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) & noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::forward(wrapper), on_sig, do_wait_now(sigset, false)); } template requires(std::is_invocable_v) inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper, sigset_t const& sigset) && noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::forward(wrapper), std::move(on_sig), do_wait_now(sigset, true)); } public: template U = F> inline wait_for_signals(U&& on_sig, std::initializer_list sigs) noexcept(std::is_nothrow_convertible_v) requires(std::is_invocable_v) : base_t(sigs) , on_sig(std::forward(on_sig)){} template U = F> inline wait_for_signals(U&& on_sig, std::convertible_to auto const&... sigs) noexcept(std::is_nothrow_convertible_v) requires(sizeof...(sigs) > 0) : wait_for_signals(std::forward(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) requires(std::is_move_constructible_v) = default; wait_for_signals& operator=(wait_for_signals&&) = delete; //TODO: Once 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) { return std::invoke(on_sig, do_wait_now(false)); } inline decltype(auto) operator()() && noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::move(on_sig), do_wait_now(true)); } using base_t::detach; using base_t::reset_mask; template requires(std::is_invocable_v) inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) & noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::forward(wrapper), on_sig, do_wait_now(false)); } template requires(std::is_invocable_v) inline decltype(auto) operator()(wrap_callback_t const&, W&& wrapper) && noexcept(std::is_nothrow_invocable_v) { return std::invoke(std::forward(wrapper), std::move(on_sig), do_wait_now(true)); } virtual ~wait_for_signals() noexcept = default; }; template 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; extern template class wait_for_signals; extern template class wait_for_signals< std::function< void(int) > >; template requires(std::is_invocable_v or std::is_invocable_v) class thread_wait_for_signals : wait_for_signals< F > { using base_t = wait_for_signals; constexpr static inline bool TAKES_TOKEN = std::is_invocable_v; 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 U = F> inline thread_wait_for_signals(U&& on_sig, std::initializer_list sigs, int exit = SIGTERM) noexcept(std::is_nothrow_convertible_v) : base_t(std::forward(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) -> decltype(auto) { return std::invoke(std::forward(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(*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 >; extern template class thread_wait_for_signals >; extern template class thread_wait_for_signals >; extern template class thread_wait_for_signals >; template thread_wait_for_signals(F&& func, std::initializer_list, int) -> thread_wait_for_signals; }