commit 076e77ff53a209cf5e2c6bcfdac7a608ae67708a Author: Tom Date: Sat Sep 14 22:41:56 2019 +0200 initial import diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1aaa8f7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.8 FATAL_ERROR) +project(signal-handler VERSION 0.1.0) + +# Add the top-level cmake module directory to CMAKE_MODULE_PATH +list(INSERT CMAKE_MODULE_PATH 0 ${PROJECT_SOURCE_DIR}/cmake) + +find_package(Threads REQUIRED) +if( NOT CMAKE_USE_PTHREADS_INIT ) + message(FATAL_ERROR "pthreads required") +endif() + +add_library(sgnl INTERFACE) +add_library(sgnl::sgnl ALIAS sgnl) +target_include_directories( + sgnl INTERFACE + $ + $) +target_link_libraries(sgnl INTERFACE Threads::Threads) +target_compile_features(sgnl INTERFACE cxx_std_17) + +include(EnableWarnings) +enable_warnings(sgnl INTERFACE) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/SgnlConfigVersion.cmake" + COMPATIBILITY SameMajorVersion) + +include(GNUInstallDirs) +install(TARGETS sgnl + EXPORT SgnlTargets) +install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/sgnl" + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES "${PROJECT_SOURCE_DIR}/cmake/SgnlConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/SgnlConfigVersion.cmake" + DESTINATION lib/cmake/sgnl) +install(EXPORT SgnlTargets + FILE SgnlTargets.cmake + NAMESPACE sgnl:: + DESTINATION lib/cmake/sgnl) + +enable_testing() +add_subdirectory("test") + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ecb517 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2019 Thomas Trapp + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..67be132 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +Signal handler for multithreaded C++ applications on Linux +========================================================== + + +Example usage: +``` +bool worker(sgnl::AtomicCondition& exit_condition) +{ + while( true ) + { + exit_condition.wait_for(std::chrono::milliseconds(1000), true); + if( exit_condition.get() ) + return true; + } +} + +sgnl::AtomicCondition exit_condition(false); + +sgnl::SignalHandler signal_handler( + {{SIGINT, true}, {SIGTERM, true}}, + exit_condition); + +std::promise signal_handler_thread_id; +std::future ft_sig_handler = + std::async(std::launch::async, [&]() { + signal_handler_thread_id.set_value(pthread_self()); + return signal_handler(); + }); + +std::vector> futures; +for(int i = 0; i < 10; ++i) + futures.push_back( + std::async( + std::launch::async, + &worker, + std::ref(exit_condition))); + +// simulate [ctrl]+[c], which sends SIGINT +std::this_thread::sleep_for(std::chrono::milliseconds(100)); +pthread_kill( + signal_handler_thread_id.get_future().get(), + SIGINT); + +for(auto& future : futures) + future.get(); + +int signal = ft_sig_handler.get(); +``` + diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +* diff --git a/cmake/DefaultBuildRelease.cmake b/cmake/DefaultBuildRelease.cmake new file mode 100644 index 0000000..6c8d601 --- /dev/null +++ b/cmake/DefaultBuildRelease.cmake @@ -0,0 +1,6 @@ +# If build type was not specified, build Release. + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Release") +endif() + diff --git a/cmake/EnableWarnings.cmake b/cmake/EnableWarnings.cmake new file mode 100644 index 0000000..b801d8a --- /dev/null +++ b/cmake/EnableWarnings.cmake @@ -0,0 +1,67 @@ +function(enable_warnings_gnu) + target_compile_options( + ${ARGN} + "-Wall" + "-Wcast-align" + "-Wcast-qual" + "-Wconversion" + "-Wctor-dtor-privacy" + "-Wdisabled-optimization" + "-Weffc++" + "-Wextra" + "-Wfloat-equal" + "-Wformat=2" + "-Wimport" + "-Winvalid-pch" + "-Wlogical-op" + "-Wmissing-format-attribute" + "-Wmissing-include-dirs" + "-Wmissing-noreturn" + "-Woverloaded-virtual" + "-Wpacked" + "-Wpointer-arith" + "-Wredundant-decls" + "-Wshadow" + "-Wsign-conversion" + "-Wsign-promo" + "-Wstack-protector" + "-Wstrict-aliasing=2" + "-Wstrict-null-sentinel" + "-Wstrict-overflow" + "-Wswitch" + "-Wundef" + "-Wunreachable-code" + "-Wunused" + "-Wvariadic-macros" + "-Wwrite-strings" + "-pedantic" + "-pedantic-errors" + ) +endfunction() + +function(enable_warnings_clang) + target_compile_options( + ${ARGN} + "-Weverything" + "-Wno-c++98-compat" + "-Wno-documentation" + "-Wno-documentation-html" + "-Wno-documentation-unknown-command" + "-Wno-exit-time-destructors" + "-Wno-global-constructors" + "-Wno-padded" + "-Wno-switch-enum" + "-Wno-covered-switch-default" + "-Wno-weak-vtables") +endfunction() + +function(enable_warnings) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + enable_warnings_clang(${ARGN}) + endif() + + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + enable_warnings_gnu(${ARGN}) + endif() +endfunction() + diff --git a/cmake/SgnlConfig.cmake b/cmake/SgnlConfig.cmake new file mode 100644 index 0000000..0b61c0a --- /dev/null +++ b/cmake/SgnlConfig.cmake @@ -0,0 +1,3 @@ +include(CMakeFindDependencyMacro) +find_dependency(Threads) +include("${CMAKE_CURRENT_LIST_DIR}/SgnlTargets.cmake") diff --git a/include/sgnl/AtomicCondition.h b/include/sgnl/AtomicCondition.h new file mode 100644 index 0000000..d4f2687 --- /dev/null +++ b/include/sgnl/AtomicCondition.h @@ -0,0 +1,63 @@ +// Author: Thomas Trapp - https://thomastrapp.com/ +// License: MIT + +#pragma once + +#include +#include +#include +#include +#include + + +namespace sgnl { + + +template +class AtomicCondition +{ +public: + explicit AtomicCondition(ValueType val) + : value_(val) + , condvar_mutex_() + , condvar_() + { + // requirement of std::signal + if( !this->value_.is_lock_free() ) + throw std::runtime_error("atomic is not lock-free"); + } + + ValueType get() const + { + return this->value_.load(); + } + + void set(ValueType val) + { + this->value_.store(val); + } + + void wait_for(const std::chrono::milliseconds& time, ValueType val) const + { + std::unique_lock lock(this->condvar_mutex_); + + // This while-loop takes care of "spurious wakeups" + while( this->value_.load() != val ) + if( this->condvar_.wait_for(lock, time) == std::cv_status::timeout ) + return; + } + + void notify_all() + { + this->condvar_.notify_all(); + } + +private: + std::atomic value_; + mutable std::mutex condvar_mutex_; + mutable std::condition_variable condvar_; +}; + + +} // namespace sgnl + diff --git a/include/sgnl/SignalHandler.h b/include/sgnl/SignalHandler.h new file mode 100644 index 0000000..04410d1 --- /dev/null +++ b/include/sgnl/SignalHandler.h @@ -0,0 +1,75 @@ +// Author: Thomas Trapp - https://thomastrapp.com/ +// License: MIT + +#pragma once + +#include + +#include +#include +#include +#include +#include + + +namespace sgnl { + + +class SignalHandlerException : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + + +template +class SignalHandler +{ +public: + SignalHandler(std::map signal_map, + AtomicCondition& condition) + : signal_map_(std::move(signal_map)) + , set_() + , condition_(condition) + { + if( sigemptyset(&this->set_) != 0 ) + throw SignalHandlerException("sigemptyset error"); + + for( const auto& p : this->signal_map_ ) + if( sigaddset(&this->set_, p.first) != 0 ) + throw SignalHandlerException("sigaddset error"); + + int s = pthread_sigmask(SIG_BLOCK, &this->set_, nullptr); + if( s != 0 ) + throw SignalHandlerException( + std::string("pthread_sigmask: ") + std::strerror(s)); + } + + int operator()() + { + while( true ) + { + int signum = 0; + int ret = sigwait(&this->set_, &signum); + if( ret != 0 ) + throw SignalHandlerException( + std::string("sigwait: ") + std::strerror(ret)); + + if( auto it = this->signal_map_.find(signum); + it != this->signal_map_.end() ) + { + this->condition_.set(it->second); + this->condition_.notify_all(); + return it->first; + } + } + } + +private: + std::map signal_map_; + sigset_t set_; + AtomicCondition& condition_; +}; + + +} // namespace sgnl + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..70ccdc6 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.8 FATAL_ERROR) +project(signal-handler-test) + +include(EnableWarnings) +include(DefaultBuildRelease) + +find_package(Catch2 REQUIRED) + +add_executable( + sgnl-test + "${PROJECT_SOURCE_DIR}/test.cpp") +enable_warnings(sgnl-test PRIVATE) +target_link_libraries(sgnl-test sgnl::sgnl Catch2::Catch2 "-fsanitize=thread") +target_compile_features(sgnl-test PUBLIC cxx_std_17) +target_compile_options(sgnl-test PUBLIC "-fsanitize=thread") + +add_test("signal-handler-test" sgnl-test) + diff --git a/test/test.cpp b/test/test.cpp new file mode 100644 index 0000000..61638ec --- /dev/null +++ b/test/test.cpp @@ -0,0 +1,63 @@ +#define CATCH_CONFIG_MAIN + +#include + +#include + +#include + +#include +#include +#include + + +namespace { + + +bool worker(sgnl::AtomicCondition& exit_condition) +{ + exit_condition.wait_for(std::chrono::milliseconds(1000), true); + return exit_condition.get(); +} + + +} // namespace + + +TEST_CASE("main") +{ + std::vector signals({SIGINT, SIGTERM, SIGUSR1, SIGUSR2}); + for( auto test_signal : signals ) + { + sgnl::AtomicCondition exit_condition(false); + + sgnl::SignalHandler signal_handler({{test_signal, true}}, exit_condition); + std::promise signal_handler_thread_id; + std::future ft_sig_handler = + std::async(std::launch::async, [&]() { + signal_handler_thread_id.set_value(pthread_self()); + return signal_handler(); + }); + + std::vector> futures; + for(int i = 0; i < 10; ++i) + futures.push_back( + std::async( + std::launch::async, + &worker, + std::ref(exit_condition))); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + REQUIRE( + pthread_kill( + signal_handler_thread_id.get_future().get(), + test_signal) == 0 ); + + for(auto& future : futures) + REQUIRE(future.get() == true); + + int signal = ft_sig_handler.get(); + REQUIRE( signal == test_signal ); + } +} +