From f02c965929cb9ae0a123e0dd447a7601177474db Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 25 Mar 2026 11:00:14 -0500 Subject: [PATCH] fix: macOS block index corruption, dbcache auto-sizing, import key rescan height MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shutdown: 3-phase stop (wait for RPC stop → SIGTERM → SIGKILL) prevents LevelDB flush interruption on macOS/APFS that caused full re-sync on restart - dbcache: auto-detect RAM and set -dbcache to 12.5% (clamped 450-4096 MB) on macOS (sysctl), Linux (sysconf), and Windows (GlobalMemoryStatusEx) - Import key: pass user-entered start height to z_importkey and trigger rescanblockchain from that height for t-key imports - Bump version to 1.1.1 --- CMakeLists.txt | 2 +- res/ObsidianDragon.manifest | 2 +- src/app.cpp | 16 +++--- src/config/version.h | 4 +- src/daemon/embedded_daemon.cpp | 80 +++++++++++++++++++++++----- src/ui/windows/import_key_dialog.cpp | 30 +++++++++-- 6 files changed, 109 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7abb8f7..9f82863 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.20) project(ObsidianDragon - VERSION 1.1.0 + VERSION 1.1.1 LANGUAGES C CXX DESCRIPTION "DragonX Cryptocurrency Wallet" ) diff --git a/res/ObsidianDragon.manifest b/res/ObsidianDragon.manifest index 7856e20..2952366 100644 --- a/res/ObsidianDragon.manifest +++ b/res/ObsidianDragon.manifest @@ -5,7 +5,7 @@ diff --git a/src/app.cpp b/src/app.cpp index 6825c90..eb9e398 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2124,15 +2124,19 @@ void App::stopEmbeddedDaemon() } if (stop_sent) { - DEBUG_LOGF("Waiting for daemon to begin shutdown...\n"); - shutdown_status_ = "Waiting for daemon to begin shutdown..."; - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n"); + shutdown_status_ = "Waiting for daemon to flush block index..."; + // Give the daemon time to flush LevelDB to disk before we + // escalate to SIGTERM. On macOS/APFS, LevelDB compaction + + // fsync can take 15-20s on a large chain. The stop() method + // will wait this long for a *natural* exit (via the RPC stop + // we already sent) before falling back to SIGTERM. } - // Wait for process to exit; SIGTERM/TerminateProcess as last resort. - // 10 seconds is generous — if the daemon hasn't exited by then it's stuck. shutdown_status_ = "Waiting for dragonxd process to exit..."; - embedded_daemon_->stop(10000); + // 20s grace period for the RPC "stop" to complete (LevelDB flush). + // Only after that does stop() escalate to SIGTERM, then SIGKILL. + embedded_daemon_->stop(20000); } bool App::isEmbeddedDaemonRunning() const diff --git a/src/config/version.h b/src/config/version.h index 9aece2b..05485b5 100644 --- a/src/config/version.h +++ b/src/config/version.h @@ -7,10 +7,10 @@ // !! DO NOT EDIT version.h — it is generated from version.h.in by CMake. // !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) -#define DRAGONX_VERSION "1.1.0" +#define DRAGONX_VERSION "1.1.1" #define DRAGONX_VERSION_MAJOR 1 #define DRAGONX_VERSION_MINOR 1 -#define DRAGONX_VERSION_PATCH 0 +#define DRAGONX_VERSION_PATCH 1 #define DRAGONX_APP_NAME "ObsidianDragon" #define DRAGONX_ORG_NAME "Hush" diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index 97f0129..3f2642a 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -34,6 +34,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #endif namespace fs = std::filesystem; @@ -152,6 +155,41 @@ std::vector EmbeddedDaemon::getChainParams() // DragonX chain parameters. // On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout. // On Linux, -printtoconsole is used for pipe-based output capture. + // Auto-detect a reasonable -dbcache based on available physical RAM. + // Default LevelDB cache is small (~450MB); larger caches improve sync + // performance and reduce disk I/O — especially on macOS with APFS. + std::string dbcache_arg = "-dbcache=450"; + { +#ifdef __APPLE__ + // sysctl hw.memsize returns total physical RAM in bytes + int64_t memsize = 0; + size_t len = sizeof(memsize); + if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) { + int totalMB = static_cast(memsize / (1024 * 1024)); + // Use ~12.5% of RAM for dbcache, clamped to [450, 4096] + int cache = std::max(450, std::min(4096, totalMB / 8)); + dbcache_arg = "-dbcache=" + std::to_string(cache); + } +#elif defined(__linux__) + long pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + if (pages > 0 && page_size > 0) { + int totalMB = static_cast((static_cast(pages) * page_size) / (1024 * 1024)); + int cache = std::max(450, std::min(4096, totalMB / 8)); + dbcache_arg = "-dbcache=" + std::to_string(cache); + } +#elif defined(_WIN32) + MEMORYSTATUSEX memInfo; + memInfo.dwLength = sizeof(memInfo); + if (GlobalMemoryStatusEx(&memInfo)) { + int totalMB = static_cast(memInfo.ullTotalPhys / (1024 * 1024)); + int cache = std::max(450, std::min(4096, totalMB / 8)); + dbcache_arg = "-dbcache=" + std::to_string(cache); + } +#endif + DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str()); + } + return { "-tls=only", #ifndef _WIN32 @@ -166,7 +204,8 @@ std::vector EmbeddedDaemon::getChainParams() "-ac_private=1", "-addnode=176.126.87.241", "-experimentalfeatures", - "-developerencryptwallet" + "-developerencryptwallet", + dbcache_arg }; } @@ -1045,14 +1084,12 @@ void EmbeddedDaemon::stop(int wait_ms) if (process_pid_ > 0) { setState(State::Stopping, "Stopping dragonxd..."); - // Send SIGTERM to the entire process group (negative PID). - // This ensures that if dragonxd is a shell script wrapper, - // both bash AND the actual dragonxd child receive the signal. - // Without this, only bash is killed and dragonxd is orphaned. - DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_); - kill(-process_pid_, SIGTERM); - - // Wait for process to exit, draining stdout each iteration + // Phase 1: Wait for the daemon to exit naturally. + // The caller (stopEmbeddedDaemon) already sent an RPC "stop" which + // tells the daemon to flush LevelDB, close sockets, and exit cleanly. + // On macOS/APFS the LevelDB flush can take several seconds — we must + // NOT send SIGTERM until the daemon has had enough time to finish. + DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms); auto start = std::chrono::steady_clock::now(); while (isRunning()) { drainOutput(); @@ -1061,15 +1098,34 @@ void EmbeddedDaemon::stop(int wait_ms) std::chrono::steady_clock::now() - start).count(); if (elapsed >= wait_ms) { - // Force kill the entire process group - DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_); - kill(-process_pid_, SIGKILL); break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + // Phase 2: If still running, send SIGTERM and wait a further 10s. + if (isRunning()) { + DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_); + kill(-process_pid_, SIGTERM); + + auto sigterm_start = std::chrono::steady_clock::now(); + while (isRunning()) { + drainOutput(); + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - sigterm_start).count(); + if (elapsed >= 10000) { + // Phase 3: Force kill + DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_); + kill(-process_pid_, SIGKILL); + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } else { + DEBUG_LOGF("Daemon exited cleanly after RPC stop\n"); + } + drainOutput(); // read any remaining output // Reap the child process diff --git a/src/ui/windows/import_key_dialog.cpp b/src/ui/windows/import_key_dialog.cpp index c317ed4..8074d04 100644 --- a/src/ui/windows/import_key_dialog.cpp +++ b/src/ui/windows/import_key_dialog.cpp @@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app) // Import keys on worker thread to avoid freezing UI bool rescan = s_rescan; + int rescanHeight = s_rescan_height; if (app->worker()) { - app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb { + app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb { int imported = 0; int failed = 0; @@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app) try { if (keyType == "z-spending") { - rpc->call("z_importkey", {key, rescan ? "yes" : "no"}); + // z_importkey "key" "yes"|"no" startheight + if (rescan && rescanHeight > 0) { + rpc->call("z_importkey", {key, "yes", rescanHeight}); + } else { + rpc->call("z_importkey", {key, rescan ? "yes" : "no"}); + } imported++; } else if (keyType == "t-privkey") { - rpc->call("importprivkey", {key, "", rescan}); + // importprivkey does not accept a startheight; + // import without rescan, then rescanblockchain + // from the requested height afterward. + if (rescan && rescanHeight > 0) { + rpc->call("importprivkey", {key, "", false}); + } else { + rpc->call("importprivkey", {key, "", rescan}); + } imported++; } else { failed++; @@ -329,6 +342,17 @@ void ImportKeyDialog::render(App* app) failed++; } } + + // If t-keys were imported without rescan (because a + // specific start height was requested), trigger a + // single rescanblockchain from that height now. + if (rescan && rescanHeight > 0 && imported > 0) { + try { + rpc->call("rescanblockchain", {rescanHeight}); + } catch (...) { + // rescan failure is non-fatal; user can retry + } + } return [imported, failed]() { s_imported_keys = imported;