feat(Config): Implement configuration severity policy and logging mechanism (#23284)

This commit is contained in:
Yehonal
2025-10-25 01:16:09 +02:00
committed by GitHub
parent d58046032b
commit a05cc525f0
23 changed files with 541 additions and 124 deletions

View File

@@ -74,11 +74,16 @@ jobs:
- name: Configure AzerothCore settings - name: Configure AzerothCore settings
run: | run: |
# Create basic configuration touch conf/config.sh
cp conf/dist/config.sh conf/config.sh echo 'MTHREADS=4' >> conf/config.sh
# Configure dashboard echo 'CBUILD_TESTING=ON' >> conf/config.sh
sed -i 's/MTHREADS=.*/MTHREADS="4"/' conf/config.sh echo 'AC_ENABLE_ROOT_CMAKE_INSTALL=1' >> conf/config.sh
sed -i 's/CBUILD_TESTING=.*/CBUILD_TESTING="ON"/' conf/config.sh echo 'export AC_CONFIG_POLICY=$AC_CONFIG_POLICY_PRESET_ZERO_CONF' >> conf/config.sh
echo 'AC_ENABLE_CONF_COPY_ON_INSTALL=0' >> conf/config.sh
cat conf/config.sh
# debug content of AC_CONFIG_POLICY
./acore.sh config show AC_CONFIG_POLICY
- name: Test module commands - name: Test module commands
run: | run: |
@@ -92,8 +97,6 @@ jobs:
./acore.sh module update --all ./acore.sh module update --all
- name: Run complete installation (deps, compile, database, client-data) - name: Run complete installation (deps, compile, database, client-data)
env:
AC_ENABLE_ROOT_CMAKE_INSTALL: 1
run: | run: |
# This runs: install-deps, compile, database setup, client-data download # This runs: install-deps, compile, database setup, client-data download
./acore.sh init ./acore.sh init
@@ -113,12 +116,14 @@ jobs:
- name: Test authserver dry-run - name: Test authserver dry-run
run: | run: |
source ./acore.sh config load
cd env/dist/bin cd env/dist/bin
timeout 5m ./authserver -dry-run timeout 5m ./authserver -dry-run
continue-on-error: false continue-on-error: false
- name: Test worldserver dry-run - name: Test worldserver dry-run
run: | run: |
source ./acore.sh config load
cd env/dist/bin cd env/dist/bin
timeout 5m ./worldserver -dry-run timeout 5m ./worldserver -dry-run
continue-on-error: false continue-on-error: false

View File

@@ -1,6 +1,7 @@
function registerHooks() { acore_event_registerHooks "$@"; } function registerHooks() { acore_event_registerHooks "$@"; }
function runHooks() { acore_event_runHooks "$@"; } function runHooks() { acore_event_runHooks "$@"; }
function acore_common_loadConfig() {
#shellcheck source=../../conf/dist/config.sh #shellcheck source=../../conf/dist/config.sh
source "$AC_PATH_CONF/dist/config.sh" # include dist to avoid missing conf variables source "$AC_PATH_CONF/dist/config.sh" # include dist to avoid missing conf variables
@@ -12,6 +13,7 @@ if [ -f "$USER_CONF_PATH" ]; then
else else
echo "NOTICE: file <$USER_CONF_PATH> not found, we use default configuration only." echo "NOTICE: file <$USER_CONF_PATH> not found, we use default configuration only."
fi fi
}
# #
# Load modules # Load modules

View File

@@ -25,4 +25,6 @@ export AC_PATH_MODULES="$AC_PATH_ROOT/modules"
export AC_PATH_DEPS="$AC_PATH_ROOT/deps" export AC_PATH_DEPS="$AC_PATH_ROOT/deps"
export AC_BASH_LIB_PATH="$AC_PATH_DEPS/acore/bash-lib/src"
export AC_PATH_VAR="$AC_PATH_ROOT/var" export AC_PATH_VAR="$AC_PATH_ROOT/var"

View File

@@ -16,6 +16,8 @@ source "$AC_PATH_DEPS/acore/bash-lib/src/event/hooks.sh"
# shellcheck source=./common.sh # shellcheck source=./common.sh
source "$AC_PATH_SHARED/common.sh" source "$AC_PATH_SHARED/common.sh"
acore_common_loadConfig
if [[ "$OSTYPE" = "msys" ]]; then if [[ "$OSTYPE" = "msys" ]]; then
AC_BINPATH_FULL="$BINPATH" AC_BINPATH_FULL="$BINPATH"
else else

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck source=../../../deps/acore/bash-lib/src/common/boolean.sh
source "$AC_BASH_LIB_PATH/common/boolean.sh"
# Set SUDO variable - one liner # Set SUDO variable - one liner
SUDO="" SUDO=""
@@ -135,7 +138,8 @@ function comp_compile() {
echo "Done" echo "Done"
;; ;;
linux*|darwin*) linux*|darwin*)
local confDir=${CONFDIR:-"$AC_BINPATH_FULL/../etc"} local confDir
confDir=${CONFDIR:-"$AC_BINPATH_FULL/../etc"}
# create the folders before installing to # create the folders before installing to
# set the current user and permissions # set the current user and permissions
@@ -145,6 +149,8 @@ function comp_compile() {
mkdir -p "$confDir" mkdir -p "$confDir"
mkdir -p "$confDir/modules" mkdir -p "$confDir/modules"
confDir=$(realpath "$confDir")
echo "Cmake install..." echo "Cmake install..."
$SUDO cmake --install . --config $CTYPE $SUDO cmake --install . --config $CTYPE
@@ -161,18 +167,25 @@ function comp_compile() {
$SUDO setcap cap_sys_nice=eip "$AC_BINPATH_FULL/authserver" $SUDO setcap cap_sys_nice=eip "$AC_BINPATH_FULL/authserver"
fi fi
[[ -f "$confDir/worldserver.conf.dist" ]] && \
cp -v --no-clobber "$confDir/worldserver.conf.dist" "$confDir/worldserver.conf" if ( isTrue "$AC_ENABLE_CONF_COPY_ON_INSTALL" ) then
[[ -f "$confDir/authserver.conf.dist" ]] && \ echo "Copying default configuration files to $confDir ..."
cp -v --no-clobber "$confDir/authserver.conf.dist" "$confDir/authserver.conf" [[ -f "$confDir/worldserver.conf.dist" && ! -f "$confDir/worldserver.conf" ]] && \
[[ -f "$confDir/dbimport.conf.dist" ]] && \ cp -v "$confDir/worldserver.conf.dist" "$confDir/worldserver.conf"
cp -v --no-clobber "$confDir/dbimport.conf.dist" "$confDir/dbimport.conf" [[ -f "$confDir/authserver.conf.dist" && ! -f "$confDir/authserver.conf" ]] && \
cp -v "$confDir/authserver.conf.dist" "$confDir/authserver.conf"
[[ -f "$confDir/dbimport.conf.dist" && ! -f "$confDir/dbimport.conf" ]] && \
cp -v "$confDir/dbimport.conf.dist" "$confDir/dbimport.conf"
for f in "$confDir/modules/"*.dist for f in "$confDir/modules/"*.dist
do do
[[ -e $f ]] || break # handle the case of no *.dist files [[ -e $f ]] || break # handle the case of no *.dist files
cp -v --no-clobber "$f" "${f%.dist}"; if [[ ! -f "${f%.dist}" ]]; then
echo "Copying module config $(basename "${f%.dist}")"
cp -v "$f" "${f%.dist}";
fi
done done
fi
echo "Done" echo "Done"
;; ;;

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd )
# shellcheck source=./config.sh
source "$CURRENT_PATH/config.sh"
acore_dash_config "$@"

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd )
# shellcheck source=../../../bash_shared/includes.sh
source "$CURRENT_PATH/../../../bash_shared/includes.sh"
# shellcheck source=../includes.sh
source "$CURRENT_PATH/../includes.sh"
# shellcheck source=../../../bash_shared/menu_system.sh
source "$AC_PATH_APPS/bash_shared/menu_system.sh"
function acore_dash_configShowValue() {
if [ $# -ne 1 ]; then
echo "Usage: show <VAR_NAME>"
return 1
fi
local varName="$1"
local varValue="${!varName}"
if [ -z "$varValue" ]; then
echo "$varName is not set."
else
echo "$varName=$varValue"
fi
}
function acore_dash_configLoad() {
acore_common_loadConfig
echo "Configuration loaded into the current shell session."
}
# Configuration management menu definition
# Format: "key|short|description"
config_menu_items=(
"show|s|Show configuration variable value"
"load|l|Load configurations variables within the current shell session"
"help|h|Show detailed help"
"quit|q|Close this menu"
)
# Menu command handler for configuration operations
function handle_config_command() {
local key="$1"
shift
case "$key" in
"show")
acore_dash_configShowValue "$@"
;;
"load")
acore_dash_configLoad
;;
esac
}
function acore_dash_config() {
menu_run_with_items "CONFIG MANAGER" handle_config_command -- "${config_menu_items[@]}" -- "$@"
return $?
}

View File

@@ -183,3 +183,5 @@ function inst_download_client_data {
&& echo "Remove downloaded file" && rm "$zipPath" \ && echo "Remove downloaded file" && rm "$zipPath" \
&& echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile" && echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile"
} }

View File

@@ -2,6 +2,7 @@
CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd ) CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd )
# shellcheck source=../../bash_shared/includes.sh
source "$CURRENT_PATH/../../bash_shared/includes.sh" source "$CURRENT_PATH/../../bash_shared/includes.sh"
AC_PATH_INSTALLER="$AC_PATH_APPS/installer" AC_PATH_INSTALLER="$AC_PATH_APPS/installer"
@@ -9,14 +10,14 @@ AC_PATH_INSTALLER="$AC_PATH_APPS/installer"
J_PATH="$AC_PATH_DEPS/acore/joiner" J_PATH="$AC_PATH_DEPS/acore/joiner"
J_PATH_MODULES="$AC_PATH_MODULES" J_PATH_MODULES="$AC_PATH_MODULES"
# shellcheck source=../../../deps/acore/joiner/joiner.sh
source "$J_PATH/joiner.sh" source "$J_PATH/joiner.sh"
if [ -f "$AC_PATH_INSTALLER/config.sh" ]; then # shellcheck source=../../compiler/includes/includes.sh
source "$AC_PATH_INSTALLER/config.sh" # should overwrite previous
fi
source "$AC_PATH_APPS/compiler/includes/includes.sh" source "$AC_PATH_APPS/compiler/includes/includes.sh"
# shellcheck source=../../../deps/semver_bash/semver.sh
source "$AC_PATH_DEPS/semver_bash/semver.sh" source "$AC_PATH_DEPS/semver_bash/semver.sh"
# shellcheck source=../includes/functions.sh
source "$AC_PATH_INSTALLER/includes/functions.sh" source "$AC_PATH_INSTALLER/includes/functions.sh"

View File

@@ -59,7 +59,6 @@ else
C_GREEN='' C_GREEN=''
C_YELLOW='' C_YELLOW=''
C_BLUE='' C_BLUE=''
C_MAGENTA=''
C_CYAN='' C_CYAN=''
fi fi
@@ -174,42 +173,8 @@ function inst_module_list() {
# Usage: ./acore.sh module <search|install|update|remove> [args...] # Usage: ./acore.sh module <search|install|update|remove> [args...]
# ./acore.sh module # Interactive menu # ./acore.sh module # Interactive menu
function inst_module() { function inst_module() {
# If no arguments provided, start interactive menu menu_run_with_items "MODULE MANAGER" handle_module_command -- "${module_menu_items[@]}" -- "$@"
if [[ $# -eq 0 ]]; then
menu_run_with_items "MODULE MANAGER" handle_module_command -- "${module_menu_items[@]}" --
return $? return $?
fi
# Normalize arguments into an array
local tokens=()
read -r -a tokens <<< "$*"
local cmd="${tokens[0]}"
local args=("${tokens[@]:1}")
case "$cmd" in
""|"help"|"-h"|"--help")
inst_module_help
;;
"search"|"s")
inst_module_search "${args[@]}"
;;
"install"|"i")
inst_module_install "${args[@]}"
;;
"update"|"u")
inst_module_update "${args[@]}"
;;
"remove"|"r")
inst_module_remove "${args[@]}"
;;
"list"|"l")
inst_module_list "${args[@]}"
;;
*)
print_error "Unknown module command: $cmd. Use 'help' to see available commands."
return 1
;;
esac
} }
# ============================================================================= # =============================================================================

View File

@@ -45,6 +45,7 @@ menu_items=(
"docker|dr|Run docker tools" "docker|dr|Run docker tools"
"version|v|Show AzerothCore version" "version|v|Show AzerothCore version"
"service-manager|sm|Run service manager to run authserver and worldserver in background" "service-manager|sm|Run service manager to run authserver and worldserver in background"
"config|cf|Configuration manager"
"quit|q|Exit from this menu" "quit|q|Exit from this menu"
) )
@@ -100,6 +101,9 @@ function handle_menu_command() {
bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "$@" bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "$@"
exit exit
;; ;;
"config")
bash "$AC_PATH_APPS/installer/includes/config/config-main.sh" "$@"
;;
"quit") "quit")
echo "Goodbye!" echo "Goodbye!"
exit exit

View File

@@ -751,5 +751,5 @@ EOF
run inst_module "unknown-command" run inst_module "unknown-command"
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
[[ "$output" =~ "Unknown module command" ]] [[ "$output" =~ "Invalid option" ]]
} }

View File

@@ -51,6 +51,8 @@ fi
while true; do while true; do
STARTING_TIME=$(date +%s) STARTING_TIME=$(date +%s)
echo "AC_CONFIG_POLICY: $AC_CONFIG_POLICY"
# Use starter script to launch the binary with all parameters # Use starter script to launch the binary with all parameters
"$STARTER_SCRIPT" "$BINPATH" "$BINFILE" "$GDB_FILE" "$CONFIG" "$SYSLOG" "$SYSERR" "$GDB_ENABLED" "$CRASHES_PATH" "$STARTER_SCRIPT" "$BINPATH" "$BINFILE" "$GDB_FILE" "$CONFIG" "$SYSLOG" "$SYSERR" "$GDB_ENABLED" "$CRASHES_PATH"

36
conf/dist/config.sh vendored
View File

@@ -118,6 +118,12 @@ export CCACHE_DIR=${CCACHE_DIR:-"$AC_PATH_VAR/ccache"}
# #
export AC_ENABLE_ROOT_CMAKE_INSTALL=${AC_ENABLE_ROOT_CMAKE_INSTALL:-0} export AC_ENABLE_ROOT_CMAKE_INSTALL=${AC_ENABLE_ROOT_CMAKE_INSTALL:-0}
#
# Enable copying configuration files on install
# Default: 1 (true)
#
export AC_ENABLE_CONF_COPY_ON_INSTALL=${AC_ENABLE_CONF_COPY_ON_INSTALL:-1}
############################################## ##############################################
# #
# GOOGLE PERF TOOLS # GOOGLE PERF TOOLS
@@ -182,4 +188,34 @@ export MODULES_EXCLUDE_LIST=""
NO_COLOR=${NO_COLOR:-} NO_COLOR=${NO_COLOR:-}
FORCE_COLOR=${FORCE_COLOR:-} FORCE_COLOR=${FORCE_COLOR:-}
##############################################
#
# CONFIGURATION SEVERITY POLICY
#
# Controls how the core reacts to missing configuration files,
# missing/unknown options and invalid values.
# The policy string follows the format "key=severity" separated by commas.
# Supported severities: skip, warn, error, fatal.
# Possible keys: default, missing_file, missing_option, critical_option,
# unknown_option, value_error.
#
# Examples:
# export AC_CONFIG_POLICY="$AC_CONFIG_POLICY_PRESET_DEFAULT"
# export AC_CONFIG_POLICY="default=skip,critical_option=fatal,unknown_option=warn"
# export AC_CONFIG_POLICY="missing_file=fatal,missing_option=error"
#
# Presets:
# AC_CONFIG_POLICY_PRESET_DEFAULT -> mirrors the core default behaviour
# (errors on missing files, fatal on critical)
# AC_CONFIG_POLICY_PRESET_ZERO_CONF -> skips non-critical gaps so the core
# can boot from environment defaults
# AC_CONFIG_POLICY_PRESET_STRICT -> escalates everything to errors/fatals
#
export AC_CONFIG_POLICY_PRESET_ZERO_CONF='default=skip'
export AC_CONFIG_POLICY_PRESET_DEFAULT='missing_file=error,missing_option=warn,critical_option=fatal,unknown_option=error,value_error=error'
export AC_CONFIG_POLICY_PRESET_STRICT='default=error,missing_file=fatal,missing_option=error,critical_option=fatal,unknown_option=error,value_error=error'
export AC_CONFIG_POLICY=$AC_CONFIG_POLICY_PRESET_DEFAULT

View File

@@ -0,0 +1,5 @@
function isTrue() {
local val
val=$(echo "$1" | tr '[:upper:]' '[:lower:]')
[[ "$val" == "1" || "$val" == "true" || "$val" == "yes" || "$val" == "on" ]]
}

101
doc/ConfigPolicy.md Normal file
View File

@@ -0,0 +1,101 @@
# Configuration Severity Policy
The configuration loader can decide how strictly it should react when it
encounters missing files, undefined options or invalid values. This document
describes the available knobs and provides ready-to-use presets.
## Severity Levels
Each policy entry maps a **key** to one of the following severities:
| Severity | Description |
|----------|-----------------------------------------------------------------------------|
| `skip` | Ignore the problem and continue silently. |
| `warn` | Log a warning and continue. |
| `error` | Log an error and continue (useful to surface issues without aborting). |
| `fatal` | Log a fatal message and abort the process immediately. |
## Policy Keys
The following keys can be customised:
| Key | Applies to |
|--------------------|----------------------------------------------------------------------|
| `default` | Fallback severity for any key that is not explicitly overridden. |
| `missing_file` | Missing or empty configuration files (worldserver.conf, modules, …). |
| `missing_option` | Options looked up in code but not present in any config file. |
| `critical_option` | Required options (`RealmID`, `*DatabaseInfo`, …). |
| `unknown_option` | Options found in optional configs that the core does not recognise. |
| `value_error` | Options that cannot be converted to the expected type. |
> Critical options remain fatal by default to prevent the core from booting with
> incomplete database details; you can relax them if required.
## Configuration Channels
### `config.sh`
`conf/dist/config.sh` exposes the `AC_CONFIG_POLICY` variable alongside a few
presets:
```bash
# Mirrors the default behaviour (errors, with fatal criticals)
export AC_CONFIG_POLICY="$AC_CONFIG_POLICY_PRESET_DEFAULT"
# Skip anything non-critical so the core can bootstrap from defaults + env vars
export AC_CONFIG_POLICY="$AC_CONFIG_POLICY_PRESET_ZERO_CONF"
# Treat everything strictly (useful for CI)
export AC_CONFIG_POLICY="$AC_CONFIG_POLICY_PRESET_STRICT"
```
The presets are defined as:
```bash
AC_CONFIG_POLICY_PRESET_DEFAULT='missing_file=error,missing_option=warn,critical_option=fatal,unknown_option=error,value_error=error'
AC_CONFIG_POLICY_PRESET_ZERO_CONF='default=skip,critical_option=fatal,unknown_option=warn,value_error=warn'
AC_CONFIG_POLICY_PRESET_STRICT='default=error,missing_file=fatal,missing_option=error,critical_option=fatal,unknown_option=error,value_error=error'
```
Modify or extend these entries to suit your deployment.
### Environment Variable
The runtime honours the `AC_CONFIG_POLICY` environment variable, so you can
override the policy without editing `config.sh`:
```bash
export AC_CONFIG_POLICY="default=skip,critical_option=fatal"
./acore.sh run-worldserver
```
### CLI Override
Every server/tool executable accepts `--config-policy`:
```bash
./bin/worldserver --config-policy="missing_file=fatal,unknown_option=warn"
./bin/authserver --config-policy "$AC_CONFIG_POLICY_PRESET_STRICT"
```
The CLI flag takes precedence over the environment and `config.sh`.
## Quick Presets
| Preset | Intended use |
|---------------|---------------------------------------------------------------------------|
| `legacy` | Default behaviour before this feature (errors for missing files/options). |
| `zero-conf` | Zero-touch deployments; rely on defaults/env vars where possible. |
| `strict` | Fail-fast in CI or controlled environments. |
Feel free to clone these presets and store your own variants inside
`config.sh` or deployment scripts.
## Tips
- Pair `fatal` severities with monitoring so regressions in configuration
surface quickly.
- When experimenting locally, start with `zero-conf` and elevate specific keys
to `error`/`fatal` as you validate your setup.
- Remember that number parsing errors (`value_error`) often indicate typos;
keep them at least `error` unless you have a very good reason.

View File

@@ -21,10 +21,14 @@
#include "StringFormat.h" #include "StringFormat.h"
#include "Tokenize.h" #include "Tokenize.h"
#include "Util.h" #include "Util.h"
#include <algorithm>
#include <cctype>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <locale>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
namespace namespace
{ {
@@ -34,13 +38,14 @@ namespace
std::unordered_map<std::string /*name*/, std::string /*value*/> _configOptions; std::unordered_map<std::string /*name*/, std::string /*value*/> _configOptions;
std::unordered_map<std::string /*name*/, std::string /*value*/> _envVarCache; std::unordered_map<std::string /*name*/, std::string /*value*/> _envVarCache;
std::mutex _configLock; std::mutex _configLock;
ConfigPolicy _policy;
std::vector<std::string> _fatalConfigOptions = std::unordered_set<std::string> _criticalConfigOptions =
{ {
{ "RealmID" }, "RealmID",
{ "LoginDatabaseInfo" }, "LoginDatabaseInfo",
{ "WorldDatabaseInfo" }, "WorldDatabaseInfo",
{ "CharacterDatabaseInfo" }, "CharacterDatabaseInfo",
}; };
// Check system configs like *server.conf* // Check system configs like *server.conf*
@@ -62,6 +67,29 @@ namespace
return foundAppender != std::string_view::npos || foundLogger != std::string_view::npos; return foundAppender != std::string_view::npos || foundLogger != std::string_view::npos;
} }
Optional<ConfigSeverity> ParseSeverity(std::string_view value)
{
if (value.empty())
return std::nullopt;
std::string lowered(value);
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return std::tolower(c); });
if (lowered == "skip")
return ConfigSeverity::Skip;
if (lowered == "warn" || lowered == "warning")
return ConfigSeverity::Warn;
if (lowered == "error")
return ConfigSeverity::Error;
if (lowered == "fatal" || lowered == "abort" || lowered == "panic")
return ConfigSeverity::Fatal;
return std::nullopt;
}
template<typename Format, typename... Args> template<typename Format, typename... Args>
inline void PrintError(std::string_view filename, Format&& fmt, Args&& ... args) inline void PrintError(std::string_view filename, Format&& fmt, Args&& ... args)
{ {
@@ -77,6 +105,138 @@ namespace
} }
} }
template<typename Format, typename... Args>
inline void LogWithSeverity(ConfigSeverity severity, std::string_view filename, Format&& fmt, Args&&... args)
{
std::string message = Acore::StringFormat(std::forward<Format>(fmt), std::forward<Args>(args)...);
switch (severity)
{
case ConfigSeverity::Skip:
return;
case ConfigSeverity::Warn:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_WARN("server.loading", message);
return;
}
case ConfigSeverity::Error:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_ERROR("server.loading", message);
return;
}
case ConfigSeverity::Fatal:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_FATAL("server.loading", message);
ABORT(message);
}
}
}
ConfigPolicy ApplyPolicyString(ConfigPolicy policy, std::string_view input)
{
if (input.empty())
return policy;
std::vector<std::pair<std::string, ConfigSeverity>> overrides;
Optional<ConfigSeverity> defaultOverride;
std::string tokenBuffer(input);
for (std::string_view rawToken : Acore::Tokenize(tokenBuffer, ',', false))
{
std::string token = Acore::String::Trim(std::string(rawToken), std::locale());
if (token.empty())
continue;
auto separator = token.find('=');
if (separator == std::string::npos)
continue;
std::string key = Acore::String::Trim(token.substr(0, separator), std::locale());
std::string value = Acore::String::Trim(token.substr(separator + 1), std::locale());
if (key.empty() || value.empty())
continue;
auto severity = ParseSeverity(value);
if (!severity)
continue;
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return std::tolower(c); });
if (key == "default")
{
defaultOverride = severity;
continue;
}
overrides.emplace_back(std::move(key), *severity);
}
if (defaultOverride)
{
policy.defaultSeverity = *defaultOverride;
policy.missingFileSeverity = *defaultOverride;
policy.missingOptionSeverity = *defaultOverride;
policy.criticalOptionSeverity = *defaultOverride;
policy.unknownOptionSeverity = *defaultOverride;
policy.valueErrorSeverity = *defaultOverride;
}
for (auto const& [key, severity] : overrides)
{
if (key == "missing_file" || key == "file")
policy.missingFileSeverity = severity;
else if (key == "missing_option" || key == "option")
policy.missingOptionSeverity = severity;
else if (key == "critical_option" || key == "critical")
policy.criticalOptionSeverity = severity;
else if (key == "unknown_option" || key == "unknown")
policy.unknownOptionSeverity = severity;
else if (key == "value_error" || key == "value")
policy.valueErrorSeverity = severity;
}
return policy;
}
ConfigPolicy ApplyPolicyFromArgs(ConfigPolicy policy, std::vector<std::string> const& args)
{
for (std::size_t i = 0; i < args.size(); ++i)
{
std::string const& arg = args[i];
std::string_view value;
constexpr std::string_view shortOpt = "--config-policy";
if (arg.rfind(shortOpt, 0) == 0)
{
if (arg.size() == shortOpt.size() && (i + 1) < args.size())
{
value = args[i + 1];
++i;
}
else if (arg.size() > shortOpt.size() && arg[shortOpt.size()] == '=')
{
value = std::string_view(arg).substr(shortOpt.size() + 1);
}
if (!value.empty())
policy = ApplyPolicyString(policy, value);
}
}
return policy;
}
void AddKey(std::string const& optionName, std::string const& optionKey, std::string_view fileName, bool isOptional, [[maybe_unused]] bool isReload) 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); auto const& itr = _configOptions.find(optionName);
@@ -86,7 +246,7 @@ namespace
{ {
if (!IsLoggingSystemOptions(optionName) && !isReload) if (!IsLoggingSystemOptions(optionName) && !isReload)
{ {
PrintError(fileName, "> Config::LoadFile: Found incorrect option '{}' in config file '{}'. Skip", optionName, fileName); LogWithSeverity(_policy.unknownOptionSeverity, fileName, "> Config::LoadFile: Found incorrect option '{}' in config file '{}'. Skip", optionName, fileName);
#ifdef CONFIG_ABORT_INCORRECT_OPTIONS #ifdef CONFIG_ABORT_INCORRECT_OPTIONS
ABORT("> Core can't start if found incorrect options"); ABORT("> Core can't start if found incorrect options");
@@ -111,13 +271,10 @@ namespace
if (in.fail()) if (in.fail())
{ {
if (isOptional) ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity;
{ LogWithSeverity(severity, file, "> Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file);
// No display erorr if file optional // Treat SKIP as a successful no-op so the app can proceed
return false; return severity == ConfigSeverity::Skip;
}
throw ConfigException(Acore::StringFormat("Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file));
} }
uint32 count = 0; uint32 count = 0;
@@ -181,13 +338,10 @@ namespace
// No lines read // No lines read
if (!count) if (!count)
{ {
if (isOptional) ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity;
{ LogWithSeverity(severity, file, "> Config::LoadFile: Empty file '{}'", file);
// No display erorr if file optional // Treat SKIP as a successful no-op
return false; return severity == ConfigSeverity::Skip;
}
throw ConfigException(Acore::StringFormat("Config::LoadFile: Empty file '{}'", file));
} }
// Add correct keys if file load without errors // Add correct keys if file load without errors
@@ -382,7 +536,6 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
std::string strValue; std::string strValue;
auto const& itr = _configOptions.find(name); auto const& itr = _configOptions.find(name);
bool fatalConfig = false;
bool notFound = itr == _configOptions.end(); bool notFound = itr == _configOptions.end();
auto envVarName = GetEnvVarName(name); auto envVarName = GetEnvVarName(name);
Optional<std::string> envVar = GetEnvFromCache(name, envVarName); Optional<std::string> envVar = GetEnvFromCache(name, envVarName);
@@ -401,23 +554,23 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
{ {
if (showLogs) if (showLogs)
{ {
for (std::string s : _fatalConfigOptions) bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end();
if (s == name) ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity;
{
fatalConfig = true;
break;
}
if (fatalConfig) if (isCritical)
LOG_FATAL("server.loading", "> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable\n\nYour server cannot start without this option!", {
LogWithSeverity(severity, _filename,
"> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable\n\nYour server cannot start without this option!",
name, _filename, name, Acore::ToString(def), envVarName); name, _filename, name, Acore::ToString(def), envVarName);
}
else else
{ {
std::string configs = _filename; std::string configs = _filename;
if (!_moduleConfigFiles.empty()) if (!_moduleConfigFiles.empty())
configs += " or module config"; configs += " or module config";
LOG_WARN("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.", LogWithSeverity(severity, _filename,
"> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
name, configs, name, def, envVarName); name, configs, name, def, envVarName);
} }
} }
@@ -433,7 +586,8 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
{ {
if (showLogs) if (showLogs)
{ {
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead", LogWithSeverity(_policy.valueErrorSeverity, _filename,
"> Config: Bad value defined for name '{}', going to use '{}' instead",
name, Acore::ToString(def)); name, Acore::ToString(def));
} }
@@ -447,7 +601,6 @@ template<>
std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std::string const& def, bool showLogs /*= true*/) const std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std::string const& def, bool showLogs /*= true*/) const
{ {
auto const& itr = _configOptions.find(name); auto const& itr = _configOptions.find(name);
bool fatalConfig = false;
bool notFound = itr == _configOptions.end(); bool notFound = itr == _configOptions.end();
auto envVarName = GetEnvVarName(name); auto envVarName = GetEnvVarName(name);
Optional<std::string> envVar = GetEnvFromCache(name, envVarName); Optional<std::string> envVar = GetEnvFromCache(name, envVarName);
@@ -466,23 +619,23 @@ std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std
{ {
if (showLogs) if (showLogs)
{ {
for (std::string s : _fatalConfigOptions) bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end();
if (s == name) ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity;
{
fatalConfig = true;
break;
}
if (fatalConfig) if (isCritical)
LOG_FATAL("server.loading", "> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.\n\nYour server cannot start without this option!", {
LogWithSeverity(severity, _filename,
"> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.\n\nYour server cannot start without this option!",
name, _filename, name, def, envVarName); name, _filename, name, def, envVarName);
}
else else
{ {
std::string configs = _filename; std::string configs = _filename;
if (!_moduleConfigFiles.empty()) if (!_moduleConfigFiles.empty())
configs += " or module config"; configs += " or module config";
LOG_WARN("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.", LogWithSeverity(severity, _filename,
"> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
name, configs, name, def, envVarName); name, configs, name, def, envVarName);
} }
} }
@@ -509,7 +662,8 @@ bool ConfigMgr::GetOption<bool>(std::string const& name, bool const& def, bool s
{ {
if (showLogs) if (showLogs)
{ {
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead", LogWithSeverity(_policy.valueErrorSeverity, _filename,
"> Config: Bad value defined for name '{}', going to use '{}' instead",
name, def ? "true" : "false"); name, def ? "true" : "false");
} }
@@ -558,16 +712,26 @@ std::string const ConfigMgr::GetConfigPath()
#endif #endif
} }
void ConfigMgr::Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList /*= {}*/) void ConfigMgr::Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList /*= {}*/, ConfigPolicy policy /*= {}*/)
{ {
_filename = initFileName; _filename = initFileName;
_args = std::move(args); _args = std::move(args);
_policy = policy;
if (char const* env = std::getenv("AC_CONFIG_POLICY"))
_policy = ApplyPolicyString(_policy, env);
_policy = ApplyPolicyFromArgs(_policy, _args);
_additonalFiles.clear();
_moduleConfigFiles.clear();
// Add modules config if exist // Add modules config if exist
if (!modulesConfigList.empty()) if (!modulesConfigList.empty())
{ {
for (auto const& itr : Acore::Tokenize(modulesConfigList, ',', false)) for (auto const& itr : Acore::Tokenize(modulesConfigList, ',', false))
{ {
if (!itr.empty())
_additonalFiles.emplace_back(itr); _additonalFiles.emplace_back(itr);
} }
} }

View File

@@ -18,10 +18,29 @@
#ifndef CONFIG_H #ifndef CONFIG_H
#define CONFIG_H #define CONFIG_H
#include <cstdint>
#include <stdexcept> #include <stdexcept>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
enum class ConfigSeverity : uint8_t
{
Skip,
Warn,
Error,
Fatal
};
struct ConfigPolicy
{
ConfigSeverity defaultSeverity = ConfigSeverity::Warn;
ConfigSeverity missingFileSeverity = ConfigSeverity::Error;
ConfigSeverity missingOptionSeverity = ConfigSeverity::Warn;
ConfigSeverity criticalOptionSeverity = ConfigSeverity::Fatal;
ConfigSeverity unknownOptionSeverity = ConfigSeverity::Error;
ConfigSeverity valueErrorSeverity = ConfigSeverity::Error;
};
class ConfigMgr class ConfigMgr
{ {
ConfigMgr() = default; ConfigMgr() = default;
@@ -32,7 +51,7 @@ class ConfigMgr
public: public:
bool LoadAppConfigs(bool isReload = false); bool LoadAppConfigs(bool isReload = false);
bool LoadModulesConfigs(bool isReload = false, bool isNeedPrintInfo = true); bool LoadModulesConfigs(bool isReload = false, bool isNeedPrintInfo = true);
void Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList = {}); void Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList = {}, ConfigPolicy policy = {});
static ConfigMgr* instance(); static ConfigMgr* instance();

View File

@@ -211,13 +211,16 @@ void Log::ReadLoggersFromConfig()
AppenderConsole* appender = new AppenderConsole(NextAppenderId(), "Console", LOG_LEVEL_DEBUG, APPENDER_FLAGS_NONE, {}); AppenderConsole* appender = new AppenderConsole(NextAppenderId(), "Console", LOG_LEVEL_DEBUG, APPENDER_FLAGS_NONE, {});
appenders[appender->getId()].reset(appender); appenders[appender->getId()].reset(appender);
Logger* rootLogger = new Logger(LOGGER_ROOT, LOG_LEVEL_ERROR); Logger* rootLogger = new Logger(LOGGER_ROOT, LOG_LEVEL_WARN);
rootLogger->addAppender(appender->getId(), appender); rootLogger->addAppender(appender->getId(), appender);
loggers[LOGGER_ROOT].reset(rootLogger); loggers[LOGGER_ROOT].reset(rootLogger);
Logger* serverLogger = new Logger("server", LOG_LEVEL_INFO); Logger* serverLogger = new Logger("server", LOG_LEVEL_INFO);
serverLogger->addAppender(appender->getId(), appender); serverLogger->addAppender(appender->getId(), appender);
loggers["server"].reset(serverLogger); loggers["server"].reset(serverLogger);
highestLogLevel = LOG_LEVEL_INFO;
return;
} }
} }

View File

@@ -278,7 +278,8 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile)
("help,h", "print usage message") ("help,h", "print usage message")
("version,v", "print version build info") ("version,v", "print version build info")
("dry-run,d", "Dry run") ("dry-run,d", "Dry run")
("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_REALM_CONFIG))), "use <arg> as configuration file"); ("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_REALM_CONFIG))), "use <arg> as configuration file")
("config-policy", value<std::string>()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)");
variables_map variablesMap; variables_map variablesMap;

View File

@@ -423,7 +423,7 @@ bool StartDB()
MySQL::Library_Init(); MySQL::Library_Init();
// Load databases // Load databases
DatabaseLoader loader("server.worldserver", DatabaseLoader::DATABASE_NONE, AC_MODULES_LIST); DatabaseLoader loader("server.worldserver", DatabaseLoader::DATABASE_MASK_ALL, AC_MODULES_LIST);
loader loader
.AddDatabase(LoginDatabase, "Login") .AddDatabase(LoginDatabase, "Login")
.AddDatabase(CharacterDatabase, "Character") .AddDatabase(CharacterDatabase, "Character")
@@ -433,7 +433,7 @@ bool StartDB()
return false; return false;
///- Get the realm Id from the configuration file ///- Get the realm Id from the configuration file
realm.Id.Realm = sConfigMgr->GetOption<uint32>("RealmID", 0); realm.Id.Realm = sConfigMgr->GetOption<uint32>("RealmID", 1);
if (!realm.Id.Realm) if (!realm.Id.Realm)
{ {
LOG_ERROR("server.worldserver", "Realm ID not defined in configuration file"); LOG_ERROR("server.worldserver", "Realm ID not defined in configuration file");
@@ -710,7 +710,8 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, [
("help,h", "print usage message") ("help,h", "print usage message")
("version,v", "print version build info") ("version,v", "print version build info")
("dry-run,d", "Dry run") ("dry-run,d", "Dry run")
("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG))), "use <arg> as configuration file"); ("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG))), "use <arg> as configuration file")
("config-policy", value<std::string>()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)");
#if AC_PLATFORM == AC_PLATFORM_WINDOWS #if AC_PLATFORM == AC_PLATFORM_WINDOWS
options_description win("Windows platform specific options"); options_description win("Windows platform specific options");

View File

@@ -24,6 +24,24 @@
#include <errmsg.h> #include <errmsg.h>
#include <mysqld_error.h> #include <mysqld_error.h>
#include <thread> #include <thread>
#include <string_view>
namespace
{
std::string const EMPTY_DATABASE_INFO;
std::string const LOGIN_DATABASE_INFO_DEFAULT = "127.0.0.1;3306;acore;acore;acore_auth";
std::string const WORLD_DATABASE_INFO_DEFAULT = "127.0.0.1;3306;acore;acore;acore_world";
std::string const CHARACTER_DATABASE_INFO_DEFAULT = "127.0.0.1;3306;acore;acore;acore_characters";
std::string const& GetDefaultDatabaseInfo(std::string_view name)
{
if (name == "Login")
return LOGIN_DATABASE_INFO_DEFAULT;
if (name == "World")
return WORLD_DATABASE_INFO_DEFAULT;
if (name == "Character")
return CHARACTER_DATABASE_INFO_DEFAULT;
return EMPTY_DATABASE_INFO;
}
}
DatabaseLoader::DatabaseLoader(std::string const& logger, uint32 const defaultUpdateMask, std::string_view modulesList) DatabaseLoader::DatabaseLoader(std::string const& logger, uint32 const defaultUpdateMask, std::string_view modulesList)
: _logger(logger), : _logger(logger),
@@ -38,7 +56,8 @@ DatabaseLoader& DatabaseLoader::AddDatabase(DatabaseWorkerPool<T>& pool, std::st
_open.push([this, name, updatesEnabledForThis, &pool]() -> bool _open.push([this, name, updatesEnabledForThis, &pool]() -> bool
{ {
std::string const dbString = sConfigMgr->GetOption<std::string>(name + "DatabaseInfo", ""); std::string const& defaultDatabaseInfo = GetDefaultDatabaseInfo(name);
std::string const dbString = sConfigMgr->GetOption<std::string>(name + "DatabaseInfo", defaultDatabaseInfo);
if (dbString.empty()) if (dbString.empty())
{ {
LOG_ERROR(_logger, "Database {} not specified in configuration file!", name); LOG_ERROR(_logger, "Database {} not specified in configuration file!", name);

View File

@@ -109,8 +109,8 @@ bool StartDB()
DatabaseLoader loader = DatabaseLoader loader =
modules.empty() ? DatabaseLoader("dbimport") : modules.empty() ? DatabaseLoader("dbimport") :
(modules == "all") ? DatabaseLoader("dbimport", DatabaseLoader::DATABASE_NONE, AC_MODULES_LIST) : (modules == "all") ? DatabaseLoader("dbimport", DatabaseLoader::DATABASE_MASK_ALL, AC_MODULES_LIST) :
DatabaseLoader("dbimport", DatabaseLoader::DATABASE_NONE, modules); DatabaseLoader("dbimport", DatabaseLoader::DATABASE_MASK_ALL, modules);
loader loader
.AddDatabase(LoginDatabase, "Login") .AddDatabase(LoginDatabase, "Login")
@@ -140,7 +140,8 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile)
("help,h", "print usage message") ("help,h", "print usage message")
("version,v", "print version build info") ("version,v", "print version build info")
("dry-run,d", "Dry run") ("dry-run,d", "Dry run")
("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_DB_IMPORT_CONFIG))), "use <arg> as configuration file"); ("config,c", value<fs::path>(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_DB_IMPORT_CONFIG))), "use <arg> as configuration file")
("config-policy", value<std::string>()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)");
variables_map variablesMap; variables_map variablesMap;