diff --git a/src/viam/sdk/module/signal_manager.cpp b/src/viam/sdk/module/signal_manager.cpp index 5946f00d2..ff1462634 100644 --- a/src/viam/sdk/module/signal_manager.cpp +++ b/src/viam/sdk/module/signal_manager.cpp @@ -3,19 +3,58 @@ #include #include +#include +#include + +#include + namespace viam { namespace sdk { -SignalManager::SignalManager() { +using signal_map = SignalManager::signal_map; + +SignalManager::SignalManager(SignalManager::signal_map&& sig_map) + : signal_map_(std::move(sig_map)) { sigemptyset(&sigset_); - sigaddset(&sigset_, SIGINT); - sigaddset(&sigset_, SIGTERM); + + for_each_entry([this](const auto& handler_entry) { + if (handler_entry) { + sigaddset(&sigset_, handler_entry.value); + } + }); + pthread_sigmask(SIG_BLOCK, &sigset_, NULL); } int SignalManager::wait() { - int sig = 0; - return sigwait(&sigset_, &sig); + int result = 0; + bool should_continue = false; + + do { + int sig = 0; + result = sigwait(&sigset_, &sig); + for_each_entry([&should_continue, sig](const auto& handler_entry) { + if (sig == handler_entry.value && handler_entry) { + should_continue = handler_entry(sig); + } + }); + } while (should_continue); + + return result; +} + +template +void SignalManager::for_each_entry(Callable&& c) { + static constexpr auto pmd_tuple = std::make_tuple(&signal_map::sigint_handler, + &signal_map::sigterm_handler, + &signal_map::sigusr1_handler, + &signal_map::sigusr2_handler); + + namespace mp11 = boost::mp11; + mp11::tuple_for_each( + mp11::tuple_transform( + [this](auto ptr) -> const auto& { return signal_map_.*ptr; }, pmd_tuple), + std::forward(c)); } } // namespace sdk diff --git a/src/viam/sdk/module/signal_manager.hpp b/src/viam/sdk/module/signal_manager.hpp index 90e62d12d..5fdb7b176 100644 --- a/src/viam/sdk/module/signal_manager.hpp +++ b/src/viam/sdk/module/signal_manager.hpp @@ -2,24 +2,66 @@ #include +#include + namespace viam { namespace sdk { /// @class SignalManager /// @brief Defines handling logic for SIGINT and SIGTERM required by all C++ -/// modules. +/// modules, and optionally for handling SIGUSR1 and SIGUSR2. /// @ingroup Module class SignalManager { public: - /// @brief Creates a new SignalManager. - explicit SignalManager(); + /// @brief A callable object for handling async signals. + /// Will be called with signal number of the signal which is raised. Return true to indicate + /// module execution should continue, false if it should stop. + using SignalHandler = std::function; + + /// @brief A registry of SignalHandlers for a predefined set of signals. + /// A default-constructed instance will block on SIGINT and SIGTERM, exiting when they are + /// raised. + struct signal_map { + template + struct handler_entry : std::integral_constant { + handler_entry() = default; + + template + handler_entry(Callable&& c) : handler(std::move(c)) {} + + operator bool() const { + return static_cast(handler); + } + + bool operator()(int sig) const { + return handler(sig); + } + + SignalHandler handler; + }; - /// @brief Wait for SignalManager to receive SIGINT or SIGTERM. - /// @return The signal number if successful. - /// @throws `std::runtime_error` if the underlying sigwait call was unsuccessful. + handler_entry sigint_handler = [](int) { return false; }; + handler_entry sigterm_handler = [](int) { return false; }; + handler_entry sigusr1_handler{nullptr}; + handler_entry sigusr2_handler{nullptr}; + }; + + /// @brief Creates a new SignalManager which waits until SIGINT or SIGTERM are received. + explicit SignalManager() : SignalManager(signal_map{}) {} + + /// @brief Creates a SignalManager with blocking set and callback behavior defined by sig_map. + SignalManager(signal_map&& sig_map); + + /// @brief Waits on and handles the signals specified by signal_map_. + /// @return The return value of the underlying call to POSIX sigwait. int wait(); private: + template + void for_each_entry(Callable&& c); + + signal_map signal_map_; + sigset_t sigset_; }; diff --git a/src/viam/sdk/tests/CMakeLists.txt b/src/viam/sdk/tests/CMakeLists.txt index b07f67243..ba0264649 100644 --- a/src/viam/sdk/tests/CMakeLists.txt +++ b/src/viam/sdk/tests/CMakeLists.txt @@ -59,6 +59,7 @@ viamcppsdk_add_boost_test(test_movement_sensor.cpp) viamcppsdk_add_boost_test(test_pose_tracker.cpp) viamcppsdk_add_boost_test(test_power_sensor.cpp) viamcppsdk_add_boost_test(test_resource.cpp) +viamcppsdk_add_boost_test(test_signal_manager.cpp) viamcppsdk_add_boost_test(test_sensor.cpp) viamcppsdk_add_boost_test(test_servo.cpp) viamcppsdk_add_boost_test(test_robot.cpp) diff --git a/src/viam/sdk/tests/test_signal_manager.cpp b/src/viam/sdk/tests/test_signal_manager.cpp new file mode 100644 index 000000000..afba6a439 --- /dev/null +++ b/src/viam/sdk/tests/test_signal_manager.cpp @@ -0,0 +1,108 @@ +#include + +#include + +#include +#include + +#include +#include + +#define BOOST_TEST_MODULE test module test_signal_manager +#include + +#include + +struct signal_waiter { + std::condition_variable cv; + std::mutex m; + std::thread t; + + bool started_waiting = false; + int result = -1; + + void run(viam::sdk::SignalManager mgr) { + t = std::thread{[&] { + std::unique_lock lk(m); + started_waiting = true; + cv.notify_one(); + lk.unlock(); + + result = mgr.wait(); + }}; + } + + void block_on_setup() { + std::unique_lock lk(m); + cv.wait(lk, [this] { return started_waiting; }); + } + + int kill_thread(int sig) { + return pthread_kill(t.native_handle(), sig); + } +}; + +namespace viam { +namespace sdktests { +namespace signal_manager { + +BOOST_AUTO_TEST_SUITE(test_signal_manager) + +BOOST_AUTO_TEST_CASE(test_default_manager) { + for (int sig : {SIGINT, SIGTERM}) { + signal_waiter waiter; + + waiter.run(sdk::SignalManager()); + waiter.block_on_setup(); + + BOOST_CHECK_EQUAL(waiter.kill_thread(sig), 0); + waiter.t.join(); + + BOOST_CHECK_EQUAL(waiter.result, 0); + } +} + +BOOST_AUTO_TEST_CASE(test_install_nonfatal) { + int usr1_count = 0; + + signal_waiter waiter; + + sdk::SignalManager::signal_map sig_map; + sig_map.sigusr1_handler = [&usr1_count, &waiter](int) { + std::unique_lock lk(waiter.m); + waiter.started_waiting = false; + waiter.cv.notify_one(); + + return ++usr1_count; + }; + + waiter.run(sdk::SignalManager(std::move(sig_map))); + + waiter.block_on_setup(); + + auto count_usr1 = [&](int iteration) { + BOOST_CHECK_EQUAL(waiter.kill_thread(SIGUSR1), 0); + + // make sure that the handler actually gets to run before we spam more signals + { + std::unique_lock lk(waiter.m); + waiter.cv.wait(lk, [&waiter] { return !waiter.started_waiting; }); + } + BOOST_CHECK_EQUAL(usr1_count, iteration); + waiter.started_waiting = true; + }; + + for (int iteration : {1, 2}) { + count_usr1(iteration); + } + + BOOST_CHECK_EQUAL(pthread_kill(waiter.t.native_handle(), SIGTERM), 0); + waiter.t.join(); + BOOST_CHECK_EQUAL(waiter.result, 0); +} + +BOOST_AUTO_TEST_SUITE_END() + +} // namespace signal_manager +} // namespace sdktests +} // namespace viam