diff --git a/src/logos_blockchain_module.cpp b/src/logos_blockchain_module.cpp index 57ef34e..3c7a40b 100644 --- a/src/logos_blockchain_module.cpp +++ b/src/logos_blockchain_module.cpp @@ -6,10 +6,12 @@ #include #include #include +#include #include #include #include +namespace fs = std::filesystem; using json = nlohmann::json; // Define static member @@ -120,6 +122,8 @@ namespace { std::string http_addr_data; std::string external_address_data; std::string state_path_data; + std::string storage_path_data; + std::string logs_path_data; bool ibd_val; std::string log_filter_data; std::string kms_file_data; @@ -195,6 +199,22 @@ namespace { ffi_args.state_path = nullptr; } + // storage_path (string -> const char*) — maps to storage.backend.folder_name + if (args.contains("storage_path") && args["storage_path"].is_string()) { + storage_path_data = args["storage_path"].get(); + ffi_args.storage_path = storage_path_data.c_str(); + } else { + ffi_args.storage_path = nullptr; + } + + // logs_path (string -> const char*) — maps to tracing.logger.file.directory + if (args.contains("logs_path") && args["logs_path"].is_string()) { + logs_path_data = args["logs_path"].get(); + ffi_args.logs_path = logs_path_data.c_str(); + } else { + ffi_args.logs_path = nullptr; + } + // ibd (bool -> const bool*) if (args.contains("ibd") && args["ibd"].is_boolean()) { ibd_val = args["ibd"].get(); @@ -258,6 +278,75 @@ StdLogosResult LogosBlockchainModule::generate_user_config(const std::string& js return result::err(std::string("Failed to parse JSON args: ") + e.what()); } + // The module-context getters are populated by every logos-core host + // (logoscore-cli and Basecamp alike), so their mere presence can't tell the + // two apart. The bundled app therefore opts in explicitly by passing + // "use_persistence_paths": true; only then do we route the node's runtime + // directories — state, storage (db) and logs — under the host-owned + // per-instance persistence dir, so they all share one writable base. CLI and + // standalone callers omit the flag and keep their own paths (or the node + // defaults). Any path the caller set explicitly is left untouched. + bool use_persistence_paths = false; + if (const auto it = parsed_args.find("use_persistence_paths"); + it != parsed_args.end() && it->is_boolean()) { + use_persistence_paths = it->get(); + } + parsed_args.erase("use_persistence_paths"); // not an FFI field + + if (use_persistence_paths) { + const std::string& persistence = instancePersistencePath(); + if (!persistence.empty()) { + const fs::path base(persistence); + // Only fill a path the caller didn't pin (non-empty string wins). + const auto set_if_absent = [&parsed_args](const char* key, const std::string& value) { + const bool provided = parsed_args.contains(key) + && parsed_args[key].is_string() + && !parsed_args[key].get().empty(); + if (!provided) + parsed_args[key] = value; + }; + set_if_absent("state_path", (base / "state").string()); + set_if_absent("storage_path", (base / "db").string()); + set_if_absent("logs_path", (base / "logs").string()); + + // The config file itself is written under the same base, using the + // caller's path as the relative part below it ("config/user_config.yaml" + // → "/config/user_config.yaml"). Absolute / root-anchored inputs + // (e.g. "//user_config.yaml" from QDir::currentPath()=="/") are treated + // as relative to the base; a missing output defaults to + // "/user_config.yaml". + fs::path output_rel = "user_config.yaml"; + if (parsed_args.contains("output") && parsed_args["output"].is_string() + && !parsed_args["output"].get().empty()) { + const fs::path given(localPathFromFileUrl(parsed_args["output"].get())); + const fs::path rel = given.relative_path(); + output_rel = rel.empty() ? given.filename() : rel; + if (output_rel.empty()) + output_rel = "user_config.yaml"; + } + parsed_args["output"] = (base / output_rel).lexically_normal().string(); + + fprintf( + stderr, + "generate_user_config: routing output/state/storage/logs under instance persistence path: %s\n", + persistence.c_str() + ); + } else { + fprintf( + stderr, + "generate_user_config: use_persistence_paths requested but no instance persistence path is set; leaving paths unchanged.\n" + ); + } + } + + // The path the config is actually written to (after any persistence routing). + // Returned to the caller so it can hand the exact path to start(). Empty only + // when no output was given and no routing applied (the node wrote its own + // default relative to the cwd, which the module can't resolve). + std::string resolved_output; + if (parsed_args.contains("output") && parsed_args["output"].is_string()) + resolved_output = parsed_args["output"].get(); + const OwnedGenerateConfigArgs owned_args(parsed_args); const OperationStatus status = ::generate_user_config(owned_args.ffi_args); @@ -266,7 +355,7 @@ StdLogosResult LogosBlockchainModule::generate_user_config(const std::string& js return result::err("Failed to generate user config."); } - return result::ok(); + return result::ok(resolved_output); } StdLogosResult LogosBlockchainModule::start(const std::string& config_path, const std::string& deployment) { diff --git a/src/logos_blockchain_module.h b/src/logos_blockchain_module.h index df79054..d1959a4 100644 --- a/src/logos_blockchain_module.h +++ b/src/logos_blockchain_module.h @@ -26,7 +26,15 @@ public: [[nodiscard]] StdLogosResult stop(); // Config management - [[nodiscard]] static StdLogosResult generate_user_config(const std::string& json_args); + // Not static: when the JSON args set "use_persistence_paths": true it routes + // the node's output/state/storage/logs paths under instancePersistencePath() + // (config at /; state/db/logs at /state, /db, + // /logs), so it needs the instance context. Basecamp opts in via that flag; + // logoscore-cli/standalone omit it and keep their own paths. An explicit + // state/storage/logs path always wins; output is always re-anchored under the + // base when flagged. On success the result value is the path the config was + // written to — pass it straight to start(). + [[nodiscard]] StdLogosResult generate_user_config(const std::string& json_args); [[nodiscard]] static StdLogosResult update_user_config( const std::string& user_config_path, const std::string& keystore_path diff --git a/tests/mocks/mock_logos_blockchain.cpp b/tests/mocks/mock_logos_blockchain.cpp index 83b6c4b..233cfaa 100644 --- a/tests/mocks/mock_logos_blockchain.cpp +++ b/tests/mocks/mock_logos_blockchain.cpp @@ -6,6 +6,15 @@ #include #include #include +#include + +// Captures the paths passed to the most recent generate_user_config call so tests +// can assert the module's path routing. A null pointer is recorded as the +// sentinel "". +std::string g_lastGeneratedOutput; +std::string g_lastGeneratedStatePath; +std::string g_lastGeneratedStoragePath; +std::string g_lastGeneratedLogsPath; static char s_fakeNode = 0; static CryptarchiaInfo s_fakeCryptarchiaInfo = {}; @@ -26,6 +35,10 @@ bool is_ok(const OperationStatus* status) { OperationStatus generate_user_config(GenerateConfigArgs args) { LOGOS_CMOCK_RECORD("generate_user_config"); + g_lastGeneratedOutput = args.output ? args.output : ""; + g_lastGeneratedStatePath = args.state_path ? args.state_path : ""; + g_lastGeneratedStoragePath = args.storage_path ? args.storage_path : ""; + g_lastGeneratedLogsPath = args.logs_path ? args.logs_path : ""; return LOGOS_CMOCK_RETURN(int, "generate_user_config"); } diff --git a/tests/stubs/logos_blockchain.h b/tests/stubs/logos_blockchain.h index d8e5e6f..d9caee1 100644 --- a/tests/stubs/logos_blockchain.h +++ b/tests/stubs/logos_blockchain.h @@ -40,6 +40,8 @@ typedef struct { const char* http_addr; const char* external_address; const char* state_path; + const char* storage_path; + const char* logs_path; const bool* ibd; const char* log_filter; const char* kms_file; diff --git a/tests/test_blockchain.cpp b/tests/test_blockchain.cpp index 6668a8e..24b2a0e 100644 --- a/tests/test_blockchain.cpp +++ b/tests/test_blockchain.cpp @@ -86,6 +86,122 @@ LOGOS_TEST(generate_user_config_from_json_string) { LOGOS_ASSERT(t.cFunctionCalled("generate_user_config")); } +// The mock records the paths handed to the FFI (see mock_logos_blockchain.cpp). +extern std::string g_lastGeneratedOutput; +extern std::string g_lastGeneratedStatePath; +extern std::string g_lastGeneratedStoragePath; +extern std::string g_lastGeneratedLogsPath; + +static void clearGeneratedPaths() { + g_lastGeneratedOutput.clear(); + g_lastGeneratedStatePath.clear(); + g_lastGeneratedStoragePath.clear(); + g_lastGeneratedLogsPath.clear(); +} + +// Basecamp opts in with the flag: output and state/storage/logs are routed under +// the per-instance persistence dir. The output keeps its given path as the +// relative part below the base. +LOGOS_TEST(generate_user_config_routes_paths_under_persistence_when_flagged) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + module._logosCoreSetContext_("/mod", "id", "/persist/data"); + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + StdLogosResult result = + module.generate_user_config(R"({"output":"config/user_config.yaml","use_persistence_paths":true})"); + LOGOS_ASSERT_TRUE(result.success); + // The resolved config path is returned for the caller to hand to start(). + LOGOS_ASSERT_EQ(result.value.get(), std::string("/persist/data/config/user_config.yaml")); + LOGOS_ASSERT_EQ(g_lastGeneratedOutput, std::string("/persist/data/config/user_config.yaml")); + LOGOS_ASSERT_EQ(g_lastGeneratedStatePath, std::string("/persist/data/state")); + LOGOS_ASSERT_EQ(g_lastGeneratedStoragePath, std::string("/persist/data/db")); + LOGOS_ASSERT_EQ(g_lastGeneratedLogsPath, std::string("/persist/data/logs")); +} + +// A root-anchored output (the UI's "//user_config.yaml" artifact) is treated as +// relative to the base, not written to the read-only filesystem root. +LOGOS_TEST(generate_user_config_routes_root_anchored_output_under_persistence) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + module._logosCoreSetContext_("/mod", "id", "/persist/data"); + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + LOGOS_ASSERT_TRUE( + module.generate_user_config(R"({"output":"//user_config.yaml","use_persistence_paths":true})").success); + LOGOS_ASSERT_EQ(g_lastGeneratedOutput, std::string("/persist/data/user_config.yaml")); +} + +// With no output given, the config defaults to /user_config.yaml. +LOGOS_TEST(generate_user_config_defaults_output_under_persistence) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + module._logosCoreSetContext_("/mod", "id", "/persist/data"); + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + LOGOS_ASSERT_TRUE(module.generate_user_config(R"({"use_persistence_paths":true})").success); + LOGOS_ASSERT_EQ(g_lastGeneratedOutput, std::string("/persist/data/user_config.yaml")); +} + +// A host load WITHOUT the flag (e.g. logoscore-cli) does NOT redirect any path, +// even though the persistence path is populated. +LOGOS_TEST(generate_user_config_host_without_flag_leaves_paths_unset) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + module._logosCoreSetContext_("/mod", "id", "/persist/data"); + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + StdLogosResult result = module.generate_user_config(R"({"output":"/tmp/out.json"})"); + LOGOS_ASSERT_TRUE(result.success); + // Without routing, the caller's output is returned unchanged. + LOGOS_ASSERT_EQ(result.value.get(), std::string("/tmp/out.json")); + LOGOS_ASSERT_EQ(g_lastGeneratedOutput, std::string("/tmp/out.json")); + LOGOS_ASSERT_EQ(g_lastGeneratedStatePath, std::string("")); + LOGOS_ASSERT_EQ(g_lastGeneratedStoragePath, std::string("")); + LOGOS_ASSERT_EQ(g_lastGeneratedLogsPath, std::string("")); +} + +// An explicitly provided path wins over the flag; the others are still routed. +LOGOS_TEST(generate_user_config_explicit_path_wins_over_flag) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + module._logosCoreSetContext_("/mod", "id", "/persist/data"); + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + LOGOS_ASSERT_TRUE( + module.generate_user_config( + R"({"output":"/tmp/out.json","state_path":"/tmp/state","use_persistence_paths":true})").success); + LOGOS_ASSERT_EQ(g_lastGeneratedStatePath, std::string("/tmp/state")); + LOGOS_ASSERT_EQ(g_lastGeneratedStoragePath, std::string("/persist/data/db")); + LOGOS_ASSERT_EQ(g_lastGeneratedLogsPath, std::string("/persist/data/logs")); +} + +// The flag with no host context (standalone) leaves paths unset. +LOGOS_TEST(generate_user_config_flag_without_context_leaves_paths_unset) { + auto t = LogosTestContext("blockchain_module"); + LogosBlockchainModule module; + + t.mockCFunction("generate_user_config").returns(0); + clearGeneratedPaths(); + + LOGOS_ASSERT_TRUE( + module.generate_user_config(R"({"output":"/tmp/out.json","use_persistence_paths":true})").success); + LOGOS_ASSERT_EQ(g_lastGeneratedOutput, std::string("/tmp/out.json")); + LOGOS_ASSERT_EQ(g_lastGeneratedStatePath, std::string("")); + LOGOS_ASSERT_EQ(g_lastGeneratedStoragePath, std::string("")); + LOGOS_ASSERT_EQ(g_lastGeneratedLogsPath, std::string("")); +} + LOGOS_TEST(generate_user_config_with_all_fields) { auto t = LogosTestContext("blockchain_module"); LogosBlockchainModule module;