diff --git a/Makefile.am b/Makefile.am index 83480f3f..54ef6e80 100644 --- a/Makefile.am +++ b/Makefile.am @@ -96,6 +96,7 @@ console_bs_SOURCES = \ console/executor_store.cpp \ console/executor_test_reader.cpp \ console/executor_test_writer.cpp \ + console/executor_windows.cpp \ console/localize.hpp \ console/main.cpp \ console/stack_trace.cpp \ diff --git a/builds/cmake/CMakeLists.txt b/builds/cmake/CMakeLists.txt index 878836d5..ef062c6b 100644 --- a/builds/cmake/CMakeLists.txt +++ b/builds/cmake/CMakeLists.txt @@ -334,6 +334,7 @@ if (with-console) "../../console/executor_store.cpp" "../../console/executor_test_reader.cpp" "../../console/executor_test_writer.cpp" + "../../console/executor_windows.cpp" "../../console/libbitcoin.ico" "../../console/localize.hpp" "../../console/main.cpp" diff --git a/builds/msvc/libbitcoin.ico b/builds/msvc/libbitcoin.ico new file mode 100644 index 00000000..a15428a0 Binary files /dev/null and b/builds/msvc/libbitcoin.ico differ diff --git a/builds/msvc/resource.h b/builds/msvc/resource.h index b13f4e28..44ace40a 100644 Binary files a/builds/msvc/resource.h and b/builds/msvc/resource.h differ diff --git a/builds/msvc/resource.rc b/builds/msvc/resource.rc index 1bf31eb2..e6d99437 100644 Binary files a/builds/msvc/resource.rc and b/builds/msvc/resource.rc differ diff --git a/builds/msvc/vs2022/bs/bs.vcxproj b/builds/msvc/vs2022/bs/bs.vcxproj index a1decddc..a33ff0aa 100644 --- a/builds/msvc/vs2022/bs/bs.vcxproj +++ b/builds/msvc/vs2022/bs/bs.vcxproj @@ -139,6 +139,7 @@ + diff --git a/builds/msvc/vs2022/bs/bs.vcxproj.filters b/builds/msvc/vs2022/bs/bs.vcxproj.filters index 8a10a8c0..ff4d012d 100644 --- a/builds/msvc/vs2022/bs/bs.vcxproj.filters +++ b/builds/msvc/vs2022/bs/bs.vcxproj.filters @@ -81,6 +81,9 @@ src + + src + src @@ -116,4 +119,4 @@ resource - \ No newline at end of file + diff --git a/console/executor.cpp b/console/executor.cpp index da034e98..7b27be3d 100644 --- a/console/executor.cpp +++ b/console/executor.cpp @@ -31,9 +31,11 @@ namespace server { using boost::format; using namespace std::placeholders; -std::atomic_bool executor::cancel_{}; +// static initializers. std::thread executor::stop_poller_{}; std::promise executor::stopping_{}; +std::atomic executor::initialized_{}; +std::atomic executor::signal_{ unsignalled }; executor::executor(parser& metadata, std::istream& input, std::ostream& output, std::ostream&) @@ -56,45 +58,35 @@ executor::executor(parser& metadata, std::istream& input, std::ostream& output, metadata.configured.log.verbose } { - initialize_stop(); -} + BC_ASSERT(!initialized_); + initialized_ = true; -// Stop signal. -// ---------------------------------------------------------------------------- + initialize_stop(); #if defined(HAVE_MSC) -BOOL WINAPI executor::win32_handler(DWORD signal) + create_hidden_window(); +#endif +} + +executor::~executor() { - ////if (auto* log = fopen("shutdown.log", "a")) - ////{ - //// fprintf(log, "Signal %lu at %llu\n", signal, GetTickCount64()); - //// fflush(log); - //// fclose(log); - ////} + initialized_ = false; - switch (signal) - { - case CTRL_C_EVENT: - case CTRL_BREAK_EVENT: - case CTRL_CLOSE_EVENT: - case CTRL_LOGOFF_EVENT: - case CTRL_SHUTDOWN_EVENT: - executor::handle_stop({}); - return TRUE; - default: - return FALSE; - } -} +#if defined(HAVE_MSC) + destroy_hidden_window(); #endif +} + +// Stop signal. +// ---------------------------------------------------------------------------- +// static -// Call only once. void executor::initialize_stop() { poll_for_stopping(); #if defined(HAVE_MSC) - // TODO: use RegisterServiceCtrlHandlerEx for service registration. - ::SetConsoleCtrlHandler(&executor::win32_handler, TRUE); + ::SetConsoleCtrlHandler(&executor::control_handler, TRUE); #else // Restart interrupted system calls. struct sigaction action @@ -123,17 +115,51 @@ void executor::initialize_stop() #endif } +// Handle the stop signal and invoke stop method (requries signal safe code). +void executor::handle_stop(int signal) +{ + stop(signal); +} + +// Manage race between console stop and server stop. +void executor::stop(int signal) +{ + ////if (auto* log = fopen("shutdown.log", "a")) + ////{ + //// fprintf(log, "stop %lu at %llu\n", signal, GetTickCount64()); + //// fflush(log); + //// fclose(log); + ////} + + // Implementation is limited to signal safe code. + static_assert(std::atomic::is_always_lock_free); + + // Capture first handled signal value. + auto unset = unsignalled; + signal_.compare_exchange_strong(unset, signal, std::memory_order_acq_rel); +} + +// Any thread can monitor this for stopping. +bool executor::canceled() +{ + return signal_.load(std::memory_order_acquire) != unsignalled; +} + +// Spinning must be used in signal handler, cannot wait on a promise. void executor::poll_for_stopping() { + using namespace std::this_thread; + stop_poller_ = std::thread([]() { - while (!cancel_.load(std::memory_order_acquire)) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + while (!canceled()) + sleep_for(std::chrono::milliseconds(10)); stopping_.set_value(true); }); } +// Blocks until stopping is signalled by poller. void executor::wait_for_stopping() { stopping_.get_future().wait(); @@ -141,16 +167,20 @@ void executor::wait_for_stopping() stop_poller_.join(); } -// Implementation is limited to signal safe code. -void executor::handle_stop(int) +// Suspend verbose logging and log the stop signal. +void executor::log_stopping() { - stop(); -} + const auto signal = signal_.load(); + if (signal == signal_none) + return; -// Manage the race between console stop and server stop. -void executor::stop() -{ - cancel_.store(true, std::memory_order_release); + // 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 3f739247..f0180b53 100644 --- a/console/executor.hpp +++ b/console/executor.hpp @@ -28,12 +28,10 @@ #include #include -// This class is just an ad-hoc user interface wrapper on the node. -// It will be factored and cleaned up for final release. - namespace libbitcoin { namespace server { +// This class is just an ad-hoc user interface wrapper on the node. class executor { public: @@ -43,21 +41,38 @@ class executor executor(parser& metadata, std::istream&, std::ostream& output, std::ostream& error); + // Clean up. + ~executor(); + // 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 poll_for_stopping(); static void wait_for_stopping(); static void handle_stop(int code); - static void stop(); + static void stop(int signal=signal_none); + static bool canceled(); + #if defined(HAVE_MSC) - static BOOL WINAPI win32_handler(DWORD signal); + 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_{}; #endif // 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); @@ -155,8 +170,9 @@ class executor static const std::unordered_map fired_; // Shutdown. - static std::atomic_bool cancel_; static std::thread stop_poller_; + static std::atomic signal_; + static std::atomic initialized_; static std::promise stopping_; std::promise log_suspended_{}; diff --git a/console/executor_runner.cpp b/console/executor_runner.cpp index 7f6cca0d..09ff8251 100644 --- a/console/executor_runner.cpp +++ b/console/executor_runner.cpp @@ -36,7 +36,7 @@ void executor::stopper(const std::string& message) capture_.stop(); // Stop log, causing final message to be buffered by handler. - log_.stop(message, network::levels::application); + log_.stop(message,levels::application); // Suspend process termination until final message is buffered. log_suspended_.get_future().wait(); @@ -46,7 +46,7 @@ void executor::subscribe_connect() { node_->subscribe_connect ( - [&](const code&, const network::channel::ptr&) + [&](const code&, const channel::ptr&) { log_.write(levels::verbose) << "{in:" << node_->inbound_channel_count() << "}" @@ -162,13 +162,12 @@ bool executor::do_run() logger(BS_NETWORK_STARTING); node_->start(std::bind(&executor::handle_started, this, _1)); - // Wait on signal to stop node (). + // Wait on signal to stop node (, etc). wait_for_stopping(); - toggle_.at(levels::protocol) = false; - logger(BS_NETWORK_STOPPING); // Stop network (if not already stopped by self). // Blocks on join of server/node/network threadpool. + log_stopping(); node_->close(); // Sizes and records change, buckets don't. diff --git a/console/executor_scans.cpp b/console/executor_scans.cpp index 22149a92..edf634b1 100644 --- a/console/executor_scans.cpp +++ b/console/executor_scans.cpp @@ -45,7 +45,7 @@ void executor::scan_flags() const logger(BS_OPERATION_INTERRUPT); - for (size_t height{}; !cancel_ && height <= top; ++height) + for (size_t height{}; !canceled() && height <= top; ++height) { database::context ctx{}; const auto link = query_.to_candidate(height); @@ -65,7 +65,7 @@ void executor::scan_flags() const } } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); const auto span = duration_cast(logger::now() - start); @@ -86,7 +86,7 @@ void executor::scan_slabs() const // Tx (record) links are sequential and so iterable, however the terminal // condition assumes all tx entries fully written (ok for stopped node). // A running node cannot safely iterate over record links, but stopped can. - for (auto puts = query_.put_counts(link); to_bool(puts.first) && !cancel_; + for (auto puts = query_.put_counts(link); to_bool(puts.first) && !canceled(); puts = query_.put_counts(++link)) { inputs += puts.first; @@ -95,7 +95,7 @@ void executor::scan_slabs() const logger(format(BS_MEASURE_SLABS_ROW) % link % inputs % outputs); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); const auto span = duration_cast(logger::now() - start); @@ -114,7 +114,7 @@ void executor::scan_buckets() const auto filled = zero; auto bucket = max_size_t; auto start = logger::now(); - while (!cancel_ && (++bucket < query_.header_buckets())) + while (!canceled() && (++bucket < query_.header_buckets())) { const auto top = query_.top_header(bucket); if (!top.is_terminal()) @@ -125,7 +125,7 @@ void executor::scan_buckets() const duration_cast(logger::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); auto span = duration_cast(logger::now() - start); @@ -137,7 +137,7 @@ void executor::scan_buckets() const filled = zero; bucket = max_size_t; start = logger::now(); - while (!cancel_ && (++bucket < query_.tx_buckets())) + while (!canceled() && (++bucket < query_.tx_buckets())) { const auto top = query_.top_tx(bucket); if (!top.is_terminal()) @@ -148,7 +148,7 @@ void executor::scan_buckets() const duration_cast(logger::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); span = duration_cast(logger::now() - start); @@ -160,7 +160,7 @@ void executor::scan_buckets() const filled = zero; bucket = max_size_t; start = logger::now(); - while (!cancel_ && (++bucket < query_.point_buckets())) + while (!canceled() && (++bucket < query_.point_buckets())) { const auto top = query_.top_point(bucket); if (!top.is_terminal()) @@ -171,7 +171,7 @@ void executor::scan_buckets() const duration_cast(logger::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); span = duration_cast(logger::now() - start); @@ -219,7 +219,7 @@ void executor::scan_collisions() const const auto header_records = query_.header_records(); std_vector header(header_buckets, empty); std_vector txs(header_buckets, empty); - while (!cancel_ && (++index < header_records)) + while (!canceled() && (++index < header_records)) { const header_link link{ possible_narrow_cast(index) }; const auto key = query_.get_header_key(link.value); @@ -233,7 +233,7 @@ void executor::scan_collisions() const duration_cast(logger::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); // ........................................................................ @@ -275,7 +275,7 @@ void executor::scan_collisions() const const auto tx_records = query_.tx_records(); std_vector tx(tx_buckets, empty); std_vector strong_tx(tx_buckets, empty); - while (!cancel_ && (++index < tx_records)) + while (!canceled() && (++index < tx_records)) { const tx_link link{ possible_narrow_cast(index) }; const auto key = query_.get_tx_key(link.value); @@ -288,7 +288,7 @@ void executor::scan_collisions() const duration_cast(logger::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); // ........................................................................ @@ -352,7 +352,7 @@ void executor::scan_collisions() const size_t window{}; const auto top = query_.get_top_associated(); - for (index = zero; index <= top && !cancel_; ++index) + for (index = zero; index <= top && !canceled(); ++index) { ++coinbases; const auto link = query_.to_candidate(index); @@ -404,7 +404,7 @@ void executor::scan_collisions() const } } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); // ........................................................................ diff --git a/console/executor_test_reader.cpp b/console/executor_test_reader.cpp index e685368d..81e16dc2 100644 --- a/console/executor_test_reader.cpp +++ b/console/executor_test_reader.cpp @@ -36,7 +36,7 @@ void executor::read_test(const hash_digest&) const const auto concurrency = metadata_.configured.node.maximum_concurrency_(); size_t size{}; - for (auto height = zero; !cancel_ && height <= top; ++height) + for (auto height = zero; !canceled() && height <= top; ++height) { const auto link = query_.to_candidate(height); if (link.is_terminal()) @@ -89,7 +89,7 @@ void executor::read_test(const hash_digest&) const logger(format("Getting first [%1%] output address hashes.") % target_count); auto start = fine_clock::now(); - while (!cancel_ && keys.size() < target_count) + while (!canceled() && keys.size() < target_count) { const auto outputs = query_.get_outputs(tx++); if (is_null(outputs)) @@ -101,7 +101,7 @@ void executor::read_test(const hash_digest&) const for (const auto& put: *outputs) { keys.emplace(put->script().hash()); - if (cancel_ || keys.size() == target_count) + if (canceled() || keys.size() == target_count) break; } } @@ -138,7 +138,7 @@ void executor::read_test(const hash_digest&) const start = fine_clock::now(); for (auto& key: keys) { - if (cancel_) + if (canceled()) return; ////size_t found{}; @@ -151,7 +151,7 @@ void executor::read_test(const hash_digest&) const do { - if (cancel_) + if (canceled()) break; table::address::record address{}; @@ -276,7 +276,7 @@ void executor::read_test(const hash_digest&) const for (const auto& row: outs) { - if (cancel_) + if (canceled()) break; const auto output = !row.output ? "{error}" : @@ -322,7 +322,7 @@ void executor::read_test(const hash_digest&) const uint32_t block{ one }; logger("Find strong blocks."); - while (!cancel_ && (block < count) && query_.is_strong_block(block)) + while (!canceled() && (block < count) && query_.is_strong_block(block)) { ++block; } @@ -334,7 +334,7 @@ void executor::read_test(const hash_digest&) const uint32_t milestone{ 295'001 }; logger("Find milestone blocks."); - while (!cancel_ && (milestone < count) && query_.is_milestone(milestone)) + while (!canceled() && (milestone < count) && query_.is_milestone(milestone)) { ++milestone; } @@ -346,7 +346,7 @@ void executor::read_test(const hash_digest&) const logger("Find strong txs."); count = query_.tx_records(); - while (!cancel_ && (tx < count) && query_.is_strong_tx(tx)) + while (!canceled() && (tx < count) && query_.is_strong_tx(tx)) { ++tx; } @@ -366,7 +366,7 @@ void executor::read_test(const hash_digest&) const size_t total{}; logger("Get all coinbases."); - while (!cancel_ && (block <= top)) + while (!canceled() && (block <= top)) { const auto count = query_.get_tx_count(query_.to_candidate(block++)); if (is_zero(count)) @@ -422,7 +422,7 @@ void executor::read_test(const hash_digest&) const database::tx_link spender_link{}; const auto hash_spender = system::base16_hash("1ff970ec310c000595929bd290bbc8f4603ee18b2b4e3239dfb072aaca012b28"); - for (auto position = zero; !cancel_ && position < txs.size(); ++position) + for (auto position = zero; !canceled() && position < txs.size(); ++position) { const auto temp = txs.at(position); if (query_.get_tx_key(temp) == hash_spender) @@ -466,7 +466,7 @@ void executor::read_test(const hash_digest&) const database::tx_link spent_link{}; const auto hash_spent = system::base16_hash("85f65b57b88b74fd945a66a6ba392a5f3c8a7c0f78c8397228dece885d788841"); - for (auto position = zero; !cancel_ && position < txs.size(); ++position) + for (auto position = zero; !canceled() && position < txs.size(); ++position) { const auto temp = txs.at(position); if (query_.get_tx_key(temp) == hash_spent) @@ -640,7 +640,7 @@ void executor::read_test(const hash_digest&) const auto tx = 664'400'000_size; // Read all data except genesis (ie. for validation). - while (!cancel_ && (++tx < query_.tx_records())) + while (!canceled() && (++tx < query_.tx_records())) { const tx_link link{ system::possible_narrow_cast(tx) }; @@ -681,7 +681,7 @@ void executor::read_test(const hash_digest&) const duration_cast(fine_clock::now() - start).count()); } - if (cancel_) + if (canceled()) logger(BS_OPERATION_CANCELED); const auto span = duration_cast(fine_clock::now() - start); @@ -698,7 +698,7 @@ void executor::read_test(const hash_digest&) const std::getline(input_, line); const auto start = fine_clock::now(); - for (size_t height = 492'224; (height <= 492'224) && !cancel_; ++height) + for (size_t height = 492'224; (height <= 492'224) && !canceled(); ++height) { // 2s 0s const auto link = query_.to_header(hash492224); diff --git a/console/executor_windows.cpp b/console/executor_windows.cpp new file mode 100644 index 00000000..e61fd2d5 --- /dev/null +++ b/console/executor_windows.cpp @@ -0,0 +1,157 @@ +/** + * 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" + +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 +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; + } +} + +// static +LRESULT CALLBACK executor::window_proc(HWND handle, UINT message, + WPARAM wparam, LPARAM lparam) +{ + switch (message) + { + // Reject session close until process completion, initiate stop, and + // 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)); + return FALSE; + } + default: + { + return ::DefWindowProcW(handle, message, wparam, lparam); + } + } +} + +void executor::create_hidden_window() +{ + thread_ = std::thread([this]() + { + const auto instance = ::GetModuleHandleW(NULL); + const WNDCLASSEXW window_class + { + .cbSize = sizeof(WNDCLASSEXW), + .style = CS_HREDRAW | CS_VREDRAW, + .lpfnWndProc = &executor::window_proc, + .hInstance = instance, + .hIcon = ::LoadIconW(instance, MAKEINTRESOURCEW(101)), + .lpszClassName = window_name, + .hIconSm = ::LoadIconW(instance, MAKEINTRESOURCEW(101)) + }; + + // fault + if (is_zero(::RegisterClassExW(&window_class))) + return; + + // Zero sizing results in title bar only. + // WS_EX_NOACTIVATE: prevents focus-stealing. + // WS_VISIBLE: required to capture WM_QUERYENDSESSION. + window_ = ::CreateWindowExW + ( + WS_EX_NOACTIVATE, + window_name, + window_title, + WS_VISIBLE, + 0, 0, 0, 0, + NULL, + NULL, + ::GetModuleHandleW(NULL), + NULL); + + // fault + if (is_null(window_)) + return; + + MSG message{}; + BOOL result{}; + while (!is_zero(result = ::GetMessageW(&message, NULL, 0, 0))) + { + // fault + if (is_negative(result)) + return; + + ::TranslateMessage(&message); + ::DispatchMessageW(&message); + } + }); +} + +void executor::destroy_hidden_window() +{ + if (!is_null(window_)) + ::PostMessageW(window_, WM_QUIT, 0, 0); + + if (thread_.joinable()) + thread_.join(); + + if (!is_null(window_)) + { + ::DestroyWindow(window_); + window_ = NULL; + } + + const auto handle = ::GetConsoleWindow(); + if (!is_null(handle)) + ::ShutdownBlockReasonDestroy(handle); +} + +#endif // HAVE_MSC + +} // namespace server +} // namespace libbitcoin diff --git a/console/libbitcoin.ico b/console/libbitcoin.ico deleted file mode 100644 index 79464a84..00000000 Binary files a/console/libbitcoin.ico and /dev/null differ diff --git a/console/localize.hpp b/console/localize.hpp index 914fc738..09f25a87 100644 --- a/console/localize.hpp +++ b/console/localize.hpp @@ -180,6 +180,8 @@ namespace server { "Node failed to start with error '%1%'." #define BS_NODE_UNAVAILABLE \ "Command not available until node started." +#define BS_NODE_INTERRUPTED \ + "Node was interrupted by signal (%1%)." #define BS_NODE_BACKUP_STARTED \ "Snapshot is started."