mirror of
https://github.com/liyunfan1223/azerothcore-wotlk.git
synced 2026-08-06 23:07:59 +00:00
* Core/Config: Implement config override with env vars Implement overriding of configuration from the .conf file with environment variables. Environment variables keys are autogenerated based on the keys defined in .conf file. Usage example: $ export TC_DATA_DIR=/usr $ AC_WORLD_SERVER_PORT=8080 ./worldserver * Add tests for env vars config
639 lines
18 KiB
C++
639 lines
18 KiB
C++
/*
|
|
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Affero General Public License as published by the
|
|
* Free Software Foundation; either version 3 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "Config.h"
|
|
#include "Log.h"
|
|
#include "StringConvert.h"
|
|
#include "StringFormat.h"
|
|
#include "Tokenize.h"
|
|
#include "Util.h"
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
|
|
namespace
|
|
{
|
|
std::string _filename;
|
|
std::vector<std::string> _additonalFiles;
|
|
std::vector<std::string> _args;
|
|
std::unordered_map<std::string /*name*/, std::string /*value*/> _configOptions;
|
|
std::mutex _configLock;
|
|
|
|
// Check system configs like *server.conf*
|
|
bool IsAppConfig(std::string_view fileName)
|
|
{
|
|
size_t foundAuth = fileName.find("authserver.conf");
|
|
size_t foundWorld = fileName.find("worldserver.conf");
|
|
|
|
return foundAuth != std::string_view::npos || foundWorld != std::string_view::npos;
|
|
}
|
|
|
|
// Check logging system configs like Appender.* and Logger.*
|
|
bool IsLoggingSystemOptions(std::string_view optionName)
|
|
{
|
|
size_t foundAppender = optionName.find("Appender.");
|
|
size_t foundLogger = optionName.find("Logger.");
|
|
|
|
return foundAppender != std::string_view::npos || foundLogger != std::string_view::npos;
|
|
}
|
|
|
|
template<typename Format, typename... Args>
|
|
inline void PrintError(std::string_view filename, Format&& fmt, Args&& ... args)
|
|
{
|
|
std::string message = Acore::StringFormatFmt(std::forward<Format>(fmt), std::forward<Args>(args)...);
|
|
|
|
if (IsAppConfig(filename))
|
|
{
|
|
fmt::print("{}\n", message);
|
|
}
|
|
else
|
|
{
|
|
LOG_ERROR("server.loading", message);
|
|
}
|
|
}
|
|
|
|
void AddKey(std::string const& optionName, std::string const& optionKey, std::string_view fileName, bool isOptional, [[maybe_unused]] bool isReload)
|
|
{
|
|
auto const& itr = _configOptions.find(optionName);
|
|
|
|
// Check old option
|
|
if (isOptional && itr == _configOptions.end())
|
|
{
|
|
if (!IsLoggingSystemOptions(optionName) && !isReload)
|
|
{
|
|
PrintError(fileName, "> Config::LoadFile: Found incorrect option '{}' in config file '{}'. Skip", optionName, fileName);
|
|
|
|
#ifdef CONFIG_ABORT_INCORRECT_OPTIONS
|
|
ABORT("> Core can't start if found incorrect options");
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check exit option
|
|
if (itr != _configOptions.end())
|
|
{
|
|
_configOptions.erase(optionName);
|
|
}
|
|
|
|
_configOptions.emplace(optionName, optionKey);
|
|
}
|
|
|
|
bool ParseFile(std::string const& file, bool isOptional, bool isReload)
|
|
{
|
|
std::ifstream in(file);
|
|
|
|
if (in.fail())
|
|
{
|
|
if (isOptional)
|
|
{
|
|
// No display erorr if file optional
|
|
return false;
|
|
}
|
|
|
|
throw ConfigException(Acore::StringFormatFmt("Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file));
|
|
}
|
|
|
|
uint32 count = 0;
|
|
uint32 lineNumber = 0;
|
|
std::unordered_map<std::string /*name*/, std::string /*value*/> fileConfigs;
|
|
|
|
auto IsDuplicateOption = [&](std::string const& confOption)
|
|
{
|
|
auto const& itr = fileConfigs.find(confOption);
|
|
if (itr != fileConfigs.end())
|
|
{
|
|
PrintError(file, "> Config::LoadFile: Dublicate key name '{}' in config file '{}'", confOption, file);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
while (in.good())
|
|
{
|
|
lineNumber++;
|
|
std::string line;
|
|
std::getline(in, line);
|
|
|
|
// read line error
|
|
if (!in.good() && !in.eof())
|
|
{
|
|
throw ConfigException(Acore::StringFormatFmt("> Config::LoadFile: Failure to read line number {} in file '{}'", lineNumber, file));
|
|
}
|
|
|
|
// remove whitespace in line
|
|
line = Acore::String::Trim(line, in.getloc());
|
|
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// comments
|
|
if (line[0] == '#' || line[0] == '[')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
size_t found = line.find_first_of('#');
|
|
if (found != std::string::npos)
|
|
{
|
|
line = line.substr(0, found);
|
|
}
|
|
|
|
auto const equal_pos = line.find('=');
|
|
|
|
if (equal_pos == std::string::npos || equal_pos == line.length())
|
|
{
|
|
PrintError(file, "> Config::LoadFile: Failure to read line number {} in file '{}'. Skip this line", lineNumber, file);
|
|
continue;
|
|
}
|
|
|
|
auto entry = Acore::String::Trim(line.substr(0, equal_pos), in.getloc());
|
|
auto value = Acore::String::Trim(line.substr(equal_pos + 1, std::string::npos), in.getloc());
|
|
|
|
value.erase(std::remove(value.begin(), value.end(), '"'), value.end());
|
|
|
|
// Skip if 2+ same options in one config file
|
|
if (IsDuplicateOption(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Add to temp container
|
|
fileConfigs.emplace(entry, value);
|
|
count++;
|
|
}
|
|
|
|
// No lines read
|
|
if (!count)
|
|
{
|
|
if (isOptional)
|
|
{
|
|
// No display erorr if file optional
|
|
return false;
|
|
}
|
|
|
|
throw ConfigException(Acore::StringFormatFmt("Config::LoadFile: Empty file '{}'", file));
|
|
}
|
|
|
|
// Add correct keys if file load without errors
|
|
for (auto const& [entry, key] : fileConfigs)
|
|
{
|
|
AddKey(entry, key, file, isOptional, isReload);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LoadFile(std::string const& file, bool isOptional, bool isReload)
|
|
{
|
|
try
|
|
{
|
|
return ParseFile(file, isOptional, isReload);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
PrintError(file, "> {}", e.what());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Converts ini keys to the environment variable key (upper snake case).
|
|
// Example of conversions:
|
|
// SomeConfig => SOME_CONFIG
|
|
// myNestedConfig.opt1 => MY_NESTED_CONFIG_OPT_1
|
|
// LogDB.Opt.ClearTime => LOG_DB_OPT_CLEAR_TIME
|
|
std::string IniKeyToEnvVarKey(std::string const& key)
|
|
{
|
|
std::string result;
|
|
|
|
const char* str = key.c_str();
|
|
size_t n = key.length();
|
|
|
|
char curr;
|
|
bool isEnd;
|
|
bool nextIsUpper;
|
|
bool currIsNumeric;
|
|
bool nextIsNumeric;
|
|
|
|
for (size_t i = 0; i < n; ++i)
|
|
{
|
|
curr = str[i];
|
|
if (curr == ' ' || curr == '.' || curr == '-')
|
|
{
|
|
result += '_';
|
|
continue;
|
|
}
|
|
|
|
isEnd = i == n - 1;
|
|
if (!isEnd)
|
|
{
|
|
nextIsUpper = isupper(str[i + 1]);
|
|
|
|
// handle "aB" to "A_B"
|
|
if (!isupper(curr) && nextIsUpper)
|
|
{
|
|
result += static_cast<char>(std::toupper(curr));
|
|
result += '_';
|
|
continue;
|
|
}
|
|
|
|
currIsNumeric = isNumeric(curr);
|
|
nextIsNumeric = isNumeric(str[i + 1]);
|
|
|
|
// handle "a1" to "a_1"
|
|
if (!currIsNumeric && nextIsNumeric)
|
|
{
|
|
result += static_cast<char>(std::toupper(curr));
|
|
result += '_';
|
|
continue;
|
|
}
|
|
|
|
// handle "1a" to "1_a"
|
|
if (currIsNumeric && !nextIsNumeric)
|
|
{
|
|
result += static_cast<char>(std::toupper(curr));
|
|
result += '_';
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result += static_cast<char>(std::toupper(curr));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Optional<std::string> EnvVarForIniKey(std::string const& key)
|
|
{
|
|
std::string envKey = "AC_" + IniKeyToEnvVarKey(key);
|
|
char* val = std::getenv(envKey.c_str());
|
|
if (!val)
|
|
return std::nullopt;
|
|
|
|
return std::string(val);
|
|
}
|
|
}
|
|
|
|
bool ConfigMgr::LoadInitial(std::string const& file, bool isReload /*= false*/)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
_configOptions.clear();
|
|
return LoadFile(file, false, isReload);
|
|
}
|
|
|
|
bool ConfigMgr::LoadAdditionalFile(std::string file, bool isOptional /*= false*/, bool isReload /*= false*/)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
return LoadFile(file, isOptional, isReload);
|
|
}
|
|
|
|
ConfigMgr* ConfigMgr::instance()
|
|
{
|
|
static ConfigMgr instance;
|
|
return &instance;
|
|
}
|
|
|
|
bool ConfigMgr::Reload()
|
|
{
|
|
if (!LoadAppConfigs(true))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!LoadModulesConfigs(true, false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
OverrideWithEnvVariablesIfAny();
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> ConfigMgr::OverrideWithEnvVariablesIfAny()
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
|
|
std::vector<std::string> overriddenKeys;
|
|
|
|
for (auto& itr : _configOptions)
|
|
{
|
|
if (itr.first.empty())
|
|
continue;
|
|
|
|
Optional<std::string> envVar = EnvVarForIniKey(itr.first);
|
|
if (!envVar)
|
|
continue;
|
|
|
|
itr.second = *envVar;
|
|
|
|
overriddenKeys.push_back(itr.first);
|
|
}
|
|
|
|
return overriddenKeys;
|
|
}
|
|
|
|
template<class T>
|
|
T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLogs /*= true*/) const
|
|
{
|
|
std::string strValue;
|
|
auto const& itr = _configOptions.find(name);
|
|
if (itr == _configOptions.end())
|
|
{
|
|
Optional<std::string> envVar = EnvVarForIniKey(name);
|
|
if (!envVar)
|
|
{
|
|
if (showLogs)
|
|
{
|
|
LOG_ERROR("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file.",
|
|
name, _filename, name, Acore::ToString(def));
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
if (showLogs)
|
|
{
|
|
LOG_WARN("server.loading", "Missing property {} in config file {}, recovered with environment '{}' value.",
|
|
name.c_str(), _filename.c_str(), envVar->c_str());
|
|
}
|
|
|
|
strValue = *envVar;
|
|
}
|
|
else
|
|
{
|
|
strValue = itr->second;
|
|
}
|
|
|
|
auto value = Acore::StringTo<T>(strValue);
|
|
if (!value)
|
|
{
|
|
if (showLogs)
|
|
{
|
|
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead",
|
|
name, Acore::ToString(def));
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
return *value;
|
|
}
|
|
|
|
template<>
|
|
std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std::string const& def, bool showLogs /*= true*/) const
|
|
{
|
|
auto const& itr = _configOptions.find(name);
|
|
if (itr == _configOptions.end())
|
|
{
|
|
Optional<std::string> envVar = EnvVarForIniKey(name);
|
|
if (envVar)
|
|
{
|
|
if (showLogs)
|
|
{
|
|
LOG_WARN("server.loading", "Missing property {} in config file {}, recovered with environment '{}' value.",
|
|
name.c_str(), _filename.c_str(), envVar->c_str());
|
|
}
|
|
|
|
return *envVar;
|
|
}
|
|
|
|
if (showLogs)
|
|
{
|
|
LOG_ERROR("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file.",
|
|
name, _filename, name, def);
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
return itr->second;
|
|
}
|
|
|
|
template<class T>
|
|
T ConfigMgr::GetOption(std::string const& name, T const& def, bool showLogs /*= true*/) const
|
|
{
|
|
return GetValueDefault<T>(name, def, showLogs);
|
|
}
|
|
|
|
template<>
|
|
bool ConfigMgr::GetOption<bool>(std::string const& name, bool const& def, bool showLogs /*= true*/) const
|
|
{
|
|
std::string val = GetValueDefault(name, std::string(def ? "1" : "0"), showLogs);
|
|
|
|
auto boolVal = Acore::StringTo<bool>(val);
|
|
if (!boolVal)
|
|
{
|
|
if (showLogs)
|
|
{
|
|
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead",
|
|
name, def ? "true" : "false");
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
return *boolVal;
|
|
}
|
|
|
|
std::vector<std::string> ConfigMgr::GetKeysByString(std::string const& name)
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
|
|
std::vector<std::string> keys;
|
|
|
|
for (auto const& [optionName, key] : _configOptions)
|
|
{
|
|
if (!optionName.compare(0, name.length(), name))
|
|
{
|
|
keys.emplace_back(optionName);
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
std::string const ConfigMgr::GetFilename()
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
return _filename;
|
|
}
|
|
|
|
std::vector<std::string> const& ConfigMgr::GetArguments() const
|
|
{
|
|
return _args;
|
|
}
|
|
|
|
std::string const ConfigMgr::GetConfigPath()
|
|
{
|
|
std::lock_guard<std::mutex> lock(_configLock);
|
|
|
|
#if AC_PLATFORM == AC_PLATFORM_WINDOWS
|
|
return "configs/";
|
|
#else
|
|
return std::string(_CONF_DIR) + "/";
|
|
#endif
|
|
}
|
|
|
|
void ConfigMgr::Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList /*= {}*/)
|
|
{
|
|
_filename = initFileName;
|
|
_args = std::move(args);
|
|
|
|
// Add modules config if exist
|
|
if (!modulesConfigList.empty())
|
|
{
|
|
for (auto const& itr : Acore::Tokenize(modulesConfigList, ',', false))
|
|
{
|
|
_additonalFiles.emplace_back(itr);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ConfigMgr::LoadAppConfigs(bool isReload /*= false*/)
|
|
{
|
|
// #1 - Load init config file .conf
|
|
if (!LoadInitial(_filename, isReload))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ConfigMgr::LoadModulesConfigs(bool isReload /*= false*/, bool isNeedPrintInfo /*= true*/)
|
|
{
|
|
if (_additonalFiles.empty())
|
|
{
|
|
// Send successful load if no found files
|
|
return true;
|
|
}
|
|
|
|
if (isNeedPrintInfo)
|
|
{
|
|
LOG_INFO("server.loading", " ");
|
|
LOG_INFO("server.loading", "Loading Modules Configuration...");
|
|
}
|
|
|
|
// Start loading module configs
|
|
std::string const& moduleConfigPath = GetConfigPath() + "modules/";
|
|
bool isExistDefaultConfig = true;
|
|
bool isExistDistConfig = true;
|
|
|
|
for (auto const& distFileName : _additonalFiles)
|
|
{
|
|
std::string defaultFileName = distFileName;
|
|
|
|
if (!defaultFileName.empty())
|
|
{
|
|
defaultFileName.erase(defaultFileName.end() - 5, defaultFileName.end());
|
|
}
|
|
|
|
// Load .conf.dist config
|
|
isExistDistConfig = LoadAdditionalFile(moduleConfigPath + distFileName, false, isReload);
|
|
|
|
if (!isReload && !isExistDistConfig)
|
|
{
|
|
LOG_FATAL("server.loading", "> ConfigMgr::LoadModulesConfigs: Not found original config '{}'. Stop loading", distFileName);
|
|
ABORT();
|
|
}
|
|
|
|
// Load .conf config
|
|
isExistDefaultConfig = LoadAdditionalFile(moduleConfigPath + defaultFileName, true, isReload);
|
|
|
|
if (isExistDefaultConfig && isExistDistConfig)
|
|
{
|
|
_moduleConfigFiles.emplace_back(defaultFileName);
|
|
}
|
|
else if (!isExistDefaultConfig && isExistDistConfig)
|
|
{
|
|
_moduleConfigFiles.emplace_back(distFileName);
|
|
}
|
|
}
|
|
|
|
if (isNeedPrintInfo)
|
|
{
|
|
if (!_moduleConfigFiles.empty())
|
|
{
|
|
// Print modules configurations
|
|
LOG_INFO("server.loading", " ");
|
|
LOG_INFO("server.loading", "Using modules configuration:");
|
|
|
|
for (auto const& itr : _moduleConfigFiles)
|
|
{
|
|
LOG_INFO("server.loading", "> {}", itr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG_INFO("server.loading", "> Not found modules config files");
|
|
}
|
|
}
|
|
|
|
if (isNeedPrintInfo)
|
|
{
|
|
LOG_INFO("server.loading", " ");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// @deprecated DO NOT USE - use GetOption<std::string> instead.
|
|
std::string ConfigMgr::GetStringDefault(std::string const& name, const std::string& def, bool showLogs /*= true*/)
|
|
{
|
|
return GetOption<std::string>(name, def, showLogs);
|
|
}
|
|
|
|
/// @deprecated DO NOT USE - use GetOption<bool> instead.
|
|
bool ConfigMgr::GetBoolDefault(std::string const& name, bool def, bool showLogs /*= true*/)
|
|
{
|
|
return GetOption<bool>(name, def, showLogs);
|
|
}
|
|
|
|
/// @deprecated DO NOT USE - use GetOption<int32> instead.
|
|
int ConfigMgr::GetIntDefault(std::string const& name, int def, bool showLogs /*= true*/)
|
|
{
|
|
return GetOption<int32>(name, def, showLogs);
|
|
}
|
|
|
|
/// @deprecated DO NOT USE - use GetOption<float> instead.
|
|
float ConfigMgr::GetFloatDefault(std::string const& name, float def, bool showLogs /*= true*/)
|
|
{
|
|
return GetOption<float>(name, def, showLogs);
|
|
}
|
|
|
|
#define TEMPLATE_CONFIG_OPTION(__typename) \
|
|
template __typename ConfigMgr::GetOption<__typename>(std::string const& name, __typename const& def, bool showLogs /*= true*/) const;
|
|
|
|
TEMPLATE_CONFIG_OPTION(std::string)
|
|
TEMPLATE_CONFIG_OPTION(uint8)
|
|
TEMPLATE_CONFIG_OPTION(int8)
|
|
TEMPLATE_CONFIG_OPTION(uint16)
|
|
TEMPLATE_CONFIG_OPTION(int16)
|
|
TEMPLATE_CONFIG_OPTION(uint32)
|
|
TEMPLATE_CONFIG_OPTION(int32)
|
|
TEMPLATE_CONFIG_OPTION(uint64)
|
|
TEMPLATE_CONFIG_OPTION(int64)
|
|
TEMPLATE_CONFIG_OPTION(float)
|
|
|
|
#undef TEMPLATE_CONFIG_OPTION
|