From a172d0b7ea3255e78a63bbbfa1e4f63157780c6a Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 20 Aug 2025 12:38:51 +0100 Subject: [PATCH 1/9] feat: Validate unexpected config values (#2457) --- docs/configure-clio.md | 8 +-- src/util/config/ConfigDefinition.cpp | 6 +++ src/util/config/ConfigDefinition.hpp | 4 -- src/util/config/ConfigFileInterface.hpp | 9 ++++ src/util/config/ConfigFileJson.cpp | 10 ++++ src/util/config/ConfigFileJson.hpp | 9 ++++ src/util/config/ConfigFileYaml.hpp | 49 ------------------- .../data/cassandra/SettingsProviderTests.cpp | 3 +- tests/unit/etl/LoadBalancerTests.cpp | 2 +- tests/unit/etlng/LoadBalancerTests.cpp | 2 +- .../util/config/ClioConfigDefinitionTests.cpp | 32 ++++++++++++ .../unit/util/config/ConfigFileJsonTests.cpp | 26 ++++++++++ 12 files changed, 101 insertions(+), 59 deletions(-) delete mode 100644 src/util/config/ConfigFileYaml.hpp diff --git a/docs/configure-clio.md b/docs/configure-clio.md index 3b4557c6..cbc6930a 100644 --- a/docs/configure-clio.md +++ b/docs/configure-clio.md @@ -88,13 +88,15 @@ Exactly equal password gains admin rights for the request or a websocket connect Clio can cache requests to ETL sources to reduce the load on the ETL source. Only following commands are cached: `server_info`, `server_state`, `server_definitions`, `fee`, `ledger_closed`. By default the forwarding cache is off. -To enable the caching for a source, `forwarding_cache_timeout` value should be added to the configuration file, e.g.: +To enable the caching for a source, `forwarding.cache_timeout` value should be added to the configuration file, e.g.: ```json -"forwarding_cache_timeout": 0.250, +"forwarding": { + "cache_timeout": 0.250, +} ``` -`forwarding_cache_timeout` defines for how long (in seconds) a cache entry will be valid after being placed into the cache. +`forwarding.cache_timeout` defines for how long (in seconds) a cache entry will be valid after being placed into the cache. Zero value turns off the cache feature. ## Graceful shutdown (not fully implemented yet) diff --git a/src/util/config/ConfigDefinition.cpp b/src/util/config/ConfigDefinition.cpp index 72a21edd..8ae7eaf9 100644 --- a/src/util/config/ConfigDefinition.cpp +++ b/src/util/config/ConfigDefinition.cpp @@ -237,6 +237,12 @@ ClioConfigDefinition::parse(ConfigFileInterface const& config) }); } + for (auto const& key : config.getAllKeys()) { + if (!map_.contains(key) && !arrayPrefixesToKeysMap.contains(key)) { + listOfErrors.emplace_back("Unknown key: " + key); + } + } + if (!listOfErrors.empty()) return listOfErrors; diff --git a/src/util/config/ConfigDefinition.hpp b/src/util/config/ConfigDefinition.hpp index 5eebb9f0..6109f55b 100644 --- a/src/util/config/ConfigDefinition.hpp +++ b/src/util/config/ConfigDefinition.hpp @@ -19,7 +19,6 @@ #pragma once -#include "rpc/common/APIVersion.hpp" #include "util/Assert.hpp" #include "util/config/Array.hpp" #include "util/config/ConfigConstraints.hpp" @@ -27,18 +26,15 @@ #include "util/config/ConfigValue.hpp" #include "util/config/Error.hpp" #include "util/config/ObjectView.hpp" -#include "util/config/Types.hpp" #include "util/config/ValueView.hpp" #include #include #include -#include #include #include #include #include -#include #include #include #include diff --git a/src/util/config/ConfigFileInterface.hpp b/src/util/config/ConfigFileInterface.hpp index dfdd0faa..a815d512 100644 --- a/src/util/config/ConfigFileInterface.hpp +++ b/src/util/config/ConfigFileInterface.hpp @@ -22,6 +22,7 @@ #include "util/config/Types.hpp" #include +#include #include #include @@ -63,6 +64,14 @@ public: */ virtual bool containsKey(std::string_view key) const = 0; + + /** + * @brief Retrieves all keys in the configuration file. + * + * @return A vector of all keys in the configuration file. + */ + virtual std::vector + getAllKeys() const = 0; }; } // namespace util::config diff --git a/src/util/config/ConfigFileJson.cpp b/src/util/config/ConfigFileJson.cpp index 4bca197a..ee82a877 100644 --- a/src/util/config/ConfigFileJson.cpp +++ b/src/util/config/ConfigFileJson.cpp @@ -142,6 +142,16 @@ ConfigFileJson::containsKey(std::string_view key) const return jsonObject_.contains(key); } +std::vector +ConfigFileJson::getAllKeys() const +{ + std::vector keys; + for (auto const& [key, value] : jsonObject_) { + keys.push_back(key); + } + return keys; +} + boost::json::object const& ConfigFileJson::inner() const { diff --git a/src/util/config/ConfigFileJson.hpp b/src/util/config/ConfigFileJson.hpp index 8eb72b55..cdfb1ead 100644 --- a/src/util/config/ConfigFileJson.hpp +++ b/src/util/config/ConfigFileJson.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -73,6 +74,14 @@ public: [[nodiscard]] bool containsKey(std::string_view key) const override; + /** + * @brief Retrieves all keys in the configuration file. + * + * @return A vector of all keys in the configuration file. + */ + [[nodiscard]] std::vector + getAllKeys() const override; + /** * @brief Creates a new ConfigFileJson by parsing the provided JSON file and * stores the values in the object. diff --git a/src/util/config/ConfigFileYaml.hpp b/src/util/config/ConfigFileYaml.hpp deleted file mode 100644 index c67a797b..00000000 --- a/src/util/config/ConfigFileYaml.hpp +++ /dev/null @@ -1,49 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2024, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#pragma once - -#include "util/config/ConfigFileInterface.hpp" -#include "util/config/Types.hpp" - -#include - -#include -#include - -// TODO: implement when we support yaml - -namespace util::config { - -/** @brief Yaml representation of config */ -class ConfigFileYaml final : public ConfigFileInterface { -public: - ConfigFileYaml() = default; - - Value - getValue(std::string_view key) const override; - - std::vector - getArray(std::string_view key) const override; - - bool - containsKey(std::string_view key) const override; -}; - -} // namespace util::config diff --git a/tests/unit/data/cassandra/SettingsProviderTests.cpp b/tests/unit/data/cassandra/SettingsProviderTests.cpp index 54c7570b..e26fd681 100644 --- a/tests/unit/data/cassandra/SettingsProviderTests.cpp +++ b/tests/unit/data/cassandra/SettingsProviderTests.cpp @@ -81,7 +81,8 @@ class SettingsProviderTest : public NoLoggerFixture {}; TEST_F(SettingsProviderTest, Defaults) { - auto const cfg = getParseSettingsConfig(json::parse(R"JSON({"contact_points": "127.0.0.1"})JSON")); + auto const cfg = + getParseSettingsConfig(json::parse(R"JSON({"database.cassandra.contact_points": "127.0.0.1"})JSON")); SettingsProvider const provider{cfg.getObject("database.cassandra")}; auto const settings = provider.getSettings(); diff --git a/tests/unit/etl/LoadBalancerTests.cpp b/tests/unit/etl/LoadBalancerTests.cpp index f527357d..a7011169 100644 --- a/tests/unit/etl/LoadBalancerTests.cpp +++ b/tests/unit/etl/LoadBalancerTests.cpp @@ -160,7 +160,7 @@ TEST_F(LoadBalancerConstructorTests, construct) TEST_F(LoadBalancerConstructorTests, forwardingTimeoutPassedToSourceFactory) { auto const forwardingTimeout = 10; - configJson_.as_object()["forwarding"] = boost::json::object{{"timeout", float{forwardingTimeout}}}; + configJson_.as_object()["forwarding"] = boost::json::object{{"cache_timeout", float{forwardingTimeout}}}; EXPECT_CALL( sourceFactory_, makeSource( diff --git a/tests/unit/etlng/LoadBalancerTests.cpp b/tests/unit/etlng/LoadBalancerTests.cpp index 97f70495..57e71e92 100644 --- a/tests/unit/etlng/LoadBalancerTests.cpp +++ b/tests/unit/etlng/LoadBalancerTests.cpp @@ -182,7 +182,7 @@ TEST_F(LoadBalancerConstructorNgTests, construct) TEST_F(LoadBalancerConstructorNgTests, forwardingTimeoutPassedToSourceFactory) { auto const forwardingTimeout = 10; - configJson_.as_object()["forwarding"] = boost::json::object{{"timeout", float{forwardingTimeout}}}; + configJson_.as_object()["forwarding"] = boost::json::object{{"cache_timeout", float{forwardingTimeout}}}; EXPECT_CALL( sourceFactory_, makeSource( diff --git a/tests/unit/util/config/ClioConfigDefinitionTests.cpp b/tests/unit/util/config/ClioConfigDefinitionTests.cpp index 279e8ca0..3993836a 100644 --- a/tests/unit/util/config/ClioConfigDefinitionTests.cpp +++ b/tests/unit/util/config/ClioConfigDefinitionTests.cpp @@ -465,3 +465,35 @@ TEST_F(ClioConfigDefinitionParseArrayTest, missingAllRequiredFields) EXPECT_EQ(result->size(), 1); EXPECT_THAT(result->at(0).error, testing::StartsWith("array.[].int")); } + +TEST(ClioConfigDefinitionParse, unexpectedFields) +{ + ClioConfigDefinition config{ + {"expected", ConfigValue{ConfigType::String}.optional()}, + }; + + auto const configJson = boost::json::parse(R"JSON({ + "expected": "present", + "unexpected_string": "", + "unexpected_non_empty_array": [ + {"string": ""}, + {"string": ""} + ], + "unexpected_empty_array": [], + "unexpected_object": { + "string": "" + } + })JSON") + .as_object(); + + auto const configFile = ConfigFileJson{configJson}; + auto result = config.parse(configFile); + std::ranges::sort(*result, [](auto const& lhs, auto const& rhs) { return lhs.error < rhs.error; }); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result->size(), 4); + + EXPECT_EQ(result->at(0).error, "Unknown key: unexpected_empty_array.[]"); + EXPECT_EQ(result->at(1).error, "Unknown key: unexpected_non_empty_array.[].string"); + EXPECT_EQ(result->at(2).error, "Unknown key: unexpected_object.string"); + EXPECT_EQ(result->at(3).error, "Unknown key: unexpected_string"); +} diff --git a/tests/unit/util/config/ConfigFileJsonTests.cpp b/tests/unit/util/config/ConfigFileJsonTests.cpp index d639e938..521bdae2 100644 --- a/tests/unit/util/config/ConfigFileJsonTests.cpp +++ b/tests/unit/util/config/ConfigFileJsonTests.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -488,6 +489,31 @@ TEST_F(ConfigFileJsonTest, containsKey) EXPECT_FALSE(jsonFileObj.containsKey("array_of_objects.[].object")); } +TEST_F(ConfigFileJsonTest, getAllKeys) +{ + auto const jsonStr = R"JSON({ + "int": 42, + "object": { "string": "some string", "array": [1, 2, 3] }, + "array2": [1, 2, 3], + "array_of_objects": [ {"int": 42}, {"string": "some string"} ] + })JSON"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + + auto allKeys = jsonFileObj.getAllKeys(); + std::ranges::sort(allKeys); + EXPECT_EQ(allKeys.size(), 6); + + std::vector const expectedKeys{ + {"array2.[]", + "array_of_objects.[].int", + "array_of_objects.[].string", + "int", + "object.array.[]", + "object.string"} + }; + EXPECT_EQ(allKeys, expectedKeys); +} + struct ConfigFileJsonMakeTest : ConfigFileJsonTest {}; TEST_F(ConfigFileJsonMakeTest, invalidFile) From b5892dd139dc09eaf6a027d12a93211450f55b6d Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 20 Aug 2025 12:56:27 +0100 Subject: [PATCH 2/9] fix: Print error and help on unknown arg (#2460) --- src/app/CliArgs.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/app/CliArgs.cpp b/src/app/CliArgs.cpp index fad339ba..580ccd6b 100644 --- a/src/app/CliArgs.cpp +++ b/src/app/CliArgs.cpp @@ -56,12 +56,22 @@ CliArgs::parse(int argc, char const* argv[]) po::positional_options_description positional; positional.add("conf", 1); + auto const printHelp = [&description]() { + std::cout << "Clio server " << util::build::getClioFullVersionString() << "\n\n" << description; + }; + po::variables_map parsed; - po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed); - po::notify(parsed); + try { + po::store(po::command_line_parser(argc, argv).options(description).positional(positional).run(), parsed); + po::notify(parsed); + } catch (po::error const& e) { + std::cerr << "Error: " << e.what() << std::endl << std::endl; + printHelp(); + return Action{Action::Exit{EXIT_FAILURE}}; + } if (parsed.contains("help")) { - std::cout << "Clio server " << util::build::getClioFullVersionString() << "\n\n" << description; + printHelp(); return Action{Action::Exit{EXIT_SUCCESS}}; } From 7a2090bc00f35d705157e6084d033d521e5b0519 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 20 Aug 2025 13:45:22 +0100 Subject: [PATCH 3/9] docs: Add log format description (#2461) --- docs/config-description.md | 18 +++++++++++++++++- src/util/config/ConfigDescription.hpp | 20 +++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/config-description.md b/docs/config-description.md index 85fe6251..3b46c045 100644 --- a/docs/config-description.md +++ b/docs/config-description.md @@ -445,7 +445,23 @@ This document provides a list of all available Clio configuration properties in - **Type**: string - **Default value**: `%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v` - **Constraints**: None -- **Description**: The format string for log messages using spdlog format patterns. Documentation can be found at: . +- **Description**: The format string for log messages using spdlog format patterns. + +Each of the variables expands like so: + +- `%Y-%m-%d %H:%M:%S.%f`: The full date and time of the log entry with microsecond precision +- `%^`: Start color range +- `%3!l`: The severity (aka log level) the entry was sent at stripped to 3 characters +- `%n`: The logger name (channel) that this log entry was sent to +- `%$`: End color range +- `%v`: The actual log message + +Some additional variables that might be useful: + +- `%@`: A partial path to the C++ file and the line number in the said file (`src/file/path:linenumber`) +- `%t`: The ID of the thread the log entry is written from + +Documentation can be found at: . ### log.is_async diff --git a/src/util/config/ConfigDescription.hpp b/src/util/config/ConfigDescription.hpp index 28711ddb..00646d38 100644 --- a/src/util/config/ConfigDescription.hpp +++ b/src/util/config/ConfigDescription.hpp @@ -263,9 +263,23 @@ private: KV{.key = "log.level", .value = "The general logging level of Clio. This level is applied to all log channels that do not have an " "explicitly defined logging level."}, - KV{.key = "log.format", - .value = "The format string for log messages using spdlog format patterns. Documentation can be found at: " - "."}, + KV{.key = "log.format", .value = R"(The format string for log messages using spdlog format patterns. + +Each of the variables expands like so: + +- `%Y-%m-%d %H:%M:%S.%f`: The full date and time of the log entry with microsecond precision +- `%^`: Start color range +- `%3!l`: The severity (aka log level) the entry was sent at stripped to 3 characters +- `%n`: The logger name (channel) that this log entry was sent to +- `%$`: End color range +- `%v`: The actual log message + +Some additional variables that might be useful: + +- `%@`: A partial path to the C++ file and the line number in the said file (`src/file/path:linenumber`) +- `%t`: The ID of the thread the log entry is written from + +Documentation can be found at: .)"}, KV{.key = "log.is_async", .value = "Whether spdlog is asynchronous or not."}, KV{.key = "log.enable_console", .value = "Enables or disables logging to the console."}, KV{.key = "log.directory", .value = "The directory path for the log files."}, From d833d36896015c4ec3c6c285fad073396435faaf Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 20 Aug 2025 15:29:55 +0100 Subject: [PATCH 4/9] refactor: Add kCONFIG_DESCRIPTION_HEADER (#2463) --- docs/config-description.md | 4 +++- src/util/config/ConfigDescription.hpp | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/config-description.md b/docs/config-description.md index 3b46c045..9bf877d1 100644 --- a/docs/config-description.md +++ b/docs/config-description.md @@ -3,7 +3,9 @@ This document provides a list of all available Clio configuration properties in detail. > [!NOTE] -> Dot notation in configuration key names represents nested fields. For example, **database.scylladb** refers to the _scylladb_ field inside the _database_ object. If a key name includes "[]", it indicates that the nested field is an array (e.g., etl_sources.[]). +> Dot notation in configuration key names represents nested fields. +> For example, **database.scylladb** refers to the _scylladb_ field inside the _database_ object. +> If a key name includes "[]", it indicates that the nested field is an array (e.g., etl_sources.[]). ## Configuration Details diff --git a/src/util/config/ConfigDescription.hpp b/src/util/config/ConfigDescription.hpp index 00646d38..b586fd1a 100644 --- a/src/util/config/ConfigDescription.hpp +++ b/src/util/config/ConfigDescription.hpp @@ -110,13 +110,7 @@ public: static void writeConfigDescriptionToFile(std::ostream& file) { - file << "# Clio Config Description\n\n"; - file << "This document provides a list of all available Clio configuration properties in detail.\n\n"; - file << "> [!NOTE]\n"; - file << "> Dot notation in configuration key names represents nested fields. For example, " - "**database.scylladb** refers to the _scylladb_ field inside the _database_ object. If a key name " - "includes \"[]\", it indicates that the nested field is an array (e.g., etl_sources.[]).\n\n"; - file << "## Configuration Details\n"; + file << kCONFIG_DESCRIPTION_HEADER; for (auto const& [key, val] : kCONFIG_DESCRIPTION) { file << "\n### " << key << "\n\n"; @@ -133,6 +127,19 @@ public: } private: + static constexpr auto kCONFIG_DESCRIPTION_HEADER = + R"(# Clio Config Description + +This document provides a list of all available Clio configuration properties in detail. + +> [!NOTE] +> Dot notation in configuration key names represents nested fields. +> For example, **database.scylladb** refers to the _scylladb_ field inside the _database_ object. +> If a key name includes "[]", it indicates that the nested field is an array (e.g., etl_sources.[]). + +## Configuration Details +)"; + static constexpr auto kCONFIG_DESCRIPTION = std::array{ KV{ .key = "database.type", From c780ef8a0b0a3e735eceea796c89213079c0e812 Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:13:05 -0400 Subject: [PATCH 5/9] refactor: output struct (#2456) fixes #2452 --- src/rpc/handlers/AccountInfo.cpp | 36 ++++++++++---------------- src/rpc/handlers/AccountInfo.hpp | 33 ----------------------- src/rpc/handlers/GetAggregatePrice.cpp | 8 +++++- src/rpc/handlers/GetAggregatePrice.hpp | 19 +++----------- 4 files changed, 25 insertions(+), 71 deletions(-) diff --git a/src/rpc/handlers/AccountInfo.cpp b/src/rpc/handlers/AccountInfo.cpp index 15781a56..1771e03e 100644 --- a/src/rpc/handlers/AccountInfo.cpp +++ b/src/rpc/handlers/AccountInfo.cpp @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -90,6 +91,17 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons auto const isClawbackEnabled = isEnabled(Amendments::Clawback); auto const isTokenEscrowEnabled = isEnabled(Amendments::TokenEscrow); + Output out{ + .ledgerIndex = lgrInfo.seq, + .ledgerHash = ripple::strHex(lgrInfo.hash), + .accountData = sle, + .isDisallowIncomingEnabled = isDisallowIncomingEnabled, + .isClawbackEnabled = isClawbackEnabled, + .isTokenEscrowEnabled = isTokenEscrowEnabled, + .apiVersion = ctx.apiVersion, + .signerLists = std::nullopt + }; + // Return SignerList(s) if that is requested. if (input.signerLists) { // We put the SignerList in an array because of an anticipated @@ -99,7 +111,6 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons // This code will need to be revisited if in the future we // support multiple SignerLists on one account. auto const signers = sharedPtrBackend_->fetchLedgerObject(signersKey.key, lgrInfo.seq, ctx.yield); - std::vector signerList; if (signers) { ripple::STLedgerEntry const sleSigners{ @@ -109,30 +120,11 @@ AccountInfoHandler::process(AccountInfoHandler::Input const& input, Context cons if (!signersKey.check(sleSigners)) return Error{Status{RippledError::rpcDB_DESERIALIZATION}}; - signerList.push_back(sleSigners); + out.signerLists = std::vector{sleSigners}; } - - return Output( - lgrInfo.seq, - ripple::strHex(lgrInfo.hash), - sle, - isDisallowIncomingEnabled, - isClawbackEnabled, - isTokenEscrowEnabled, - ctx.apiVersion, - signerList - ); } - return Output( - lgrInfo.seq, - ripple::strHex(lgrInfo.hash), - sle, - isDisallowIncomingEnabled, - isClawbackEnabled, - isTokenEscrowEnabled, - ctx.apiVersion - ); + return out; } void diff --git a/src/rpc/handlers/AccountInfo.hpp b/src/rpc/handlers/AccountInfo.hpp index 263516df..2c2fa05a 100644 --- a/src/rpc/handlers/AccountInfo.hpp +++ b/src/rpc/handlers/AccountInfo.hpp @@ -66,39 +66,6 @@ public: std::optional> signerLists; // validated should be sent via framework bool validated = true; - - /** - * @brief Construct a new Output object - * - * @param ledgerId The ledger index - * @param ledgerHash The ledger hash - * @param sle The account data - * @param isDisallowIncomingEnabled Whether disallow incoming is enabled - * @param isClawbackEnabled Whether clawback is enabled - * @param isTokenEscrowEnabled Whether token escrow is enabled - * @param version The API version - * @param signerLists The signer lists - */ - Output( - uint32_t ledgerId, - std::string ledgerHash, - ripple::STLedgerEntry sle, - bool isDisallowIncomingEnabled, - bool isClawbackEnabled, - bool isTokenEscrowEnabled, - uint32_t version, - std::optional> signerLists = std::nullopt - ) - : ledgerIndex(ledgerId) - , ledgerHash(std::move(ledgerHash)) - , accountData(std::move(sle)) - , isDisallowIncomingEnabled(isDisallowIncomingEnabled) - , isClawbackEnabled(isClawbackEnabled) - , isTokenEscrowEnabled(isTokenEscrowEnabled) - , apiVersion(version) - , signerLists(std::move(signerLists)) - { - } }; /** diff --git a/src/rpc/handlers/GetAggregatePrice.cpp b/src/rpc/handlers/GetAggregatePrice.cpp index a91ecc84..258ed903 100644 --- a/src/rpc/handlers/GetAggregatePrice.cpp +++ b/src/rpc/handlers/GetAggregatePrice.cpp @@ -124,7 +124,13 @@ GetAggregatePriceHandler::process(GetAggregatePriceHandler::Input const& input, auto const latestTime = timestampPricesBiMap.left.begin()->first; - Output out(latestTime, ripple::to_string(lgrInfo.hash), lgrInfo.seq); + Output out{ + .time = latestTime, + .trimStats = std::nullopt, + .ledgerHash = ripple::to_string(lgrInfo.hash), + .ledgerIndex = lgrInfo.seq, + .median = "" + }; if (input.timeThreshold) { auto const oldestTime = timestampPricesBiMap.left.rbegin()->first; diff --git a/src/rpc/handlers/GetAggregatePrice.hpp b/src/rpc/handlers/GetAggregatePrice.hpp index fe4c9a12..b66247bf 100644 --- a/src/rpc/handlers/GetAggregatePrice.hpp +++ b/src/rpc/handlers/GetAggregatePrice.hpp @@ -43,7 +43,6 @@ #include #include #include -#include #include namespace rpc { @@ -59,8 +58,9 @@ public: * @brief A struct to hold the statistics */ struct Stats { - ripple::STAmount avg; - ripple::Number sd; // standard deviation + ripple::STAmount avg{}; // NOLINT(readability-redundant-member-init) + // standard deviation + ripple::Number sd{}; // NOLINT(readability-redundant-member-init) uint32_t size{0}; }; @@ -69,23 +69,12 @@ public: */ struct Output { uint32_t time; - Stats extireStats; + Stats extireStats{}; std::optional trimStats; std::string ledgerHash; uint32_t ledgerIndex; std::string median; bool validated = true; - - /** - * @brief Construct a new Output object - * @param time The time of the latest oracle data - * @param ledgerHash The hash of the ledger - * @param ledgerIndex The index of the ledger - */ - Output(uint32_t time, std::string ledgerHash, uint32_t ledgerIndex) - : time(time), ledgerHash(std::move(ledgerHash)), ledgerIndex(ledgerIndex) - { - } }; /** From 2d48de372b1ae9ecd853f00ce438494072d7afda Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Thu, 21 Aug 2025 00:07:29 -0700 Subject: [PATCH 6/9] feat: Generate Debian packages with CPack (#2282) Builds the Linux packages with CPack. Generate them by running Conan with `--options:host "&:package=True" --options:host "&:static=True"` then after the build you can run `cpack .` in the build directory. @mathbunnyru Where do you think this should be built? QA needs a package per-commit. @godexsoft What to do with the `config.json` and service file. I can just remove them or strip the comment out but it still won't work out the box with the default `rippled.cfg`. Relates to #2191. --------- Co-authored-by: Ayaz Salikhov --- .github/actions/generate/action.yml | 6 ++++ .github/workflows/build.yml | 17 ++++++++++ .github/workflows/build_and_test.yml | 7 +++++ .github/workflows/build_impl.yml | 17 ++++++++-- CMakeLists.txt | 4 +-- cmake/ClioPackage.cmake | 8 +++++ cmake/install/clio.service.in | 17 ---------- cmake/install/install.cmake | 12 ++++---- cmake/pkg/deb.cmake | 12 ++++++++ cmake/pkg/postinst | 46 ++++++++++++++++++++++++++++ conanfile.py | 4 +-- 11 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 cmake/ClioPackage.cmake delete mode 100644 cmake/install/clio.service.in create mode 100644 cmake/pkg/deb.cmake create mode 100755 cmake/pkg/postinst diff --git a/.github/actions/generate/action.yml b/.github/actions/generate/action.yml index 698de53f..b39b116c 100644 --- a/.github/actions/generate/action.yml +++ b/.github/actions/generate/action.yml @@ -37,6 +37,10 @@ inputs: description: Whether to use mold linker required: true default: "false" + package: + description: Whether to generate Debian package + required: true + default: "false" runs: using: composite @@ -55,6 +59,7 @@ runs: BENCHMARK_OPTION: "${{ inputs.build_benchmark == 'true' && 'True' || 'False' }}" TIME_TRACE: "${{ inputs.time_trace == 'true' && 'True' || 'False' }}" USE_MOLD: "${{ inputs.use_mold == 'true' && 'True' || 'False' }}" + PACKAGE: "${{ inputs.package == 'true' && 'True' || 'False' }}" run: | cd build conan \ @@ -70,6 +75,7 @@ runs: -o "&:coverage=${CODE_COVERAGE}" \ -o "&:time_trace=${TIME_TRACE}" \ -o "&:use_mold=${USE_MOLD}" \ + -o "&:package=${PACKAGE}" \ --profile:all "${{ inputs.conan_profile }}" - name: Run cmake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f5e8357..f66d77ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,6 +85,23 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + package: + name: Build packages + + uses: ./.github/workflows/build_impl.yml + with: + runs_on: heavy + container: '{ "image": "ghcr.io/xrplf/clio-ci:8ad111655c4d04bfedb7e7cb3bbfba6d4204852d" }' + conan_profile: gcc + build_type: Release + disable_cache: false + code_coverage: false + static: true + upload_clio_server: false + package: true + targets: package + analyze_build_time: false + check_config: name: Check Config Description needs: build-and-test diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 8beb6309..21ee0402 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -63,6 +63,12 @@ on: type: string default: "" + package: + description: Whether to generate Debian package + required: false + type: boolean + default: false + jobs: build: uses: ./.github/workflows/build_impl.yml @@ -78,6 +84,7 @@ jobs: targets: ${{ inputs.targets }} analyze_build_time: false expected_version: ${{ inputs.expected_version }} + package: ${{ inputs.package }} test: needs: build diff --git a/.github/workflows/build_impl.yml b/.github/workflows/build_impl.yml index e16c37ae..9e248b57 100644 --- a/.github/workflows/build_impl.yml +++ b/.github/workflows/build_impl.yml @@ -59,6 +59,11 @@ on: type: string default: "" + package: + description: Whether to generate Debian package + required: false + type: boolean + secrets: CODECOV_TOKEN: required: false @@ -111,6 +116,7 @@ jobs: static: ${{ inputs.static }} time_trace: ${{ inputs.analyze_build_time }} use_mold: ${{ runner.os != 'macOS' }} + package: ${{ inputs.package }} - name: Build Clio uses: ./.github/actions/build_clio @@ -158,19 +164,26 @@ jobs: path: build/clio_server - name: Upload clio_tests - if: ${{ !inputs.code_coverage && !inputs.analyze_build_time }} + if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }} uses: actions/upload-artifact@v4 with: name: clio_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} path: build/clio_tests - name: Upload clio_integration_tests - if: ${{ !inputs.code_coverage && !inputs.analyze_build_time }} + if: ${{ !inputs.code_coverage && !inputs.analyze_build_time && !inputs.package }} uses: actions/upload-artifact@v4 with: name: clio_integration_tests_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} path: build/clio_integration_tests + - name: Upload Clio Linux package + if: inputs.package + uses: actions/upload-artifact@v4 + with: + name: clio_deb_package_${{ runner.os }}_${{ inputs.build_type }}_${{ inputs.conan_profile }} + path: build/*.deb + - name: Save cache if: ${{ !inputs.disable_cache && github.ref == 'refs/heads/develop' }} uses: ./.github/actions/save_cache diff --git a/CMakeLists.txt b/CMakeLists.txt index 2633a373..485c3982 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,8 +94,8 @@ if (docs) endif () include(install/install) -if (packaging) - include(cmake/packaging.cmake) # This file exists only in build runner +if (package) + include(ClioPackage) endif () if (snapshot) diff --git a/cmake/ClioPackage.cmake b/cmake/ClioPackage.cmake new file mode 100644 index 00000000..01f97dc3 --- /dev/null +++ b/cmake/ClioPackage.cmake @@ -0,0 +1,8 @@ +include("${CMAKE_CURRENT_LIST_DIR}/ClioVersion.cmake") + +set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/clio") +set(CPACK_PACKAGE_VERSION "${CLIO_VERSION}") +set(CPACK_STRIP_FILES TRUE) + +include(pkg/deb) +include(CPack) diff --git a/cmake/install/clio.service.in b/cmake/install/clio.service.in deleted file mode 100644 index 1ecdddb6..00000000 --- a/cmake/install/clio.service.in +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Clio XRPL API server -Documentation=https://github.com/XRPLF/clio.git - -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -ExecStart=@CLIO_INSTALL_DIR@/bin/clio_server @CLIO_INSTALL_DIR@/etc/config.json -Restart=on-failure -User=clio -Group=clio -LimitNOFILE=65536 - -[Install] -WantedBy=multi-user.target diff --git a/cmake/install/install.cmake b/cmake/install/install.cmake index 2d6b3baa..a640447f 100644 --- a/cmake/install/install.cmake +++ b/cmake/install/install.cmake @@ -1,13 +1,13 @@ set(CLIO_INSTALL_DIR "/opt/clio") -set(CMAKE_INSTALL_PREFIX ${CLIO_INSTALL_DIR}) +set(CMAKE_INSTALL_PREFIX "${CLIO_INSTALL_DIR}" CACHE PATH "Install prefix" FORCE) -install(TARGETS clio_server DESTINATION bin) +set(CPACK_PACKAGING_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}") + +include(GNUInstallDirs) + +install(TARGETS clio_server DESTINATION ${CMAKE_INSTALL_BINDIR}) file(READ docs/examples/config/example-config.json config) string(REGEX REPLACE "./clio_log" "/var/log/clio/" config "${config}") file(WRITE ${CMAKE_BINARY_DIR}/install-config.json "${config}") install(FILES ${CMAKE_BINARY_DIR}/install-config.json DESTINATION etc RENAME config.json) - -configure_file("${CMAKE_SOURCE_DIR}/cmake/install/clio.service.in" "${CMAKE_BINARY_DIR}/clio.service") - -install(FILES "${CMAKE_BINARY_DIR}/clio.service" DESTINATION /lib/systemd/system) diff --git a/cmake/pkg/deb.cmake b/cmake/pkg/deb.cmake new file mode 100644 index 00000000..67eabd4c --- /dev/null +++ b/cmake/pkg/deb.cmake @@ -0,0 +1,12 @@ +set(CPACK_GENERATOR "DEB") +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/XRPLF/clio") +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Ripple Labs Inc. ") + +set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) + +set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) + +set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ${CMAKE_SOURCE_DIR}/cmake/pkg/postinst) + +# We must replace "-" with "~" otherwise dpkg will sort "X.Y.Z-b1" as greater than "X.Y.Z" +string(REPLACE "-" "~" git "${CPACK_PACKAGE_VERSION}") diff --git a/cmake/pkg/postinst b/cmake/pkg/postinst new file mode 100755 index 00000000..19780b21 --- /dev/null +++ b/cmake/pkg/postinst @@ -0,0 +1,46 @@ +#!/bin/sh + +set -e + +USER_NAME=clio +GROUP_NAME="${USER_NAME}" +CLIO_EXECUTABLE="clio_server" +CLIO_PREFIX="/opt/clio" +CLIO_BIN="$CLIO_PREFIX/bin/${CLIO_EXECUTABLE}" +CLIO_CONFIG="$CLIO_PREFIX/etc/config.json" + +case "$1" in + configure) + if ! id -u "$USER_NAME" >/dev/null 2>&1; then + # Users who should not have a home directory should have their home directory set to /nonexistent + # https://www.debian.org/doc/debian-policy/ch-opersys.html#non-existent-home-directories + useradd \ + --system \ + --home-dir /nonexistent \ + --no-create-home \ + --shell /usr/sbin/nologin \ + --comment "system user for ${CLIO_EXECUTABLE}" \ + --user-group \ + ${USER_NAME} + fi + + install -d -o "$USER_NAME" -g "$GROUP_NAME" /var/log/clio + + if [ -f "$CLIO_CONFIG" ]; then + chown "$USER_NAME:$GROUP_NAME" "$CLIO_CONFIG" + fi + + chown -R "$USER_NAME:$GROUP_NAME" "$CLIO_PREFIX" + + ln -sf "$CLIO_BIN" "/usr/bin/${CLIO_EXECUTABLE}" + + ;; + abort-upgrade|abort-remove|abort-deconfigure) + ;; + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/conanfile.py b/conanfile.py index 9248c88a..779bb036 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ class ClioConan(ConanFile): 'integration_tests': [True, False], # build integration tests; create `clio_integration_tests` binary 'benchmark': [True, False], # build benchmarks; create `clio_benchmarks` binary 'docs': [True, False], # doxygen API docs; create custom target 'docs' - 'packaging': [True, False], # create distribution packages + 'package': [True, False], # create distribution packages 'coverage': [True, False], # build for test coverage report; create custom target `clio_tests-ccov` 'lint': [True, False], # run clang-tidy checks during compilation 'snapshot': [True, False], # build export/import snapshot tool @@ -43,7 +43,7 @@ class ClioConan(ConanFile): 'tests': False, 'integration_tests': False, 'benchmark': False, - 'packaging': False, + 'package': False, 'coverage': False, 'lint': False, 'docs': False, From e2fbf5627751c85a1e27eff7506fe6549deaa954 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 10:31:22 +0100 Subject: [PATCH 7/9] style: clang-tidy auto fixes (#2466) --- src/app/CliArgs.cpp | 1 + tests/unit/util/config/ConfigFileJsonTests.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/CliArgs.cpp b/src/app/CliArgs.cpp index 580ccd6b..024b17f4 100644 --- a/src/app/CliArgs.cpp +++ b/src/app/CliArgs.cpp @@ -23,6 +23,7 @@ #include "util/build/Build.hpp" #include "util/config/ConfigDescription.hpp" +#include #include #include #include diff --git a/tests/unit/util/config/ConfigFileJsonTests.cpp b/tests/unit/util/config/ConfigFileJsonTests.cpp index 521bdae2..194ae575 100644 --- a/tests/unit/util/config/ConfigFileJsonTests.cpp +++ b/tests/unit/util/config/ConfigFileJsonTests.cpp @@ -31,10 +31,10 @@ #include #include -#include #include #include #include +#include using namespace util::config; From 3681ef4e412bc6d7887f785eebf1056161e632f2 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 21 Aug 2025 15:33:01 +0100 Subject: [PATCH 8/9] feat: Do not print critical errors in stdout (#2468) --- benchmarks/util/log/LoggerBenchmark.cpp | 3 +- src/util/log/Logger.cpp | 55 +++++++++++++++++++++---- src/util/log/Logger.hpp | 2 +- tests/common/util/LoggerFixtures.cpp | 4 +- tests/unit/LogServiceInitTests.cpp | 4 +- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/benchmarks/util/log/LoggerBenchmark.cpp b/benchmarks/util/log/LoggerBenchmark.cpp index b492f042..cc36c0da 100644 --- a/benchmarks/util/log/LoggerBenchmark.cpp +++ b/benchmarks/util/log/LoggerBenchmark.cpp @@ -45,7 +45,7 @@ struct BenchmarkLoggingInitializer { static std::shared_ptr createFileSink(LogService::FileLoggingParams const& params) { - return LogService::createFileSink(params); + return LogService::createFileSink(params, kLOG_FORMAT); } static Logger @@ -107,7 +107,6 @@ benchmarkConcurrentFileLogging(benchmark::State& state) auto logger = std::make_shared( channel, fileSink, spdlog::thread_pool(), spdlog::async_overflow_policy::block ); - logger->set_pattern(kLOG_FORMAT); spdlog::register_logger(logger); Logger const threadLogger = BenchmarkLoggingInitializer::getLogger(std::move(logger)); diff --git a/src/util/log/Logger.cpp b/src/util/log/Logger.cpp index 57f69ee6..d505a96b 100644 --- a/src/util/log/Logger.cpp +++ b/src/util/log/Logger.cpp @@ -31,7 +31,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -41,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -120,26 +124,63 @@ getSeverityLevel(std::string_view logLevel) std::unreachable(); } +/** + * @brief Custom formatter that filters out critical messages + * + * This formatter only processes and formats messages with severity level less than critical. + * Critical messages will be handled separately. + */ +class NonCriticalFormatter : public spdlog::formatter { +public: + NonCriticalFormatter(std::unique_ptr wrappedFormatter) + : wrapped_formatter_(std::move(wrappedFormatter)) + { + } + + void + format(spdlog::details::log_msg const& msg, spdlog::memory_buf_t& dest) override + { + // Only format messages with severity less than critical + if (msg.level != spdlog::level::critical) { + wrapped_formatter_->format(msg, dest); + } + } + + std::unique_ptr + clone() const override + { + return std::make_unique(wrapped_formatter_->clone()); + } + +private: + std::unique_ptr wrapped_formatter_; +}; + /** * @brief Initializes console logging. * * @param logToConsole A boolean indicating whether to log to console. + * @param format A string representing the log format. * @return Vector of sinks for console logging. */ static std::vector -createConsoleSinks(bool logToConsole) +createConsoleSinks(bool logToConsole, std::string const& format) { std::vector sinks; if (logToConsole) { auto consoleSink = std::make_shared(); consoleSink->set_level(spdlog::level::trace); + consoleSink->set_formatter( + std::make_unique(std::make_unique(format)) + ); sinks.push_back(std::move(consoleSink)); } // Always add stderr sink for fatal logs auto stderrSink = std::make_shared(); stderrSink->set_level(spdlog::level::critical); + stderrSink->set_formatter(std::make_unique(format)); sinks.push_back(std::move(stderrSink)); return sinks; @@ -153,7 +194,7 @@ createConsoleSinks(bool logToConsole) * @return File sink for logging. */ spdlog::sink_ptr -LogService::createFileSink(FileLoggingParams const& params) +LogService::createFileSink(FileLoggingParams const& params, std::string const& format) { std::filesystem::path const dirPath(params.logDir); // the below are taken from user in MB, but spdlog needs it to be in bytes @@ -163,6 +204,7 @@ LogService::createFileSink(FileLoggingParams const& params) (dirPath / "clio.log").string(), rotationSize, params.dirMaxFiles ); fileSink->set_level(spdlog::level::trace); + fileSink->set_formatter(std::make_unique(format)); return fileSink; } @@ -229,11 +271,13 @@ LogService::init(config::ClioConfigDefinition const& config) data.isAsync = config.get("log.is_async"); data.defaultSeverity = getSeverityLevel(config.get("log.level")); + std::string const format = config.get("log.format"); + if (data.isAsync) { spdlog::init_thread_pool(8192, 1); } - data.allSinks = createConsoleSinks(config.get("log.enable_console")); + data.allSinks = createConsoleSinks(config.get("log.enable_console"), format); if (auto const logDir = config.maybeValue("log.directory"); logDir.has_value()) { std::filesystem::path const dirPath{logDir.value()}; @@ -250,7 +294,7 @@ LogService::init(config::ClioConfigDefinition const& config) .rotationSizeMB = config.get("log.rotation_size"), .dirMaxFiles = config.get("log.directory_max_files"), }; - data.allSinks.push_back(createFileSink(params)); + data.allSinks.push_back(createFileSink(params, format)); } // get min severity per channel, can be overridden using the `log.channels` array @@ -269,9 +313,6 @@ LogService::init(config::ClioConfigDefinition const& config) spdlog::set_default_logger(spdlog::get("General")); - std::string const format = config.get("log.format"); - spdlog::set_pattern(format); - LOG(LogService::info()) << "Default log level = " << toString(data.defaultSeverity); return {}; } diff --git a/src/util/log/Logger.hpp b/src/util/log/Logger.hpp index f94e550e..d8db859d 100644 --- a/src/util/log/Logger.hpp +++ b/src/util/log/Logger.hpp @@ -346,7 +346,7 @@ private: [[nodiscard]] static std::shared_ptr - createFileSink(FileLoggingParams const& params); + createFileSink(FileLoggingParams const& params, std::string const& format); }; }; // namespace util diff --git a/tests/common/util/LoggerFixtures.cpp b/tests/common/util/LoggerFixtures.cpp index e4ecea53..d0649be6 100644 --- a/tests/common/util/LoggerFixtures.cpp +++ b/tests/common/util/LoggerFixtures.cpp @@ -22,6 +22,7 @@ #include "util/log/Logger.hpp" #include +#include #include #include @@ -35,6 +36,7 @@ LoggerFixture::LoggerFixture() // Create ostream sink for testing auto ostreamSink = std::make_shared(stream_); + ostreamSink->set_formatter(std::make_unique("%^%3!l:%n%$ - %v")); // Create loggers for each channel std::ranges::for_each(util::Logger::kCHANNELS, [&ostreamSink](char const* channel) { @@ -50,8 +52,6 @@ LoggerFixture::LoggerFixture() spdlog::register_logger(traceLogger); spdlog::set_default_logger(spdlog::get("General")); - - spdlog::set_pattern("%^%3!l:%n%$ - %v"); } NoLoggerFixture::NoLoggerFixture() diff --git a/tests/unit/LogServiceInitTests.cpp b/tests/unit/LogServiceInitTests.cpp index 3309997c..29ec2ac6 100644 --- a/tests/unit/LogServiceInitTests.cpp +++ b/tests/unit/LogServiceInitTests.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -84,6 +85,7 @@ protected: replaceSinks() { auto ostreamSink = std::make_shared(stream_); + ostreamSink->set_formatter(std::make_unique("%^%3!l:%n%$ - %v")); for (auto const& channel : Logger::kCHANNELS) { auto logger = spdlog::get(channel); @@ -93,8 +95,6 @@ protected: logger->sinks().clear(); logger->sinks().push_back(ostreamSink); } - - spdlog::set_pattern("%^%3!l:%n%$ - %v"); } private: From ec40cc93ffc33f7d579ebcfb164c41d4ef22488b Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 21 Aug 2025 15:43:30 +0100 Subject: [PATCH 9/9] fix: Shutdown LogService after exceptions are printed (#2464) --- src/main/Main.cpp | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main/Main.cpp b/src/main/Main.cpp index 5813ec7b..01af89e7 100644 --- a/src/main/Main.cpp +++ b/src/main/Main.cpp @@ -34,19 +34,16 @@ using namespace util::config; +[[nodiscard]] int -main(int argc, char const* argv[]) -try { - util::setTerminationHandler(); - util::ScopeGuard const loggerShutdownGuard{[]() { util::LogService::shutdown(); }}; - +runApp(int argc, char const* argv[]) +{ auto const action = app::CliArgs::parse(argc, argv); return action.apply( [](app::CliArgs::Action::Exit const& exit) { return exit.exitCode; }, [](app::CliArgs::Action::VerifyConfig const& verify) { if (app::parseConfig(verify.configPath)) { - std::cout << "Config " << verify.configPath << " is correct" - << "\n"; + std::cout << "Config " << verify.configPath << " is correct" << "\n"; return EXIT_SUCCESS; } return EXIT_FAILURE; @@ -76,10 +73,22 @@ try { return migrator.run(); } ); -} catch (std::exception const& e) { - LOG(util::LogService::fatal()) << "Exit on exception: " << e.what(); - return EXIT_FAILURE; -} catch (...) { - LOG(util::LogService::fatal()) << "Exit on exception: unknown"; - return EXIT_FAILURE; +} + +int +main(int argc, char const* argv[]) +{ + util::setTerminationHandler(); + + util::ScopeGuard const loggerShutdownGuard{[] { util::LogService::shutdown(); }}; + + try { + return runApp(argc, argv); + } catch (std::exception const& e) { + LOG(util::LogService::fatal()) << "Exit on exception: " << e.what(); + return EXIT_FAILURE; + } catch (...) { + LOG(util::LogService::fatal()) << "Exit on exception: unknown"; + return EXIT_FAILURE; + } }