Compare commits

...

2 Commits

Author SHA1 Message Date
Bart
8fabef3b50 fix: Fail when no config file is read, and report the search
When Config::setup found no config file, it left the config file
pointing at the last name it tried in the last directory. The read
failed there, so the operator was told only that /etc/xrpld/rippled.cfg
was missing. setup then tried to create /var/lib/xrpld, and that throw
reached an uncaught main and aborted the process. A config file named
with --conf and unreadable produced only a warning, and setup carried on
with no settings at all.

The config file is now a local std::optional that setup passes to load,
rather than a configFile_ member. A file that is there but cannot be
read throws, as one with invalid contents already did, so load needs no
return value. When the search found nothing, setup writes every
directory it tried to stderr, skips the data directory, and returns
false. It is [[nodiscard]] bool.

So a false return means one thing only. The search found no config file
at all, which is fatal only without a positional argument, the same
condition that chooses between starting a server and running an RPC
command. That rule is missingConfigIsFatal in the new Main.h. setup
still sets up the defaults and the SSL context in that case, so the RPC
client runs; rpc_call::fromCommandLine takes its destination from
--rpc_ip.

An unreadable config file used to be fatal only when --conf named it. A
file the search found and could not read left RPC mode running on
defaults, so the same file gave exit 1 under --conf and exit 73 when
found by the search. Throwing removes that difference.

--vacuum is excluded from that condition. It returns before the RPC
branch and reads the config itself, so a positional argument beside it
used to skip the check. On a host with no config file, xrpld --vacuum
server_info ran vacuum unconfigured and died with "exception
database_path must be set. in function run" and exit 255. It now reports
the search and exits 1, as --vacuum alone already did.

run() wraps its setup call to catch what setup throws, print "xrpld:
invalid configuration: <reason>", and exit 1.

Also fixes the read-failure message, which wrapped the path in single
quotes on top of the double quotes that streaming a
std::filesystem::path adds, and ran the error number onto a period.
Every message here is built with std::format. The data directory throw
names the reason from its error_code, which was captured and then
discarded.

Config_test asserts the result of every setup call, covers a config file
that cannot be read, and covers the case where no directory holds one.
The second captures stderr and asserts all three searched directories
are named. The first asserts the message thrown and that no data
directory is left beside the file.

The guard that skips the data directory when nothing was read is not
reachable from a suite. Its one remaining path is a search that found
nothing, where the data directory is the last candidate's, /var/lib
under the system name, which a test cannot redirect. A built binary
covers it. On a host with no config file the run reports the searched
directories and exits 1, creating no data directory.

missingConfigIsFatal is a header-only predicate so that xrpld_tests can
reach it without linking server code, and its four combinations are
covered there.

run() itself is not reachable from a suite, so it is verified against a
built binary. An unreadable config file exits 1 whether --conf named it
or the search found it, and with or without a positional argument. With
no config file anywhere and a positional argument, the run still reaches
the RPC client.
2026-10-03 09:32:29 -04:00
Bart
f95f7889f9 refactor: Search config directories over a candidate list
Config::setup searched the working directory, the XDG config directory
and the system config directory with a do/while(false) block and breaks,
trying both file names in each.

Replace it with a list of `Candidate` values, each naming a config
directory and its data directory, and a loop that takes the first
holding a config file. Add a docstring for the search order.

The directories chosen and the file reported are unchanged. The last
candidate is now also checked under the legacy name, which the old code
assigned without checking, so one more filesystem check runs when
nothing is found.
2026-10-02 21:19:48 +00:00
8 changed files with 371 additions and 107 deletions

View File

@@ -194,6 +194,7 @@ tests.libxrpl > xrpl.resource
tests.libxrpl > xrpl.server
tests.libxrpl > xrpl.shamap
tests.libxrpl > xrpl.tx
tests.xrpld > xrpld.app
tests.xrpld > xrpld.rpc
tests.xrpld > xrpl.json
tests.xrpld > xrpl.protocol

View File

@@ -20,10 +20,13 @@
#include <filesystem>
#include <format>
#include <fstream>
#include <iostream>
#include <optional>
#include <ostream>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <string_view>
#include <typeinfo>
@@ -199,6 +202,53 @@ public:
#endif // _MSC_VER
/**
* Send a stream's output to a buffer of our own, and restore it when done.
*/
class StreamRedirectGuard
{
private:
// The saved_ member is initialized from captured_.rdbuf(), so captured_
// must stay declared before it.
std::ostream& stream_;
std::ostringstream captured_;
std::streambuf* const saved_;
public:
/**
* Send the stream's output to this guard's own buffer.
*
* @param stream Stream to redirect for the guard's lifetime.
*/
explicit StreamRedirectGuard(std::ostream& stream)
: stream_(stream), saved_(stream.rdbuf(captured_.rdbuf()))
{
}
StreamRedirectGuard(StreamRedirectGuard const&) = delete;
StreamRedirectGuard&
operator=(StreamRedirectGuard const&) = delete;
/**
* Read what the stream has written so far.
*
* @return Everything written to the stream since construction.
*/
[[nodiscard]] std::string
str() const
{
return captured_.str();
}
/**
* Send the stream's output back where it went before.
*/
~StreamRedirectGuard()
{
stream_.rdbuf(saved_);
}
};
/**
* Write an xrpld config file and remove when done.
*/
@@ -233,11 +283,13 @@ public:
dataDir_ = subdir() / Path(Config::kDatabaseDirName);
rmDataDir_ = !exists(dataDir_);
config_.setup(
file_.string(),
/* bQuiet */ true,
/* bSilent */ false,
/* bStandalone */ false);
test_.expect(
config_.setup(
file_.string(),
/* bQuiet */ true,
/* bSilent */ false,
/* bStandalone */ false),
"setup failed for " + file_.string());
}
[[nodiscard]] Config const&
@@ -404,13 +456,35 @@ port_wss_admin
// Load the config file from the current directory and verify it.
Config c;
c.setup("", true, false, true);
BEAST_EXPECT(c.setup("", true, false, true));
BEAST_EXPECT(c.section(Sections::kDebugLogfile).values().size() == 1);
BEAST_EXPECT(
c.section(Sections::kDebugLogfile).values()[0] ==
"/Users/dummy/xrpld/config/log/debug.log");
}
// A config file that cannot be read throws, whether it was named or
// found by the search. The throw leaves the data directory alone, so
// nothing is created beside the file that could not be read.
{
TempDir const td;
Path const missing = Path(td.file("does_not_exist.cfg"));
std::string error;
try
{
Config c;
BEAST_EXPECT(!c.setup(missing.string(), true, false, false));
}
catch (std::runtime_error const& e)
{
error = e.what();
}
BEAST_EXPECT(error.starts_with("Failed to read '" + missing.string() + "'"));
// Nothing was read, so no data directory is created.
BEAST_EXPECT(!exists(Path(td.path()) / std::string{Config::kDatabaseDirName}));
}
// Config file in HOME or XDG_CONFIG_HOME directory.
#ifndef _MSC_VER
// Save the values the guards below must put back.
@@ -445,7 +519,7 @@ port_wss_admin
// Load the config file from the config directory and verify it.
Config c;
c.setup("", true, false, true);
BEAST_EXPECT(c.setup("", true, false, true));
BEAST_EXPECT(c.section(Sections::kDebugLogfile).values().size() == 1);
BEAST_EXPECT(
c.section(Sections::kDebugLogfile).values()[0] ==
@@ -480,7 +554,7 @@ port_wss_admin
// Load the config file from the config directory and verify it.
Config c;
c.setup("", true, false, true);
BEAST_EXPECT(c.setup("", true, false, true));
BEAST_EXPECT(c.section(Sections::kDebugLogfile).values().size() == 1);
BEAST_EXPECT(
c.section(Sections::kDebugLogfile).values()[0] ==
@@ -491,6 +565,60 @@ port_wss_admin
BEAST_EXPECT(detail::envVar("HOME") == home);
BEAST_EXPECT(detail::envVar("XDG_CONFIG_HOME") == xdgConfigHome);
}
// No config file under either name in any searched directory.
{
// The system config directory is the last one searched and is not
// under the test's control, so only run this when it holds no
// config file.
Path const systemDir = Path("/etc") / systemName();
if (exists(systemDir / std::string{Config::kConfigFileName}) ||
exists(systemDir / std::string{Config::kConfigLegacyName}))
{
log << "Skipping: " << systemDir.string() << " holds a config file." << std::endl;
}
else
{
// Point the working directory and HOME at empty temporary
// directories. The guards are declared after tc, so they restore
// before it is removed.
TempDir const td;
current_path(td.path());
TempDir const tc;
detail::EnvVarGuard const homeGuard("HOME", tc.path());
detail::EnvVarGuard const xdgGuard("XDG_CONFIG_HOME", std::nullopt);
// The three directories setup() reports. Read the working
// directory back, because current_path() can resolve a symlink.
Path const searchedCwd = current_path();
std::string const searchedXdgDir = tc.path() + "/.config/" + systemName();
std::string const searchedSystemDir = systemDir.string();
// Capture stderr around the setup() call only, so the
// assertions below keep their own output.
Config c;
bool setupResult = true;
std::string reported;
{
detail::StreamRedirectGuard const capture(std::cerr);
setupResult = c.setup("", true, false, true);
reported = capture.str();
}
BEAST_EXPECT(!setupResult);
// Assert the directory paths, not the wording of the header.
BEAST_EXPECT(reported.contains(searchedCwd.string()));
BEAST_EXPECT(reported.contains(searchedXdgDir));
BEAST_EXPECT(reported.contains(searchedSystemDir));
// A run with no config file names no data directory. This call
// is standalone, which clears it anyway, so the check states the
// result rather than proving the creation step was skipped.
BEAST_EXPECT(c.legacy(Sections::kDatabasePath).empty());
}
}
#endif // _MSC_VER
// Restore the current working directory.
@@ -1085,11 +1213,11 @@ trust-these-validators.gov
*/
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ false,
/* bSilent */ false,
/* bStandalone */ false);
/* bStandalone */ false));
BEAST_EXPECT(!config.quiet());
BEAST_EXPECT(!config.silent());
BEAST_EXPECT(!config.standalone());
@@ -1098,11 +1226,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ true,
/* bSilent */ false,
/* bStandalone */ false);
/* bStandalone */ false));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(!config.silent());
BEAST_EXPECT(!config.standalone());
@@ -1111,11 +1239,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ false,
/* bSilent */ true,
/* bStandalone */ false);
/* bStandalone */ false));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(config.silent());
BEAST_EXPECT(!config.standalone());
@@ -1124,11 +1252,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ true,
/* bSilent */ true,
/* bStandalone */ false);
/* bStandalone */ false));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(config.silent());
BEAST_EXPECT(!config.standalone());
@@ -1137,11 +1265,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ false,
/* bSilent */ false,
/* bStandalone */ true);
/* bStandalone */ true));
BEAST_EXPECT(!config.quiet());
BEAST_EXPECT(!config.silent());
BEAST_EXPECT(config.standalone());
@@ -1150,11 +1278,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ true,
/* bSilent */ false,
/* bStandalone */ true);
/* bStandalone */ true));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(!config.silent());
BEAST_EXPECT(config.standalone());
@@ -1163,11 +1291,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ false,
/* bSilent */ true,
/* bStandalone */ true);
/* bStandalone */ true));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(config.silent());
BEAST_EXPECT(config.standalone());
@@ -1176,11 +1304,11 @@ trust-these-validators.gov
}
{
Config config;
config.setup(
BEAST_EXPECT(config.setup(
cfg.configFile(),
/*bQuiet*/ true,
/* bSilent */ true,
/* bStandalone */ true);
/* bStandalone */ true));
BEAST_EXPECT(config.quiet());
BEAST_EXPECT(config.silent());
BEAST_EXPECT(config.standalone());

View File

@@ -18,7 +18,7 @@ target_link_libraries(
PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl rpcspec::rpcspec
)
set(test_modules rpc)
set(test_modules app rpc)
foreach(module IN LISTS test_modules)
file(

View File

@@ -0,0 +1,31 @@
#include <xrpld/app/main/Main.h>
#include <gtest/gtest.h>
namespace xrpl {
// A config file that was read is never fatal.
TEST(MissingConfigIsFatal, read_in_rpc_mode)
{
EXPECT_FALSE(missingConfigIsFatal(true, true));
}
TEST(MissingConfigIsFatal, read_for_a_server_start)
{
EXPECT_FALSE(missingConfigIsFatal(true, false));
}
// The search found nothing. The RPC client runs anyway, a server does not.
TEST(MissingConfigIsFatal, search_found_nothing_in_rpc_mode)
{
EXPECT_FALSE(missingConfigIsFatal(false, true));
}
TEST(MissingConfigIsFatal, search_found_nothing_for_a_server_start)
{
EXPECT_TRUE(missingConfigIsFatal(false, false));
}
} // namespace xrpl

View File

@@ -1,3 +1,5 @@
#include <xrpld/app/main/Main.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/core/Config.h>
#include <xrpld/core/TimeKeeper.h>
@@ -574,9 +576,25 @@ run(int argc, char** argv)
auto configFile = vm.contains("conf") ? vm["conf"].as<std::string>() : std::string();
// config file, quiet flag.
config->setup(
configFile, vm.contains("quiet"), vm.contains("silent"), vm.contains("standalone"));
// A positional argument selects the RPC client, which needs no config
// file. --vacuum returns before that branch and does need one.
bool const rpcClientMode = vm.contains("parameters") && !vm.contains("vacuum");
// Setup returns false when the search found no config file, and throws for
// one it cannot read or whose contents are invalid. It reports the reason.
try
{
bool const configRead = config->setup(
configFile, vm.contains("quiet"), vm.contains("silent"), vm.contains("standalone"));
if (missingConfigIsFatal(configRead, rpcClientMode))
return 1;
}
catch (std::exception const& e)
{
std::cerr << "xrpld: invalid configuration: " << e.what() << std::endl;
return 1;
}
if (vm.contains("vacuum"))
{

22
src/xrpld/app/main/Main.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
namespace xrpl {
/**
* Whether the absence of a config file must end the run.
*
* Config::setup throws for a file it found but could not read, so reaching here
* with nothing read means the search found no file at all. That is fatal only
* for a server start, because the RPC client runs without one.
*
* @param configRead Whether setup read a config file.
* @param rpcClientMode Whether a positional argument selects the RPC client.
* @return True when the run cannot continue.
*/
[[nodiscard]] inline bool
missingConfigIsFatal(bool configRead, bool rpcClientMode)
{
return !configRead && !rpcClientMode;
}
} // namespace xrpl

View File

@@ -100,17 +100,21 @@ public:
[[nodiscard]] std::filesystem::path
getDebugLogFile() const;
private:
std::filesystem::path configFile_;
public:
std::filesystem::path configDir;
private:
std::filesystem::path debugLogfile_;
/**
* Read and parse the given config file.
*
* @param configFile Path of the config file to read.
* @throws std::runtime_error When the file cannot be read, or its contents
* are invalid.
*/
void
load();
load(std::filesystem::path const& configFile);
beast::Journal const j_;
bool quiet_ = false; // Minimize logging verbosity.
@@ -330,9 +334,28 @@ public:
public:
Config();
/* Be very careful to make sure these bool params
are in the right order. */
void
/**
* Set up the configuration and determine the config and data directories.
*
* When no config file is named, searches the working directory, then the XDG
* config directory when HOME is set, then the system config directory, and
* takes the first holding a config file under either name.
*
* Be very careful to make sure these bool params are in the right order.
*
* @param strConf Path of the config file, or empty to search for it.
* @param bQuiet Whether to minimize logging verbosity.
* @param bSilent Whether to write no console output after startup.
* @param bStandalone Whether to operate in stand-alone mode.
* @return True when a config file was found and read. False means the
* search found none, and writes the directories it tried to
* stderr. The defaults and the SSL context are still set up; only
* the data directory is skipped.
* @throws std::runtime_error When a config file was found but cannot be
* read, when its contents are invalid, or when the data directory
* cannot be created.
*/
[[nodiscard]] bool
setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStandalone);
void

View File

@@ -43,6 +43,7 @@
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <thread>
#include <type_traits>
@@ -297,7 +298,7 @@ Config::setupControl(bool bQuiet, bool bSilent, bool bStandalone)
XRPL_ASSERT(nodeSize <= 4, "xrpl::Config::setupControl : node size is set");
}
void
bool
Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStandalone)
{
setupControl(bQuiet, bSilent, bStandalone);
@@ -309,95 +310,130 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
// directory.
std::filesystem::path dataDir;
// The config file to read. It stays unset when the search for one finds
// nothing.
std::optional<std::filesystem::path> configFile;
if (!strConf.empty())
{
// --conf=<path> : everything is relative that file.
configFile_ = strConf;
configDir = std::filesystem::absolute(configFile_);
configFile = strConf;
configDir = std::filesystem::absolute(*configFile);
configDir.remove_filename();
dataDir = configDir / kDatabaseDirName;
}
else
{
do
// A config directory, paired with its data directory.
struct Candidate
{
// Check if either of the config files exist in the current working
// directory, in which case the databases will be stored in a
// subdirectory.
configDir = std::filesystem::current_path();
dataDir = configDir / kDatabaseDirName;
configFile_ = configDir / kConfigFileName;
if (std::filesystem::exists(configFile_))
break;
configFile_ = configDir / kConfigLegacyName;
if (std::filesystem::exists(configFile_))
break;
std::filesystem::path configDir;
std::filesystem::path dataDir;
};
// Check if the home directory is set, and optionally the XDG config
// and/or data directories, as the config may be there. See
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html.
auto const strHome = getEnvVar("HOME");
if (!strHome.empty())
// The candidates to search, in order.
std::vector<Candidate> candidates;
candidates.reserve(3);
// The current working directory, with the databases in a subdirectory.
auto const cwd = std::filesystem::current_path();
candidates.emplace_back(cwd, cwd / kDatabaseDirName);
// The XDG config and data directories, which only apply when the home
// directory is set. See
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html.
if (auto const strHome = getEnvVar("HOME"); !strHome.empty())
{
auto strXdgConfigHome = getEnvVar("XDG_CONFIG_HOME");
if (strXdgConfigHome.empty())
{
auto strXdgConfigHome = getEnvVar("XDG_CONFIG_HOME");
auto strXdgDataHome = getEnvVar("XDG_DATA_HOME");
if (strXdgConfigHome.empty())
{
// $XDG_CONFIG_HOME was not set, use default based on $HOME.
strXdgConfigHome = strHome + "/.config";
}
if (strXdgDataHome.empty())
{
// $XDG_DATA_HOME was not set, use default based on $HOME.
strXdgDataHome = strHome + "/.local/share";
}
// $XDG_CONFIG_HOME was not set, use default based on $HOME.
strXdgConfigHome = strHome + "/.config";
}
auto strXdgDataHome = getEnvVar("XDG_DATA_HOME");
if (strXdgDataHome.empty())
{
// $XDG_DATA_HOME was not set, use default based on $HOME.
strXdgDataHome = strHome + "/.local/share";
}
candidates.emplace_back(
strXdgConfigHome + "/" + systemName(), strXdgDataHome + "/" + systemName());
}
// Check if either of the config files exist in the XDG config
// dir.
dataDir = strXdgDataHome + "/" + systemName();
configDir = strXdgConfigHome + "/" + systemName();
configFile_ = configDir / kConfigFileName;
if (std::filesystem::exists(configFile_))
break;
configFile_ = configDir / kConfigLegacyName;
if (std::filesystem::exists(configFile_))
// The system config directory, as a last resort.
candidates.emplace_back("/etc/" + systemName(), "/var/lib/" + systemName());
// Take the first candidate holding a config file under either name.
// When none does, configFile stays unset.
for (auto const& candidate : candidates)
{
configDir = candidate.configDir;
dataDir = candidate.dataDir;
for (std::string_view const fileName : {kConfigFileName, kConfigLegacyName})
{
auto candidateFile = configDir / fileName;
if (std::filesystem::exists(candidateFile))
{
configFile = std::move(candidateFile);
break;
}
}
// As a last resort, check the system config directory.
dataDir = "/var/lib/" + systemName();
configDir = "/etc/" + systemName();
configFile_ = configDir / kConfigFileName;
if (std::filesystem::exists(configFile_))
if (configFile)
break;
configFile_ = configDir / kConfigLegacyName;
} while (false);
}
// Update default values
load();
{
// load() may have set a new value for the dataDir
std::string const dbPath(legacy(Sections::kDatabasePath));
if (!dbPath.empty())
{
dataDir = std::filesystem::path(dbPath);
}
else if (runStandalone_)
if (!configFile)
{
dataDir.clear();
// Report every directory searched, even when quiet, since this is
// the reason for the failure.
std::cerr << std::format(
"No {} or {} found. Searched:\n", kConfigFileName, kConfigLegacyName);
for (auto const& candidate : candidates)
std::cerr << std::format(" {}\n", candidate.configDir.string());
}
}
if (!dataDir.empty())
// Read the config file, which updates the default values. A file that is
// there but cannot be read throws, so getting past this means the file was
// read. The rest of this function still runs when the search found none,
// so that the command line RPC client gets the defaults and the SSL
// context. The caller decides whether that is fatal.
bool const configRead = configFile.has_value();
if (configRead)
load(*configFile);
// Only a config file that was read can name a data directory.
if (configRead)
{
std::error_code ec;
std::filesystem::create_directories(dataDir, ec);
{
// load() may have set a new value for the dataDir
std::string const dbPath(legacy(Sections::kDatabasePath));
if (!dbPath.empty())
{
dataDir = std::filesystem::path(dbPath);
}
else if (runStandalone_)
{
dataDir.clear();
}
}
if (ec)
Throw<std::runtime_error>(std::format("Can not create {}", dataDir.string()));
if (!dataDir.empty())
{
std::error_code ec;
std::filesystem::create_directories(dataDir, ec);
legacy(Sections::kDatabasePath, std::filesystem::absolute(dataDir).string());
if (ec)
{
Throw<std::runtime_error>(
std::format("Can not create {}: {}", dataDir.string(), ec.message()));
}
legacy(Sections::kDatabasePath, std::filesystem::absolute(dataDir).string());
}
}
HTTPClient::initializeSSLContext(this->sslVerifyDir, this->sslVerifyFile, this->sslVerify, j_);
@@ -410,6 +446,8 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
Section const& nodeDbSection{section(Sections::kNodeDatabase)};
getIfExists(nodeDbSection, Keys::kFastLoad, fastLoad);
return configRead;
}
// 0 ports are allowed for unit tests, but still not allowed to be present in
@@ -441,22 +479,25 @@ checkZeroPorts(Config const& config)
}
void
Config::load()
Config::load(std::filesystem::path const& configFile)
{
// A std::filesystem::path has no std::formatter, and streaming one adds
// quotes of its own.
auto const fileName = configFile.string();
// NOTE: this writes to cerr because we want cout to be reserved
// for the writing of the json response (so that stdout can be part of a
// pipeline, for instance)
if (!quiet_)
std::cerr << "Loading: " << configFile_ << "\n";
std::cerr << std::format("Loading: '{}'\n", fileName);
std::error_code ec;
auto const fileContents = getFileContents(ec, configFile_);
auto const fileContents = getFileContents(ec, configFile);
if (ec)
{
std::cerr << "Failed to read '" << configFile_ << "'." << ec.value() << ": " << ec.message()
<< std::endl;
return;
Throw<std::runtime_error>(
std::format("Failed to read '{}': {} (error {})", fileName, ec.message(), ec.value()));
}
loadFromString(fileContents);