From a05cc525f03e12be8da9a9cba08ed62f4b8c42d4 Mon Sep 17 00:00:00 2001 From: Yehonal Date: Sat, 25 Oct 2025 01:16:09 +0200 Subject: [PATCH] feat(Config): Implement configuration severity policy and logging mechanism (#23284) --- .github/workflows/dashboard-ci.yml | 19 +- apps/bash_shared/common.sh | 20 +- apps/bash_shared/defines.sh | 2 + apps/bash_shared/includes.sh | 2 + apps/compiler/includes/functions.sh | 37 ++- apps/installer/includes/config/config-main.sh | 9 + apps/installer/includes/config/config.sh | 60 +++++ apps/installer/includes/functions.sh | 2 + apps/installer/includes/includes.sh | 9 +- .../includes/modules-manager/modules.sh | 39 +-- apps/installer/main.sh | 4 + apps/installer/test/test_module_commands.bats | 2 +- apps/startup-scripts/src/simple-restarter | 2 + conf/dist/config.sh | 36 +++ deps/acore/bash-lib/src/common/boolean.sh | 5 + doc/ConfigPolicy.md | 101 +++++++ src/common/Configuration/Config.cpp | 252 +++++++++++++++--- src/common/Configuration/Config.h | 21 +- src/common/Logging/Log.cpp | 5 +- src/server/apps/authserver/Main.cpp | 3 +- src/server/apps/worldserver/Main.cpp | 7 +- .../database/Database/DatabaseLoader.cpp | 21 +- src/tools/dbimport/Main.cpp | 7 +- 23 files changed, 541 insertions(+), 124 deletions(-) create mode 100644 apps/installer/includes/config/config-main.sh create mode 100644 apps/installer/includes/config/config.sh create mode 100644 deps/acore/bash-lib/src/common/boolean.sh create mode 100644 doc/ConfigPolicy.md diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 93e4e6791..06b7940d3 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -74,11 +74,16 @@ jobs: - name: Configure AzerothCore settings run: | - # Create basic configuration - cp conf/dist/config.sh conf/config.sh - # Configure dashboard - sed -i 's/MTHREADS=.*/MTHREADS="4"/' conf/config.sh - sed -i 's/CBUILD_TESTING=.*/CBUILD_TESTING="ON"/' conf/config.sh + touch conf/config.sh + echo 'MTHREADS=4' >> conf/config.sh + echo 'CBUILD_TESTING=ON' >> conf/config.sh + echo 'AC_ENABLE_ROOT_CMAKE_INSTALL=1' >> 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 run: | @@ -92,8 +97,6 @@ jobs: ./acore.sh module update --all - name: Run complete installation (deps, compile, database, client-data) - env: - AC_ENABLE_ROOT_CMAKE_INSTALL: 1 run: | # This runs: install-deps, compile, database setup, client-data download ./acore.sh init @@ -113,12 +116,14 @@ jobs: - name: Test authserver dry-run run: | + source ./acore.sh config load cd env/dist/bin timeout 5m ./authserver -dry-run continue-on-error: false - name: Test worldserver dry-run run: | + source ./acore.sh config load cd env/dist/bin timeout 5m ./worldserver -dry-run continue-on-error: false diff --git a/apps/bash_shared/common.sh b/apps/bash_shared/common.sh index acd23eacd..46422119b 100644 --- a/apps/bash_shared/common.sh +++ b/apps/bash_shared/common.sh @@ -1,17 +1,19 @@ function registerHooks() { acore_event_registerHooks "$@"; } function runHooks() { acore_event_runHooks "$@"; } -#shellcheck source=../../conf/dist/config.sh -source "$AC_PATH_CONF/dist/config.sh" # include dist to avoid missing conf variables +function acore_common_loadConfig() { + #shellcheck source=../../conf/dist/config.sh + source "$AC_PATH_CONF/dist/config.sh" # include dist to avoid missing conf variables -# first check if it's defined in env, otherwise use the default -USER_CONF_PATH=${USER_CONF_PATH:-"$AC_PATH_CONF/config.sh"} + # first check if it's defined in env, otherwise use the default + USER_CONF_PATH=${USER_CONF_PATH:-"$AC_PATH_CONF/config.sh"} -if [ -f "$USER_CONF_PATH" ]; then - source "$USER_CONF_PATH" # should overwrite previous -else - echo "NOTICE: file <$USER_CONF_PATH> not found, we use default configuration only." -fi + if [ -f "$USER_CONF_PATH" ]; then + source "$USER_CONF_PATH" # should overwrite previous + else + echo "NOTICE: file <$USER_CONF_PATH> not found, we use default configuration only." + fi +} # # Load modules diff --git a/apps/bash_shared/defines.sh b/apps/bash_shared/defines.sh index 4b014bd9c..af9e9dfc9 100644 --- a/apps/bash_shared/defines.sh +++ b/apps/bash_shared/defines.sh @@ -25,4 +25,6 @@ export AC_PATH_MODULES="$AC_PATH_ROOT/modules" 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" diff --git a/apps/bash_shared/includes.sh b/apps/bash_shared/includes.sh index d2bf07db1..679fc8e6d 100644 --- a/apps/bash_shared/includes.sh +++ b/apps/bash_shared/includes.sh @@ -16,6 +16,8 @@ source "$AC_PATH_DEPS/acore/bash-lib/src/event/hooks.sh" # shellcheck source=./common.sh source "$AC_PATH_SHARED/common.sh" +acore_common_loadConfig + if [[ "$OSTYPE" = "msys" ]]; then AC_BINPATH_FULL="$BINPATH" else diff --git a/apps/compiler/includes/functions.sh b/apps/compiler/includes/functions.sh index c955f2847..4428f9132 100644 --- a/apps/compiler/includes/functions.sh +++ b/apps/compiler/includes/functions.sh @@ -1,5 +1,8 @@ #!/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 SUDO="" @@ -135,7 +138,8 @@ function comp_compile() { echo "Done" ;; linux*|darwin*) - local confDir=${CONFDIR:-"$AC_BINPATH_FULL/../etc"} + local confDir + confDir=${CONFDIR:-"$AC_BINPATH_FULL/../etc"} # create the folders before installing to # set the current user and permissions @@ -145,6 +149,8 @@ function comp_compile() { mkdir -p "$confDir" mkdir -p "$confDir/modules" + confDir=$(realpath "$confDir") + echo "Cmake install..." $SUDO cmake --install . --config $CTYPE @@ -161,18 +167,25 @@ function comp_compile() { $SUDO setcap cap_sys_nice=eip "$AC_BINPATH_FULL/authserver" fi - [[ -f "$confDir/worldserver.conf.dist" ]] && \ - cp -v --no-clobber "$confDir/worldserver.conf.dist" "$confDir/worldserver.conf" - [[ -f "$confDir/authserver.conf.dist" ]] && \ - cp -v --no-clobber "$confDir/authserver.conf.dist" "$confDir/authserver.conf" - [[ -f "$confDir/dbimport.conf.dist" ]] && \ - cp -v --no-clobber "$confDir/dbimport.conf.dist" "$confDir/dbimport.conf" - for f in "$confDir/modules/"*.dist - do - [[ -e $f ]] || break # handle the case of no *.dist files - cp -v --no-clobber "$f" "${f%.dist}"; - done + if ( isTrue "$AC_ENABLE_CONF_COPY_ON_INSTALL" ) then + echo "Copying default configuration files to $confDir ..." + [[ -f "$confDir/worldserver.conf.dist" && ! -f "$confDir/worldserver.conf" ]] && \ + cp -v "$confDir/worldserver.conf.dist" "$confDir/worldserver.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 + do + [[ -e $f ]] || break # handle the case of no *.dist files + if [[ ! -f "${f%.dist}" ]]; then + echo "Copying module config $(basename "${f%.dist}")" + cp -v "$f" "${f%.dist}"; + fi + done + fi echo "Done" ;; diff --git a/apps/installer/includes/config/config-main.sh b/apps/installer/includes/config/config-main.sh new file mode 100644 index 000000000..f5f0c01f6 --- /dev/null +++ b/apps/installer/includes/config/config-main.sh @@ -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 "$@" + diff --git a/apps/installer/includes/config/config.sh b/apps/installer/includes/config/config.sh new file mode 100644 index 000000000..40192c400 --- /dev/null +++ b/apps/installer/includes/config/config.sh @@ -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 " + 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 $? +} + diff --git a/apps/installer/includes/functions.sh b/apps/installer/includes/functions.sh index 3bc7e13e4..28e5b4137 100644 --- a/apps/installer/includes/functions.sh +++ b/apps/installer/includes/functions.sh @@ -183,3 +183,5 @@ function inst_download_client_data { && echo "Remove downloaded file" && rm "$zipPath" \ && echo "INSTALLED_VERSION=$VERSION" > "$dataVersionFile" } + + diff --git a/apps/installer/includes/includes.sh b/apps/installer/includes/includes.sh index c0d6bb8bd..e4c1b9f2b 100644 --- a/apps/installer/includes/includes.sh +++ b/apps/installer/includes/includes.sh @@ -2,6 +2,7 @@ CURRENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd ) +# shellcheck source=../../bash_shared/includes.sh source "$CURRENT_PATH/../../bash_shared/includes.sh" 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_MODULES="$AC_PATH_MODULES" +# shellcheck source=../../../deps/acore/joiner/joiner.sh source "$J_PATH/joiner.sh" -if [ -f "$AC_PATH_INSTALLER/config.sh" ]; then - source "$AC_PATH_INSTALLER/config.sh" # should overwrite previous -fi - +# shellcheck source=../../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" +# shellcheck source=../includes/functions.sh source "$AC_PATH_INSTALLER/includes/functions.sh" diff --git a/apps/installer/includes/modules-manager/modules.sh b/apps/installer/includes/modules-manager/modules.sh index 91ae3785b..89c7ea50a 100644 --- a/apps/installer/includes/modules-manager/modules.sh +++ b/apps/installer/includes/modules-manager/modules.sh @@ -59,7 +59,6 @@ else C_GREEN='' C_YELLOW='' C_BLUE='' - C_MAGENTA='' C_CYAN='' fi @@ -174,42 +173,8 @@ function inst_module_list() { # Usage: ./acore.sh module [args...] # ./acore.sh module # Interactive menu function inst_module() { - # If no arguments provided, start interactive menu - if [[ $# -eq 0 ]]; then - menu_run_with_items "MODULE MANAGER" handle_module_command -- "${module_menu_items[@]}" -- - 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 + menu_run_with_items "MODULE MANAGER" handle_module_command -- "${module_menu_items[@]}" -- "$@" + return $? } # ============================================================================= diff --git a/apps/installer/main.sh b/apps/installer/main.sh index fea9dc3ac..a64787269 100644 --- a/apps/installer/main.sh +++ b/apps/installer/main.sh @@ -45,6 +45,7 @@ menu_items=( "docker|dr|Run docker tools" "version|v|Show AzerothCore version" "service-manager|sm|Run service manager to run authserver and worldserver in background" + "config|cf|Configuration manager" "quit|q|Exit from this menu" ) @@ -100,6 +101,9 @@ function handle_menu_command() { bash "$AC_PATH_APPS/startup-scripts/src/service-manager.sh" "$@" exit ;; + "config") + bash "$AC_PATH_APPS/installer/includes/config/config-main.sh" "$@" + ;; "quit") echo "Goodbye!" exit diff --git a/apps/installer/test/test_module_commands.bats b/apps/installer/test/test_module_commands.bats index 1223a80a6..d829c1a32 100755 --- a/apps/installer/test/test_module_commands.bats +++ b/apps/installer/test/test_module_commands.bats @@ -751,5 +751,5 @@ EOF run inst_module "unknown-command" [ "$status" -eq 1 ] - [[ "$output" =~ "Unknown module command" ]] + [[ "$output" =~ "Invalid option" ]] } \ No newline at end of file diff --git a/apps/startup-scripts/src/simple-restarter b/apps/startup-scripts/src/simple-restarter index a158b38be..1865eaa87 100755 --- a/apps/startup-scripts/src/simple-restarter +++ b/apps/startup-scripts/src/simple-restarter @@ -50,6 +50,8 @@ fi # Main restart loop while true; do STARTING_TIME=$(date +%s) + + echo "AC_CONFIG_POLICY: $AC_CONFIG_POLICY" # Use starter script to launch the binary with all parameters "$STARTER_SCRIPT" "$BINPATH" "$BINFILE" "$GDB_FILE" "$CONFIG" "$SYSLOG" "$SYSERR" "$GDB_ENABLED" "$CRASHES_PATH" diff --git a/conf/dist/config.sh b/conf/dist/config.sh index f8e78a8b8..5860cb2a8 100644 --- a/conf/dist/config.sh +++ b/conf/dist/config.sh @@ -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} +# +# 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 @@ -182,4 +188,34 @@ export MODULES_EXCLUDE_LIST="" NO_COLOR=${NO_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 + diff --git a/deps/acore/bash-lib/src/common/boolean.sh b/deps/acore/bash-lib/src/common/boolean.sh new file mode 100644 index 000000000..4f2e365db --- /dev/null +++ b/deps/acore/bash-lib/src/common/boolean.sh @@ -0,0 +1,5 @@ +function isTrue() { + local val + val=$(echo "$1" | tr '[:upper:]' '[:lower:]') + [[ "$val" == "1" || "$val" == "true" || "$val" == "yes" || "$val" == "on" ]] +} \ No newline at end of file diff --git a/doc/ConfigPolicy.md b/doc/ConfigPolicy.md new file mode 100644 index 000000000..daa20b2fb --- /dev/null +++ b/doc/ConfigPolicy.md @@ -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. diff --git a/src/common/Configuration/Config.cpp b/src/common/Configuration/Config.cpp index 258d15d65..0d23591f9 100644 --- a/src/common/Configuration/Config.cpp +++ b/src/common/Configuration/Config.cpp @@ -21,10 +21,14 @@ #include "StringFormat.h" #include "Tokenize.h" #include "Util.h" +#include +#include #include #include +#include #include #include +#include namespace { @@ -34,13 +38,14 @@ namespace std::unordered_map _configOptions; std::unordered_map _envVarCache; std::mutex _configLock; + ConfigPolicy _policy; - std::vector _fatalConfigOptions = + std::unordered_set _criticalConfigOptions = { - { "RealmID" }, - { "LoginDatabaseInfo" }, - { "WorldDatabaseInfo" }, - { "CharacterDatabaseInfo" }, + "RealmID", + "LoginDatabaseInfo", + "WorldDatabaseInfo", + "CharacterDatabaseInfo", }; // Check system configs like *server.conf* @@ -62,6 +67,29 @@ namespace return foundAppender != std::string_view::npos || foundLogger != std::string_view::npos; } + Optional 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 inline void PrintError(std::string_view filename, Format&& fmt, Args&& ... args) { @@ -77,6 +105,138 @@ namespace } } + template + inline void LogWithSeverity(ConfigSeverity severity, std::string_view filename, Format&& fmt, Args&&... args) + { + std::string message = Acore::StringFormat(std::forward(fmt), std::forward(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> overrides; + Optional 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 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) { auto const& itr = _configOptions.find(optionName); @@ -86,7 +246,7 @@ namespace { 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 ABORT("> Core can't start if found incorrect options"); @@ -111,13 +271,10 @@ namespace if (in.fail()) { - if (isOptional) - { - // No display erorr if file optional - return false; - } - - throw ConfigException(Acore::StringFormat("Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file)); + ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity; + LogWithSeverity(severity, file, "> Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file); + // Treat SKIP as a successful no-op so the app can proceed + return severity == ConfigSeverity::Skip; } uint32 count = 0; @@ -181,13 +338,10 @@ namespace // No lines read if (!count) { - if (isOptional) - { - // No display erorr if file optional - return false; - } - - throw ConfigException(Acore::StringFormat("Config::LoadFile: Empty file '{}'", file)); + ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity; + LogWithSeverity(severity, file, "> Config::LoadFile: Empty file '{}'", file); + // Treat SKIP as a successful no-op + return severity == ConfigSeverity::Skip; } // 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; auto const& itr = _configOptions.find(name); - bool fatalConfig = false; bool notFound = itr == _configOptions.end(); auto envVarName = GetEnvVarName(name); Optional envVar = GetEnvFromCache(name, envVarName); @@ -401,23 +554,23 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog { if (showLogs) { - for (std::string s : _fatalConfigOptions) - if (s == name) - { - fatalConfig = true; - break; - } + bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end(); + ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity; - if (fatalConfig) - 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!", + if (isCritical) + { + 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); + } else { std::string configs = _filename; if (!_moduleConfigFiles.empty()) 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); } } @@ -433,7 +586,8 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog { 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)); } @@ -447,7 +601,6 @@ template<> std::string ConfigMgr::GetValueDefault(std::string const& name, std::string const& def, bool showLogs /*= true*/) const { auto const& itr = _configOptions.find(name); - bool fatalConfig = false; bool notFound = itr == _configOptions.end(); auto envVarName = GetEnvVarName(name); Optional envVar = GetEnvFromCache(name, envVarName); @@ -466,23 +619,23 @@ std::string ConfigMgr::GetValueDefault(std::string const& name, std { if (showLogs) { - for (std::string s : _fatalConfigOptions) - if (s == name) - { - fatalConfig = true; - break; - } + bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end(); + ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity; - if (fatalConfig) - 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!", + if (isCritical) + { + 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); + } else { std::string configs = _filename; if (!_moduleConfigFiles.empty()) 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); } } @@ -509,7 +662,8 @@ bool ConfigMgr::GetOption(std::string const& name, bool const& def, bool s { 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"); } @@ -558,17 +712,27 @@ std::string const ConfigMgr::GetConfigPath() #endif } -void ConfigMgr::Configure(std::string const& initFileName, std::vector args, std::string_view modulesConfigList /*= {}*/) +void ConfigMgr::Configure(std::string const& initFileName, std::vector args, std::string_view modulesConfigList /*= {}*/, ConfigPolicy policy /*= {}*/) { _filename = initFileName; _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 if (!modulesConfigList.empty()) { for (auto const& itr : Acore::Tokenize(modulesConfigList, ',', false)) { - _additonalFiles.emplace_back(itr); + if (!itr.empty()) + _additonalFiles.emplace_back(itr); } } } diff --git a/src/common/Configuration/Config.h b/src/common/Configuration/Config.h index ccb35132d..dad439826 100644 --- a/src/common/Configuration/Config.h +++ b/src/common/Configuration/Config.h @@ -18,10 +18,29 @@ #ifndef CONFIG_H #define CONFIG_H +#include #include #include #include +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 { ConfigMgr() = default; @@ -32,7 +51,7 @@ class ConfigMgr public: bool LoadAppConfigs(bool isReload = false); bool LoadModulesConfigs(bool isReload = false, bool isNeedPrintInfo = true); - void Configure(std::string const& initFileName, std::vector args, std::string_view modulesConfigList = {}); + void Configure(std::string const& initFileName, std::vector args, std::string_view modulesConfigList = {}, ConfigPolicy policy = {}); static ConfigMgr* instance(); diff --git a/src/common/Logging/Log.cpp b/src/common/Logging/Log.cpp index 2b473a873..ba868a6cb 100644 --- a/src/common/Logging/Log.cpp +++ b/src/common/Logging/Log.cpp @@ -211,13 +211,16 @@ void Log::ReadLoggersFromConfig() AppenderConsole* appender = new AppenderConsole(NextAppenderId(), "Console", LOG_LEVEL_DEBUG, APPENDER_FLAGS_NONE, {}); 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); loggers[LOGGER_ROOT].reset(rootLogger); Logger* serverLogger = new Logger("server", LOG_LEVEL_INFO); serverLogger->addAppender(appender->getId(), appender); loggers["server"].reset(serverLogger); + + highestLogLevel = LOG_LEVEL_INFO; + return; } } diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index b5fbb319a..5295685de 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -278,7 +278,8 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile) ("help,h", "print usage message") ("version,v", "print version build info") ("dry-run,d", "Dry run") - ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_REALM_CONFIG))), "use as configuration file"); + ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_REALM_CONFIG))), "use as configuration file") + ("config-policy", value()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)"); variables_map variablesMap; diff --git a/src/server/apps/worldserver/Main.cpp b/src/server/apps/worldserver/Main.cpp index bdc2f860f..56d26bcae 100644 --- a/src/server/apps/worldserver/Main.cpp +++ b/src/server/apps/worldserver/Main.cpp @@ -423,7 +423,7 @@ bool StartDB() MySQL::Library_Init(); // Load databases - DatabaseLoader loader("server.worldserver", DatabaseLoader::DATABASE_NONE, AC_MODULES_LIST); + DatabaseLoader loader("server.worldserver", DatabaseLoader::DATABASE_MASK_ALL, AC_MODULES_LIST); loader .AddDatabase(LoginDatabase, "Login") .AddDatabase(CharacterDatabase, "Character") @@ -433,7 +433,7 @@ bool StartDB() return false; ///- Get the realm Id from the configuration file - realm.Id.Realm = sConfigMgr->GetOption("RealmID", 0); + realm.Id.Realm = sConfigMgr->GetOption("RealmID", 1); if (!realm.Id.Realm) { 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") ("version,v", "print version build info") ("dry-run,d", "Dry run") - ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG))), "use as configuration file"); + ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_CORE_CONFIG))), "use as configuration file") + ("config-policy", value()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)"); #if AC_PLATFORM == AC_PLATFORM_WINDOWS options_description win("Windows platform specific options"); diff --git a/src/server/database/Database/DatabaseLoader.cpp b/src/server/database/Database/DatabaseLoader.cpp index 5ad77d35d..32bbb107f 100644 --- a/src/server/database/Database/DatabaseLoader.cpp +++ b/src/server/database/Database/DatabaseLoader.cpp @@ -24,6 +24,24 @@ #include #include #include +#include +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) : _logger(logger), @@ -38,7 +56,8 @@ DatabaseLoader& DatabaseLoader::AddDatabase(DatabaseWorkerPool& pool, std::st _open.push([this, name, updatesEnabledForThis, &pool]() -> bool { - std::string const dbString = sConfigMgr->GetOption(name + "DatabaseInfo", ""); + std::string const& defaultDatabaseInfo = GetDefaultDatabaseInfo(name); + std::string const dbString = sConfigMgr->GetOption(name + "DatabaseInfo", defaultDatabaseInfo); if (dbString.empty()) { LOG_ERROR(_logger, "Database {} not specified in configuration file!", name); diff --git a/src/tools/dbimport/Main.cpp b/src/tools/dbimport/Main.cpp index 87371f62a..0274091a6 100644 --- a/src/tools/dbimport/Main.cpp +++ b/src/tools/dbimport/Main.cpp @@ -109,8 +109,8 @@ bool StartDB() DatabaseLoader loader = modules.empty() ? DatabaseLoader("dbimport") : - (modules == "all") ? DatabaseLoader("dbimport", DatabaseLoader::DATABASE_NONE, AC_MODULES_LIST) : - DatabaseLoader("dbimport", DatabaseLoader::DATABASE_NONE, modules); + (modules == "all") ? DatabaseLoader("dbimport", DatabaseLoader::DATABASE_MASK_ALL, AC_MODULES_LIST) : + DatabaseLoader("dbimport", DatabaseLoader::DATABASE_MASK_ALL, modules); loader .AddDatabase(LoginDatabase, "Login") @@ -140,7 +140,8 @@ variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile) ("help,h", "print usage message") ("version,v", "print version build info") ("dry-run,d", "Dry run") - ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_DB_IMPORT_CONFIG))), "use as configuration file"); + ("config,c", value(&configFile)->default_value(fs::path(sConfigMgr->GetConfigPath() + std::string(_ACORE_DB_IMPORT_CONFIG))), "use as configuration file") + ("config-policy", value()->value_name("policy"), "override config severity policy (e.g. default=skip,critical_option=fatal)"); variables_map variablesMap;