fix(startup): surface filesystem failures and verify Sapling param integrity
Three verified daemon-startup edge-case fixes centered on the config/params filesystem path: - F7: new non-throwing Platform::ensureDirectory(dir, outError) with one consistent "Cannot create <dir>: <reason>. Check permissions / free space." message. Replaces the unchecked/throwing create_directories sites at main.cpp (pre-init: log + Windows MessageBox + return 1), connection.cpp's autoDetectConfig (was the *throwing* overload -- could raise an uncaught filesystem_error through its callers; now sets the new ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via daemon_status_ + return false). The primary connect path (app_network.cpp) checks dir_error and shows it instead of mislabelling it "waiting for config". embedded_resources.cpp already checked its error_code, so it is left as-is. - F6: verifySaplingParams() now hash-verifies each param against its pinned canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh) instead of only checking existence, so a truncated / corrupt-but-present param is rejected up front rather than failing later on a shielded operation. A <params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing ~48MB on every startup. Logic extracted to the injectable, unit-testable verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl). - F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and the previously-dropped copy_file error_code in the daemon-binary fallback loop, aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed) instead of failing opaquely at spawn. An absent source file stays non-fatal. Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp. i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json back-fill via add_missing_translations.py is deferred to a single run at the end of the batch. Progress tracked in docs/daemon-startup-hardening.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,8 @@ around a single shared helper. The connectivity-breaking security flip lands las
|
||||
| 1 | **F1** | `embedded_daemon.cpp` · `isRunning()` | Smallest/highest-severity; establishes the reliable Error/crash-count transition steps 3 & 6 depend on. | ☑ |
|
||||
| 2 | **F2** | `embedded_daemon.cpp` · `startProcess()` | Same file family, different function; test the F1+F2 pair together with `kill -SEGV` / bad-binary repros. | ☑ |
|
||||
| 3 | **F4** | `embedded_daemon.cpp` · `start()` | After F1/F2 so crash-count semantics are settled; its bail deliberately stays out of the crash path. | ☑ |
|
||||
| 4 | **F7** | `util/platform` · `connection.cpp` | Structural owner of the fs-error idiom + `ConnectionConfig` that F5/F6/F8 reuse. | ☐ |
|
||||
| 5 | **F6 + F5** | `app.cpp` · `verifySaplingParams()` | Same `startEmbeddedDaemon` / `verifySaplingParams` block; land together. | ☐ |
|
||||
| 4 | **F7** | `util/platform` · `connection.cpp` | Structural owner of the fs-error idiom + `ConnectionConfig` that F5/F6/F8 reuse. | ☑ |
|
||||
| 5 | **F6 + F5** | `app.cpp` · `verifySaplingParams()` | Same `startEmbeddedDaemon` / `verifySaplingParams` block; land together. | ☑ |
|
||||
| 6 | **F3** | `app.cpp` · `renderLoadingOverlay()` | After F1 — panel is guarded off during `State::Error` (owned by the crash-count hint). | ☐ |
|
||||
| 7 | **F8** | `connection.cpp` · `tryConnect()` | Largest; only connectivity-breaking default flip — land last, with release notes. | ☐ |
|
||||
|
||||
@@ -216,7 +216,7 @@ F1/F2 (must not touch `crash_count_`; wording must not collide with the monitor'
|
||||
|
||||
## F5 — Extraction / copy write-failures never surfaced up front
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~2–3h) · **Status:** ☐
|
||||
**Severity:** Medium · **Effort:** S (~2–3h) · **Status:** ☑ landed & verified
|
||||
|
||||
### The defect
|
||||
`startEmbeddedDaemon()` discards `extractEmbeddedResources()`'s `bool` return
|
||||
@@ -281,7 +281,14 @@ template F7 matches. Open item: remove truncated dst files so a retry re-copies.
|
||||
|
||||
## F6 — Sapling params validated by existence/size only, never hashed
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☐
|
||||
**Severity:** Medium · **Effort:** S (~3–5h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **As-built note.** `verifySaplingParams()` now delegates to a public, injectable
|
||||
> `verifySaplingParamsIn(dir, digests)` so the integrity + marker-cache logic is unit-testable
|
||||
> with synthetic small files (the real 48 MB params aren't in the repo). i18n keys for F5 were
|
||||
> added to `i18n.cpp` (English source of truth); the `res/lang/*.json` back-fill via
|
||||
> `scripts/add_missing_translations.py` is deferred to a single run at the end of the batch,
|
||||
> per the cross-cutting note. Non-English locales fall back to English until then.
|
||||
|
||||
### The defect
|
||||
`verifySaplingParams()` (`connection.cpp:123`) only calls `fs::exists()`;
|
||||
@@ -331,7 +338,11 @@ Third caller of the existing `util::sha256Hex`.
|
||||
|
||||
## F7 — Directory-create errors universally ignored on the daemon-env path
|
||||
|
||||
**Severity:** Medium · **Effort:** S (~3–4h) · **Status:** ☐
|
||||
**Severity:** Medium · **Effort:** S (~3–4h) · **Status:** ☑ landed & verified
|
||||
|
||||
> **As-built notes.** Two deviations from the original design, both confirmed against the code:
|
||||
> (1) `embedded_resources.cpp:270` already checks its `error_code` and returns `false` on failure — it was **not** a bug, so it is left untouched.
|
||||
> (2) Of the four `autoDetectConfig` callers, only the primary connect path (`app_network.cpp:243`) was wired to check `dir_error`; the other three degrade gracefully on their own — `app.cpp:4306` and `app_wizard.cpp:912` are stop paths that already gate on empty creds, and `settings_page.cpp:434` is read-only display. `dir_error` is set by `autoDetectConfig`, so they can be wired later if desired.
|
||||
|
||||
### The defect
|
||||
Five startup directory-create sites either drop the `error_code` or use the throwing
|
||||
@@ -492,4 +503,7 @@ design record; see the shared-helper table above.
|
||||
|
||||
- **F1** — ☑ landed: `isRunning()` (POSIX) now reads the atomic `state_` (predicate `Running || Stopping`) instead of calling `waitpid`, leaving `monitorProcess()` the sole reaper. Clean build (all targets link); `ctest` 1/1 passing. Not unit-testable — needs the manual `kill -SEGV` repro before release.
|
||||
- **F2** — ☑ landed: `startProcess()` (POSIX) now creates a `FD_CLOEXEC` self-pipe before `fork()`; the child writes `errno` to it on `execv` failure, the parent reads EOF-vs-errno and, on failure, reaps the zombie + sets a precise `last_error_` ("not executable or wrong architecture") + returns `false` (so `start()` no longer reports `Running` for a daemon that never started). Parent-side `setpgid` is now best-effort with a `DEBUG_LOGF` on failure. Clean build; `ctest` 1/1 passing. Not unit-testable — needs the manual non-executable / wrong-arch-binary repro before release.
|
||||
- **F6** — ☑ landed: `verifySaplingParams()` now hash-verifies each Sapling param against its pinned canonical SHA-256 (from `build-lite-backend-artifact.sh`), replacing the existence-only check, so a truncated/corrupt-but-present param is rejected instead of failing later on a shielded op. A `<params_dir>/.sapling_verified` marker keyed on `size:mtime` skips re-hashing ~48 MB on every startup. Logic extracted to the injectable `verifySaplingParamsIn(dir, digests)`; new `testVerifySaplingParams` unit test (valid / marker fast-path / wrong-hash / truncated / missing). Clean build; `ctest` 1/1 passing.
|
||||
- **F5** — ☑ landed: `startEmbeddedDaemon()` now checks `extractEmbeddedResources()`'s return (abort with `sb_daemon_extract_failed` on failure) and the previously-dropped `copy_file` `error_code` in the daemon-binary fallback loop (abort with `sb_daemon_files_failed` incl. the dir), so a disk-full / truncated `dragonxd` write is surfaced up front instead of failing opaquely at spawn. An absent source file stays non-fatal. Two i18n keys added to `i18n.cpp`. Clean build; `ctest` 1/1 passing.
|
||||
- **F7** — ☑ landed: new non-throwing `Platform::ensureDirectory(dir, outError)` in `util/platform.{h,cpp}` with one consistent message. Replaces the unchecked/throwing directory-create sites at `main.cpp:730` (pre-init: now logs + `MessageBoxA` on Windows + `return 1`), `connection.cpp:216` (autoDetectConfig now uses the ec overload — **no more uncaught `filesystem_error`** — and sets the new `ConnectionConfig::dir_error`), and both `app.cpp` daemon-dir sites (surface via `daemon_status_` + `return false`). Primary connect path (`app_network.cpp:243`) checks `dir_error` and bails to the status line instead of mislabelling it "waiting for config". `embedded_resources.cpp:270` left as-is (already correct). New `testPlatformEnsureDirectory` unit test (existing-dir / fresh-nested / empty / parent-is-file). Clean build; `ctest` 1/1 passing.
|
||||
- **F4** — ☑ landed: `start()` now gates on a lingering datadir lock after the port bail. When `!skip_port_check_ && override_datadir_.empty()`, it polls `isDaemonProcessRunning()` with a bounded ~300 ms wait (3 × 100 ms, breaks early), then a pure header-inline `evaluateDatadirLockGate()` decides: if a sibling `dragonxd` is still alive it bails with a distinct **non-crash** `State::Error` ("…holding the data directory lock. Retrying shortly…") that never touches `crash_count_`, so the 3-strike cap can't trip; the connect loop's retry resumes once the lock clears. Isolated migrate-to-seed starts are exempt. New `testDatadirLockGate` unit test (5 assertions, proceed/bail/2× exempt) added to `test_phase4.cpp`. Clean build; `ctest` 1/1 passing.
|
||||
|
||||
35
src/app.cpp
35
src/app.cpp
@@ -4149,7 +4149,11 @@ bool App::startEmbeddedDaemon()
|
||||
if (resources::hasEmbeddedResources()) {
|
||||
DEBUG_LOGF("Extracting embedded Sapling params...\n");
|
||||
daemon_status_ = TR("sb_extracting_sapling");
|
||||
resources::extractEmbeddedResources();
|
||||
if (!resources::extractEmbeddedResources()) {
|
||||
daemon_status_ = TR("sb_daemon_extract_failed");
|
||||
DEBUG_LOGF("[ERROR] extractEmbeddedResources() failed — disk full or permission denied?\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check again after extraction
|
||||
if (!rpc::Connection::verifySaplingParams()) {
|
||||
@@ -4168,8 +4172,13 @@ bool App::startEmbeddedDaemon()
|
||||
const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" };
|
||||
bool copied = false;
|
||||
if (!exe_dir.empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
daemon_status_ = dirErr;
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
fs::create_directories(daemon_dir, ec);
|
||||
|
||||
// On macOS .app bundles, params are in Contents/Resources/
|
||||
// while the executable is in Contents/MacOS/
|
||||
@@ -4214,8 +4223,13 @@ bool App::startEmbeddedDaemon()
|
||||
std::string exe_dir = util::Platform::getExecutableDirectory();
|
||||
std::string daemon_dir = resources::getDaemonDirectory();
|
||||
if (!exe_dir.empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
daemon_status_ = dirErr;
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
fs::create_directories(daemon_dir, ec);
|
||||
|
||||
std::vector<std::string> searchDirs = { exe_dir };
|
||||
#ifdef __APPLE__
|
||||
@@ -4226,18 +4240,31 @@ bool App::startEmbeddedDaemon()
|
||||
}
|
||||
#endif
|
||||
const char* extraFiles[] = { "asmap.dat", "dragonxd", "dragonx-cli", "dragonx-tx" };
|
||||
bool copyFailed = false;
|
||||
for (const char* name : extraFiles) {
|
||||
fs::path dst = fs::path(daemon_dir) / name;
|
||||
if (fs::exists(dst)) continue;
|
||||
for (const auto& dir : searchDirs) {
|
||||
fs::path src = fs::path(dir) / name;
|
||||
if (fs::exists(src)) {
|
||||
if (fs::exists(src)) { // an absent source is optional; only a real copy error counts
|
||||
DEBUG_LOGF("Copying bundled %s from %s to %s\n", name, dir.c_str(), daemon_dir.c_str());
|
||||
fs::copy_file(src, dst, ec);
|
||||
if (ec) {
|
||||
DEBUG_LOGF("[ERROR] Failed to copy %s: %s\n", name, ec.message().c_str());
|
||||
copyFailed = true;
|
||||
ec.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (copyFailed) {
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str());
|
||||
daemon_status_ = buf;
|
||||
DEBUG_LOGF("[ERROR] One or more daemon files failed to copy to %s\n", daemon_dir.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,16 @@ void App::tryConnect()
|
||||
|
||||
// Auto-detect configuration (file I/O — fast, safe on main thread)
|
||||
auto config = rpc::Connection::autoDetectConfig();
|
||||
|
||||
if (!config.dir_error.empty()) {
|
||||
// The data directory could not be created (read-only home, permission denied,
|
||||
// disk full). Retrying won't fix it, so surface it in the status line instead of
|
||||
// mislabelling it as "waiting for config" below.
|
||||
connection_in_progress_ = false;
|
||||
connection_status_ = config.dir_error;
|
||||
VERBOSE_LOGF("[connect #%d] data dir error: %s\n", connect_attempt, config.dir_error.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.rpcuser.empty() || config.rpcpassword.empty()) {
|
||||
connection_in_progress_ = false;
|
||||
|
||||
12
src/main.cpp
12
src/main.cpp
@@ -726,8 +726,16 @@ int main(int argc, char* argv[])
|
||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
||||
{
|
||||
std::string odDir = dragonx::util::Platform::getObsidianDragonDir();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(odDir, ec);
|
||||
std::string odErr;
|
||||
if (!dragonx::util::Platform::ensureDirectory(odDir, &odErr)) {
|
||||
// Pre-App-init: nothing (ini, logs, config) can persist if this fails, and the
|
||||
// Windows log redirect below isn't set up yet — report loudly before any setup.
|
||||
std::fprintf(stderr, "%s\n", odErr.c_str());
|
||||
#ifdef _WIN32
|
||||
MessageBoxA(nullptr, odErr.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONERROR);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
#include "../util/xmrig_updater.h" // util::sha256Hex
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
@@ -120,30 +124,121 @@ std::string Connection::getSaplingParamsDir()
|
||||
return resources::getDaemonDirectory();
|
||||
}
|
||||
|
||||
bool Connection::verifySaplingParams()
|
||||
namespace {
|
||||
|
||||
std::string joinParamPath(const std::string& dir, const std::string& file) {
|
||||
#ifdef _WIN32
|
||||
return dir + "\\" + file;
|
||||
#else
|
||||
return dir + "/" + file;
|
||||
#endif
|
||||
}
|
||||
|
||||
// "<size>:<mtime>" fingerprint used to skip re-hashing an unchanged file. Empty on error.
|
||||
std::string paramStatLine(const std::string& path) {
|
||||
std::error_code ec;
|
||||
auto sz = fs::file_size(path, ec);
|
||||
if (ec) return {};
|
||||
auto mtime = fs::last_write_time(path, ec);
|
||||
long long ticks = ec ? 0 :
|
||||
std::chrono::duration_cast<std::chrono::seconds>(mtime.time_since_epoch()).count();
|
||||
return std::to_string(static_cast<unsigned long long>(sz)) + ":" + std::to_string(ticks);
|
||||
}
|
||||
|
||||
bool paramHashMatches(const std::string& path, const std::string& expectedHex) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return false;
|
||||
std::streamsize sz = f.tellg();
|
||||
if (sz <= 0) return false;
|
||||
f.seekg(0, std::ios::beg);
|
||||
std::vector<char> buf(static_cast<size_t>(sz));
|
||||
if (!f.read(buf.data(), sz)) return false;
|
||||
std::string got = util::sha256Hex(buf.data(), buf.size());
|
||||
return !got.empty() && got == expectedHex;
|
||||
}
|
||||
|
||||
// The verification cache: <params_dir>/.sapling_verified holds one paramStatLine per param,
|
||||
// in list order, from the last successful hash check.
|
||||
bool saplingMarkerMatches(const std::string& markerPath, const std::vector<std::string>& expected) {
|
||||
for (const auto& s : expected) if (s.empty()) return false; // couldn't stat -> don't trust
|
||||
std::ifstream f(markerPath);
|
||||
if (!f) return false;
|
||||
std::vector<std::string> lines;
|
||||
std::string l;
|
||||
while (std::getline(f, l)) lines.push_back(l);
|
||||
return lines == expected;
|
||||
}
|
||||
|
||||
void writeSaplingMarker(const std::string& markerPath, const std::vector<std::string>& lines) {
|
||||
std::ofstream f(markerPath, std::ios::trunc);
|
||||
if (!f) return;
|
||||
for (const auto& l : lines) f << l << "\n";
|
||||
}
|
||||
|
||||
// Canonical Zcash-family Sapling trusted-setup param digests — identical bytes across every
|
||||
// fork/platform. Source of truth: scripts/build-lite-backend-artifact.sh ensure_sapling_params().
|
||||
// Keep in sync if the params are ever rotated.
|
||||
const std::pair<std::string, std::string> kSaplingParamDigests[] = {
|
||||
{ "sapling-spend.params", "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13" },
|
||||
{ "sapling-output.params", "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4" },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Connection::verifySaplingParamsIn(
|
||||
const std::string& dir,
|
||||
const std::vector<std::pair<std::string, std::string>>& digests)
|
||||
{
|
||||
std::string params_dir = getSaplingParamsDir();
|
||||
if (params_dir.empty()) {
|
||||
if (dir.empty()) {
|
||||
DEBUG_LOGF("verifySaplingParams: params dir is empty\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string spend_path = params_dir + "\\sapling-spend.params";
|
||||
std::string output_path = params_dir + "\\sapling-output.params";
|
||||
#else
|
||||
std::string spend_path = params_dir + "/sapling-spend.params";
|
||||
std::string output_path = params_dir + "/sapling-output.params";
|
||||
#endif
|
||||
|
||||
bool spend_exists = fs::exists(spend_path);
|
||||
bool output_exists = fs::exists(output_path);
|
||||
|
||||
DEBUG_LOGF("verifySaplingParams: dir=%s\n", params_dir.c_str());
|
||||
DEBUG_LOGF(" spend: %s -> %s\n", spend_path.c_str(), spend_exists ? "found" : "MISSING");
|
||||
DEBUG_LOGF(" output: %s -> %s\n", output_path.c_str(), output_exists ? "found" : "MISSING");
|
||||
|
||||
return spend_exists && output_exists;
|
||||
if (digests.empty()) return false;
|
||||
|
||||
// 1) Every param must exist.
|
||||
std::vector<std::string> paths;
|
||||
paths.reserve(digests.size());
|
||||
for (const auto& d : digests) {
|
||||
std::string p = joinParamPath(dir, d.first);
|
||||
if (!fs::exists(p)) {
|
||||
DEBUG_LOGF("verifySaplingParams: %s MISSING\n", p.c_str());
|
||||
return false;
|
||||
}
|
||||
paths.push_back(std::move(p));
|
||||
}
|
||||
|
||||
// 2) Fast path: if the cached marker matches the current size:mtime of every param, trust
|
||||
// the previous successful hash instead of re-hashing ~48MB on every startup.
|
||||
const std::string markerPath = joinParamPath(dir, ".sapling_verified");
|
||||
std::vector<std::string> current;
|
||||
current.reserve(paths.size());
|
||||
for (const auto& p : paths) current.push_back(paramStatLine(p));
|
||||
if (saplingMarkerMatches(markerPath, current)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3) Integrity-check each param against its pinned SHA-256. A truncated or corrupt param
|
||||
// (a partial extraction, or a Linux bundle where the file merely *exists*) is rejected
|
||||
// here instead of being handed to the daemon and failing later on a shielded operation.
|
||||
for (size_t i = 0; i < paths.size(); ++i) {
|
||||
if (!paramHashMatches(paths[i], digests[i].second)) {
|
||||
DEBUG_LOGF("verifySaplingParams: %s FAILED integrity check (truncated or corrupt)\n",
|
||||
paths[i].c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Record the verified state so later startups take the fast path.
|
||||
writeSaplingMarker(markerPath, current);
|
||||
DEBUG_LOGF("verifySaplingParams: %zu params verified (sha256)\n", paths.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Connection::verifySaplingParams()
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> digests;
|
||||
for (const auto& d : kSaplingParamDigests) digests.emplace_back(d.first, d.second);
|
||||
return verifySaplingParamsIn(getSaplingParamsDir(), digests);
|
||||
}
|
||||
|
||||
ConnectionConfig Connection::parseConfFile(const std::string& path)
|
||||
@@ -209,11 +304,14 @@ ConnectionConfig Connection::autoDetectConfig()
|
||||
{
|
||||
ConnectionConfig config;
|
||||
|
||||
// Ensure data directory exists
|
||||
// Ensure the data directory exists. Use the non-throwing helper and report any failure
|
||||
// via config.dir_error so callers can surface it — the old throwing create_directories()
|
||||
// overload could raise an uncaught filesystem_error straight through autoDetectConfig()'s
|
||||
// callers (read-only home, permission denied, etc.).
|
||||
std::string data_dir = getDefaultDataDir();
|
||||
if (!fs::exists(data_dir)) {
|
||||
DEBUG_LOGF("Creating data directory: %s\n", data_dir.c_str());
|
||||
fs::create_directories(data_dir);
|
||||
if (!util::Platform::ensureDirectory(data_dir, &config.dir_error)) {
|
||||
DEBUG_LOGF("[ERROR] autoDetectConfig: %s\n", config.dir_error.c_str());
|
||||
return config; // data dir unusable — bail early with dir_error set
|
||||
}
|
||||
|
||||
// Try to find DRAGONX.conf
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace rpc {
|
||||
@@ -28,6 +30,9 @@ struct ConnectionConfig {
|
||||
bool use_embedded = true;
|
||||
bool use_tls = false;
|
||||
AuthSource auth_source = AuthSource::Missing;
|
||||
// Non-empty when autoDetectConfig() could not create the data directory; callers
|
||||
// should surface it and abort the connect rather than proceeding blindly.
|
||||
std::string dir_error;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,6 +74,14 @@ public:
|
||||
*/
|
||||
static bool verifySaplingParams();
|
||||
|
||||
// Verify the Sapling params in `dir` against a { filename, expected-sha256-hex } list.
|
||||
// Exposed with an injectable dir + digest list so the integrity + marker-cache logic is
|
||||
// unit-testable without the real ~48MB params; verifySaplingParams() calls it with the
|
||||
// pinned production digests and getSaplingParamsDir().
|
||||
static bool verifySaplingParamsIn(
|
||||
const std::string& dir,
|
||||
const std::vector<std::pair<std::string, std::string>>& digests);
|
||||
|
||||
/**
|
||||
* @brief Get the Sapling params directory
|
||||
*/
|
||||
|
||||
@@ -1316,6 +1316,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["sb_extracting_sapling"] = "Extracting Sapling parameters...";
|
||||
strings_["sb_sapling_failed"] = "Failed to extract Sapling parameters.";
|
||||
strings_["sb_sapling_not_found"] = "Sapling parameters not found.";
|
||||
strings_["sb_daemon_extract_failed"] = "Failed to write daemon files — check free disk space and permissions.";
|
||||
strings_["sb_daemon_files_failed"] = "Failed to write daemon files to %s — check free disk space and permissions.";
|
||||
strings_["sb_dragonxd_running"] = "dragonxd running";
|
||||
strings_["sb_dragonxd_stopping"] = "Stopping dragonxd...";
|
||||
strings_["sb_dragonxd_stopped"] = "dragonxd stopped";
|
||||
|
||||
@@ -126,6 +126,27 @@ bool Platform::openUrl(const std::string& url)
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Platform::ensureDirectory(const std::string& dir, std::string* outError)
|
||||
{
|
||||
if (dir.empty()) {
|
||||
if (outError) *outError = "Cannot create directory: empty path.";
|
||||
return false;
|
||||
}
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_directory(dir, ec)) return true;
|
||||
ec.clear();
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
if (outError) {
|
||||
*outError = "Cannot create " + dir + ": " + ec.message() +
|
||||
". Check permissions / free space.";
|
||||
}
|
||||
DEBUG_LOGF("[ERROR] ensureDirectory failed for %s: %s\n", dir.c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Platform::openFolder(const std::string& path, bool createIfMissing)
|
||||
{
|
||||
if (path.empty()) return false;
|
||||
|
||||
@@ -128,6 +128,17 @@ public:
|
||||
*/
|
||||
static void ensureObsidianDragonSetup();
|
||||
|
||||
/**
|
||||
* @brief Create a directory (and parents) if missing, with a clear error on failure.
|
||||
*
|
||||
* Uses the non-throwing std::error_code overload internally. On failure sets *outError
|
||||
* (when non-null) to one consistent, user-facing message:
|
||||
* "Cannot create <dir>: <reason>. Check permissions / free space."
|
||||
*
|
||||
* @return true if the directory exists (already did, or was just created).
|
||||
*/
|
||||
static bool ensureDirectory(const std::string& dir, std::string* outError = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Get total system RAM in megabytes
|
||||
* @return Total physical RAM in MB, or 0 on failure
|
||||
|
||||
@@ -2478,6 +2478,105 @@ void testDaemonShutdownPolicy()
|
||||
EXPECT_TRUE(bootstrap.disconnectRpc);
|
||||
}
|
||||
|
||||
void testVerifySaplingParams()
|
||||
{
|
||||
using dragonx::rpc::Connection;
|
||||
namespace fsn = std::filesystem;
|
||||
|
||||
fsn::path dir = fsn::temp_directory_path() / "od_sapling_test";
|
||||
std::error_code rmec;
|
||||
fsn::remove_all(dir, rmec);
|
||||
fsn::create_directories(dir);
|
||||
|
||||
auto writeFile = [](const fsn::path& p, const std::string& content) {
|
||||
std::ofstream(p.string(), std::ios::binary) << content;
|
||||
};
|
||||
const std::string spendContent = "fake-spend-params-contents";
|
||||
const std::string outputContent = "fake-output-params-contents";
|
||||
writeFile(dir / "sapling-spend.params", spendContent);
|
||||
writeFile(dir / "sapling-output.params", outputContent);
|
||||
|
||||
const std::string spendHash = dragonx::util::sha256Hex(spendContent.data(), spendContent.size());
|
||||
const std::string outputHash = dragonx::util::sha256Hex(outputContent.data(), outputContent.size());
|
||||
const std::vector<std::pair<std::string, std::string>> good = {
|
||||
{ "sapling-spend.params", spendHash },
|
||||
{ "sapling-output.params", outputHash },
|
||||
};
|
||||
|
||||
// Valid params → pass, and a verification marker is written.
|
||||
EXPECT_TRUE(Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
EXPECT_TRUE(fsn::exists(dir / ".sapling_verified"));
|
||||
|
||||
// Second call → marker fast-path, still true (round-trips the cache).
|
||||
EXPECT_TRUE(Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
// Wrong expected hash → integrity failure (fresh dir so no marker can short-circuit it).
|
||||
fsn::path dir2 = fsn::temp_directory_path() / "od_sapling_test2";
|
||||
fsn::remove_all(dir2, rmec);
|
||||
fsn::create_directories(dir2);
|
||||
writeFile(dir2 / "sapling-spend.params", spendContent);
|
||||
writeFile(dir2 / "sapling-output.params", outputContent);
|
||||
const std::vector<std::pair<std::string, std::string>> wrong = {
|
||||
{ "sapling-spend.params", std::string(64, 'a') },
|
||||
{ "sapling-output.params", outputHash },
|
||||
};
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir2.string(), wrong));
|
||||
|
||||
// Truncated content (size change) invalidates the marker AND fails the hash.
|
||||
writeFile(dir / "sapling-spend.params", std::string("x"));
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
// A missing param → fail.
|
||||
fsn::remove(dir / "sapling-output.params", rmec);
|
||||
EXPECT_TRUE(!Connection::verifySaplingParamsIn(dir.string(), good));
|
||||
|
||||
fsn::remove_all(dir, rmec);
|
||||
fsn::remove_all(dir2, rmec);
|
||||
}
|
||||
|
||||
void testPlatformEnsureDirectory()
|
||||
{
|
||||
using dragonx::util::Platform;
|
||||
|
||||
// An existing directory → true (temp_directory_path always exists).
|
||||
{
|
||||
std::string err = "sentinel";
|
||||
EXPECT_TRUE(Platform::ensureDirectory(std::filesystem::temp_directory_path().string(), &err));
|
||||
}
|
||||
|
||||
// A fresh nested path → created, no error.
|
||||
{
|
||||
std::filesystem::path base = std::filesystem::temp_directory_path() / "od_ensuredir_test";
|
||||
std::error_code rmec; std::filesystem::remove_all(base, rmec);
|
||||
std::filesystem::path nested = base / "a" / "b" / "c";
|
||||
std::string err;
|
||||
EXPECT_TRUE(Platform::ensureDirectory(nested.string(), &err));
|
||||
EXPECT_TRUE(std::filesystem::is_directory(nested));
|
||||
EXPECT_TRUE(err.empty());
|
||||
std::filesystem::remove_all(base, rmec);
|
||||
}
|
||||
|
||||
// Empty path → false with a message.
|
||||
{
|
||||
std::string err;
|
||||
EXPECT_TRUE(!Platform::ensureDirectory("", &err));
|
||||
EXPECT_TRUE(!err.empty());
|
||||
}
|
||||
|
||||
// A path whose parent component is a regular file cannot be created. This fails the
|
||||
// same way for root and non-root, so it's a stable negative case across environments.
|
||||
{
|
||||
std::filesystem::path f = std::filesystem::temp_directory_path() / "od_ensuredir_file";
|
||||
std::error_code rmec; std::filesystem::remove_all(f, rmec);
|
||||
{ std::ofstream(f.string()) << "x"; }
|
||||
std::string err;
|
||||
bool ok = Platform::ensureDirectory((f / "child").string(), &err);
|
||||
std::filesystem::remove_all(f, rmec);
|
||||
EXPECT_TRUE(!ok);
|
||||
EXPECT_TRUE(err.find("Cannot create") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
@@ -6643,6 +6742,8 @@ int main()
|
||||
testWalletSecurityWorkflowExecutor();
|
||||
testDaemonShutdownPolicy();
|
||||
testDatadirLockGate();
|
||||
testPlatformEnsureDirectory();
|
||||
testVerifySaplingParams();
|
||||
testDaemonLifecycleExecution();
|
||||
testDaemonLifecycleAdapters();
|
||||
testConsoleTextLayout();
|
||||
|
||||
Reference in New Issue
Block a user