From a1da3bfc12f77aacae9fb409df40d5151465dea4 Mon Sep 17 00:00:00 2001 From: Hodlinator <172445034+hodlinator@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:09:06 +0100 Subject: [PATCH] qa debug: Add --debug_runs/-waitfordebugger Makes bitcoind spin during startup, waiting for a debugger to be attached. Useful for debugging one or several bitcoind nodes running in the context of a functional test. TODO: Confirm code works on Mac. --- CMakeLists.txt | 5 ++ src/bitcoind.cpp | 70 +++++++++++++++++++ src/init.cpp | 7 ++ .../test_framework/test_framework.py | 4 ++ test/functional/test_framework/test_node.py | 13 +++- 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c84e011088a..b71916f5fe0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ option(ENABLE_HARDENING "Attempt to harden the resulting executables." ON) option(REDUCE_EXPORTS "Attempt to reduce exported symbols in the resulting executables." OFF) option(WERROR "Treat compiler warnings as errors." OFF) option(WITH_CCACHE "Attempt to use ccache for compiling." ON) +option(WAIT_FOR_DEBUGGER "Support for waiting during startup for debugger to be attached." OFF) option(WITH_ZMQ "Enable ZMQ notifications." OFF) if(WITH_ZMQ) @@ -243,6 +244,10 @@ target_compile_definitions(core_interface_debug INTERFACE ABORT_ON_FAILED_ASSUME ) +if(WAIT_FOR_DEBUGGER) + add_compile_definitions(WAIT_FOR_DEBUGGER=1) +endif() + if(WIN32) #[=[ This build system supports two ways to build binaries for Windows. diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp index ceb3c99410c..fd77d35256d 100644 --- a/src/bitcoind.cpp +++ b/src/bitcoind.cpp @@ -28,7 +28,19 @@ #include #include +#ifdef WIN32 +#include +#include +#elif defined(__APPLE__) +#include +#elif defined(__linux__) +#include +#include +#endif + #include +#include +#include #include #include @@ -159,6 +171,60 @@ static bool ProcessInitCommands(ArgsManager& args) return false; } +#ifdef WAIT_FOR_DEBUGGER +static int HandleWaitForDebugger(int argc, char* argv[]) +{ + for (int i = 0; i < argc; ++i) { + if (strcmp(argv[i], "-waitfordebugger") == 0) { + while (true) { + bool attached{false}; +# if defined(__linux__) + // Allow any process to attach to us. + prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY); + + std::ifstream sf{"/proc/self/status", std::ios::in}; + if (!sf.good()) { + return EXIT_FAILURE; + } + + std::string s; + uint pid{0}; + while (sf >> s) { + if (s == "TracerPid:") { + sf >> pid; + break; + } + + std::getline(sf, s); + } + attached = pid > 0; +# elif defined(__APPLE__) + const int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() }; + kinfo_proc info; + size_t size{sizeof(info)}; + const int ret{sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0)}; + if (ret != EXIT_SUCCESS) { + return ret; + } + attached = info.kp_proc.p_flag & P_TRACED; +# elif defined(WIN32) + attached = IsDebuggerPresent(); +# else +# error "Platform doesn't support -waitfordebugger."; +# endif // platform + if (attached) { + break; + } else { + std::this_thread::sleep_for(100ms); + } + } + } + } + + return EXIT_SUCCESS; +} +#endif // WAIT_FOR_DEBUGGER + static bool AppInit(NodeContext& node) { bool fRet = false; @@ -254,6 +320,10 @@ static bool AppInit(NodeContext& node) MAIN_FUNCTION { +#ifdef WAIT_FOR_DEBUGGER + if (int ret{HandleWaitForDebugger(argc, argv)}; ret != EXIT_SUCCESS) return ret; +#endif + #ifdef WIN32 common::WinCmdLineArgs winArgs; std::tie(argc, argv) = winArgs.get(); diff --git a/src/init.cpp b/src/init.cpp index d46318fd45e..89e6fa64e22 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -680,6 +680,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) hidden_args.emplace_back("-daemon"); hidden_args.emplace_back("-daemonwait"); #endif + argsman.AddArg("-waitfordebugger", "Spin until a debugger is attached", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); // Add the hidden options argsman.AddHiddenArgs(hidden_args); @@ -1092,6 +1093,12 @@ bool AppInitParameterInteraction(const ArgsManager& args) } } +#ifndef WAIT_FOR_DEBUGGER + if (args.GetBoolArg("-waitfordebugger", false)) { + return InitError(Untranslated("-waitfordebugger support was not included as WAIT_FOR_DEBUGGER is not #defined.")); + } +#endif + return true; } diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 921f12d9fb4..b9b515fb00b 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -205,6 +205,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)") parser.add_argument("--test_methods", dest="test_methods", nargs='*', help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.") + parser.add_argument("--debug_runs", dest="debug_runs", nargs="*", type=int, default=[], + help="Node executions in the test to stall and wait for debugger, 0-based.") self.add_options(parser) # Running TestShell in a Jupyter notebook causes an additional -f argument @@ -243,6 +245,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): PortSeed.n = self.options.port_seed + TestNode.debug_runs = self.options.debug_runs + def set_binary_paths(self): """Update self.options with the paths of all binaries from environment variables or their default values""" diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index f7d6ba78d23..5ad6015ca66 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -76,6 +76,9 @@ class TestNode(): To make things easier for the test writer, any unrecognised messages will be dispatched to the RPC connection.""" + runs = 0 + debug_runs = None + def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False, v2transport=False): """ Kwargs: @@ -253,10 +256,18 @@ class TestNode(): if env is not None: subp_env.update(env) + wait_for_debugger = TestNode.debug_runs is not None and TestNode.runs in TestNode.debug_runs + if wait_for_debugger: + extra_args.append("-waitfordebugger") + self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs) self.running = True - self.log.debug("bitcoind started, waiting for RPC to come up") + if wait_for_debugger: + self.log.info(f"bitcoind started (run #{TestNode.runs}, node #{self.index}), waiting for debugger, PID: {self.process.pid}") + else: + self.log.debug(f"bitcoind started (run #{TestNode.runs}, node #{self.index}), waiting for RPC to come up") + TestNode.runs += 1 if self.start_perf: self._start_perf()