diff --git a/Makefile.am b/Makefile.am index bce2ace0..25dd320b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -93,6 +93,7 @@ console_bs_SOURCES = \ console/executor_options.cpp \ console/executor_runner.cpp \ console/executor_scans.cpp \ + console/executor_signals.cpp \ console/executor_store.cpp \ console/executor_test_reader.cpp \ console/executor_test_writer.cpp \ diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 9162cbb0..cbd650dd 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -331,6 +331,7 @@ if (with-console) "../../console/executor_options.cpp" "../../console/executor_runner.cpp" "../../console/executor_scans.cpp" + "../../console/executor_signals.cpp" "../../console/executor_store.cpp" "../../console/executor_test_reader.cpp" "../../console/executor_test_writer.cpp" diff --git a/builds/msvc/vs2022/bs/bs.vcxproj b/builds/msvc/vs2022/bs/bs.vcxproj index 030b3ac3..90aa552c 100644 --- a/builds/msvc/vs2022/bs/bs.vcxproj +++ b/builds/msvc/vs2022/bs/bs.vcxproj @@ -138,6 +138,7 @@ + diff --git a/builds/msvc/vs2022/bs/bs.vcxproj.filters b/builds/msvc/vs2022/bs/bs.vcxproj.filters index 5da9d976..2de592cb 100644 --- a/builds/msvc/vs2022/bs/bs.vcxproj.filters +++ b/builds/msvc/vs2022/bs/bs.vcxproj.filters @@ -72,6 +72,9 @@ src + + src + src diff --git a/console/executor.cpp b/console/executor.cpp index ac10c581..90e34f4e 100644 --- a/console/executor.cpp +++ b/console/executor.cpp @@ -21,9 +21,8 @@ #include #include -#include #include -#include +#include namespace libbitcoin { namespace server { @@ -32,12 +31,24 @@ using boost::format; using namespace system; using namespace std::placeholders; +// Construction. +// ---------------------------------------------------------------------------- + // static initializers. +std::promise executor::stopped_{}; std::promise executor::stopping_{}; -std::atomic executor::initialized_{}; std::atomic executor::signal_{ unsignalled }; -std::unique_ptr executor::stop_poller_{}; +std::optional executor::poller_thread_{}; + +// class factory (singleton) +executor& executor::factory(parser& metadata, std::istream& input, + std::ostream& output, std::ostream& error) +{ + static executor instance(metadata, input, output, error); + return instance; +} +// private constructor (singleton) executor::executor(parser& metadata, std::istream& input, std::ostream& output, std::ostream&) : metadata_(metadata), @@ -60,18 +71,11 @@ executor::executor(parser& metadata, std::istream& input, std::ostream& output, } { initialize_stop(); - -#if defined(HAVE_MSC) - create_hidden_window(); -#endif } +// executor::~executor() { -#if defined(HAVE_MSC) - destroy_hidden_window(); -#endif - uninitialize_stop(); } @@ -79,84 +83,23 @@ executor::~executor() // ---------------------------------------------------------------------------- // static -#if defined(HAVE_MSC) -BOOL WINAPI executor::control_handler(DWORD signal) -{ - switch (signal) - { - // Keyboard events. These prevent exit altogether when TRUE returned. - // handle_stop(signal) therefore shuts down gracefully/completely. - case CTRL_C_EVENT: - case CTRL_BREAK_EVENT: - - // A signal that the system sends to all processes attached to a - // console when the user closes the console (by clicking Close on the - // console window's window menu). Returning TRUE here does not - // materially delay exit, so aside from capture this is a noop. - case CTRL_CLOSE_EVENT: - executor::handle_stop(possible_narrow_sign_cast(signal)); - return TRUE; - - ////// Only services receive this (*any* user is logging off). - ////case CTRL_LOGOFF_EVENT: - ////// Only services receive this (all users already logged off). - ////case CTRL_SHUTDOWN_EVENT: - default: - return FALSE; - } -} -#endif - void executor::initialize_stop() { - BC_ASSERT(!initialized_); - initialized_ = true; - poll_for_stopping(); - -#if defined(HAVE_MSC) - ::SetConsoleCtrlHandler(&executor::control_handler, TRUE); -#else - // struct keywork avoids name conflict with posix function sigaction. - struct sigaction action{}; - - // Restart interrupted system calls. - action.sa_flags = SA_RESTART; - - // sa_handler is actually a macro :o - action.sa_handler = handle_stop; - - // Set masking. - sigemptyset(&action.sa_mask); - - // Block during handling to prevent reentrancy. - sigaddset(&action.sa_mask, SIGINT); - sigaddset(&action.sa_mask, SIGTERM); - sigaddset(&action.sa_mask, SIGHUP); - sigaddset(&action.sa_mask, SIGUSR2); - - sigaction(SIGINT, &action, nullptr); - sigaction(SIGTERM, &action, nullptr); - sigaction(SIGHUP, &action, nullptr); - sigaction(SIGUSR2, &action, nullptr); - - #if defined(HAVE_LINUX) - sigaction(SIGPWR, &action, nullptr); - #endif -#endif + create_hidden_window(); + set_signal_handlers(); } void executor::uninitialize_stop() { - BC_ASSERT(initialized_); - initialized_ = false; - stop(); - if (stop_poller_ && stop_poller_->joinable()) + if (poller_thread_.has_value() && poller_thread_.value().joinable()) { - stop_poller_->join(); - stop_poller_.reset(); + poller_thread_.value().join(); + poller_thread_.reset(); } + + destroy_hidden_window(); } // Handle the stop signal and invoke stop method (requries signal safe code). @@ -192,13 +135,13 @@ bool executor::canceled() // Spinning must be used in signal handler, cannot wait on a promise. void executor::poll_for_stopping() { - stop_poller_ = std::make_unique([]() + poller_thread_.emplace(std::thread([]() { while (!canceled()) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); stopping_.set_value(true); - }); + })); } // Blocks until stopping is signalled by poller. @@ -207,22 +150,6 @@ void executor::wait_for_stopping() stopping_.get_future().wait(); } -// Suspend verbose logging and log the stop signal. -void executor::log_stopping() -{ - const auto signal = signal_.load(); - if (signal == signal_none) - return; - - // A high level of consolve logging can obscure and delay stop. - toggle_.at(network::levels::protocol) = false; - toggle_.at(network::levels::verbose) = false; - toggle_.at(network::levels::proxy) = false; - - logger(format(BS_NODE_INTERRUPTED) % signal); - logger(BS_NETWORK_STOPPING); -} - // Event handlers. // ---------------------------------------------------------------------------- diff --git a/console/executor.hpp b/console/executor.hpp index a995b531..43d19bbb 100644 --- a/console/executor.hpp +++ b/console/executor.hpp @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -34,47 +35,26 @@ namespace server { // This class is just an ad-hoc user interface wrapper on the node. class executor { -public: - DELETE_COPY(executor); - - // Construct. +private: + // Singleton, private constructor. executor(parser& metadata, std::istream&, std::ostream& output, std::ostream& error); - // Clean up. - ~executor(); +public: + DELETE_COPY_MOVE(executor); + + // Get the singleton instance. + static executor& factory(parser& metadata, std::istream& input, + std::ostream& output, std::ostream& error); // Called from main. bool dispatch(); -private: - static constexpr int unsignalled{ -1 }; - static constexpr int signal_none{ -2 }; - - // Executor (static). - static void initialize_stop(); - static void uninitialize_stop(); - static void poll_for_stopping(); - static void wait_for_stopping(); - static void handle_stop(int code); - static void stop(int signal=signal_none); - static bool canceled(); - -#if defined(HAVE_MSC) - static BOOL WINAPI control_handler(DWORD signal); - static LRESULT CALLBACK window_proc(HWND handle, UINT message, - WPARAM wparam, LPARAM lparam); - - void create_hidden_window(); - void destroy_hidden_window(); - - HWND window_{}; - std::thread thread_{}; - std::promise ready_{}; -#endif + // Release shutdown monitor. + ~executor(); +private: // Executor. - void log_stopping(); void handle_started(const system::code& ec); void handle_subscribed(const system::code& ec, size_t key); void handle_running(const system::code& ec); @@ -148,6 +128,7 @@ class executor void subscribe_events(std::ostream& sink); void logger(const boost::format& message) const; void logger(const std::string& message) const; + void log_stopping(); // Runner. void stopper(const std::string& message); @@ -171,13 +152,6 @@ class executor // Runtime events. static const std::unordered_map fired_; - // Shutdown. - static std::atomic signal_; - static std::atomic initialized_; - static std::promise stopping_; - static std::unique_ptr stop_poller_; - std::promise log_suspended_{}; - parser& metadata_; server_node::ptr node_{}; server_node::store store_; @@ -189,6 +163,37 @@ class executor network::logger log_{}; network::capture capture_{ input_, close_ }; std_array toggle_; + + // Shutdown. + // ------------------------------------------------------------------------ + + static constexpr int unsignalled{ -1 }; + static constexpr int signal_none{ -2 }; + + static std::atomic signal_; + static std::promise stopped_; + static std::promise stopping_; + static std::optional poller_thread_; + + static void initialize_stop(); + static void uninitialize_stop(); + static void poll_for_stopping(); + static void wait_for_stopping(); + static void set_signal_handlers(); + static void create_hidden_window(); + static void destroy_hidden_window(); + static void stop(int signal=signal_none); + static void handle_stop(int code); + static bool canceled(); + +#if defined(HAVE_MSC) + static HWND window_handle_; + static std::promise window_ready_; + static std::optional window_thread_; + static BOOL WINAPI control_handler(DWORD signal); + static LRESULT CALLBACK window_proc(HWND handle, UINT message, + WPARAM wparam, LPARAM lparam); +#endif }; } // namespace server diff --git a/console/executor_logging.cpp b/console/executor_logging.cpp index 7d9f1bcf..e1116c28 100644 --- a/console/executor_logging.cpp +++ b/console/executor_logging.cpp @@ -91,7 +91,7 @@ void executor::subscribe_log(std::ostream& sink) // only after all logging work is completed. After the above // message is queued, no more are accepted, and the executor // may initiate its own (and thereby this log's) destruction. - log_suspended_.set_value(true); + stopped_.set_value(true); return false; } else @@ -105,6 +105,22 @@ void executor::subscribe_log(std::ostream& sink) ); } +// Suspend verbose logging and log the stop signal. +void executor::log_stopping() +{ + const auto signal = signal_.load(); + if (signal == signal_none) + return; + + // A high level of consolve logging can obscure and delay stop. + toggle_.at(network::levels::protocol) = false; + toggle_.at(network::levels::verbose) = false; + toggle_.at(network::levels::proxy) = false; + + logger(boost::format(BS_NODE_INTERRUPTED) % signal); + logger(BS_NETWORK_STOPPING); +} + void executor::logger(const std::string& message) const { if (log_.stopped()) diff --git a/console/executor_runner.cpp b/console/executor_runner.cpp index 09ff8251..39f976b5 100644 --- a/console/executor_runner.cpp +++ b/console/executor_runner.cpp @@ -39,7 +39,7 @@ void executor::stopper(const std::string& message) log_.stop(message,levels::application); // Suspend process termination until final message is buffered. - log_suspended_.get_future().wait(); + stopped_.get_future().wait(); } void executor::subscribe_connect() diff --git a/console/executor_signals.cpp b/console/executor_signals.cpp new file mode 100644 index 00000000..01d75e1a --- /dev/null +++ b/console/executor_signals.cpp @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2011-2025 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "executor.hpp" + +#include + +namespace libbitcoin { +namespace server { + +// TODO: use RegisterServiceCtrlHandlerEx for service registration. + +#if defined(HAVE_MSC) +BOOL WINAPI executor::control_handler(DWORD signal) +{ + switch (signal) + { + // Keyboard events. These prevent exit altogether when TRUE returned. + // handle_stop(signal) therefore shuts down gracefully/completely. + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + + // A signal that the system sends to all processes attached to a + // console when the user closes the console (by clicking Close on the + // console window's window menu). Returning TRUE here does not + // materially delay exit, so aside from capture this is a noop. + case CTRL_CLOSE_EVENT: + { + const auto value = system::possible_narrow_sign_cast(signal); + executor::handle_stop(value); + return TRUE; + } + + ////// Only services receive this (*any* user is logging off). + ////case CTRL_LOGOFF_EVENT: + ////// Only services receive this (all users already logged off). + ////case CTRL_SHUTDOWN_EVENT: + default: + return FALSE; + } +} +#endif + +// static +void executor::set_signal_handlers() +{ +#if defined(HAVE_MSC) + ::SetConsoleCtrlHandler(&executor::control_handler, TRUE); +#else + // struct keywork avoids name conflict with posix function sigaction. + struct sigaction action{}; + + // Restart interrupted system calls. + action.sa_flags = SA_RESTART; + + // sa_handler is actually a macro :o + action.sa_handler = handle_stop; + + // Set masking. + sigemptyset(&action.sa_mask); + + // Block during handling to prevent reentrancy. + sigaddset(&action.sa_mask, SIGINT); + sigaddset(&action.sa_mask, SIGTERM); + sigaddset(&action.sa_mask, SIGHUP); + sigaddset(&action.sa_mask, SIGUSR2); + + sigaction(SIGINT, &action, nullptr); + sigaction(SIGTERM, &action, nullptr); + sigaction(SIGHUP, &action, nullptr); + sigaction(SIGUSR2, &action, nullptr); + +#if defined(HAVE_LINUX) + sigaction(SIGPWR, &action, nullptr); +#endif +#endif +} + +} // namespace server +} // namespace libbitcoin diff --git a/console/executor_window.cpp b/console/executor_window.cpp index 76e6ad3e..5a417c2e 100644 --- a/console/executor_window.cpp +++ b/console/executor_window.cpp @@ -18,19 +18,15 @@ */ #include "executor.hpp" +#include +#include +#include +#include "localize.hpp" + namespace libbitcoin { namespace server { #if defined(HAVE_MSC) - -// TODO: use RegisterServiceCtrlHandlerEx for service registration. - -using namespace system; -constexpr auto window_name = L"HiddenShutdownWindow"; -constexpr auto window_text = L"Flushing tables..."; -constexpr auto window_title = L"Libbitcoin Server"; - -// static LRESULT CALLBACK executor::window_proc(HWND handle, UINT message, WPARAM wparam, LPARAM lparam) { @@ -40,8 +36,9 @@ LRESULT CALLBACK executor::window_proc(HWND handle, UINT message, // provide reason text that the operating system may show to the user. case WM_QUERYENDSESSION: { - ::ShutdownBlockReasonCreate(handle, window_text); - executor::handle_stop(possible_narrow_sign_cast(message)); + ::ShutdownBlockReasonCreate(handle, BS_WINDOW_TEXT); + const auto value = system::possible_narrow_sign_cast(message); + executor::handle_stop(value); return FALSE; } default: @@ -50,17 +47,29 @@ LRESULT CALLBACK executor::window_proc(HWND handle, UINT message, } } } +#endif + +// static initializers. +#if defined(HAVE_MSC) +HWND executor::window_handle_{}; +std::promise executor::window_ready_{}; +std::optional executor::window_thread_{}; +#endif +// static void executor::create_hidden_window() { - thread_ = std::thread([this]() +#if defined(HAVE_MSC) + static constexpr auto window_name = L"HiddenShutdownWindow"; + + window_thread_.emplace(std::thread([]() { const auto instance = ::GetModuleHandleW(NULL); const WNDCLASSEXW window_class { .cbSize = sizeof(WNDCLASSEXW), .style = CS_HREDRAW | CS_VREDRAW, - .lpfnWndProc = &executor::window_proc, + .lpfnWndProc = &window_proc, .hInstance = instance, .hIcon = ::LoadIconW(instance, MAKEINTRESOURCEW(101)), .lpszClassName = window_name, @@ -74,11 +83,11 @@ void executor::create_hidden_window() // Zero sizing results in title bar only. // WS_EX_NOACTIVATE: prevents focus-stealing. // WS_VISIBLE: required to capture WM_QUERYENDSESSION. - window_ = ::CreateWindowExW + window_handle_ = ::CreateWindowExW ( WS_EX_NOACTIVATE, window_name, - window_title, + BS_WINDOW_TITLE, WS_VISIBLE, 0, 0, 0, 0, NULL, @@ -87,47 +96,51 @@ void executor::create_hidden_window() NULL); // fault - if (is_null(window_)) + if (is_null(window_handle_)) return; MSG message{}; BOOL result{}; - ready_.set_value(true); + window_ready_.set_value(true); while (!is_zero(result = ::GetMessageW(&message, NULL, 0, 0))) { // fault - if (is_negative(result)) + if (system::is_negative(result)) return; ::TranslateMessage(&message); ::DispatchMessageW(&message); } - }); + })); +#endif } void executor::destroy_hidden_window() { +#if defined(HAVE_MSC) // Wait until window is accepting messages, so WM_QUIT isn't missed. - ready_.get_future().wait(); + window_ready_.get_future().wait(); - if (!is_null(window_)) - ::PostMessageW(window_, WM_QUIT, 0, 0); + if (!is_null(window_handle_)) + ::PostMessageW(window_handle_, WM_QUIT, 0, 0); - if (thread_.joinable()) - thread_.join(); + if (window_thread_.has_value() && window_thread_.value().joinable()) + { + window_thread_.value().join(); + window_thread_.reset(); + } - if (!is_null(window_)) + if (!is_null(window_handle_)) { - ::DestroyWindow(window_); - window_ = NULL; + ::DestroyWindow(window_handle_); + window_handle_ = NULL; } const auto handle = ::GetConsoleWindow(); if (!is_null(handle)) ::ShutdownBlockReasonDestroy(handle); +#endif } -#endif // HAVE_MSC - } // namespace server } // namespace libbitcoin diff --git a/console/localize.hpp b/console/localize.hpp index 09f25a87..d3e0eac4 100644 --- a/console/localize.hpp +++ b/console/localize.hpp @@ -21,8 +21,8 @@ /// Localizable messages. -namespace libbitcoin { -namespace server { +#define BS_WINDOW_TITLE L"Libbitcoin Server" +#define BS_WINDOW_TEXT L"Flushing tables..." #define BS_OPERATION_INTERRUPT \ "Press CTRL-C to cancel operation." @@ -273,7 +273,4 @@ namespace server { #define BS_NODE_TERMINATE \ "Press to exit..." -} // namespace server -} // namespace libbitcoin - #endif diff --git a/console/main.cpp b/console/main.cpp index e0f0e5c6..e6a2549d 100644 --- a/console/main.cpp +++ b/console/main.cpp @@ -104,6 +104,6 @@ int bc::system::main(int argc, char* argv[]) set_memory_priority(metadata.configured.node.memory_priority_()); - executor host(metadata, cin, cout, cerr); + executor& host = executor::factory(metadata, cin, cout, cerr); return host.dispatch() ? 0 : -1; }