fix: macOS block index corruption, dbcache auto-sizing, import key rescan height
- 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
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="1.1.0.0"
|
||||
version="1.1.1.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
|
||||
16
src/app.cpp
16
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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#ifdef __APPLE__
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -152,6 +155,41 @@ std::vector<std::string> 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<int>(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<int>((static_cast<int64_t>(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<int>(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<std::string> 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::milliseconds>(
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user