From 3552ff4b197f87df6799efdd481cfa5d350c4441 Mon Sep 17 00:00:00 2001 From: H0zen Date: Fri, 17 Jul 2026 08:12:10 +0300 Subject: [PATCH] ACE removed. New network engine. OpenSSL 3.x mandatory. New mangosd UI. Also include the float/limits headers (/) that ACE used to pull in transitively: PathFinder, PetAI, bot item use, the crash reporter, and the Movemap-Generator (via game/vmap/MapTree.h, so the extractor submodule is untouched). GCC/Ubuntu 24.04 and MSVC verified: full build, 0 errors. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/core_codestyle.yml | 13 +- .github/workflows/core_linux_build.yml | 36 +- .github/workflows/core_windows_build.yml | 24 +- .github/workflows/docker_build.yml | 48 +- .gitignore | 26 - CLAUDE.md | 4 +- CMakeLists.txt | 57 +- README.md | 4 +- cmake/FindMySQL.cmake | 24 +- cmake/FindReadline.cmake | 75 ++ cmake/GenRevision.cmake | 34 +- cmake/PCHSupport.cmake | 38 +- dep | 2 +- dockercontainer/DockerFile-mangosd | 2 +- dockercontainer/DockerFile-realmd | 2 +- linux/getmangos.sh | 124 +-- src/game/BattleGround/BattleGround.cpp | 1 + src/game/BattleGround/BattleGroundMgr.cpp | 4 - src/game/BattleGround/BattleGroundMgr.h | 4 - src/game/BattleGround/BattleGroundMgrUtil.cpp | 3 +- src/game/BattleGround/BattleGroundQueue.cpp | 7 - src/game/CMakeLists.txt | 2 + src/game/ChatCommands/CreatureCommands.cpp | 3 +- src/game/Maps/MapUpdater.cpp | 212 ++--- src/game/Maps/MapUpdater.h | 89 +- src/game/MotionGenerators/PathFinder.cpp | 2 + src/game/Object/ObjectAccessor.cpp | 14 +- src/game/Object/ObjectAccessor.h | 80 +- src/game/Object/ObjectMgr.cpp | 1 + src/game/Object/ObjectMgr.h | 2 +- src/game/Object/PetAI.cpp | 2 + src/game/Server/WorldSession.cpp | 18 +- src/game/Server/WorldSession.h | 7 +- src/game/Server/WorldSocket.cpp | 795 +++------------- src/game/Server/WorldSocket.h | 217 ++--- src/game/Server/WorldSocketMgr.cpp | 194 +--- src/game/Server/WorldSocketMgr.h | 70 +- src/game/Warden/WardenCheckMgr.cpp | 10 +- src/game/Warden/WardenCheckMgr.h | 3 +- src/game/Warden/WardenMac.cpp | 14 +- src/game/Warden/WardenWin.cpp | 8 +- .../WorldHandlers/AuctionHouseHandler.cpp | 31 +- src/game/WorldHandlers/ChatMessage.cpp | 1 + src/game/WorldHandlers/GridDefines.h | 6 +- src/game/WorldHandlers/GridMap.cpp | 18 +- src/game/WorldHandlers/GridMap.h | 12 +- src/game/WorldHandlers/Map.h | 4 - src/game/WorldHandlers/MapManager.cpp | 16 +- src/game/WorldHandlers/MapManager.h | 10 +- .../WorldHandlers/MapPersistentStateMgr.cpp | 1 + .../WorldHandlers/MapPersistentStateMgr.h | 3 +- src/game/WorldHandlers/ScriptMgr.cpp | 2 +- src/game/WorldHandlers/ScriptMgr.h | 17 +- src/game/WorldHandlers/ScriptMgrBinding.cpp | 8 +- src/game/WorldHandlers/ScriptMgrDbScripts.cpp | 4 +- src/game/WorldHandlers/World.cpp | 341 ++++--- src/game/WorldHandlers/World.h | 8 +- src/game/vmap/MapTree.h | 6 + src/ipc/BoundedQueue.h | 2 +- src/ipc/CMakeLists.txt | 5 +- src/ipc/IpcChannel.cpp | 155 +-- src/ipc/IpcChannel.h | 6 +- src/ipc/IpcClientHandler.cpp | 339 ++----- src/ipc/IpcClientHandler.h | 100 +- src/ipc/IpcLink.h | 185 ++-- src/ipc/IpcOutboundNotifier.h | 98 -- src/ipc/IpcProcess.cpp | 198 ++++ src/ipc/IpcProcess.h | 88 ++ src/ipc/IpcServerHandler.cpp | 482 ++-------- src/ipc/IpcServerHandler.h | 163 +--- src/ipc/IpcSocket.cpp | 261 +++++ src/ipc/IpcSocket.h | 99 ++ src/ipc/IpcThread.cpp | 332 ++----- src/ipc/IpcThread.h | 91 +- src/ipc/WorkerSupervisor.cpp | 195 ++-- src/ipc/WorkerSupervisor.h | 30 +- src/mangosd/AFThread.cpp | 84 -- src/mangosd/CMakeLists.txt | 18 +- src/mangosd/CliThread.cpp | 165 ---- src/mangosd/CliThread.h | 50 - src/mangosd/Master.cpp | 890 ++++++++++++++++++ src/mangosd/Master.h | 99 ++ src/mangosd/RASession.cpp | 323 +++++++ src/mangosd/RASession.h | 133 +++ src/mangosd/RAThread.cpp | 438 --------- src/mangosd/Service.h | 77 ++ src/mangosd/WorldThread.cpp | 163 ---- src/mangosd/mangosd.conf.dist.in | 11 + src/mangosd/mangosd.cpp | 603 +++--------- src/modules/AhWorker/BrowseHandler.cpp | 2 +- src/modules/AhWorker/BrowseHandler.h | 4 +- src/modules/AhWorker/Console_posix.cpp | 5 +- src/modules/AhWorker/Main.cpp | 38 +- .../AhWorker/difftest/AhUsabilityDiffTest.cpp | 6 - src/modules/Bots/ahbot/AhBot.cpp | 18 +- src/modules/Bots/botpch.h | 2 +- src/modules/Bots/playerbot/PlayerbotAI.cpp | 1 + .../Bots/playerbot/strategy/Action.cpp | 1 + .../Bots/playerbot/strategy/Engine.cpp | 1 + .../strategy/actions/UseItemAction.cpp | 2 + src/modules/Eluna | 2 +- src/modules/SD3 | 2 +- src/realmd | 2 +- src/shared/Auth/ARC4.cpp | 42 +- src/shared/Auth/ARC4.h | 12 +- src/shared/Auth/HMACSHA1.cpp | 80 +- src/shared/Auth/HMACSHA1.h | 15 +- src/shared/Auth/OpenSSLProvider.cpp | 5 +- src/shared/Auth/OpenSSLProvider.h | 4 +- src/shared/Auth/Sha1.cpp | 43 +- src/shared/Auth/Sha1.h | 26 +- src/shared/CMakeLists.txt | 78 +- src/shared/Common/Common.h | 76 +- src/shared/Config/Config.cpp | 170 ++-- src/shared/Config/Config.h | 30 +- src/shared/Database/Database.cpp | 18 +- src/shared/Database/Database.h | 24 +- src/shared/Database/DatabaseImpl.h | 1 + src/shared/Database/DatabaseMysql.cpp | 4 +- src/shared/Database/DatabaseMysql.h | 3 - src/shared/Database/SqlDelayThread.cpp | 4 +- src/shared/Database/SqlDelayThread.h | 5 +- src/shared/Database/SqlOperations.cpp | 1 + src/shared/Database/SqlOperations.h | 3 +- src/shared/Database/SqlPreparedStatement.h | 1 - src/shared/Dynamic/ObjectRegistry.h | 2 +- src/shared/GameSystem/Grid.h | 1 - src/shared/LockedQueue/LockedQueue.h | 88 +- src/shared/Log/ConsoleLogWriter.cpp | 22 +- src/shared/Log/ConsoleLogWriter.h | 10 +- src/shared/Log/Log.cpp | 96 +- src/shared/Log/Log.h | 57 +- src/shared/Platform/Define.h | 143 +-- src/shared/Policies/CreationPolicy.h | 194 ---- src/shared/Policies/ObjectLifeTime.cpp | 39 - src/shared/Policies/ObjectLifeTime.h | 86 -- src/shared/Policies/Singleton.h | 155 +-- src/shared/Policies/ThreadingModel.h | 277 ------ src/shared/Threading/DelayExecutor.cpp | 208 ---- src/shared/Threading/DelayExecutor.h | 59 -- src/shared/Threading/ThreadLocalStore.h | 70 ++ src/shared/Threading/Threading.cpp | 345 +------ src/shared/Threading/Threading.h | 173 +--- src/shared/Utilities/ByteBuffer.cpp | 21 +- src/shared/Utilities/ConsoleStyle.cpp | 307 ++++++ src/shared/Utilities/ConsoleStyle.h | 137 +++ src/shared/Utilities/Errors.h | 27 +- src/shared/Utilities/ProgressBar.cpp | 64 +- src/shared/Utilities/ProgressBar.h | 35 + src/shared/Utilities/ProgressBarRender.h | 2 +- src/shared/Utilities/RNGen.h | 20 +- src/shared/Utilities/StartupUI.cpp | 556 +++++++++++ src/shared/Utilities/StartupUI.h | 141 +++ src/shared/Utilities/StartupUIDemo.cpp | 159 ++++ src/shared/Utilities/Util.cpp | 29 +- src/shared/Utilities/Util.h | 10 +- src/shared/Win/ServiceWin32.cpp | 5 - src/shared/Win/WheatyExceptionReport.cpp | 2 + src/shared/net/BindAddress.cpp | 81 ++ src/shared/net/BindAddress.hpp | 51 + src/shared/net/FlowControl.hpp | 89 ++ src/shared/net/ISession.hpp | 118 +++ src/shared/net/SendQueue.hpp | 148 +++ src/shared/net/Server.hpp | 77 ++ src/shared/net/iocp/IocpServer.cpp | 412 ++++++++ src/shared/net/iocp/IocpServer.hpp | 187 ++++ src/shared/net/reactor/Connection.hpp | 95 ++ src/shared/net/reactor/EpollPoller.cpp | 120 +++ .../net/reactor/EpollPoller.hpp} | 52 +- src/shared/net/reactor/KqueuePoller.cpp | 127 +++ .../net/reactor/KqueuePoller.hpp} | 54 +- src/shared/net/reactor/Poller.hpp | 88 ++ .../net/reactor/PollerFactory.cpp} | 49 +- src/shared/net/reactor/ReactorServer.cpp | 441 +++++++++ src/shared/net/reactor/ReactorServer.hpp | 100 ++ src/shared/net/uring/UringServer.cpp | 433 +++++++++ src/shared/net/uring/UringServer.hpp | 173 ++++ src/tools/Extractor_projects | 2 +- 178 files changed, 8981 insertions(+), 6833 deletions(-) create mode 100644 cmake/FindReadline.cmake delete mode 100644 src/ipc/IpcOutboundNotifier.h create mode 100644 src/ipc/IpcProcess.cpp create mode 100644 src/ipc/IpcProcess.h create mode 100644 src/ipc/IpcSocket.cpp create mode 100644 src/ipc/IpcSocket.h delete mode 100644 src/mangosd/AFThread.cpp delete mode 100644 src/mangosd/CliThread.cpp delete mode 100644 src/mangosd/CliThread.h create mode 100644 src/mangosd/Master.cpp create mode 100644 src/mangosd/Master.h create mode 100644 src/mangosd/RASession.cpp create mode 100644 src/mangosd/RASession.h delete mode 100644 src/mangosd/RAThread.cpp create mode 100644 src/mangosd/Service.h delete mode 100644 src/mangosd/WorldThread.cpp delete mode 100644 src/shared/Policies/CreationPolicy.h delete mode 100644 src/shared/Policies/ObjectLifeTime.cpp delete mode 100644 src/shared/Policies/ObjectLifeTime.h delete mode 100644 src/shared/Policies/ThreadingModel.h delete mode 100644 src/shared/Threading/DelayExecutor.cpp delete mode 100644 src/shared/Threading/DelayExecutor.h create mode 100644 src/shared/Threading/ThreadLocalStore.h create mode 100644 src/shared/Utilities/ConsoleStyle.cpp create mode 100644 src/shared/Utilities/ConsoleStyle.h create mode 100644 src/shared/Utilities/StartupUI.cpp create mode 100644 src/shared/Utilities/StartupUI.h create mode 100644 src/shared/Utilities/StartupUIDemo.cpp create mode 100644 src/shared/net/BindAddress.cpp create mode 100644 src/shared/net/BindAddress.hpp create mode 100644 src/shared/net/FlowControl.hpp create mode 100644 src/shared/net/ISession.hpp create mode 100644 src/shared/net/SendQueue.hpp create mode 100644 src/shared/net/Server.hpp create mode 100644 src/shared/net/iocp/IocpServer.cpp create mode 100644 src/shared/net/iocp/IocpServer.hpp create mode 100644 src/shared/net/reactor/Connection.hpp create mode 100644 src/shared/net/reactor/EpollPoller.cpp rename src/{mangosd/RAThread.h => shared/net/reactor/EpollPoller.hpp} (56%) create mode 100644 src/shared/net/reactor/KqueuePoller.cpp rename src/{mangosd/AFThread.h => shared/net/reactor/KqueuePoller.hpp} (52%) create mode 100644 src/shared/net/reactor/Poller.hpp rename src/{mangosd/WorldThread.h => shared/net/reactor/PollerFactory.cpp} (53%) create mode 100644 src/shared/net/reactor/ReactorServer.cpp create mode 100644 src/shared/net/reactor/ReactorServer.hpp create mode 100644 src/shared/net/uring/UringServer.cpp create mode 100644 src/shared/net/uring/UringServer.hpp diff --git a/.github/workflows/core_codestyle.yml b/.github/workflows/core_codestyle.yml index cce46ecd96..32e6575b2a 100644 --- a/.github/workflows/core_codestyle.yml +++ b/.github/workflows/core_codestyle.yml @@ -1,11 +1,17 @@ name: Codestyle Checks + permissions: contents: read + on: push: - branches: [ master ] + branches: [master, devel] pull_request: - branches: [ master ] + branches: [master] + +concurrency: + group: codestyle-${{ github.ref }} + cancel-in-progress: true jobs: check-codestyle: @@ -15,7 +21,8 @@ jobs: runs-on: ubuntu-latest name: Check Codestyling steps: - - uses: actions/checkout@v2 + # v2 runs on a long-deprecated Node runtime. + - uses: actions/checkout@v4 - name: Check Codestyling run: source ./apps/ci/ci-codestyle.sh diff --git a/.github/workflows/core_linux_build.yml b/.github/workflows/core_linux_build.yml index b1681ac2b1..bd55c82934 100644 --- a/.github/workflows/core_linux_build.yml +++ b/.github/workflows/core_linux_build.yml @@ -2,13 +2,18 @@ name: Linux Build (GCC + Clang) on: push: - branches: [ master, devel ] + branches: [master, devel] pull_request: - branches: [ master ] + branches: [master] permissions: contents: read +# A second push to the same branch makes the first run's result worthless. +concurrency: + group: linux-build-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -34,21 +39,25 @@ jobs: submodules: recursive fetch-depth: 1 - - name: Cache CMake build + # ccache, not a cached build directory: actions/checkout rewrites every source file, + # so restored objects are always older than their sources and make rebuilds everything. + # ccache keys on translation-unit content instead. github.sha keeps the key unique so + # the cache is always re-saved; restore-keys seeds it from the newest previous run. + - name: Cache ccache uses: actions/cache@v4 with: - path: _build - key: ${{ runner.os }}-${{ matrix.name }}-cmake-${{ hashFiles('**/CMakeLists.txt') }} + path: ~/.cache/ccache + key: ${{ runner.os }}-${{ matrix.name }}-ccache-${{ github.sha }} restore-keys: | - ${{ runner.os }}-${{ matrix.name }}-cmake- + ${{ runner.os }}-${{ matrix.name }}-ccache- - name: Install build dependencies run: | sudo apt-get update -qq sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ - git cmake make build-essential \ + git cmake make build-essential ccache \ libssl-dev libbz2-dev default-libmysqlclient-dev \ - libace-dev libreadline-dev \ + libreadline-dev \ ${{ matrix.pkg }} - name: Configure project @@ -60,16 +69,19 @@ jobs: -DCMAKE_INSTALL_PREFIX=../_install \ -DCMAKE_C_COMPILER=${{ matrix.cc }} \ -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DBUILD_TOOLS=1 \ -DBUILD_MANGOSD=1 \ -DBUILD_REALMD=1 \ -DSOAP=1 \ -DSCRIPT_LIB_ELUNA=1 \ - -DSCRIPT_LIB_SD3_GATE=1 \ + -DSCRIPT_LIB_SD3=1 \ -DPLAYERBOTS=1 \ -DUSE_STORMLIB=1 \ -DBUILD_AH_SERVICE=1 \ - -DPCH=0 + -DPCH=0 \ + --warn-uninitialized - name: Build and install run: | @@ -86,3 +98,7 @@ jobs: echo "$out" echo "$out" | grep -q "intent codec selftest OK" || { echo "MISSING: intent codec selftest OK"; exit 1; } echo "$out" | grep -q "ipc selftest OK" || { echo "MISSING: ipc selftest OK"; exit 1; } + + - name: ccache statistics + if: always() + run: ccache -s diff --git a/.github/workflows/core_windows_build.yml b/.github/workflows/core_windows_build.yml index d2fa1c3356..2fca16271f 100644 --- a/.github/workflows/core_windows_build.yml +++ b/.github/workflows/core_windows_build.yml @@ -5,9 +5,14 @@ permissions: on: push: - branches: [ master , devel ] + branches: [master, devel] pull_request: - branches: [ master ] + branches: [master] + +# Supersede an in-flight run when the branch moves on: its result is already stale. +concurrency: + group: windows-build-${{ github.ref }} + cancel-in-progress: true jobs: build: @@ -24,6 +29,7 @@ jobs: fetch-depth: 1 + # Bump the -vN suffix to force a re-install of OpenSSL. - name: Cache OpenSSL id: cache-openssl uses: actions/cache@v4 @@ -31,7 +37,7 @@ jobs: path: | C:\Program Files\OpenSSL C:\Program Files\OpenSSL-Win64 - key: ${{ runner.os }}-openssl-full + key: ${{ runner.os }}-openssl-v1 - name: Install full OpenSSL (developer) @@ -63,18 +69,12 @@ jobs: echo "OPENSSL_INCLUDE_DIR=C:/Program Files/OpenSSL-Win64/include" >> $GITHUB_ENV echo "OPENSSL_CRYPTO_LIBRARY=C:/Program Files/OpenSSL-Win64/lib/VC/libcrypto64MT.lib" >> $GITHUB_ENV echo "OPENSSL_SSL_LIBRARY=C:/Program Files/OpenSSL-Win64/lib/VC/libssl64MT.lib" >> $GITHUB_ENV + else + echo "::error::OpenSSL not found in either expected location" + exit 1 fi - - name: Cache CMake build - uses: actions/cache@v4 - with: - path: build - key: ${{ runner.os }}-cmake-${{ hashFiles('**/CMakeLists.txt') }} - restore-keys: | - ${{ runner.os }}-cmake- - - - name: Configure CMake project shell: bash run: | diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 19e4fe038d..b7757c3bde 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -1,31 +1,51 @@ name: Docker Build + +# Only builds images and pushes nothing, so read is all the token ever needs. +permissions: + contents: read + on: push: - branches: [ master , devel ] + branches: [master, devel] pull_request: - branches: [ master ] + branches: [master] + +concurrency: + group: docker-build-${{ github.ref }} + cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + # v2 runs on a deprecated Node runtime. + - name: Checkout repository + uses: actions/checkout@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Checkout Submodules - shell: bash - run: | - git submodule init && git submodule update + submodules: recursive + fetch-depth: 1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 + # Layer caching across runs. - name: Build Mangosd Docker image - run: | - docker build -t mangosd:latest -f dockercontainer/DockerFile-mangosd . + uses: docker/build-push-action@v6 + with: + context: . + file: dockercontainer/DockerFile-mangosd + tags: mangosd:latest + push: false + cache-from: type=gha,scope=mangosd + cache-to: type=gha,mode=max,scope=mangosd - name: Build Realmd Docker image - run: | - docker build -t realmd:latest -f dockercontainer/DockerFile-realmd . \ No newline at end of file + uses: docker/build-push-action@v6 + with: + context: . + file: dockercontainer/DockerFile-realmd + tags: realmd:latest + push: false + cache-from: type=gha,scope=realmd + cache-to: type=gha,mode=max,scope=realmd diff --git a/.gitignore b/.gitignore index 637dd46598..2c0af85edd 100644 --- a/.gitignore +++ b/.gitignore @@ -64,8 +64,6 @@ Thumbs.db _build build /dep/libmpq/autom4te.cache/ -/ACE_wrappers/* -/ACE*.tar.bz2 # Generated files *.ncb @@ -84,31 +82,7 @@ ipch cmake.cmd msbuild.cmd -# cmake generated files in sources -/dep/ACE_wrappers/ace/ACE_vc8.vcxproj* -/dep/ACE_wrappers/ace/Backup* -/dep/ACE_wrappers/ace/Debug* -/dep/ACE_wrappers/ace/ETCL/ACE_ETCL_Parser_vc8.vcxproj* -/dep/ACE_wrappers/ace/ETCL/ACE_ETCL_vc8.vcxproj* -/dep/ACE_wrappers/ace/ETCL/Debug* -/dep/ACE_wrappers/ace/ETCL/Release* -/dep/ACE_wrappers/ace/Monitor_Control/Debug* -/dep/ACE_wrappers/ace/Monitor_Control/Monitor_Control_vc8.vcxproj* -/dep/ACE_wrappers/ace/Monitor_Control/Release* -/dep/ACE_wrappers/ace/QoS/Debug* -/dep/ACE_wrappers/ace/QoS/QoS_vc8.vcxproj* -/dep/ACE_wrappers/ace/QoS/Release* -/dep/ACE_wrappers/ace/UpgradeLog* -/dep/ACE_wrappers/ace/Release* -/dep/ACE_wrappers/ace/_UpgradeReport_Files* -/dep/ACE_wrappers/lib/* -/dep/ACE_wrappers/ace/config.h - # ned files from excluded dirs -!/dep/ACE_wrappers/ace/ace_message_table.bin -!/dep/ACE_wrappers/bin/GNUmakefile.bin -!/dep/ACE_wrappers/configure.ac~ -!/dep/ACE_wrappers/lib/.empty !/dep/tbb/src/Makefile # recastnavigation directory needs exception diff --git a/CLAUDE.md b/CLAUDE.md index e6a92e63bb..0665dcd912 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,11 +23,11 @@ this repo. Humans: also read [`doc/CodingStandard.md`](doc/CodingStandard.md). ```sh git clone --recursive https://github.com/mangoszero/server.git && cd server sudo apt-get install -y git cmake make build-essential \ - libssl-dev libbz2-dev default-libmysqlclient-dev libace-dev libreadline-dev # Debian/Ubuntu deps + libssl-dev libbz2-dev default-libmysqlclient-dev libreadline-dev # Debian/Ubuntu deps mkdir -p _build _install && cd _build cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../_install \ -DBUILD_TOOLS=1 -DBUILD_MANGOSD=1 -DBUILD_REALMD=1 -DSOAP=1 \ - -DSCRIPT_LIB_ELUNA=1 -DSCRIPT_LIB_SD3_GATE=1 -DPLAYERBOTS=1 \ + -DSCRIPT_LIB_ELUNA=1 -DSCRIPT_LIB_SD3=1 -DPLAYERBOTS=1 \ -DUSE_STORMLIB=1 -DBUILD_AH_SERVICE=1 -DPCH=0 make -j"$(nproc)" && make install -j"$(nproc)" ``` diff --git a/CMakeLists.txt b/CMakeLists.txt index cbaaf1ce19..664293caa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,17 @@ else() set(CONF_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}") endif() +# ============================================================================= +# Runtime library search path (RPATH) +# ============================================================================= +if(APPLE) + set(CMAKE_INSTALL_RPATH "@loader_path/../lib") + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +elseif(UNIX) + set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() + # ============================================================================= # Dependencies # ============================================================================= @@ -134,12 +145,56 @@ if(NOT WITHOUT_GIT) endif() find_package(Threads REQUIRED) -find_package(OpenSSL REQUIRED) +find_package(OpenSSL 3.0 REQUIRED) find_package(MySQL REQUIRED) +# ============================================================================= +# OpenSSL policy +# +# OpenSSL 3.x is the baseline: there are no version conditionals left in src/. +# Every OpenSSL decision lives here, in two interface targets: +# +# mangos_openssl - what to link against and where the headers live. +# Linked PUBLIC by anything exposing OpenSSL types in +# its own headers (`shared`), so consumers inherit it. +# +# mangos_openssl_strict - the API level we hold ourselves to. Together these +# two definitions delete the *declarations* of +# everything OpenSSL deprecated up to 3.0 -- the +# low-level MD5_*, SHA1_*, HMAC_CTX and SSLeay_* +# families. Reaching for the old API is then a +# compile error rather than a warning nobody reads. +# +# Link it PRIVATE, always: it must not leak into the +# vendored code under dep/, which still uses the old API. +# ============================================================================= +add_library(mangos_openssl INTERFACE) +target_link_libraries(mangos_openssl + INTERFACE + OpenSSL::SSL + OpenSSL::Crypto +) + +add_library(mangos_openssl_strict INTERFACE) +target_compile_definitions(mangos_openssl_strict + INTERFACE + OPENSSL_API_COMPAT=0x30000000L + OPENSSL_NO_DEPRECATED +) + find_package(ZLIB QUIET) find_package(BZip2 QUIET) +# dep/lualib (the Lua build used by Eluna) links against readline by its bare +# library name and is not itself touched here; find_package(Readline) below +# declares a GLOBAL IMPORTED target literally named `readline` (see +# cmake/FindReadline.cmake) before that subdirectory is added, so its +# existing bare-name references resolve to a properly-detected library +# instead of an unqualified linker search. Readline has no Windows build and +# is only needed when Eluna is being built. +if(NOT WIN32 AND SCRIPT_LIB_ELUNA) + find_package(Readline REQUIRED) +endif() include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenRevision.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/EnsureVersion.cmake) diff --git a/README.md b/README.md index fda370b303..db39641639 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,6 @@ their systems package management instead of source packages.** * **[Git][34] / [Github for Windows][35]**: This version control software allows you to get the source files in the first place. * **[MySQL][40]** / **[MariaDB][41]**: These databases are used to store content and user data. -* **[ACE][43]**: aka Adaptive Communication Environment, provides us with a solid cross-platform framework for abstracting operating system specific details. * **[Recast][44]**: In order to create navigation data from the client's map files, Recast is used to do the dirty work. It provides functions for rendering, pathing, etc. * **[G3D][45]**: This engine provides the basic framework for handling 3D data and is used to handle basic map data. * **[Stormlib][46]**: Provides an abstraction layer for reading from the client's data files. @@ -81,7 +80,7 @@ their systems package management instead of source packages.** * **[Bzip2][54]/[Bzip2 for Windows][52]** provides compression algorithms used in MPQ archives. * **[OpenSSL][48]/[OpenSSL for Windows][55]** provides encryption algorithms used when authenticating clients. -**ACE**, **Recast**, **G3D**, **Stormlib**, **Zlib** and **Bzip2** are included in the standard distribution as +**Recast**, **G3D**, **Stormlib**, **Zlib** and **Bzip2** are included in the standard distribution as we rely on specific versions. Optional dependencies @@ -199,7 +198,6 @@ World of Warcraft, and all related art, images, and lore are copyright [Blizzard [35]: http://windows.github.com/ "github - windows client" [40]: https://dev.mysql.com/downloads/ "MySQL - The world's most popular open source database" [41]: https://mariadb.org/download/ "MariaDB - An enhanced, drop-in replacement for MySQL" -[43]: http://www.dre.vanderbilt.edu/~schmidt/ACE.html "ACE - The ADAPTIVE Communication Environment" [44]: http://github.com/memononen/recastnavigation "Recast - Navigation-mesh Toolset for Games" [45]: http://sourceforge.net/projects/g3d/ "G3D - G3D Innovation Engine" [46]: http://zezula.net/en/mpq/stormlib.html "Stormlib - A library for reading data from MPQ archives" diff --git a/cmake/FindMySQL.cmake b/cmake/FindMySQL.cmake index 930d9be04d..001c021708 100644 --- a/cmake/FindMySQL.cmake +++ b/cmake/FindMySQL.cmake @@ -52,18 +52,21 @@ if (_MYSQL_USE_PKGCONFIG) endif () if(NOT MySQL_FOUND) -# https://mariadb.org/mariadb/all-releases/ - set(_MySQL_mariadb_versions 10.1 10.2 10.3 10.4 10.5 10.6 10.7 10.8 10.9 10.10 10.11 - 11.0 11.1 11.2 11.3 11.4 11.5 11.6 11.7 11.8 - 12.0 12.1 12.2) - set(_MySQL_versions 5.4 5.5 5.6 5.7 8.0) set(_MySQL_paths) - foreach (_MySQL_version IN LISTS _MySQL_mariadb_versions) - list(APPEND _MySQL_paths - "[HKEY_LOCAL_MACHINE\\SOFTWARE\\MariaDB ${_MySQL_version};INSTALLDIR]" - "[HKEY_LOCAL_MACHINE\\SOFTWARE\\MariaDB ${_MySQL_version} (x64);INSTALLDIR]") - endforeach () + + # MariaDB ships a new minor/major version regularly, and its registry key + # and default install directory both embed the version number (e.g. + # "MariaDB 10.11"), so hardcoding every version ever released requires + # updating this file on every release. Instead, glob the default install + # directories for any "MariaDB *" folder, which finds whatever is actually + # installed regardless of version. + file(GLOB _MySQL_mariadb_install_dirs + "C:/Program Files/MariaDB */" + "C:/Program Files (x86)/MariaDB */") + list(APPEND _MySQL_paths ${_MySQL_mariadb_install_dirs}) + unset(_MySQL_mariadb_install_dirs) + foreach (_MySQL_version IN LISTS _MySQL_versions) list(APPEND _MySQL_paths "C:/Program Files/MySQL/MySQL Server ${_MySQL_version}/lib/opt" @@ -72,7 +75,6 @@ if(NOT MySQL_FOUND) endforeach () unset(_MySQL_version) unset(_MySQL_versions) - unset(_MySQL_mariadb_versions) find_path(MySQL_INCLUDE_DIR NAMES mysql.h diff --git a/cmake/FindReadline.cmake b/cmake/FindReadline.cmake new file mode 100644 index 0000000000..4a2e7ebcca --- /dev/null +++ b/cmake/FindReadline.cmake @@ -0,0 +1,75 @@ +#[==[ +Locates the GNU Readline library, used by the Lua interpreter build (see +dep/lualib/lua/CMakeLists.txt) for interactive line editing/history. +Unix-only: there is no Windows build of readline in this project. + +dep/lualib/lua/CMakeLists.txt links against it by the bare name `readline` +(e.g. `target_link_libraries(lualib readline)`) and is not itself touched by +this module. Instead, this module declares a GLOBAL IMPORTED target literally +named `readline`, so CMake resolves those existing bare-name references to +this properly-detected library instead of falling back to an unqualified +linker search for `-lreadline`. + +Also provides the following variables: + + * `Readline_INCLUDE_DIRS`: Include directories necessary to use Readline. + * `Readline_LIBRARIES`: Libraries necessary to use Readline. +#]==] + +set(Readline_FOUND 0) + +find_path(Readline_INCLUDE_DIR + NAMES readline/readline.h + PATHS + /usr/include + /usr/local/include + /opt/homebrew/opt/readline/include # Homebrew, Apple Silicon (keg-only) + /usr/local/opt/readline/include # Homebrew, Intel macOS (keg-only) + DOC "Location of readline/readline.h") +mark_as_advanced(Readline_INCLUDE_DIR) + +find_library(Readline_LIBRARY + NAMES readline + PATHS + /usr/lib + /usr/local/lib + /opt/homebrew/opt/readline/lib + /usr/local/opt/readline/lib + DOC "Location of the readline library") +mark_as_advanced(Readline_LIBRARY) + +# On several platforms (e.g. macOS) readline's history API ships as a +# separate library that must also be linked; harmless to link where it +# doesn't exist separately, so it is optional rather than required below. +find_library(Readline_HISTORY_LIBRARY + NAMES history + PATHS + /usr/lib + /usr/local/lib + /opt/homebrew/opt/readline/lib + /usr/local/opt/readline/lib + DOC "Location of the readline history library") +mark_as_advanced(Readline_HISTORY_LIBRARY) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Readline + REQUIRED_VARS Readline_INCLUDE_DIR Readline_LIBRARY) + +if (Readline_FOUND) + if (NOT TARGET readline) + add_library(readline UNKNOWN IMPORTED GLOBAL) + set_target_properties(readline PROPERTIES + IMPORTED_LOCATION "${Readline_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Readline_INCLUDE_DIR}") + if (Readline_HISTORY_LIBRARY) + set_property(TARGET readline APPEND PROPERTY + INTERFACE_LINK_LIBRARIES "${Readline_HISTORY_LIBRARY}") + endif () + endif () + + set(Readline_INCLUDE_DIRS "${Readline_INCLUDE_DIR}") + set(Readline_LIBRARIES "${Readline_LIBRARY}") + if (Readline_HISTORY_LIBRARY) + list(APPEND Readline_LIBRARIES "${Readline_HISTORY_LIBRARY}") + endif () +endif () diff --git a/cmake/GenRevision.cmake b/cmake/GenRevision.cmake index 9d6f375039..fce5a86339 100644 --- a/cmake/GenRevision.cmake +++ b/cmake/GenRevision.cmake @@ -161,6 +161,24 @@ else() endif() endif() +# revision_data.h.in unconditionally references @dep_eluna_rev_hash@ / +# @dep_sd3_rev_hash@ (and _date@/_branch@), but the blocks above only ever +# set those variables when SCRIPT_LIB_ELUNA/SCRIPT_LIB_SD3 is enabled. When a +# module is skipped, give its revision variables clear placeholder values +# instead of leaving them undefined: configure_file() silently substitutes +# an unset @VAR@ with an empty string, which produced a garbled banner like +# "Eluna submodule revision: ( branch)" instead of saying it wasn't built. +if(NOT SCRIPT_LIB_ELUNA) + set(dep_eluna_rev_hash "not built") + set(dep_eluna_rev_date "n/a") + set(dep_eluna_rev_branch "disabled") +endif() +if(NOT SCRIPT_LIB_SD3) + set(dep_sd3_rev_hash "not built") + set(dep_sd3_rev_date "n/a") + set(dep_sd3_rev_branch "disabled") +endif() + # For package / copyright information we always need proper date string(REGEX MATCH "([0-9]+)-([0-9]+)-([0-9]+)" rev_date_fallback_match ${rev_date_fallback}) set(rev_year ${CMAKE_MATCH_1}) @@ -202,12 +220,12 @@ if( ) set(rev_hash_cached "${rev_hash}" CACHE INTERNAL "Cached commit-hash" ) set(rev_branch_cached "${rev_branch}" CACHE INTERNAL "Cached branch name" ) - if(SCRIPT_LIB_ELUNA) - set(dep_eluna_rev_hash_cached "${dep_eluna_rev_hash}" CACHE INTERNAL "Cached Eluna commit-hash" ) - set(dep_eluna_rev_branch_cached "${dep_eluna_rev_branch}" CACHE INTERNAL "Cached Eluna branch name" ) - endif() - if(SCRIPT_LIB_SD3) - set(dep_sd3_rev_hash_cached "${dep_sd3_rev_hash}" CACHE INTERNAL "Cached SD3 commit-hash" ) - set(dep_sd3_rev_branch_cached "${dep_sd3_rev_branch}" CACHE INTERNAL "Cached SD3 branch name" ) - endif() + # Cached unconditionally (not just when the module is enabled), so the + # "not built"/"disabled" placeholders set above are remembered too - + # otherwise every reconfigure with the module off would see a permanent + # cache mismatch and needlessly regenerate revision_data.h every time. + set(dep_eluna_rev_hash_cached "${dep_eluna_rev_hash}" CACHE INTERNAL "Cached Eluna commit-hash" ) + set(dep_eluna_rev_branch_cached "${dep_eluna_rev_branch}" CACHE INTERNAL "Cached Eluna branch name" ) + set(dep_sd3_rev_hash_cached "${dep_sd3_rev_hash}" CACHE INTERNAL "Cached SD3 commit-hash" ) + set(dep_sd3_rev_branch_cached "${dep_sd3_rev_branch}" CACHE INTERNAL "Cached SD3 branch name" ) endif() diff --git a/cmake/PCHSupport.cmake b/cmake/PCHSupport.cmake index fcbb6b8119..f87458d34e 100644 --- a/cmake/PCHSupport.cmake +++ b/cmake/PCHSupport.cmake @@ -27,6 +27,40 @@ # PRECOMPILED_SOURCE - MSVC specific source to do the actual precompilation. Ignored on other platforms # -function(ADD_CXX_PCH TARGET_NAME PRECOMPILED_HEADER PRECOMPILED_SOURCE) - target_precompile_headers(${TARGET_NAME} PRIVATE ${PRECOMPILED_HEADER}) +include(CheckCXXCompilerFlag) + +# Clang re-instantiates the PCH's templates in EVERY translation unit that loads it, which for +# a header set this size hands most of the saving straight back. This bakes the instantiations +# into the PCH once instead. GCC and MSVC already do the equivalent. (Clang >= 11.) +check_cxx_compiler_flag("-fpch-instantiate-templates" HAVE_FPCH_INSTANTIATE_TEMPLATES) + +# ADD_CXX_PCH(TARGET_NAME PRECOMPILED_HEADER [PRECOMPILED_SOURCE]) +# +# PRECOMPILED_SOURCE is accepted for backwards compatibility with the old hand-rolled MSVC +# /Yc scheme and ignored -- target_precompile_headers generates its own translation unit. +function(ADD_CXX_PCH TARGET_NAME PRECOMPILED_HEADER) + if(NOT TARGET ${TARGET_NAME}) + message(FATAL_ERROR "ADD_CXX_PCH: '${TARGET_NAME}' is not a target.") + endif() + + # Guard, because the failure this catches is invisible. ADD_CXX_PCH(Bots ${bots_PCH}) with + # bots_PCH unset collapses to ADD_CXX_PCH(Bots), which used to reach + # target_precompile_headers with an EMPTY header list -- a silent no-op. Bots' 277 sources + # went years without the precompiled header they all textually include anyway. Fail loudly. + if(NOT PRECOMPILED_HEADER) + message(FATAL_ERROR "ADD_CXX_PCH(${TARGET_NAME}): no precompiled header given.") + endif() + + get_filename_component(_pch "${PRECOMPILED_HEADER}" ABSOLUTE + BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + + if(NOT EXISTS "${_pch}") + message(FATAL_ERROR "ADD_CXX_PCH(${TARGET_NAME}): header '${_pch}' does not exist.") + endif() + + target_precompile_headers(${TARGET_NAME} PRIVATE "${_pch}") + + if(HAVE_FPCH_INSTANTIATE_TEMPLATES) + target_compile_options(${TARGET_NAME} PRIVATE -fpch-instantiate-templates) + endif() endfunction() diff --git a/dep b/dep index b05d623751..23bcbbf822 160000 --- a/dep +++ b/dep @@ -1 +1 @@ -Subproject commit b05d6237514468072a647f7c02a281e3b4edef26 +Subproject commit 23bcbbf822af04832f9a61f0c623ef26972d5bef diff --git a/dockercontainer/DockerFile-mangosd b/dockercontainer/DockerFile-mangosd index 1cb8e7e934..25dd0ddbc1 100644 --- a/dockercontainer/DockerFile-mangosd +++ b/dockercontainer/DockerFile-mangosd @@ -4,7 +4,7 @@ FROM ubuntu:22.04 AS build-step ENV TZ=US DEBIAN_FRONTEND=noninteractive RUN apt-get -y update -RUN apt-get -y install curl autoconf automake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool \ +RUN apt-get -y install curl autoconf automake libbz2-dev libssl-dev libmysqlclient-dev libtool \ build-essential lsb-release software-properties-common cmake libreadline-dev clang COPY . /mangoserver diff --git a/dockercontainer/DockerFile-realmd b/dockercontainer/DockerFile-realmd index 7d304e5362..e9bddb8637 100644 --- a/dockercontainer/DockerFile-realmd +++ b/dockercontainer/DockerFile-realmd @@ -4,7 +4,7 @@ FROM ubuntu:22.04 AS build-step ENV TZ=US DEBIAN_FRONTEND=noninteractive RUN apt-get -y update -RUN apt-get -y install curl autoconf automake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool \ +RUN apt-get -y install curl autoconf automake libbz2-dev libssl-dev libmysqlclient-dev libtool \ build-essential lsb-release software-properties-common cmake libreadline-dev clang COPY . /mangoserver diff --git a/linux/getmangos.sh b/linux/getmangos.sh index a6d2387618..a8b0fb1062 100755 --- a/linux/getmangos.sh +++ b/linux/getmangos.sh @@ -204,7 +204,7 @@ which apt-get >/dev/null 2>&1 if [ $? -eq 0 ]; then installer=1 - # On a fresh OS boot (EC2) libace was not found without first updating + # On a fresh OS boot (EC2) packages are not found without first updating apt-get update -y && apt-get -y install git lsb-release curl fi @@ -268,31 +268,31 @@ case ${VER} in "sarah") # Linux Mint 18 - Ubuntu Xenial based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "rosa") # Linux Mint 17.3 - Ubuntu Trusty based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "rafaela") # Linux Mint 17.2 - Ubuntu Trusty based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "rebecca") # Linux Mint 17.1 - Ubuntu Trusty based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "qiana") # Linux Mint 17 - Ubuntu Trusty based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "maya") # Linux Mint 13 - Ubuntu Precise based - su -c "aptitude -y install build-essential cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev" root + su -c "aptitude -y install build-essential cmake libbz2-dev libssl-dev libmysqlclient-dev" root ;; "betsy") # LMDE 2 - Debian Jessie based - su -c "aptitude -y install build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libace-dev libace-6.2.8 libssl-dev libmysqlclient-dev libtool zliblg-dev" root + su -c "aptitude -y install build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool zliblg-dev" root ;; *) OS_VER=0 @@ -303,39 +303,39 @@ case ${VER} in "precise") # Ubuntu 12.04 LTS - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "trusty") # Ubuntu 14.04 LTS - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "xenial") # Ubuntu 16.04 LTS - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "yakkety") # Ubuntu 16.10 - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "zesty") # Ubuntu 17.04 - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "artful") # Ubuntu 17.10 - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "bionic") # Ubuntu 18.04 LTS - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "disco") # Ubuntu 19.04 - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; "focal") # Ubuntu 20.04 - su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libace-dev libssl-dev libmysqlclient-dev libtool" root + su -c "apt-get -y install build-essential curl autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool" root ;; *) OS_VER=0 @@ -346,11 +346,11 @@ case ${VER} in "jessie") # Debian 8.0 "current" - su -c "aptitude -y install curl build-essential autoconf automake cmake libbz2-dev libace-dev libssl-dev default-libmysqlclient-dev libtool" root + su -c "aptitude -y install curl build-essential autoconf automake cmake libbz2-dev libssl-dev default-libmysqlclient-dev libtool" root ;; "stretch") # Debian Next - su -c "aptitude -y install curl build-essential autoconf automake cmake libbz2-dev libace-dev libssl-dev default-libmysqlclient-dev libtool" root + su -c "aptitude -y install curl build-essential autoconf automake cmake libbz2-dev libssl-dev default-libmysqlclient-dev libtool" root ;; *) OS_VER=0 @@ -361,11 +361,11 @@ case ${VER} in "santiago") # Red Hat 6.x - su -c "yum -y install curl build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libace-dev ace-6.3.3 libssl-dev libmysqlclient-dev libtool zliblg-dev" root + su -c "yum -y install curl build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool zliblg-dev" root ;; "maipo") # Red Hat 7.x - su -c "yum -y install curl build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libace-dev ace-6.3.3 libssl-dev libmysqlclient-dev libtool zliblg-dev" root + su -c "yum -y install curl build-essential linux-headers-$(uname -r) autoconf automake cmake libbz2-dev libssl-dev libmysqlclient-dev libtool zliblg-dev" root ;; *) OS_VER=0 @@ -376,13 +376,10 @@ case ${VER} in "Core") # Default CentOS - Adding necessary RPM third-party. - rpm -Uv ftp://ftp.pbone.net/mirror/ftp5.gwdg.de/pub/opensuse/repositories/devel:/libraries:/ACE:/micro/CentOS_7/x86_64/ace-6.3.3-55.1.x86_64.rpm rpm -Uv ftp://rpmfind.net/linux/centos/7/os/x86_64/Packages/perl-Net-Telnet-3.03-19.el7.noarch.rpm - rpm -Uv ftp://ftp.pbone.net/mirror/ftp5.gwdg.de/pub/opensuse/repositories/devel:/libraries:/ACE:/micro:/versioned/CentOS_7/x86_64/mpc-6.3.3-42.1.x86_64.rpm rpm -Uv ftp://rpmfind.net/linux/centos/7/os/x86_64/Packages/libtool-2.4.2-22.el7_3.x86_64.rpm - rpm -Uv ftp://ftp.pbone.net/mirror/ftp5.gwdg.de/pub/opensuse/repositories/devel:/libraries:/ACE:/micro/CentOS_7/x86_64/ace-devel-6.3.3-55.1.x86_64.rpm su -c "yum -y install epel-release" - su -c "yum -y install curl autoconf automake cmake3 ace-devel ace-6.3.3 openssl-devel mysql-devel libtool gcc-c++ bzip2-devel" root + su -c "yum -y install curl autoconf automake cmake3 openssl-devel mysql-devel libtool gcc-c++ bzip2-devel" root ;; *) OS_VER=0 @@ -394,68 +391,16 @@ "TwentyFive") # Fedora 25 - Adding necessary RPM third-party. su -c "yum -y install autoconf automake libtool gcc-c++" root - # Getting and building ACE. Not provided in RPM for Fedora... - rm -rf ACE-6.3.3.tar.bz2 - rm -rf ACE_wrappers - wget ftp://download.dre.vanderbilt.edu/previous_versions/ACE-6.3.3.tar.bz2 - tar xjvf ACE-6.3.3.tar.bz2 - export ACE_ROOT=/root/ACE_wrappers - echo '#include "ace/config-linux.h"' >> $ACE_ROOT/ace/config.h - # We want this to output $(ACE_ROOT) without expansion. - # This is to be resolved during make compilation, not now. - # shellcheck disable=SC2016 - echo 'include $(ACE_ROOT)/include/makeinclude/platform_linux.GNU' >> $ACE_ROOT/include/makeinclude/platform_macros.GNU - echo 'INSTALL_PREFIX=/usr/local' >> $ACE_ROOT/include/makeinclude/platform_macros.GNU - export LD_LIBRARY_PATH=$ACE_ROOT/lib:$LD_LIBRARY_PATH - CD $ACE_ROOT - make - make install - cd ~ # Installing remaining dependencies.. su -c "yum -y install cmake openssl-devel mariadb-devel" root ;; "Forty") # Fedora 40 - Adding necessary RPM third-party. - aceVersion='7.1.0' - if [[ -e "/usr/local/lib/libACE.so.$aceVersion" ]]; then - Log "ACE Library version $aceVersion already present. Skipping download, build, and install process." 1 - else - aceURLBase='ftp://download.dre.vanderbilt.edu/previous_versions' - acePackage="ACE-$aceVersion.tar.bz2" - aceDirName='ACE_wrappers' - for packageName in 'autoconf' 'automake' 'libtool' 'gcc-c++'; do - if ! rpm -qa "name=$packageName"; then - dnf -y install "$packageName" - fi - done - - # Getting and building ACE. - # Not provided by typical Fedora software repositories, - # so downloading and building directly. - - if [[ ! -e "$acePackage" ]]; then - curl --remote-name "$aceURLBase/$acePackage" - fi - if [[ -e "$aceDirName" ]]; then - tar xjf "$acePackage" + for packageName in 'autoconf' 'automake' 'libtool' 'gcc-c++'; do + if ! rpm -qa "name=$packageName"; then + dnf -y install "$packageName" fi - - export ACE_ROOT="$PWD/$aceDirName" - pushd "$ACE_ROOT" - - echo '#include "ace/config-linux.h"' >> 'ace/config.h' - # We want this to output $(ACE_ROOT) without expansion. - # This is to be resolved during make compilation, not now. - # shellcheck disable=SC2016 - echo 'include $(ACE_ROOT)/include/makeinclude/platform_linux.GNU' >> 'include/makeinclude/platform_macros.GNU' - echo 'INSTALL_PREFIX=/usr/local' >> 'include/makeinclude/platform_macros.GNU' - - export LD_LIBRARY_PATH=$ACE_ROOT/lib:$LD_LIBRARY_PATH - - make - make install - popd - fi + done # Installing remaining dependencies.. @@ -1109,16 +1054,13 @@ $DLGAPP \ --backtitle "MaNGOS Linux Build Configuration" \ --title "Build Options" \ - --checklist "Please select your build options" 0 56 7 \ - 1 "Enable Debug" Off \ - 2 "Use Standard Malloc" On \ - 3 "Use External ACE Libraries" On \ - 4 "Use PostgreSQL Instead Of MySQL/MariaDB" Off \ - 5 "Build Client Tools" On \ - 6 "Use SD3" On \ - 7 "Use Eluna" On \ - 8 "Use SOAP" Off \ - 9 "Use Player Bots AI" Off \ + --checklist "Please select your build options" 0 56 6 \ + 1 "Build Client Tools" On \ + 2 "Use Eluna" On \ + 3 "Use SD3" On \ + 4 "Use Player Bots AI" Off \ + 5 "Use SOAP" Off \ + 6 "Enable Debug" Off \ 3>&2 2>&1 1>&3 ) fi diff --git a/src/game/BattleGround/BattleGround.cpp b/src/game/BattleGround/BattleGround.cpp index 4f7a271660..67e4935e60 100644 --- a/src/game/BattleGround/BattleGround.cpp +++ b/src/game/BattleGround/BattleGround.cpp @@ -55,6 +55,7 @@ #include "Chat.h" #ifdef ENABLE_ELUNA #include "LuaEngine.h" +#include #endif /* ENABLE_ELUNA */ namespace MaNGOS diff --git a/src/game/BattleGround/BattleGroundMgr.cpp b/src/game/BattleGround/BattleGroundMgr.cpp index 9682844f31..843be0a1fe 100644 --- a/src/game/BattleGround/BattleGroundMgr.cpp +++ b/src/game/BattleGround/BattleGroundMgr.cpp @@ -214,12 +214,8 @@ void BattleGroundMgr::Update(uint32 /*diff*/) { std::vector scheduled; { - // create mutex - // ACE_Guard guard(SchedulerLock); - // copy vector and clear the other scheduled = std::vector(m_QueueUpdateScheduler); m_QueueUpdateScheduler.clear(); - // release lock } for (uint8 i = 0; i < scheduled.size(); ++i) diff --git a/src/game/BattleGround/BattleGroundMgr.h b/src/game/BattleGround/BattleGroundMgr.h index faabb3cf2b..9b276e10fd 100644 --- a/src/game/BattleGround/BattleGroundMgr.h +++ b/src/game/BattleGround/BattleGroundMgr.h @@ -56,7 +56,6 @@ #include "Common.h" #include "Policies/Singleton.h" #include "BattleGround.h" -#include #include "Utilities/EventProcessor.h" /** @@ -239,8 +238,6 @@ class BattleGroundQueue uint32 GetAverageQueueWaitTime(GroupQueueInfo* ginfo, BattleGroundBracketId bracket_id); private: - ACE_Recursive_Thread_Mutex m_Lock; /**< Mutex that should not allow changing private data, nor allowing to update Queue during private data change. */ - /** * @brief Map for storing queued players. */ @@ -747,7 +744,6 @@ class BattleGroundMgr */ static bool IsBGWeekend(BattleGroundTypeId bgTypeId); private: - ACE_Thread_Mutex SchedulerLock; /**< Mutex to protect the scheduler from concurrent access. */ BattleMastersMap mBattleMastersMap; /**< Map storing battle master entries. */ CreatureBattleEventIndexesMap m_CreatureBattleEventIndexMap; /**< Map storing creature battle event indexes. */ GameObjectBattleEventIndexesMap m_GameObjectBattleEventIndexMap; /**< Map storing game object battle event indexes. */ diff --git a/src/game/BattleGround/BattleGroundMgrUtil.cpp b/src/game/BattleGround/BattleGroundMgrUtil.cpp index 0da8cae937..3674b4b537 100644 --- a/src/game/BattleGround/BattleGroundMgrUtil.cpp +++ b/src/game/BattleGround/BattleGroundMgrUtil.cpp @@ -141,8 +141,7 @@ void BattleGroundMgr::ToggleTesting() */ void BattleGroundMgr::ScheduleQueueUpdate(BattleGroundQueueTypeId bgQueueTypeId, BattleGroundTypeId bgTypeId, BattleGroundBracketId bracket_id) { - // ACE_Guard guard(SchedulerLock); - // we will use only 1 number created of bgTypeId and bracket_id + // combine bgQueueTypeId, bgTypeId and bracket_id into a single schedule id uint32 schedule_id = (bgQueueTypeId << 16) | (bgTypeId << 8) | bracket_id; bool found = false; for (uint8 i = 0; i < m_QueueUpdateScheduler.size(); ++i) diff --git a/src/game/BattleGround/BattleGroundQueue.cpp b/src/game/BattleGround/BattleGroundQueue.cpp index 3118c0fa74..452f8bb5e8 100644 --- a/src/game/BattleGround/BattleGroundQueue.cpp +++ b/src/game/BattleGround/BattleGroundQueue.cpp @@ -189,7 +189,6 @@ GroupQueueInfo* BattleGroundQueue::AddGroup(Player* leader, Group* grp, BattleGr // add players from group to ginfo { - // ACE_Guard guard(m_Lock); if (grp) { for (GroupReference* itr = grp->GetFirstMember(); itr != NULL; itr = itr->next()) @@ -339,9 +338,6 @@ uint32 BattleGroundQueue::GetAverageQueueWaitTime(GroupQueueInfo* ginfo, BattleG */ void BattleGroundQueue::RemovePlayer(ObjectGuid guid, bool decreaseInvitedCount) { - // Player *plr = sObjectMgr.GetPlayer(guid); - // ACE_Guard guard(m_Lock); - int32 bracket_id = -1; // signed for proper for-loop finish QueuedPlayersMap::iterator itr; @@ -434,7 +430,6 @@ void BattleGroundQueue::RemovePlayer(ObjectGuid guid, bool decreaseInvitedCount) */ bool BattleGroundQueue::IsPlayerInvited(ObjectGuid pl_guid, const uint32 bgInstanceGuid, const uint32 removeTime) { - // ACE_Guard g(m_Lock); QueuedPlayersMap::const_iterator qItr = m_QueuedPlayers.find(pl_guid); return (qItr != m_QueuedPlayers.end() && qItr->second.GroupInfo->IsInvitedToBGInstanceGUID == bgInstanceGuid && @@ -453,7 +448,6 @@ bool BattleGroundQueue::IsPlayerInvited(ObjectGuid pl_guid, const uint32 bgInsta */ bool BattleGroundQueue::GetPlayerGroupInfoData(ObjectGuid guid, GroupQueueInfo* ginfo) { - // ACE_Guard g(m_Lock); QueuedPlayersMap::const_iterator qItr = m_QueuedPlayers.find(guid); if (qItr == m_QueuedPlayers.end()) { @@ -793,7 +787,6 @@ bool BattleGroundQueue::CheckNormalMatch(BattleGroundBracketId bracket_id, uint3 */ void BattleGroundQueue::Update(BattleGroundTypeId bgTypeId, BattleGroundBracketId bracket_id) { - // ACE_Guard guard(m_Lock); // if no players in queue - do nothing if (m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_ALLIANCE].empty() && m_QueuedGroups[bracket_id][BG_QUEUE_PREMADE_HORDE].empty() && diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index 88147d9637..24f5fc2728 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -289,6 +289,8 @@ target_link_libraries(game g3dlite $<$:mangosscript> $<$:lualib> + PRIVATE + mangos_openssl_strict ) # Generate precompiled header diff --git a/src/game/ChatCommands/CreatureCommands.cpp b/src/game/ChatCommands/CreatureCommands.cpp index db0f3b53d6..24e116fcd8 100644 --- a/src/game/ChatCommands/CreatureCommands.cpp +++ b/src/game/ChatCommands/CreatureCommands.cpp @@ -356,7 +356,8 @@ bool ChatHandler::HandleNpcAIInfoCommand(char* /*args*/) std::string strScript = pTarget->GetScriptName(); std::string strAI = pTarget->GetAIName(); - char const* cstrAIClass = pTarget->AI() ? typeid(*pTarget->AI()).name() : " - "; + CreatureAI* pAI = pTarget->AI(); + char const* cstrAIClass = pAI ? typeid(*pAI).name() : " - "; PSendSysMessage(LANG_NPC_AI_NAMES, strAI.empty() ? " - " : strAI.c_str(), diff --git a/src/game/Maps/MapUpdater.cpp b/src/game/Maps/MapUpdater.cpp index 55e34f0d10..db817dae38 100644 --- a/src/game/Maps/MapUpdater.cpp +++ b/src/game/Maps/MapUpdater.cpp @@ -24,168 +24,150 @@ /** * @file MapUpdater.cpp - * @brief Implementation of the MapUpdater class for managing map update requests. - * - * This file contains the implementation of the MapUpdater class which is responsible - * for managing and scheduling map updates. It includes: - * - Map update request scheduling - * - Thread management for concurrent map updates - * - Synchronization mechanisms for pending requests + * @brief Implementation of the parallel map-update worker pool. */ #include "MapUpdater.h" -#include "DelayExecutor.h" + #include "Map.h" #include "DatabaseEnv.h" -#include "Timer.h" - -#include -#include +#include "Log.h" +#include +#include -/** - * @brief A request to update a map. - */ -class MapUpdateRequest : public ACE_Method_Request -{ - private: - Map& m_map; ///< Reference to the map to be updated. - MapUpdater& m_updater; ///< Reference to the map updater. - ACE_UINT32 m_diff; ///< Time difference for the update. - - public: - /** - * @brief Constructor for MapUpdateRequest. - * @param m Reference to the map. - * @param u Reference to the map updater. - * @param d Time difference for the update. - */ - MapUpdateRequest(Map& m, MapUpdater& u, ACE_UINT32 d) - : m_map(m), m_updater(u), m_diff(d) - { - } - - /** - * @brief Executes the map update request. - * @return Always returns 0. - */ - virtual int call() - { - m_map.Update(m_diff); - m_updater.update_finished(); - return 0; - } -}; - -/** - * @brief Constructor for MapUpdater. - */ -MapUpdater::MapUpdater(): -m_executor(), m_mutex(), m_condition(m_mutex), pending_requests(0) +MapUpdater::MapUpdater() + : m_pending(0), m_stop(false) { } -/** - * @brief Destructor for MapUpdater. - */ MapUpdater::~MapUpdater() { deactivate(); } -/** - * @brief Activates the map updater with the specified number of threads. - * @param num_threads Number of threads to activate. - * @return Result of the activation. - */ int MapUpdater::activate(size_t num_threads) { - return m_executor._activate((int)num_threads); + if (num_threads == 0 || activated()) + { + return -1; + } + + { + std::lock_guard guard(m_mutex); + m_stop = false; + } + + m_workers.reserve(num_threads); + for (size_t i = 0; i < num_threads; ++i) + { + m_workers.emplace_back([this] { workerLoop(); }); + } + + return 0; } -/** - * @brief Deactivates the map updater. - * @return Result of the deactivation. - */ int MapUpdater::deactivate() { - sLog.outString("[shutdown] MapUpdater::deactivate: draining pending map updates (pending=%zu)", pending_requests); + if (!activated()) + { + return 0; + } + + sLog.outString("[shutdown] MapUpdater::deactivate: draining pending map updates (pending=%zu)", m_pending); + + // Drain first: a map must not be left half-updated, and Map::Update touches world + // state that is torn down right after this returns. wait(); + sLog.outString("[shutdown] MapUpdater::deactivate: pending drained; joining worker threads"); - int r = m_executor.deactivate(); - sLog.outString("[shutdown] MapUpdater::deactivate: worker threads joined"); - return r; -} -/** - * @brief Waits for all pending requests to be processed. - * @return Always returns 0. - */ -int MapUpdater::wait() -{ - uint32 start = getMSTime(); { - ACE_GUARD_RETURN(ACE_Thread_Mutex, guard, m_mutex, -1); + std::lock_guard guard(m_mutex); + m_stop = true; + } + m_taskAdded.notify_all(); - while (pending_requests > 0) + for (std::thread& worker : m_workers) + { + if (worker.joinable()) { - m_condition.wait(); + worker.join(); } } + m_workers.clear(); - uint32 waited = getMSTimeDiff(start, getMSTime()); - if (waited > 1000) - { - sLog.outError("MapUpdater::wait() took %u ms", waited); - } + sLog.outString("[shutdown] MapUpdater::deactivate: worker threads joined"); return 0; } -/** - * @brief Schedules a map update. - * @param map Reference to the map to be updated. - * @param diff Time difference for the update. - * @return Result of the scheduling. - */ -int MapUpdater::schedule_update(Map& map, ACE_UINT32 diff) +bool MapUpdater::activated() { - ACE_GUARD_RETURN(ACE_Thread_Mutex, guard, m_mutex, -1); + return !m_workers.empty(); +} - ++pending_requests; +int MapUpdater::schedule_update(Map& map, uint32 diff) +{ + std::unique_lock guard(m_mutex); - if (m_executor.execute(new MapUpdateRequest(map, *this, diff)) == -1) + if (m_stop || m_workers.empty()) { - ACE_DEBUG((LM_ERROR, ACE_TEXT("(%t) \n"), ACE_TEXT("Failed to schedule Map Update"))); - - --pending_requests; + sLog.outError("MapUpdater::schedule_update: pool is not running, map %u not updated", map.GetId()); return -1; } + m_tasks.push(Task(&map, diff)); + ++m_pending; + + guard.unlock(); + m_taskAdded.notify_one(); + return 0; } -/** - * @brief Checks if the map updater is activated. - * @return True if activated, false otherwise. - */ -bool MapUpdater::activated() +int MapUpdater::wait() { - return m_executor.activated(); + std::unique_lock guard(m_mutex); + + m_taskDone.wait(guard, [this] { return m_pending == 0; }); + + return 0; } -/** - * @brief Called when a map update is finished. - */ -void MapUpdater::update_finished() +void MapUpdater::workerLoop() { - ACE_GUARD(ACE_Thread_Mutex, guard, m_mutex); - - if (pending_requests == 0) + for (;;) { - ACE_ERROR((LM_ERROR, ACE_TEXT("(%t)\n"), ACE_TEXT("MapUpdater::update_finished BUG, report to devs"))); - return; - } + Task task(nullptr, 0); + + { + std::unique_lock guard(m_mutex); + + m_taskAdded.wait(guard, [this] { return m_stop || !m_tasks.empty(); }); + + // Only retire once the queue is genuinely empty, so a stop racing with a + // still-queued tick cannot drop that map's update on the floor. + if (m_tasks.empty()) + { + if (m_stop) + { + return; + } + continue; + } + + task = m_tasks.front(); + m_tasks.pop(); + } - --pending_requests; + task.first->Update(task.second); - m_condition.broadcast(); + { + std::lock_guard guard(m_mutex); + --m_pending; + } + + // Outside the lock: wait() only ever cares about the count reaching zero, and + // notifying while holding the mutex would just make the waiter block again. + m_taskDone.notify_all(); + } } diff --git a/src/game/Maps/MapUpdater.h b/src/game/Maps/MapUpdater.h index 803c70d861..95bf419986 100644 --- a/src/game/Maps/MapUpdater.h +++ b/src/game/Maps/MapUpdater.h @@ -24,87 +24,88 @@ /** * @file MapUpdater.h - * @brief Header file for the MapUpdater class. + * @brief Worker pool that ticks maps in parallel. * - * This file contains the definition of the MapUpdater class which is responsible - * for managing and scheduling map updates. It includes: - * - MapUpdateRequest class for handling individual update requests - * - Thread management for concurrent map updates - * - Synchronization mechanisms for pending requests + * The world thread hands each map's Update() to this pool via schedule_update(), then + * blocks in wait() until the whole tick has been processed. */ #ifndef _MAP_UPDATER_H_INCLUDED #define _MAP_UPDATER_H_INCLUDED -#include -#include +#include "Platform/Define.h" -#include "DelayExecutor.h" +#include +#include +#include +#include +#include +#include +#include class Map; /** - * @brief The MapUpdater class is responsible for managing map update requests. + * @brief Schedules map updates across a pool of worker threads. */ class MapUpdater { public: - /** - * @brief Constructor for MapUpdater. - */ - MapUpdater(); - /** - * @brief Destructor for MapUpdater. - */ - virtual ~MapUpdater(); + MapUpdater(); + ~MapUpdater(); - friend class MapUpdateRequest; + MapUpdater(const MapUpdater&) = delete; + MapUpdater& operator=(const MapUpdater&) = delete; /** - * @brief Schedules a map update. - * @param map Reference to the map to be updated. - * @param diff Time difference for the update. - * @return Result of the scheduling. + * @brief Queue map.Update(diff) for a worker. + * @return 0 on success, -1 if the pool is not running. */ - int schedule_update(Map& map, ACE_UINT32 diff); + int schedule_update(Map& map, uint32 diff); /** - * @brief Waits for all pending requests to be processed. - * @return Always returns 0. + * @brief Block until every scheduled update has finished. + * + * This is the tick barrier: the world thread must not advance until every map + * queued this tick has been updated. + * + * @return Always 0. */ int wait(); /** - * @brief Activates the map updater with the specified number of threads. - * @param num_threads Number of threads to activate. - * @return Result of the activation. + * @brief Start @p num_threads workers. + * @return 0 on success, -1 on failure. */ int activate(size_t num_threads); /** - * @brief Deactivates the map updater. - * @return Result of the deactivation. + * @brief Drain outstanding updates, then stop and join the workers. + * @return Always 0. */ int deactivate(); - /** - * @brief Checks if the map updater is activated. - * @return True if activated, false otherwise. - */ + /// True while worker threads are running. bool activated(); private: - DelayExecutor m_executor; ///< Executor for handling delayed tasks. - ACE_Thread_Mutex m_mutex; ///< Mutex for synchronizing access to pending requests. - ACE_Condition_Thread_Mutex m_condition; ///< Condition variable for signaling when requests are processed. - size_t pending_requests; ///< Number of pending update requests. - /** - * @brief Called when a map update is finished. - */ - void update_finished(); + /// One queued map tick. + typedef std::pair Task; + + /// Worker body: run tasks until stopped and the queue has drained. + void workerLoop(); + + std::vector m_workers; + std::queue m_tasks; + + std::mutex m_mutex; ///< Guards m_tasks, m_pending and m_stop + std::condition_variable m_taskAdded; ///< Wakes a worker when work arrives + std::condition_variable m_taskDone; ///< Wakes wait() once m_pending hits zero + + size_t m_pending; ///< Scheduled but not yet finished updates + bool m_stop; ///< Set by deactivate() to retire the workers }; #endif //_MAP_UPDATER_H_INCLUDED - diff --git a/src/game/MotionGenerators/PathFinder.cpp b/src/game/MotionGenerators/PathFinder.cpp index 27fed2f059..f0b504cf10 100644 --- a/src/game/MotionGenerators/PathFinder.cpp +++ b/src/game/MotionGenerators/PathFinder.cpp @@ -32,6 +32,8 @@ #include "Log.h" #include "Player.h" +#include + ////////////////// PathFinder ////////////////// /** diff --git a/src/game/Object/ObjectAccessor.cpp b/src/game/Object/ObjectAccessor.cpp index 186fd01bcc..57ad8be04d 100644 --- a/src/game/Object/ObjectAccessor.cpp +++ b/src/game/Object/ObjectAccessor.cpp @@ -37,9 +37,7 @@ #include -#define CLASS_LOCK MaNGOS::ClassLevelLockable - INSTANTIATE_SINGLETON_2(ObjectAccessor, CLASS_LOCK); -INSTANTIATE_CLASS_MUTEX(ObjectAccessor, ACE_Recursive_Thread_Mutex); +// Singleton instantiation is implicit now (MaNGOS::Singleton is a Meyers singleton). /** * @brief Initializes the global object accessor. @@ -140,7 +138,7 @@ Player* ObjectAccessor::FindPlayer(ObjectGuid guid, bool inWorld /*= true*/) */ Player* ObjectAccessor::FindPlayerByName(const char* name) { - ACE_READ_GUARD_RETURN(HashMapHolder::LockType, guard, i_playerMap.GetLock(), nullptr) + std::shared_lock::LockType> guard(i_playerMap.GetLock()); for (auto& iter : i_playerMap.GetContainer()) { if (iter.second->IsInWorld() && (::strcmp(name, iter.second->GetName()) == 0)) @@ -195,7 +193,7 @@ void ObjectAccessor::KickPlayer(ObjectGuid guid) */ Corpse* ObjectAccessor::GetCorpseForPlayerGUID(ObjectGuid guid) { - ACE_GUARD_RETURN(LockType, guard, i_corpseGuard, nullptr) + std::lock_guard guard(i_corpseGuard); Player2CorpsesMapType::iterator iter = i_player2corpse.find(guid); @@ -217,7 +215,7 @@ void ObjectAccessor::RemoveCorpse(Corpse* corpse) { MANGOS_ASSERT(corpse && corpse->GetType() != CORPSE_BONES); - ACE_GUARD(LockType, guard, i_corpseGuard) + std::lock_guard guard(i_corpseGuard); Player2CorpsesMapType::iterator iter = i_player2corpse.find(corpse->GetOwnerGuid()); if (iter == i_player2corpse.end()) @@ -244,7 +242,7 @@ void ObjectAccessor::AddCorpse(Corpse* corpse) { MANGOS_ASSERT(corpse && corpse->GetType() != CORPSE_BONES); - ACE_GUARD(LockType, guard, i_corpseGuard) + std::lock_guard guard(i_corpseGuard); MANGOS_ASSERT(i_player2corpse.find(corpse->GetOwnerGuid()) == i_player2corpse.end()); i_player2corpse[corpse->GetOwnerGuid()] = corpse; @@ -265,7 +263,7 @@ void ObjectAccessor::AddCorpse(Corpse* corpse) */ void ObjectAccessor::AddCorpsesToGrid(GridPair const& gridpair, GridType& grid, Map* map) { - ACE_GUARD(LockType, guard, i_corpseGuard) + std::lock_guard guard(i_corpseGuard); for (Player2CorpsesMapType::iterator iter = i_player2corpse.begin(); iter != i_player2corpse.end(); ++iter) { diff --git a/src/game/Object/ObjectAccessor.h b/src/game/Object/ObjectAccessor.h index 0b1b2053b9..6d0b51aa15 100644 --- a/src/game/Object/ObjectAccessor.h +++ b/src/game/Object/ObjectAccessor.h @@ -28,7 +28,7 @@ * * This file contains the ObjectAccessor singleton class which provides centralized * access to all players and corpses in the game world. It maintains hash maps for - * fast GUID-based lookups with thread-safe access using ACE synchronization primitives. + * fast GUID-based lookups with thread-safe access. * * Key responsibilities: * - Global player lookups by GUID or name @@ -50,14 +50,14 @@ #include "Common.h" #include "Platform/Define.h" #include "Policies/Singleton.h" -#include -#include "Policies/ThreadingModel.h" #include "UpdateData.h" #include "GridDefines.h" #include "Object.h" #include "Player.h" #include "Corpse.h" +#include +#include #include class Unit; @@ -77,14 +77,14 @@ class Map; /// - Player-to-corpse associations for PvP mechanics /// - Batch operations (save all players, etc.) /// -/// All access is protected by ACE synchronization primitives to prevent -/// race conditions during concurrent reads and writes. +/// All access is mutex-protected to prevent race conditions during concurrent +/// reads and writes. /// /// @note This is a thread-safe singleton, should not be instantiated directly /// @see sObjectAccessor for the singleton accessor macro -class ObjectAccessor : public MaNGOS::Singleton > +class ObjectAccessor : public MaNGOS::Singleton { - friend class MaNGOS::OperatorNew; + friend class MaNGOS::Singleton; ObjectAccessor(); ~ObjectAccessor(); @@ -94,87 +94,45 @@ class ObjectAccessor : public MaNGOS::Singleton struct HashMapHolder { - /// @brief Underlying hash map type (ObjectGuid -> Object pointer) - using MapType = std::unordered_map; - /// @brief Lock type for synchronization - using LockType = ACE_RW_Thread_Mutex; + using MapType = std::unordered_map; + using LockType = std::shared_mutex; - /// @brief Constructor initializes empty map - HashMapHolder() : i_lock(nullptr), m_objectMap() {} - - /// @brief Insert an object into the map with write lock. - /// - /// @param o Pointer to object to insert void Insert(T* o) { - ACE_WRITE_GUARD(LockType, guard, i_lock) + std::unique_lock guard(i_lock); m_objectMap[o->GetObjectGuid()] = o; } - /// @brief Remove an object from the map with write lock. - /// - /// @param o Pointer to object to remove void Remove(T* o) { - ACE_WRITE_GUARD(LockType, guard, i_lock) + std::unique_lock guard(i_lock); m_objectMap.erase(o->GetObjectGuid()); } - /// @brief Find an object by GUID with read lock. - /// - /// @param guid Object GUID to search for - /// @return Pointer to object if found, nullptr otherwise T* Find(ObjectGuid guid) { - ACE_READ_GUARD_RETURN (LockType, guard, i_lock, nullptr) + std::shared_lock guard(i_lock); auto itr = m_objectMap.find(guid); return (itr != m_objectMap.end()) ? itr->second : nullptr; } - /// @brief Get reference to the underlying container (for external iteration). - /// - /// @return Reference to the unordered_map - /// @warning Caller must acquire lock separately before iterating - inline MapType& GetContainer() - { - return m_objectMap; - } - - /// @brief Get reference to the synchronization lock. - /// - /// @return Reference to the ACE RW thread mutex - /// @note Use ACE guard macros when accessing this - inline LockType& GetLock() - { - return i_lock; - } + /// External iteration; caller must hold GetLock() for the duration. + inline MapType& GetContainer() { return m_objectMap; } + inline LockType& GetLock() { return i_lock; } LockType i_lock; MapType m_objectMap; - /// @brief Cache line guard to prevent false sharing (512 bytes padding) - char _cache_guard[512]; - + char _cache_guard[512]; ///< padding to avoid false sharing }; using Player2CorpsesMapType = std::unordered_map; - using LockType = ACE_Recursive_Thread_Mutex; + using LockType = std::mutex; ///< guards i_player2corpse; all uses are flat, non-recursive public: @@ -327,7 +285,7 @@ class ObjectAccessor : public MaNGOS::Singleton void DoForAllPlayers(F&& f) { - ACE_READ_GUARD(HashMapHolder::LockType, g, i_playerMap.GetLock()) + std::shared_lock::LockType> g(i_playerMap.GetLock()); for (auto& iter : i_playerMap.GetContainer()) { if (iter.second != nullptr) diff --git a/src/game/Object/ObjectMgr.cpp b/src/game/Object/ObjectMgr.cpp index 333a26c8ac..5122963620 100644 --- a/src/game/Object/ObjectMgr.cpp +++ b/src/game/Object/ObjectMgr.cpp @@ -57,6 +57,7 @@ #include "ItemEnchantmentMgr.h" #include +#include INSTANTIATE_SINGLETON_1(ObjectMgr); diff --git a/src/game/Object/ObjectMgr.h b/src/game/Object/ObjectMgr.h index 4ab030ec66..f2716d7c53 100644 --- a/src/game/Object/ObjectMgr.h +++ b/src/game/Object/ObjectMgr.h @@ -115,7 +115,7 @@ typedef UNORDERED_MAP < uint32/*mapid*/, CellObjectGuidsMap > MapObjectGuids; #define MAX_CREATURE_AI_TEXT_STRING_ID (-1000000) // Anything below MAX_CREATURE_AI_TEXT_STRING_ID is handled by the external script lib -static_assert(MAX_DB_SCRIPT_STRING_ID < ACE_INT32_MAX, "Must scope with int32 range"); +static_assert(MAX_DB_SCRIPT_STRING_ID < INT32_MAX, "Must scope with int32 range"); struct MangosStringLocale { diff --git a/src/game/Object/PetAI.cpp b/src/game/Object/PetAI.cpp index 6b7d426ed2..bf3e339450 100644 --- a/src/game/Object/PetAI.cpp +++ b/src/game/Object/PetAI.cpp @@ -36,6 +36,8 @@ #include "GridNotifiersImpl.h" #include "CellImpl.h" +#include + /** * @brief Determines whether PetAI can control the given creature. * diff --git a/src/game/Server/WorldSession.cpp b/src/game/Server/WorldSession.cpp index 7a393063a4..d198219cc4 100644 --- a/src/game/Server/WorldSession.cpp +++ b/src/game/Server/WorldSession.cpp @@ -45,7 +45,7 @@ * @see Opcodes.cpp for opcode registration */ -#include "WorldSocket.h" // must be first to make ACE happy with ACE includes in it +#include "WorldSocket.h" #include "Common.h" #include "Database/DatabaseEnv.h" #include "Log.h" @@ -72,6 +72,7 @@ // Warden #include "WardenWin.h" #include "WardenMac.h" +#include /** * @brief Helper for Map session filtering @@ -144,17 +145,16 @@ bool WorldSessionFilter::Process(WorldPacket* packet) } /// WorldSession constructor -WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, time_t mute_time, LocaleConstant locale) +WorldSession::WorldSession(uint32 id, std::shared_ptr sock, AccountTypes sec, time_t mute_time, LocaleConstant locale) : m_muteTime(mute_time), - _player(NULL), m_Socket(sock), _security(sec), _accountId(id), _warden(NULL), _build(0), _logoutTime(0), + _player(NULL), m_Socket(std::move(sock)), _security(sec), _accountId(id), _warden(NULL), _build(0), _logoutTime(0), m_inQueue(false), m_playerLoading(false), m_playerLogout(false), m_playerRecentlyLogout(false), m_playerSave(false), m_sessionDbcLocale(sWorld.GetAvailableDbcLocale(locale)), m_sessionDbLocaleIndex(sObjectMgr.GetIndexForLocale(locale)), m_latency(0), m_clientTimeDelay(0), m_tutorialState(TUTORIALDATA_UNCHANGED), m_npcWatchLastGuid() { - if (sock) + if (m_Socket) { - m_Address = sock->GetRemoteAddress(); - sock->AddReference(); + m_Address = m_Socket->GetRemoteAddress(); } } @@ -171,8 +171,7 @@ WorldSession::~WorldSession() if (m_Socket) { m_Socket->CloseSocket(); - m_Socket->RemoveReference(); - m_Socket = NULL; + m_Socket = nullptr; } // Warden @@ -431,8 +430,7 @@ bool WorldSession::Update(PacketFilter& updater) ///- Cleanup socket pointer if need if (m_Socket && m_Socket->IsClosed()) { - m_Socket->RemoveReference(); - m_Socket = NULL; + m_Socket = nullptr; } // Warden diff --git a/src/game/Server/WorldSession.h b/src/game/Server/WorldSession.h index ae98bbf1e8..4c962f05ff 100644 --- a/src/game/Server/WorldSession.h +++ b/src/game/Server/WorldSession.h @@ -35,6 +35,7 @@ #include "ObjectGuid.h" #include "AuctionHouseMgr.h" #include "Item.h" +#include struct ItemPrototype; struct AuctionEntry; @@ -223,7 +224,7 @@ class WorldSession * @param mute_time Mute time * @param locale Locale */ - WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, time_t mute_time, LocaleConstant locale); + WorldSession(uint32 id, std::shared_ptr sock, AccountTypes sec, time_t mute_time, LocaleConstant locale); /** * @brief Destructor @@ -892,7 +893,7 @@ class WorldSession void LogUnprocessedTail(WorldPacket* packet); Player* _player; - WorldSocket* m_Socket; + std::shared_ptr m_Socket; std::string m_Address; AccountTypes _security; @@ -915,7 +916,7 @@ class WorldSession TutorialDataState m_tutorialState; uint32 m_clientTimeDelay; ObjectGuid m_npcWatchLastGuid; - ACE_Based::LockedQueue _recvQueue; + MaNGOS::LockedQueue _recvQueue; }; #endif /// @} diff --git a/src/game/Server/WorldSocket.cpp b/src/game/Server/WorldSocket.cpp index 65b22723bc..58668410c4 100644 --- a/src/game/Server/WorldSocket.cpp +++ b/src/game/Server/WorldSocket.cpp @@ -22,38 +22,6 @@ * and lore are copyrighted by Blizzard Entertainment, Inc. */ -/** - * @file WorldSocket.cpp - * @brief World server network socket implementation - * - * This file implements WorldSocket which handles individual client - * connections to the world server. It manages: - * - * - TCP socket communication using ACE - * - Packet encryption/decryption (SRP6) - * - Packet fragmentation and reassembly - * - Ping/pong keepalive mechanism - * - Session creation and management - * - * The socket uses the SRP6 authentication protocol for secure - * client-server communication. - * - * @see WorldSocket for the socket class - * @see WorldSession for the player session - * @see WorldSocketMgr for the socket manager - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include "WorldSocket.h" #include "Common.h" @@ -75,32 +43,25 @@ #include "LuaEngine.h" #endif /* ENABLE_ELUNA */ +#include +#include + #if defined( __GNUC__ ) #pragma pack(1) #else #pragma pack(push,1) #endif -/** - * @brief Server packet header structure - * - * Header for packets sent from server to client. - */ struct ServerPktHeader { - uint16 size; ///< Packet size - uint16 cmd; ///< Opcode + uint16 size; + uint16 cmd; }; -/** - * @brief Client packet header structure - * - * Header for packets sent from client to server. - */ struct ClientPktHeader { - uint16 size; ///< Packet size - uint32 cmd; ///< Opcode + uint16 size; + uint32 cmd; }; #if defined( __GNUC__ ) @@ -113,538 +74,202 @@ struct ClientPktHeader std::atomic WorldSocket::s_openConnections{0}; #endif -/** - * @brief WorldSocket constructor - * - * Initializes a new client socket with default values: - * - Last ping time: zero - * - Overspeed pings: 0 - * - Session: NULL - * - Output buffer size: 64KB - * - Random seed for encryption - */ -WorldSocket::WorldSocket(void) - : WorldHandler(), - m_LastPingTime(ACE_Time_Value::zero), - m_OverSpeedPings(0), - m_Session(0), - m_RecvWPct(0), - m_RecvPct(), - m_Header(sizeof(ClientPktHeader)), - m_OutBufferLock(), - m_OutBuffer(0), - m_OutBufferSize(65536), - m_Seed(rand32()) +WorldSocket::WorldSocket() + : m_Session(0), + m_closed(false), + m_recvBuf(), + m_headerPending(false), + m_recvOpcode(0), + m_recvSize(0), + m_Seed(static_cast(rand32())), + m_LastPingTime(), + m_hasPinged(false), + m_OverSpeedPings(0) { - reference_counting_policy().value(ACE_Event_Handler::Reference_Counting_Policy::ENABLED); #ifdef _WIN32 s_openConnections.fetch_add(1, std::memory_order_relaxed); #endif } -/** - * @brief WorldSocket destructor - * - * Cleans up the receive packet and any allocated resources. - */ -WorldSocket::~WorldSocket(void) +WorldSocket::~WorldSocket() { - delete m_RecvWPct; - - if (m_OutBuffer) - { - m_OutBuffer->release(); - } - - closing_ = true; - - peer().close(); - - WorldPacket* pct; - while (m_PacketQueue.dequeue_head(pct) == 0) - { - delete pct; - } #ifdef _WIN32 s_openConnections.fetch_sub(1, std::memory_order_relaxed); #endif } -/** - * @brief Checks whether the socket is already marked as closing. - * - * @return true if the socket is closed or closing; otherwise false. - */ -bool WorldSocket::IsClosed(void) const +WorldSession* WorldSocket::GetSession() { - return closing_; + std::lock_guard guard(m_SessionLock); + return m_Session; } -/** - * @brief Begins graceful shutdown of the socket writer side. - */ -void WorldSocket::CloseSocket(void) +void WorldSocket::SetSession(WorldSession* session) { - ACE_GUARD(LockType, Guard, m_OutBufferLock); - - if (closing_) - { - return; - } - - closing_ = true; - peer().close_writer(); - - m_Session = NULL; -} - -/** - * @brief Gets the cached remote address string for this connection. - * - * @return const std::string& The remote address. - */ -const std::string& WorldSocket::GetRemoteAddress(void) const -{ - return m_Address; + std::lock_guard guard(m_SessionLock); + m_Session = session; } -/** - * @brief Sends a packet immediately or queues it for later flush. - * - * @param pkt The packet to send. - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::SendPacket(const WorldPacket& pkt) +void WorldSocket::CloseSocket() { - ACE_GUARD_RETURN(LockType, Guard, m_OutBufferLock, -1); - - if (closing_) + if (m_closed.exchange(true)) { - return -1; + return; } - WorldPacket pct = pkt; - - if (iSendPacket(pct) == -1) - { - WorldPacket* npct; - - ACE_NEW_RETURN(npct, WorldPacket(pct), -1); - - // NOTE maybe check of the size of the queue can be good ? - // to make it bounded instead of unbounded - if (m_PacketQueue.enqueue_tail(npct) == -1) - { - delete npct; - sLog.outError("WorldSocket::SendPacket: m_PacketQueue.enqueue_tail failed"); - return -1; - } - } + SetSession(NULL); - if (reactor()->schedule_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) + if (m_closer) { - sLog.outError("SendPacket failed setting WRITE mask, peer = %s", GetRemoteAddress().c_str()); - return -1; + m_closer(); } - - return 0; } -/** - * @brief Increments the socket reference count. - * - * @return long The new reference count. - */ -long WorldSocket::AddReference(void) -{ - return static_cast(add_reference()); -} - -/** - * @brief Decrements the socket reference count. - * - * @return long The new reference count. - */ -long WorldSocket::RemoveReference(void) -{ - return static_cast(remove_reference()); -} - -/** - * @brief Opens the socket handler and sends the authentication challenge. - * - * @param a The ACE open hook parameter. - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::open(void* a) +std::vector WorldSocket::EncodePacket(const WorldPacket& pct) { - ACE_UNUSED_ARG(a); - - // Prevent double call to this func. - if (m_OutBuffer) - { - return -1; - } - - // Hook for the manager. - if (sWorldSocketMgr->OnSocketOpen(this) == -1) - { - return -1; - } + ServerPktHeader header; + header.cmd = pct.GetOpcode(); + header.size = static_cast(pct.size() + 2); - // Allocate the buffer. - ACE_NEW_RETURN(m_OutBuffer, ACE_Message_Block(m_OutBufferSize), -1); + EndianConvertReverse(header.size); + EndianConvert(header.cmd); - // Store peer address. - ACE_INET_Addr remote_addr; + std::vector frame; + frame.reserve(sizeof(header) + pct.size()); - if (peer().get_remote_addr(remote_addr) == -1) { - sLog.outError("WorldSocket::open: peer ().get_remote_addr errno = %s", ACE_OS::strerror(errno)); - return -1; - } - - m_Address = remote_addr.get_host_addr(); + // EncryptSend advances the cipher, and SendPacket is reachable from both the + // world and network threads, so encrypt-and-append must be atomic. + std::lock_guard guard(m_CryptSendLock); - // Register with ACE Reactor - if (reactor()->register_handler(this, ACE_Event_Handler::READ_MASK) == -1) - { - sLog.outError("WorldSocket::open: unable to register client handler errno = %s", ACE_OS::strerror(errno)); - return -1; - } + m_Crypt.EncryptSend(reinterpret_cast(&header), sizeof(header)); - // reactor takes care of the socket from now on - remove_reference(); + const uint8_t* raw = reinterpret_cast(&header); + frame.insert(frame.end(), raw, raw + sizeof(header)); - // Send startup packet. - WorldPacket packet(SMSG_AUTH_CHALLENGE, 4); - packet << m_Seed; - - return SendPacket(packet); -} - -/** - * @brief Closes the socket and releases its final ACE reference. - * - * @return int Always returns zero. - */ -int WorldSocket::close(u_long) -{ - shutdown(); - - closing_ = true; - - remove_reference(); - - return 0; -} - -/** - * @brief Processes readable socket input. - * - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::handle_input(ACE_HANDLE) -{ - if (closing_) - { - return -1; - } - - switch (handle_input_missing_data()) - { - case -1 : - { - if ((errno == EWOULDBLOCK) || (errno == EAGAIN)) - { - return 0; - } - } - case 0: + if (!pct.empty()) { - errno = ECONNRESET; - return -1; + frame.insert(frame.end(), pct.contents(), pct.contents() + pct.size()); } - default: - return 0; } - ACE_NOTREACHED(return -1); + return frame; } -/** - * @brief Flushes pending outbound socket data. - * - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::handle_output(ACE_HANDLE) +int WorldSocket::SendPacket(const WorldPacket& pct) { - ACE_GUARD_RETURN(LockType, Guard, m_OutBufferLock, -1); - - if (closing_) - { - return -1; - } - - const size_t send_len = m_OutBuffer->length(); - - if (send_len == 0) - { - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - return 0; - } - -#ifdef MSG_NOSIGNAL - ssize_t n = peer().send(m_OutBuffer->rd_ptr(), send_len, MSG_NOSIGNAL); -#else - ssize_t n = peer().send(m_OutBuffer->rd_ptr(), send_len); -#endif // MSG_NOSIGNAL - - if (n == 0) + if (m_closed.load() || !m_sender) { return -1; } - else if (n == -1) - { - if (errno == EWOULDBLOCK || errno == EAGAIN) - { - return 0; - } - return -1; - } - else if (n < (ssize_t)send_len) // now n > 0 - { - m_OutBuffer->rd_ptr(static_cast(n)); - - // move the data to the base of the buffer - m_OutBuffer->crunch(); - - return 0; - } - else // now n == send_len - { - m_OutBuffer->reset(); - - if (!iFlushPacketQueue()) //no more packets in queue - { - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - } - return 0; - } - - ACE_NOTREACHED(return 0); -} - -/** - * @brief Handles socket closure and unregisters the handler from the reactor. - * - * @param h The closing handle. - * @return int Always returns zero. - */ -int WorldSocket::handle_close(ACE_HANDLE h, ACE_Reactor_Mask) -{ + if (sLog.IsPacketLoggingEnabled()) { - ACE_GUARD_RETURN(LockType, Guard, m_OutBufferLock, -1); - - closing_ = true; - - if (h == ACE_INVALID_HANDLE) - { - peer().close_writer(); - } + sLog.outWorldPacketDump(0, pct.GetOpcode(), pct.GetOpcodeName(), &pct, false); } - m_Session = NULL; + const std::vector frame = EncodePacket(pct); + m_sender(frame.data(), frame.size()); - reactor()->remove_handler(this, ACE_Event_Handler::DONT_CALL | ACE_Event_Handler::ALL_EVENTS_MASK); return 0; } -/** - * @brief Parses and validates an incoming packet header. - * - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::handle_input_header(void) +std::vector WorldSocket::onConnect() { - MANGOS_ASSERT(m_RecvWPct == NULL); - - MANGOS_ASSERT(m_Header.length() == sizeof(ClientPktHeader)); - - m_Crypt.DecryptRecv((uint8*) m_Header.rd_ptr(), sizeof(ClientPktHeader)); - - ClientPktHeader& header = *((ClientPktHeader*) m_Header.rd_ptr()); - - EndianConvertReverse(header.size); - EndianConvert(header.cmd); - - if ((header.size < 4) || (header.size > 10240) || (header.cmd > 10240)) - { - sLog.outError("WorldSocket::handle_input_header: client sent malformed packet size = %d , cmd = %d", - header.size, header.cmd); - - errno = EINVAL; - return -1; - } - - header.size -= 4; - - ACE_NEW_RETURN(m_RecvWPct, WorldPacket((uint16)header.cmd, header.size), -1); - - if (header.size > 0) - { - m_RecvWPct->resize(header.size); - m_RecvPct.base((char*) m_RecvWPct->contents(), m_RecvWPct->size()); - } - else - { - MANGOS_ASSERT(m_RecvPct.space() == 0); - } + WorldPacket packet(SMSG_AUTH_CHALLENGE, 4); + packet << m_Seed; - return 0; + return EncodePacket(packet); } -/** - * @brief Finalizes processing of a fully received packet payload. - * - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::handle_input_payload(void) +void WorldSocket::onClose() { - // set errno properly here on error !!! - // now have a header and payload - - MANGOS_ASSERT(m_RecvPct.space() == 0); - MANGOS_ASSERT(m_Header.space() == 0); - MANGOS_ASSERT(m_RecvWPct != NULL); - - const int ret = ProcessIncoming(m_RecvWPct); - - m_RecvPct.base(NULL, 0); - m_RecvPct.reset(); - m_RecvWPct = NULL; - - m_Header.reset(); - - if (ret == -1) - { - errno = EINVAL; - } - - return ret; + m_closed.store(true); + SetSession(NULL); } -/** - * @brief Receives and assembles any missing header or payload data from the socket. - * - * @return int A receive state code, or -1 on error. - */ -int WorldSocket::handle_input_missing_data(void) +std::vector WorldSocket::onData(const uint8_t* data, size_t len) { - char buf [4096]; - - ACE_Data_Block db(sizeof(buf), - ACE_Message_Block::MB_DATA, - buf, - 0, - 0, - ACE_Message_Block::DONT_DELETE, - 0); - - ACE_Message_Block message_block(&db, - ACE_Message_Block::DONT_DELETE, - 0); - - const size_t recv_size = message_block.space(); - - const ssize_t n = peer().recv(message_block.wr_ptr(), - recv_size); - - if (n <= 0) + if (m_closed.load()) { - return (int)n; + return {}; } - message_block.wr_ptr(n); + m_recvBuf.insert(m_recvBuf.end(), data, data + len); - while (message_block.length() > 0) + size_t pos = 0; + for (;;) { - if (m_Header.space() > 0) + if (!m_headerPending) { - // need to receive the header - const size_t to_header = (message_block.length() > m_Header.space() ? m_Header.space() : message_block.length()); - m_Header.copy(message_block.rd_ptr(), to_header); - message_block.rd_ptr(to_header); - - if (m_Header.space() > 0) + if (m_recvBuf.size() - pos < sizeof(ClientPktHeader)) { - // Couldn't receive the whole header this time. - MANGOS_ASSERT(message_block.length() == 0); - errno = EWOULDBLOCK; - return -1; + break; } - // We just received nice new header - if (handle_input_header() == -1) + ClientPktHeader header; + memcpy(&header, m_recvBuf.data() + pos, sizeof(header)); + pos += sizeof(header); + + m_Crypt.DecryptRecv(reinterpret_cast(&header), sizeof(header)); + + EndianConvertReverse(header.size); + EndianConvert(header.cmd); + + if ((header.size < 4) || (header.size > 10240) || (header.cmd > 10240)) { - MANGOS_ASSERT((errno != EWOULDBLOCK) && (errno != EAGAIN)); - return -1; + sLog.outError("WorldSocket::onData: client sent malformed packet size = %d , cmd = %d", + header.size, header.cmd); + CloseSocket(); + return {}; } + + m_recvOpcode = static_cast(header.cmd); + m_recvSize = header.size - 4u; // the opcode's own 4 bytes are counted in + m_headerPending = true; } - // Its possible on some error situations that this happens - // for example on closing when epoll receives more chunked data and stuff - // hope this is not hack ,as proper m_RecvWPct is asserted around - if (!m_RecvWPct) + if (m_recvBuf.size() - pos < m_recvSize) { - sLog.outError("Forcing close on input m_RecvWPct = NULL"); - errno = EINVAL; - return -1; + break; } - // We have full read header, now check the data payload - if (m_RecvPct.space() > 0) + WorldPacket* pct = new WorldPacket(m_recvOpcode, m_recvSize); + if (m_recvSize > 0) { - // need more data in the payload - const size_t to_data = (message_block.length() > m_RecvPct.space() ? m_RecvPct.space() : message_block.length()); - m_RecvPct.copy(message_block.rd_ptr(), to_data); - message_block.rd_ptr(to_data); + pct->resize(m_recvSize); + memcpy(const_cast(pct->contents()), m_recvBuf.data() + pos, m_recvSize); + pos += m_recvSize; + } + m_headerPending = false; - if (m_RecvPct.space() > 0) - { - // Couldn't receive the whole data this time. - MANGOS_ASSERT(message_block.length() == 0); - errno = EWOULDBLOCK; - return -1; - } + if (ProcessIncoming(pct) == -1) + { + CloseSocket(); + return {}; } - // just received fresh new payload - if (handle_input_payload() == -1) + if (m_closed.load()) { - MANGOS_ASSERT((errno != EWOULDBLOCK) && (errno != EAGAIN)); - return -1; + return {}; } } - return size_t(n) == recv_size ? 1 : 2; + if (pos > 0) + { + m_recvBuf.erase(m_recvBuf.begin(), m_recvBuf.begin() + pos); + } + + return {}; } -/** - * @brief Dispatches a fully assembled incoming packet. - * - * @param new_pct The packet to process. - * @return int Zero on success; otherwise -1. - */ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) { MANGOS_ASSERT(new_pct); - // manage memory ;) - ACE_Auto_Ptr aptr(new_pct); + std::unique_ptr aptr(new_pct); - const ACE_UINT16 opcode = new_pct->GetOpcode(); + const uint16 opcode = new_pct->GetOpcode(); if (opcode >= NUM_MSG_TYPES) { @@ -652,17 +277,18 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) return -1; } - if (closing_) + if (m_closed.load()) { return -1; } - // Dump received packet (opt-in via PacketLoggingEnabled; off by default). if (sLog.IsPacketLoggingEnabled()) { - sLog.outWorldPacketDump(uint32(get_handle()), new_pct->GetOpcode(), new_pct->GetOpcodeName(), new_pct, true); + sLog.outWorldPacketDump(0, new_pct->GetOpcode(), new_pct->GetOpcodeName(), new_pct, true); } + WorldSession* session = GetSession(); + try { switch (opcode) @@ -670,16 +296,15 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) case CMSG_PING: return HandlePing(*new_pct); case CMSG_AUTH_SESSION: - if (m_Session) + if (session) { sLog.outError("WorldSocket::ProcessIncoming: Player send CMSG_AUTH_SESSION again"); return -1; } - #ifdef ENABLE_ELUNA if (Eluna* e = sWorld.GetEluna()) { - if (!e->OnPacketReceive(m_Session, *new_pct)) + if (!e->OnPacketReceive(session, *new_pct)) { return 0; } @@ -688,35 +313,28 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) return HandleAuthSession(*new_pct); case CMSG_KEEP_ALIVE: DEBUG_LOG("CMSG_KEEP_ALIVE ,size: %zu ", new_pct->size()); - #ifdef ENABLE_ELUNA if (Eluna* e = sWorld.GetEluna()) { - e->OnPacketReceive(m_Session, *new_pct); + e->OnPacketReceive(session, *new_pct); } #endif /* ENABLE_ELUNA */ return 0; default: - { - if (m_Session != NULL) + if (session != NULL) { - // OK ,give the packet to WorldSession aptr.release(); - m_Session->QueuePacket(new_pct); + session->QueuePacket(new_pct); return 0; } - else - { - sLog.outError("WorldSocket::ProcessIncoming: Client not authed opcode = %u", uint32(opcode)); - return -1; - } - } + sLog.outError("WorldSocket::ProcessIncoming: Client not authed opcode = %u", uint32(opcode)); + return -1; } } catch (ByteBufferException&) { sLog.outError("WorldSocket::ProcessIncoming ByteBufferException occured while parsing an instant handled packet (opcode: %u) from client %s, accountid=%i.", - opcode, GetRemoteAddress().c_str(), m_Session ? m_Session->GetAccountId() : -1); + opcode, GetRemoteAddress().c_str(), session ? session->GetAccountId() : -1); if (sLog.HasLogLevelOrHigher(LOG_LVL_DEBUG)) { @@ -727,28 +345,15 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct) if (sWorld.getConfig(CONFIG_BOOL_KICK_PLAYER_ON_BAD_PACKET)) { DETAIL_LOG("Disconnecting session [account id %i / address %s] for badly formatted packet.", - m_Session ? m_Session->GetAccountId() : -1, GetRemoteAddress().c_str()); - + session ? session->GetAccountId() : -1, GetRemoteAddress().c_str()); return -1; } - else - { - return 0; - } + return 0; } - - ACE_NOTREACHED(return 0); } -/** - * @brief Authenticates a new client session and creates the world session. - * - * @param recvPacket The authentication packet. - * @return int Zero on success; otherwise -1. - */ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) { - // NOTE: ATM the socket is singlethread, have this in mind ... uint8 digest[SHA_DIGEST_LENGTH]; uint32 clientSeed; uint32 unk2; @@ -757,7 +362,6 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) BigNumber v, s, g, N, K; const bool wardenActive = (sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) || sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)); - // Read the content of the packet recvPacket >> BuiltNumberClient; recvPacket >> unk2; recvPacket >> account; @@ -765,12 +369,8 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) recvPacket.read(digest, SHA_DIGEST_LENGTH); DEBUG_LOG("WorldSocket::HandleAuthSession: client %u, unk2 %u, account %s, clientseed %u", - BuiltNumberClient, - unk2, - account.c_str(), - clientSeed); + BuiltNumberClient, unk2, account.c_str(), clientSeed); - // Check the version of client trying to connect if (!IsAcceptableClientBuild(BuiltNumberClient)) { WorldPacket packet(SMSG_AUTH_RESPONSE, 1); @@ -781,10 +381,8 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) return -1; } - // Get the account information from the realmd database std::string safe_account = account; // Duplicate, else will screw the SHA hash verification below LoginDatabase.escape_string(safe_account); - // No SQL injection, username escaped. QueryResult* result = LoginDatabase.PQuery("SELECT " @@ -802,7 +400,6 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) "WHERE `username` = '%s'", safe_account.c_str()); - // Stop if the account is not found if (!result) { WorldPacket packet(SMSG_AUTH_RESPONSE, 1); @@ -932,34 +529,29 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) std::string address = GetRemoteAddress(); DEBUG_LOG("WorldSocket::HandleAuthSession: Client '%s' authenticated successfully from %s.", - account.c_str(), - address.c_str()); + account.c_str(), address.c_str()); - // Update the last_ip in the database - // No SQL injection, username escaped. + // Update the last_ip in the database. No SQL injection, username escaped. static SqlStatementID updAccount; SqlStatement stmt = LoginDatabase.CreateStatement(updAccount, "UPDATE `account` SET `last_ip` = ? WHERE `username` = ?"); stmt.PExecute(address.c_str(), account.c_str()); - // NOTE ATM the socket is single-threaded, have this in mind ... - ACE_NEW_RETURN(m_Session, WorldSession(id, this, AccountTypes(security), mutetime, locale), -1); + WorldSession* session = new WorldSession(id, std::static_pointer_cast(shared_from_this()), + AccountTypes(security), mutetime, locale); m_Crypt.SetKey(K.AsByteArray(), 40); m_Crypt.Init(); - m_Session->LoadTutorialsData(); - - // In case needed sometime the second arg is in microseconds 1 000 000 = 1 sec - ACE_OS::sleep(ACE_Time_Value(0, 10000)); + session->LoadTutorialsData(); - // Initialize Warden system only if it is enabled by config if (wardenActive) { - m_Session->InitWarden(uint16(BuiltNumberClient), &K, os); + session->InitWarden(uint16(BuiltNumberClient), &K, os); } - sWorld.AddSession(m_Session); + SetSession(session); + sWorld.AddSession(session); // Create and send the Addon packet WorldPacket SendAddonPacked; @@ -971,33 +563,27 @@ int WorldSocket::HandleAuthSession(WorldPacket& recvPacket) return 0; } -/** - * @brief Handles a client ping and replies with a pong. - * - * @param recvPacket The ping packet. - * @return int Zero on success; otherwise -1. - */ int WorldSocket::HandlePing(WorldPacket& recvPacket) { uint32 ping; uint32 latency; - // Get the ping packet content recvPacket >> ping; recvPacket >> latency; - if (m_LastPingTime == ACE_Time_Value::zero) + const auto now = std::chrono::steady_clock::now(); + + if (!m_hasPinged) { - m_LastPingTime = ACE_OS::gettimeofday(); // for 1st ping + m_hasPinged = true; + m_LastPingTime = now; // for 1st ping } else { - ACE_Time_Value cur_time = ACE_OS::gettimeofday(); - ACE_Time_Value diff_time(cur_time); - diff_time -= m_LastPingTime; - m_LastPingTime = cur_time; + const auto diff = std::chrono::duration_cast(now - m_LastPingTime).count(); + m_LastPingTime = now; - if (diff_time < ACE_Time_Value(27)) + if (diff < 27) { ++m_OverSpeedPings; @@ -1005,12 +591,11 @@ int WorldSocket::HandlePing(WorldPacket& recvPacket) if (max_count && m_OverSpeedPings > max_count) { - if (m_Session && m_Session->GetSecurity() == SEC_PLAYER) + WorldSession* session = GetSession(); + if (session && session->GetSecurity() == SEC_PLAYER) { - sLog.outError("WorldSocket::HandlePing: Player kicked for " - "overspeeded pings address = %s", + sLog.outError("WorldSocket::HandlePing: Player kicked for overspeeded pings address = %s", GetRemoteAddress().c_str()); - return -1; } } @@ -1021,16 +606,15 @@ int WorldSocket::HandlePing(WorldPacket& recvPacket) } } - if (m_Session) + WorldSession* session = GetSession(); + if (session) { - m_Session->SetLatency(latency); - m_Session->SetClientTimeDelay(0); // recalculated on next movement packet + session->SetLatency(latency); + session->SetClientTimeDelay(0); // recalculated on next movement packet } else { - sLog.outError("WorldSocket::HandlePing: peer sent CMSG_PING, " - "but is not authenticated or got recently kicked," - " address = %s", + sLog.outError("WorldSocket::HandlePing: peer sent CMSG_PING, but is not authenticated or got recently kicked, address = %s", GetRemoteAddress().c_str()); return -1; } @@ -1039,82 +623,3 @@ int WorldSocket::HandlePing(WorldPacket& recvPacket) packet << ping; return SendPacket(packet); } - -/** - * @brief Serializes a packet into the outbound socket buffer. - * - * @param pct The packet to serialize. - * @return int Zero on success; otherwise -1. - */ -int WorldSocket::iSendPacket(const WorldPacket& pct) -{ - if (m_OutBuffer->space() < pct.size() + sizeof(ServerPktHeader)) - { - errno = ENOBUFS; - return -1; - } - - if (sLog.IsPacketLoggingEnabled()) // server packet logging (opt-in via PacketLoggingEnabled) - { - sLog.outWorldPacketDump(uint32(get_handle()), pct.GetOpcode(), pct.GetOpcodeName(), &pct, false); - } - - ServerPktHeader header; - - header.cmd = pct.GetOpcode(); - - header.size = (uint16) pct.size() + 2; - - EndianConvertReverse(header.size); - EndianConvert(header.cmd); - - m_Crypt.EncryptSend((uint8*) & header, sizeof(header)); - - if (m_OutBuffer->copy((char*) & header, sizeof(header)) == -1) - { - ACE_ASSERT(false); - } - - if (!pct.empty()) - { - if (m_OutBuffer->copy((char*) pct.contents(), pct.size()) == -1) - { - ACE_ASSERT(false); - } - } - - return 0; -} - -/** - * @brief Flushes queued packets into the outbound buffer while space remains. - * - * @return true if at least one packet was flushed; otherwise false. - */ -bool WorldSocket::iFlushPacketQueue() -{ - WorldPacket* pct; - bool haveone = false; - - while (m_PacketQueue.dequeue_head(pct) == 0) - { - if (iSendPacket(*pct) == -1) - { - if (m_PacketQueue.enqueue_head(pct) == -1) - { - delete pct; - sLog.outError("WorldSocket::iFlushPacketQueue m_PacketQueue->enqueue_head"); - return false; - } - - break; - } - else - { - haveone = true; - delete pct; - } - } - - return haveone; -} diff --git a/src/game/Server/WorldSocket.h b/src/game/Server/WorldSocket.h index 8ff6c3b030..dc8e48424a 100644 --- a/src/game/Server/WorldSocket.h +++ b/src/game/Server/WorldSocket.h @@ -31,204 +31,107 @@ #ifndef MANGOS_H_WORLDSOCKET #define MANGOS_H_WORLDSOCKET -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if !defined (ACE_LACKS_PRAGMA_ONCE) -#pragma once -#endif /* ACE_LACKS_PRAGMA_ONCE */ - #include "Common.h" #include "Auth/AuthCrypt.h" -#ifdef _WIN32 +#include "Auth/Sha1.h" + +#include "net/ISession.hpp" + #include -#endif +#include +#include +#include +#include +#include -class ACE_Message_Block; -class WorldPacket; class WorldSession; -class WorldSocket; - -typedef ACE_Svc_Handler WorldHandler; -typedef ACE_Acceptor< WorldSocket, ACE_SOCK_ACCEPTOR > WorldAcceptor; +class WorldPacket; /** - * WorldSocket. - * - * This class is responsible for the communication with - * remote clients. - * Most methods return -1 on failure. - * The class uses reference counting. - * - * For output the class uses one buffer (64K usually) and - * a queue where it stores packet if there is no space left on - * the buffer. The reason this is done, is because the server - * does really a lot of small-size writes to it, and it doesn't - * scale well to allocate memory for every. When something is - * written to the output buffer the socket is not immediately - * activated for output (again for the same reason), there - * is 10ms celling (thats why there is Update() override method). - * This concept is similar to TCP_CORK, but TCP_CORK - * uses 200ms celling. As result overhead generated by - * sending packets from "producer" threads is minimal, - * and doing a lot of writes with small size is tolerated. - * - * The calls to Update () method are managed by WorldSocketMgr. - * - * For input, the class uses one 1024 bytes buffer on stack - * to which it does recv() calls. And then received data is - * distributed where its needed. 1024 matches pretty well the - * traffic generated by client for now. - * - * The input/output do speculative reads/writes (AKA it tryes - * to read all data available in the kernel buffer or tryes to - * write everything available in userspace buffer), - * which is ok for using with Level and Edge Triggered IO - * notification. + * The world protocol spoken over one client connection. * + * The shared networking engine (net::Server) owns the socket and byte plumbing and + * hands bytes here. onData() reassembles the TCP stream into WorldPackets and routes + * them to the WorldSession; SendPacket() encrypts a header and pushes bytes back + * through the Sender, and may be called from any thread. The transport disarms the + * Sender on teardown, so a world thread still ticking a dying session merely sends + * into a no-op. */ - -class WorldSocket : protected WorldHandler +class WorldSocket : public net::ISession { public: - /// Declare some friends - friend class ACE_Acceptor< WorldSocket, ACE_SOCK_ACCEPTOR >; - friend class WorldSocketMgr; - /// Mutex type used for various synchronizations. - typedef ACE_Thread_Mutex LockType; + WorldSocket(); + ~WorldSocket() override; - /// Queue for storing packets for which there is no space. - typedef ACE_Unbounded_Queue< WorldPacket* > PacketQueueT; + // net::ISession + void setPeerAddress(const std::string& address) override { m_Address = address; } + void setSender(net::Sender sender) override { m_sender = std::move(sender); } + void setCloser(net::Closer closer) override { m_closer = std::move(closer); } + std::vector onConnect() override; + std::vector onData(const uint8_t* data, size_t len) override; + void onClose() override; + bool closed() const override { return m_closed.load(); } - /// Check if socket is closed. - bool IsClosed(void) const; + /// Check if the socket is closed. + bool IsClosed() const { return m_closed.load(); } - /// Close the socket. - void CloseSocket(void); + /// Mark the connection dead and ask the transport to close it. Any thread. + void CloseSocket(); - /// Get address of connected peer. - const std::string& GetRemoteAddress(void) const; + /// Address of the connected peer. + const std::string& GetRemoteAddress() const { return m_Address; } - /// Send A packet on the socket, this function is reentrant. - /// @param pct packet to send - /// @return -1 of failure + /// Send a packet. Reentrant, callable from any thread. Returns -1 on failure. int SendPacket(const WorldPacket& pct); - /// Add reference to this object. - long AddReference(void); - - /// Remove reference to this object. - long RemoveReference(void); - #ifdef _WIN32 - /// Number of currently open world TCP socket connections (observability). + /// Number of currently open world TCP connections (observability). static uint32 GetOpenConnectionCount() { return s_openConnections.load(std::memory_order_relaxed); } #endif - protected: - /// things called by ACE framework. - WorldSocket(void); - virtual ~WorldSocket(void); - - /// Called on open ,the void* is the acceptor. - int open(void*) override; - - /// Called on failures inside of the acceptor, don't call from your code. - int close(u_long) override; - - /// Called when we can read from the socket. - int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override; - - /// Called when the socket can write. - int handle_output(ACE_HANDLE = ACE_INVALID_HANDLE) override; - - /// Called when connection is closed or error happens. - int handle_close(ACE_HANDLE = ACE_INVALID_HANDLE, - ACE_Reactor_Mask = ACE_Event_Handler::ALL_EVENTS_MASK) override; - private: - /// Helper functions for processing incoming data. - int handle_input_header(void); - int handle_input_payload(void); - int handle_input_missing_data(void); - - /// process one incoming packet. - /// @param new_pct received packet ,note that you need to delete it. - int ProcessIncoming(WorldPacket* new_pct); - /// Called by ProcessIncoming() on CMSG_AUTH_SESSION. - int HandleAuthSession(WorldPacket& recvPacket); + std::vector EncodePacket(const WorldPacket& pct); + int ProcessIncoming(WorldPacket* new_pct); + int HandleAuthSession(WorldPacket& recvPacket); + int HandlePing(WorldPacket& recvPacket); - /// Called by ProcessIncoming() on CMSG_PING. - int HandlePing(WorldPacket& recvPacket); - - /// Try to write WorldPacket to m_OutBuffer ,return -1 if no space - /// Need to be called with m_OutBufferLock lock held - int iSendPacket(const WorldPacket& pct); - - /// Flush m_PacketQueue if there are packets in it - /// Need to be called with m_OutBufferLock lock held - /// @return true if it wrote to the buffer ( AKA you need - /// to mark the socket for output ). - bool iFlushPacketQueue(); - - private: - /// Time in which the last ping was received - ACE_Time_Value m_LastPingTime; + WorldSession* GetSession(); + void SetSession(WorldSession* session); - /// Keep track of over-speed pings ,to prevent ping flood. - uint32 m_OverSpeedPings; - - /// Address of the remote peer std::string m_Address; - /// Class used for managing encryption of the headers - AuthCrypt m_Crypt; + AuthCrypt m_Crypt; + std::mutex m_CryptSendLock; ///< serialises header encryption on send - /// Session to which received packets are routed + std::mutex m_SessionLock; WorldSession* m_Session; - /// here are stored the fragments of the received data - WorldPacket* m_RecvWPct; - - /// This block actually refers to m_RecvWPct contents, - /// which allows easy and safe writing to it. - /// It wont free memory when its deleted. m_RecvWPct takes care of freeing. - ACE_Message_Block m_RecvPct; - - /// Fragment of the received header. - ACE_Message_Block m_Header; + std::atomic m_closed; - /// Mutex for protecting output related data. - LockType m_OutBufferLock; + net::Sender m_sender; + net::Closer m_closer; - /// Buffer used for writing output. - ACE_Message_Block* m_OutBuffer; - - /// Size of the m_OutBuffer. - size_t m_OutBufferSize; - - /// Here are stored packets for which there was no space on m_OutBuffer, - /// this allows not-to kick player if its buffer is overflowed. - PacketQueueT m_PacketQueue; + // Inbound reassembly (network thread only). m_headerPending records that the + // current header has already been decrypted, so it is never decrypted twice. + std::vector m_recvBuf; + bool m_headerPending; + uint16 m_recvOpcode; + uint32 m_recvSize; const uint32 m_Seed; + // Ping-flood tracking. + std::chrono::steady_clock::time_point m_LastPingTime; + bool m_hasPinged; + uint32 m_OverSpeedPings; + #ifdef _WIN32 - /// Live count of constructed WorldSocket objects (open connections). Observability only. static std::atomic s_openConnections; #endif }; -#endif /* _WORLDSOCKET_H */ +#endif /* MANGOS_H_WORLDSOCKET */ /// @} diff --git a/src/game/Server/WorldSocketMgr.cpp b/src/game/Server/WorldSocketMgr.cpp index d93ba0e599..727b9b26e2 100644 --- a/src/game/Server/WorldSocketMgr.cpp +++ b/src/game/Server/WorldSocketMgr.cpp @@ -22,26 +22,6 @@ * and lore are copyrighted by Blizzard Entertainment, Inc. */ -/** - * @file WorldSocketMgr.cpp - * @brief World server network socket manager - * - * This file implements WorldSocketMgr which manages the network layer - * for the world server. It handles: - * - * - TCP socket creation and acceptance - * - Network thread pool management - * - Socket option configuration (buffer sizes, TCP_NODELAY) - * - Integration with ACE reactor pattern for async I/O - * - * The manager uses ACE (Adaptive Communication Environment) for portable - * networking and the TP_Reactor for thread-per-connection handling. - * - * @see WorldSocketMgr for the manager class - * @see WorldSocket for individual socket handling - * @see WorldAcceptor for connection acceptance - */ - #include "Common.h" #include "Log.h" #include "Config/Config.h" @@ -49,187 +29,51 @@ #include "WorldSocketMgr.h" #include "Opcodes.h" -#include -#include -#include -#include -#include -#include - -#include +#include +#include +#include +#include -/** - * @brief Construct WorldSocketMgr - * - * Initializes with default socket options: - * - Output kernel buffer: -1 (use default) - * - Output user buffer: 64KB - * - TCP_NODELAY: enabled - * - * Also initializes the opcode table for protocol handling. - */ WorldSocketMgr::WorldSocketMgr() - : m_SockOutKBuff(-1), m_SockOutUBuff(65536), m_UseNoDelay(true), - reactor_(NULL), acceptor_(NULL) + : m_started(false) { - InitializeOpcodes(); } -/** - * @brief Destroy WorldSocketMgr - * - * Cleans up the reactor and acceptor objects. - */ WorldSocketMgr::~WorldSocketMgr() { - if (reactor_) - { - delete reactor_; - } - if (acceptor_) - { - delete acceptor_; - } + StopNetwork(); } -/** - * @brief Service thread main function - * @return Always returns 0 - * - * Runs the ACE reactor event loop for handling network events. - * This method runs in each network thread and processes: - * - Socket read/write events - * - New connection acceptances - * - Timer events - */ -int WorldSocketMgr::svc() -{ - DEBUG_LOG("Starting Network Thread"); - - reactor_->run_reactor_event_loop(); - - DEBUG_LOG("Network Thread Exitting"); - return 0; -} - -/** - * @brief Start the network layer - * @param addr Address and port to bind to - * @return 0 on success, -1 on failure - * - * Initializes and starts the network subsystem: - * 1. Reads configuration (threads, buffer sizes, TCP_NODELAY) - * 2. Creates ACE_TP_Reactor for thread-pool handling - * 3. Opens acceptor on specified address - * 4. Spawns network threads - * - * Configuration options: - * - Network.Threads: Number of network threads (default: 1) - * - Network.OutUBuff: Output user buffer size (default: 65536) - * - Network.OutKBuff: Output kernel buffer size (default: -1 = system default) - * - Network.TcpNodelay: Enable TCP_NODELAY (default: true) - */ -int WorldSocketMgr::StartNetwork(ACE_INET_Addr& addr) +int WorldSocketMgr::StartNetwork(uint16_t port, const std::string& bindIp) { - int num_threads = sConfig.GetIntDefault("Network.Threads", 1); - if (num_threads <= 0) + if (m_started) { - sLog.outError("Network.Threads is wrong in your config file"); return -1; } - m_SockOutUBuff = sConfig.GetIntDefault("Network.OutUBuff", 65536); - if (m_SockOutUBuff <= 0) + net::SessionFactory factory = []() -> std::shared_ptr { - sLog.outError("Network.OutUBuff is wrong in your config file"); - return -1; - } + return std::make_shared(); + }; - // -1 means use default - m_SockOutKBuff = sConfig.GetIntDefault("Network.OutKBuff", -1); - m_UseNoDelay = sConfig.GetBoolDefault("Network.TcpNodelay", true); - - // Create thread-pool reactor for handling multiple connections - ACE_Reactor_Impl* imp = 0; - imp = new ACE_TP_Reactor(); - imp->max_notify_iterations(128); - reactor_ = new ACE_Reactor(imp, 1); - - acceptor_ = new WorldAcceptor; - - if (acceptor_->open(addr, reactor_, ACE_NONBLOCK) == -1) + if (!m_server.start(port, std::move(factory), bindIp)) { - sLog.outError("Failed to open acceptor, check if the port is free"); + sLog.outError("WorldSocketMgr::StartNetwork: failed to listen on %s:%u", + (bindIp.empty() ? "0.0.0.0" : bindIp.c_str()), unsigned(port)); return -1; } - if (activate(THR_NEW_LWP | THR_JOINABLE, num_threads) == -1) - { - return -1; - } - - sLog.outString("Max allowed socket connections: %d", ACE::max_handles()); + m_started = true; return 0; } -/** - * @brief Stop the network layer - * - * Gracefully shuts down the network: - * 1. Closes acceptor (stops accepting new connections) - * 2. Signals reactor to end event loop - * 3. Waits for all network threads to complete - */ void WorldSocketMgr::StopNetwork() { - if (acceptor_) - { - acceptor_->close(); - } - if (reactor_) - { - reactor_->end_reactor_event_loop(); - } - wait(); -} - -/** - * @brief Configure a newly opened socket - * @param sock Socket to configure - * @return 0 on success, -1 on failure - * - * Applies socket options to a new client connection: - * - Sets send buffer size (if configured) - * - Enables TCP_NODELAY to reduce latency (if configured) - * - Sets output buffer size - * - Associates socket with the reactor - */ -int WorldSocketMgr::OnSocketOpen(WorldSocket* sock) -{ - // Set kernel send buffer size if configured - if (m_SockOutKBuff >= 0) + if (!m_started) { - if (sock->peer().set_option(SOL_SOCKET, SO_SNDBUF, (void*)&m_SockOutKBuff, sizeof(int)) == -1 && errno != ENOTSUP) - { - sLog.outError("WorldSocketMgr::OnSocketOpen set_option SO_SNDBUF"); - return -1; - } + return; } - static const int ndoption = 1; - - // Set TCP_NODELAY to disable Nagle's algorithm for lower latency - if (m_UseNoDelay) - { - if (sock->peer().set_option(ACE_IPPROTO_TCP, TCP_NODELAY, (void*)&ndoption, sizeof(int)) == -1) - { - sLog.outError("WorldSocketMgr::OnSocketOpen: peer().set_option TCP_NODELAY errno = %s", ACE_OS::strerror(errno)); - return -1; - } - } - - sock->m_OutBufferSize = static_cast(m_SockOutUBuff); - sock->reactor(reactor_); - - return 0; + m_server.stop(); + m_started = false; } diff --git a/src/game/Server/WorldSocketMgr.h b/src/game/Server/WorldSocketMgr.h index 14a8efeea4..f93681bd21 100644 --- a/src/game/Server/WorldSocketMgr.h +++ b/src/game/Server/WorldSocketMgr.h @@ -31,73 +31,41 @@ #ifndef MANGOS_H_WORLDSOCKETMGR #define MANGOS_H_WORLDSOCKETMGR -#include -#include -#include -#include -#include -#include +#include "Common.h" +#include "Policies/Singleton.h" -class WorldSocket; +#include "net/Server.hpp" + +#include +#include /** - * @brief World socket manager class - * - * This is a pool of threads designed to be used by an ACE_TP_Reactor. - * Manages all sockets connected to peers. + * Owns the world server's listening socket: starts a net::Server on the world port + * with a factory that mints one WorldSocket per accepted connection. The engine owns + * the threads, sockets and byte plumbing; everything protocol-shaped lives in WorldSocket. */ -class WorldSocketMgr : public ACE_Task_Base +class WorldSocketMgr : public MaNGOS::Singleton { - friend class ACE_Singleton; - friend class WorldSocket; + friend class MaNGOS::Singleton; public: - /** - * @brief Start network - * @param addr Internet address - * @return Result code - */ - int StartNetwork(ACE_INET_Addr& addr); - /** - * @brief Stop network - */ + /// Bind and start accepting world connections. Returns 0 on success, -1 on failure. + int StartNetwork(uint16_t port, const std::string& bindIp); + + /// Stop accepting and tear down every live connection. void StopNetwork(); private: - /** - * @brief Handle socket open - * @param sock World socket - * @return Result code - */ - int OnSocketOpen(WorldSocket* sock); - - /** - * @brief Service method (ACE thread pool) - * @return Result code - */ - virtual int svc(); - /** - * @brief Constructor - */ WorldSocketMgr(); + ~WorldSocketMgr(); - /** - * @brief Virtual destructor - */ - virtual ~WorldSocketMgr(); - - private: - int m_SockOutKBuff; ///< Socket output kernel buffer size - int m_SockOutUBuff; ///< Socket output user buffer size - bool m_UseNoDelay; ///< Use TCP_NODELAY - - ACE_Reactor* reactor_; ///< ACE reactor - WorldAcceptor* acceptor_; ///< World acceptor + net::Server m_server; + bool m_started; }; -#define sWorldSocketMgr ACE_Singleton::instance() +#define sWorldSocketMgr MaNGOS::Singleton::Instance() #endif /// @} diff --git a/src/game/Warden/WardenCheckMgr.cpp b/src/game/Warden/WardenCheckMgr.cpp index 0e469636b1..e8c80f8b1c 100644 --- a/src/game/Warden/WardenCheckMgr.cpp +++ b/src/game/Warden/WardenCheckMgr.cpp @@ -55,7 +55,7 @@ * * Initializes the check manager with empty stores. */ -WardenCheckMgr::WardenCheckMgr() : m_lock(0), CheckStore(), CheckResultStore() {} +WardenCheckMgr::WardenCheckMgr() : CheckStore(), CheckResultStore() {} /** * @brief WardenCheckMgr destructor @@ -213,7 +213,7 @@ void WardenCheckMgr::LoadWardenOverrides() uint32 count = 0; - ACE_WRITE_GUARD(LOCK, g, m_lock) + std::unique_lock g(m_lock); do { @@ -261,7 +261,7 @@ WardenCheck* WardenCheckMgr::GetWardenDataById(uint16 build, uint16 id) { WardenCheck* result = NULL; - ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) + std::shared_lock g(m_lock); for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) { if (it->second->CheckId == id) @@ -284,7 +284,7 @@ WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 build, uint16 id) { WardenCheckResult* result = NULL; - ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) + std::shared_lock g(m_lock); for (CheckResultMap::iterator it = CheckResultStore.lower_bound(build); it != CheckResultStore.upper_bound(build); ++it) { if (it->second->Id == id) @@ -307,7 +307,7 @@ void WardenCheckMgr::GetWardenCheckIds(bool isMemCheck, uint16 build, std::list< { idl.clear(); //just to be sure - ACE_READ_GUARD(LOCK, g, m_lock) + std::shared_lock g(m_lock); for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) { if (isMemCheck) diff --git a/src/game/Warden/WardenCheckMgr.h b/src/game/Warden/WardenCheckMgr.h index 9f799b2bd3..64c5eeb78f 100644 --- a/src/game/Warden/WardenCheckMgr.h +++ b/src/game/Warden/WardenCheckMgr.h @@ -27,6 +27,7 @@ #define _WARDENCHECKMGR_H #include +#include #include "BigNumber.h" /** @@ -125,7 +126,7 @@ class WardenCheckMgr void LoadWardenOverrides(); private: - typedef ACE_RW_Thread_Mutex LOCK; + typedef std::shared_mutex LOCK; typedef std::multimap CheckMap; typedef std::multimap CheckResultMap; diff --git a/src/game/Warden/WardenMac.cpp b/src/game/Warden/WardenMac.cpp index f220008dea..ab69da129a 100644 --- a/src/game/Warden/WardenMac.cpp +++ b/src/game/Warden/WardenMac.cpp @@ -46,7 +46,7 @@ #include "Log.h" #include "Opcodes.h" #include "ByteBuffer.h" -#include +#include #include "World.h" #include "Player.h" #include "Util.h" @@ -129,10 +129,8 @@ ClientWardenModule* WardenMac::GetModuleForClient() memcpy(mod->Key, Module_0DBBF209A27B1E279A9FEC5C168A15F7_Key, 16); // md5 hash - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, mod->CompressedData, len); - MD5_Final((uint8*)&mod->Id, &ctx); + unsigned int digestLen = 0; + EVP_Digest(mod->CompressedData, len, (uint8*)&mod->Id, &digestLen, EVP_md5(), NULL); return mod; } @@ -279,11 +277,9 @@ void WardenMac::HandleData(ByteBuffer &buff) found = true; } - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, str.c_str(), str.size()); uint8 ourMD5Hash[16]; - MD5_Final(ourMD5Hash, &ctx); + unsigned int ourMD5HashLen = 0; + EVP_Digest(str.c_str(), str.size(), ourMD5Hash, &ourMD5HashLen, EVP_md5(), NULL); uint8 theirsMD5Hash[16]; buff.read(theirsMD5Hash, 16); diff --git a/src/game/Warden/WardenWin.cpp b/src/game/Warden/WardenWin.cpp index 5dc4cd50a1..1c09044c89 100644 --- a/src/game/Warden/WardenWin.cpp +++ b/src/game/Warden/WardenWin.cpp @@ -48,7 +48,7 @@ #include "Log.h" #include "Opcodes.h" #include "ByteBuffer.h" -#include +#include #include "Database/DatabaseEnv.h" #include "World.h" #include "Player.h" @@ -125,10 +125,8 @@ ClientWardenModule* WardenWin::GetModuleForClient() memcpy(mod->Key, Module.ModuleKey, 16); // md5 hash - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, mod->CompressedData, length); - MD5_Final((uint8*)&mod->Id, &ctx); + unsigned int digestLen = 0; + EVP_Digest(mod->CompressedData, length, (uint8*)&mod->Id, &digestLen, EVP_md5(), NULL); return mod; } diff --git a/src/game/WorldHandlers/AuctionHouseHandler.cpp b/src/game/WorldHandlers/AuctionHouseHandler.cpp index e4bd94f4df..5ed60372f3 100644 --- a/src/game/WorldHandlers/AuctionHouseHandler.cpp +++ b/src/game/WorldHandlers/AuctionHouseHandler.cpp @@ -1439,11 +1439,22 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recv_data) } else { + uint32 cutDebited = 0; + + // Opened BEFORE the mails: Database::Execute queues onto the calling + // thread's open transaction, so both mails, the item rows, the auction + // DELETE and the seller's gold co-commit as one unit. Sending a mail + // first would make the returned item durable independently of the + // auction DELETE -- a failed DELETE then leaves the auction in the DB + // with its item already mailed, duplicating it on the next load. + CharacterDatabase.BeginTransaction(); + if (auction->bid) // If we have a bid, we have to send him the money he paid { uint32 auctionCut = auction->GetAuctionCut(); if (pl->GetMoney() < auctionCut) // player doesn't have enough money, maybe message needed { + CharacterDatabase.RollbackTransaction(); return; } @@ -1453,6 +1464,7 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recv_data) } pl->ModifyMoney(-int32(auctionCut)); + cutDebited = auctionCut; } // Return the item by mail std::ostringstream msgAuctionCanceledOwner; @@ -1463,13 +1475,22 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recv_data) .AddItem(pItem) .SendMailTo(pl, auction, MAIL_CHECK_MASK_COPIED); - // inform player, that auction is removed - SendAuctionCommandResult(auction, AUCTION_REMOVED, AUCTION_OK); - // Now remove the auction - CharacterDatabase.BeginTransaction(); auction->DeleteFromDB(); pl->SaveInventoryAndGoldToDB(); - CharacterDatabase.CommitTransaction(); + + if (!CharacterDatabase.CommitTransactionChecked()) + { + // Nothing durable landed. Undo the only in-memory mutation; the + // auction and its item stay live (the item survives in mAitems) so + // the seller can retry. + pl->ModifyMoney(int32(cutDebited)); + SendAuctionCommandResult(auction, AUCTION_REMOVED, AUCTION_ERR_DATABASE); + sLog.outError("AH: cancel txn rolled back for auction %u; cut restored", auction->Id); + return; + } + + // inform player, that auction is removed + SendAuctionCommandResult(auction, AUCTION_REMOVED, AUCTION_OK); sAuctionMgr.RemoveAItem(auction->itemGuidLow); auctionHouse->RemoveAuction(auction->Id); diff --git a/src/game/WorldHandlers/ChatMessage.cpp b/src/game/WorldHandlers/ChatMessage.cpp index f54d822325..ec7c2aaa0c 100644 --- a/src/game/WorldHandlers/ChatMessage.cpp +++ b/src/game/WorldHandlers/ChatMessage.cpp @@ -64,6 +64,7 @@ #include "CommandMgr.h" #ifdef ENABLE_ELUNA #include "LuaEngine.h" +#include #endif /* ENABLE_ELUNA */ /** diff --git a/src/game/WorldHandlers/GridDefines.h b/src/game/WorldHandlers/GridDefines.h index 0df4444448..93aaec76fb 100644 --- a/src/game/WorldHandlers/GridDefines.h +++ b/src/game/WorldHandlers/GridDefines.h @@ -239,7 +239,7 @@ namespace MaNGOS */ inline bool IsValidMapCoord(float c) { - return finite(c) && (std::fabs(c) <= MAP_HALFSIZE - 0.5); + return std::isfinite(c) && (std::fabs(c) <= MAP_HALFSIZE - 0.5); } /** @@ -266,7 +266,7 @@ namespace MaNGOS */ inline bool IsValidMapCoord(float x, float y, float z) { - return IsValidMapCoord(x, y) && finite(z); + return IsValidMapCoord(x, y) && std::isfinite(z); } /** @@ -281,7 +281,7 @@ namespace MaNGOS */ inline bool IsValidMapCoord(float x, float y, float z, float o) { - return IsValidMapCoord(x, y, z) && finite(o); + return IsValidMapCoord(x, y, z) && std::isfinite(o); } } #endif diff --git a/src/game/WorldHandlers/GridMap.cpp b/src/game/WorldHandlers/GridMap.cpp index 84ac3760e6..266204acae 100644 --- a/src/game/WorldHandlers/GridMap.cpp +++ b/src/game/WorldHandlers/GridMap.cpp @@ -1056,14 +1056,14 @@ void TerrainInfo::CleanUpGrids(const uint32 diff) * * @param x The grid x index. * @param y The grid y index. - * @return The new reference count, or -1 on lock failure. + * @return The new reference count. */ int TerrainInfo::RefGrid(const uint32& x, const uint32& y) { MANGOS_ASSERT(x < MAX_NUMBER_OF_GRIDS); MANGOS_ASSERT(y < MAX_NUMBER_OF_GRIDS); - ACE_GUARD_RETURN(LOCK_TYPE, _lock, m_refMutex, -1) + std::lock_guard _lock(m_refMutex); return (m_GridRef[x][y] += 1); } @@ -1072,7 +1072,7 @@ int TerrainInfo::RefGrid(const uint32& x, const uint32& y) * * @param x The grid x index. * @param y The grid y index. - * @return The new reference count, or -1 on lock failure. + * @return The new reference count. */ int TerrainInfo::UnrefGrid(const uint32& x, const uint32& y) { @@ -1081,7 +1081,7 @@ int TerrainInfo::UnrefGrid(const uint32& x, const uint32& y) int16& iRef = m_GridRef[x][y]; - ACE_GUARD_RETURN(LOCK_TYPE, _lock, m_refMutex, -1) + std::lock_guard _lock(m_refMutex); if (iRef > 0) { return (iRef -= 1); @@ -1559,7 +1559,7 @@ GridMap* TerrainInfo::LoadMapAndVMap(const uint32 x, const uint32 y) // double checked lock pattern if (!m_GridMaps[x][y]) { - ACE_GUARD_RETURN(LOCK_TYPE, lock, m_mutex, NULL) + std::lock_guard lock(m_mutex); if (!m_GridMaps[x][y]) { @@ -1642,9 +1642,7 @@ float TerrainInfo::GetWaterLevel(float x, float y, float z, float* pGround /*= N ////////////////////////////////////////////////////////////////////////// -#define CLASS_LOCK MaNGOS::ClassLevelLockable - INSTANTIATE_SINGLETON_2(TerrainManager, CLASS_LOCK); -INSTANTIATE_CLASS_MUTEX(TerrainManager, ACE_Thread_Mutex); +// Singleton instantiation is implicit now (MaNGOS::Singleton is a Meyers singleton). TerrainManager::TerrainManager() : m_mutex() { @@ -1666,7 +1664,7 @@ TerrainManager::~TerrainManager() */ TerrainInfo* TerrainManager::LoadTerrain(const uint32 mapId) { - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_mutex, NULL) + std::lock_guard _guard(m_mutex); TerrainDataMap::const_iterator iter = i_TerrainMap.find(mapId); if (iter == i_TerrainMap.end()) @@ -1690,7 +1688,7 @@ void TerrainManager::UnloadTerrain(const uint32 mapId) return; } - ACE_GUARD(LOCK_TYPE, _guard, m_mutex) + std::lock_guard _guard(m_mutex); TerrainDataMap::iterator iter = i_TerrainMap.find(mapId); if (iter != i_TerrainMap.end()) diff --git a/src/game/WorldHandlers/GridMap.h b/src/game/WorldHandlers/GridMap.h index e86283a130..77711a7a86 100644 --- a/src/game/WorldHandlers/GridMap.h +++ b/src/game/WorldHandlers/GridMap.h @@ -29,8 +29,10 @@ #include "Policies/Singleton.h" #include "GridDefines.h" +#include #include #include +#include class Creature; class Unit; @@ -217,7 +219,7 @@ template Countable m_count; }; -typedef ACE_Atomic_Op AtomicLong; +typedef std::atomic AtomicLong; #define MAX_HEIGHT 100000.0f // can be use for find ground height at surface #define INVALID_HEIGHT -100000.0f // for check, must be equal to VMAP_INVALID_HEIGHT, real value for unknown height is VMAP_INVALID_HEIGHT_VALUE @@ -286,17 +288,17 @@ class TerrainInfo : public Referencable // global garbage collection timer IntervalTimer i_timer; - typedef ACE_Thread_Mutex LOCK_TYPE; + typedef std::mutex LOCK_TYPE; LOCK_TYPE m_mutex; char _cache_guard[1024]; LOCK_TYPE m_refMutex; }; // class for managing TerrainData object and all sort of geometry querying operations -class TerrainManager : public MaNGOS::Singleton > +class TerrainManager : public MaNGOS::Singleton { typedef UNORDERED_MAP TerrainDataMap; - friend class MaNGOS::OperatorNew; + friend class MaNGOS::Singleton; public: TerrainInfo* LoadTerrain(const uint32 mapId); @@ -334,7 +336,7 @@ class TerrainManager : public MaNGOS::Singleton -#include - #include "DBCStructure.h" #include "GridDefines.h" #include "Cell.h" diff --git a/src/game/WorldHandlers/MapManager.cpp b/src/game/WorldHandlers/MapManager.cpp index 6eb594a3e9..8615821524 100644 --- a/src/game/WorldHandlers/MapManager.cpp +++ b/src/game/WorldHandlers/MapManager.cpp @@ -59,9 +59,7 @@ #include "ElunaConfig.h" #endif /* ENABLE_ELUNA */ -#define CLASS_LOCK MaNGOS::ClassLevelLockable - INSTANTIATE_SINGLETON_2(MapManager, CLASS_LOCK); -INSTANTIATE_CLASS_MUTEX(MapManager, ACE_Recursive_Thread_Mutex); +// Singleton instantiation is implicit now (MaNGOS::Singleton is a Meyers singleton). MapManager::MapManager() : i_gridCleanUpDelay(sWorld.getConfig(CONFIG_UINT32_INTERVAL_GRIDCLEAN)), m_lock() @@ -164,7 +162,7 @@ void MapManager::InitializeVisibilityDistanceInfo() /// @param id - MapId of the to be created map. @param obj WorldObject for which the map is to be created. Must be player for Instancable maps. Map* MapManager::CreateMap(uint32 id, const WorldObject* obj) { - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_lock, NULL) + std::lock_guard _guard(m_lock); const MapEntry* entry = sMapStore.LookupEntry(id); if (!entry) @@ -215,7 +213,7 @@ Map* MapManager::CreateBgMap(uint32 mapid, BattleGround* bg) { sTerrainMgr.LoadTerrain(mapid); - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_lock, NULL) + std::lock_guard _guard(m_lock); return CreateBattleGroundMap(mapid, sMapMgr.GenerateInstanceId(), bg); } @@ -228,7 +226,7 @@ Map* MapManager::CreateBgMap(uint32 mapid, BattleGround* bg) */ Map* MapManager::FindMap(uint32 mapid, uint32 instanceId) const { - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_lock, NULL) + std::lock_guard _guard(m_lock); MapMapType::const_iterator iter = i_maps.find(MapID(mapid, instanceId)); if (iter == i_maps.end()) @@ -254,7 +252,7 @@ Map* MapManager::FindMap(uint32 mapid, uint32 instanceId) const */ void MapManager::DeleteInstance(uint32 mapid, uint32 instanceId) { - ACE_GUARD(LOCK_TYPE, _guard, m_lock) + std::lock_guard _guard(m_lock); MapMapType::iterator iter = i_maps.find(MapID(mapid, instanceId)); if (iter != i_maps.end()) @@ -420,7 +418,7 @@ uint32 MapManager::GetNumInstances() { uint32 ret = 0; - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_lock, ret) + std::lock_guard _guard(m_lock); for (MapMapType::iterator itr = i_maps.begin(); itr != i_maps.end(); ++itr) { Map* map = itr->second; @@ -442,7 +440,7 @@ uint32 MapManager::GetNumPlayersInInstances() { uint32 ret = 0; - ACE_GUARD_RETURN(LOCK_TYPE, _guard, m_lock, ret) + std::lock_guard _guard(m_lock); for (MapMapType::iterator itr = i_maps.begin(); itr != i_maps.end(); ++itr) { Map* map = itr->second; diff --git a/src/game/WorldHandlers/MapManager.h b/src/game/WorldHandlers/MapManager.h index 12c8d137ca..127d2c23cc 100644 --- a/src/game/WorldHandlers/MapManager.h +++ b/src/game/WorldHandlers/MapManager.h @@ -28,9 +28,10 @@ #include "Common.h" #include "Platform/Define.h" #include "Policies/Singleton.h" -#include #include "Map.h" #include "GridStates.h" + +#include #include "MapUpdater.h" class Transport; @@ -57,9 +58,9 @@ struct MapID uint32 nInstanceId; }; -class MapManager : public MaNGOS::Singleton > +class MapManager : public MaNGOS::Singleton { - friend class MaNGOS::OperatorNew; + friend class MaNGOS::Singleton; public: typedef std::map MapMapType; @@ -212,7 +213,8 @@ class MapManager : public MaNGOS::Singleton +#include INSTANTIATE_SINGLETON_1(MapPersistentStateManager); diff --git a/src/game/WorldHandlers/MapPersistentStateMgr.h b/src/game/WorldHandlers/MapPersistentStateMgr.h index 727074722d..a0208c8612 100644 --- a/src/game/WorldHandlers/MapPersistentStateMgr.h +++ b/src/game/WorldHandlers/MapPersistentStateMgr.h @@ -28,7 +28,6 @@ #include "Common.h" #include "Platform/Define.h" #include "Policies/Singleton.h" -#include #include #include #include "Utilities/UnorderedMapSet.h" @@ -331,7 +330,7 @@ class DungeonResetScheduler ResetTimeQueue m_resetTimeQueue; }; -class MapPersistentStateManager : public MaNGOS::Singleton > +class MapPersistentStateManager : public MaNGOS::Singleton { friend class DungeonResetScheduler; diff --git a/src/game/WorldHandlers/ScriptMgr.cpp b/src/game/WorldHandlers/ScriptMgr.cpp index 41daaa70f4..6281f92972 100644 --- a/src/game/WorldHandlers/ScriptMgr.cpp +++ b/src/game/WorldHandlers/ScriptMgr.cpp @@ -76,7 +76,7 @@ INSTANTIATE_SINGLETON_1(ScriptMgr); -ScriptMgr::ScriptMgr() : m_scheduledScripts(0), m_lock(0) +ScriptMgr::ScriptMgr() : m_scheduledScripts(0) { m_dbScripts.resize(DBS_END); diff --git a/src/game/WorldHandlers/ScriptMgr.h b/src/game/WorldHandlers/ScriptMgr.h index 11c9d8f6e1..4e1f2df089 100644 --- a/src/game/WorldHandlers/ScriptMgr.h +++ b/src/game/WorldHandlers/ScriptMgr.h @@ -28,9 +28,10 @@ #include "Common.h" #include "ObjectGuid.h" #include "DBCEnums.h" -#include -#include -#include + +#include +#include +#include struct AreaTriggerEntry; struct SpellEntry; @@ -722,13 +723,13 @@ class ScriptMgr ScriptNameMap m_scriptNames; DBScripts m_dbScripts; #ifdef _DEBUG - // mutex allowing to reload the script binding table; TODO just do it AWAY from any map update, e.g. right after sessions update - ACE_RW_Thread_Mutex m_bindMutex; + // guards reloads of the script binding table against concurrent map updates + std::shared_mutex m_bindMutex; #endif /* _DEBUG */ - // atomic op counter for active scripts amount - ACE_Atomic_Op m_scheduledScripts; + // active scheduled-script counter + std::atomic m_scheduledScripts; char __cache_guard[1024]; - ACE_Thread_Mutex m_lock; + std::mutex m_lock; }; // Starters for events diff --git a/src/game/WorldHandlers/ScriptMgrBinding.cpp b/src/game/WorldHandlers/ScriptMgrBinding.cpp index b254cc90b0..09e85cfee3 100644 --- a/src/game/WorldHandlers/ScriptMgrBinding.cpp +++ b/src/game/WorldHandlers/ScriptMgrBinding.cpp @@ -211,9 +211,8 @@ void ScriptMgr::LoadScriptBinding() bool ScriptMgr::ReloadScriptBinding() { #ifdef _DEBUG - m_bindMutex.acquire_write(); + std::unique_lock guard(m_bindMutex); LoadScriptBinding(); - m_bindMutex.release(); return true; #else return false; @@ -291,7 +290,7 @@ uint32 ScriptMgr::GetScriptId(const char* name) const uint32 ScriptMgr::GetBoundScriptId(ScriptedObjectType entity, int32 entry) { #ifdef _DEBUG - m_bindMutex.acquire_read(); + std::shared_lock guard(m_bindMutex); #endif /* _DEBUG */ uint32 id = 0; if (entity < SCRIPTED_MAX_TYPE) @@ -306,9 +305,6 @@ uint32 ScriptMgr::GetBoundScriptId(ScriptedObjectType entity, int32 entry) { sLog.outErrorScriptLib("asking a script for non-existing entity type %u!", entity); } -#ifdef _DEBUG - m_bindMutex.release(); -#endif /* _DEBUG */ return id; } diff --git a/src/game/WorldHandlers/ScriptMgrDbScripts.cpp b/src/game/WorldHandlers/ScriptMgrDbScripts.cpp index 7c4b588e85..4eef5218dd 100644 --- a/src/game/WorldHandlers/ScriptMgrDbScripts.cpp +++ b/src/game/WorldHandlers/ScriptMgrDbScripts.cpp @@ -81,7 +81,7 @@ */ ScriptChainMap const* ScriptMgr::GetScriptChainMap(DBScriptType type) { - ACE_GUARD_RETURN(ACE_Thread_Mutex, _guard, m_lock, NULL) + std::lock_guard _guard(m_lock); if ((type != DBS_INTERNAL) && type < DBS_END) { return &m_dbScripts[type]; @@ -929,7 +929,7 @@ void ScriptMgr::LoadDbScripts(DBScriptType t) } { - ACE_GUARD(ACE_Thread_Mutex, _g, m_lock) + std::lock_guard _g(m_lock); LoadScripts(t); } diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index 7e4ba566fe..a924245f0a 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -70,6 +70,7 @@ #include "ScriptMgr.h" #include "CreatureAIRegistry.h" #include "ProgressBar.h" +#include "StartupUI.h" #include "Policies/Singleton.h" #include "BattleGround/BattleGroundMgr.h" #include "OutdoorPvP/OutdoorPvP.h" @@ -122,6 +123,7 @@ #include "AuctionHouseBot/MutationPending.h" #include "PlayerMutations.h" +#include #include #include #include @@ -138,7 +140,7 @@ void AhSendBrowseUnavailable(WorldSession* session, uint8 kind); volatile bool World::m_stopEvent = false; uint8 World::m_ExitCode = SHUTDOWN_EXIT_CODE; -ACE_Atomic_Op World::m_worldLoopCounter = 0; +std::atomic World::m_worldLoopCounter{0}; float World::m_MaxVisibleDistanceOnContinents = DEFAULT_VISIBILITY_DISTANCE; float World::m_MaxVisibleDistanceInInstances = DEFAULT_VISIBILITY_INSTANCE; @@ -413,8 +415,10 @@ void World::SetInitialWorldSettings() exit(1); } + StartupUI::BeginPhase("Core data"); + ///- Loading strings. Getting no records means core load has to be canceled because no error message can be output. - sLog.outString("Loading MaNGOS strings..."); + StartupUI::Step("Loading MaNGOS strings..."); if (!sObjectMgr.LoadMangosStrings()) { Log::WaitBeforeContinueIfNeed(); @@ -433,21 +437,21 @@ void World::SetInitialWorldSettings() CharacterDatabase.PExecute("DELETE FROM `corpse` WHERE `corpse_type` = '0' OR `time` < (UNIX_TIMESTAMP()-'%u')", 3 * DAY); ///- Load the DBC files - sLog.outString("Initialize DBC data stores..."); + StartupUI::Step("Initialize DBC data stores..."); LoadDBCStores(m_dataPath); DetectDBCLang(); sObjectMgr.SetDBCLocaleIndex(GetDefaultDbcLocale()); // Get once for all the locale index of DBC language (console/broadcasts) - sLog.outString("Loading Script Names..."); + StartupUI::Step("Loading Script Names..."); sScriptMgr.LoadScriptNames(); - sLog.outString("Loading InstanceTemplate..."); + StartupUI::Step("Loading InstanceTemplate..."); sObjectMgr.LoadInstanceTemplate(); - sLog.outString("Loading SkillLineAbilityMultiMap Data..."); + StartupUI::Step("Loading SkillLineAbilityMultiMap Data..."); sSpellMgr.LoadSkillLineAbilityMap(); - sLog.outString("Loading SkillRaceClassInfoMultiMap Data..."); + StartupUI::Step("Loading SkillRaceClassInfoMultiMap Data..."); sSpellMgr.LoadSkillRaceClassInfoMap(); ///- Clean up and pack instances @@ -470,135 +474,137 @@ void World::SetInitialWorldSettings() // lua state begins uninitialized eluna = nullptr; - sLog.outString("Loading Eluna config..."); + StartupUI::Step("Loading Eluna config..."); sElunaConfig->Initialize(); if (sElunaConfig->IsElunaEnabled()) { ///- Initialize Lua Engine - sLog.outString("Loading Lua scripts..."); + StartupUI::Step("Loading Lua scripts..."); sElunaLoader->LoadScripts(); } #endif /* ENABLE_ELUNA */ - sLog.outString("Loading Page Texts..."); + StartupUI::BeginPhase("World data"); + + StartupUI::Step("Loading Page Texts..."); sObjectMgr.LoadPageTexts(); - sLog.outString("Loading Game Object Templates..."); // must be after LoadPageTexts + StartupUI::Step("Loading Game Object Templates..."); // must be after LoadPageTexts sObjectMgr.LoadGameobjectInfo(); - sLog.outString("Loading GameObject models..."); + StartupUI::Step("Loading GameObject models..."); LoadGameObjectModelList(); sLog.outString(); - sLog.outString("Loading Spell Chain Data..."); + StartupUI::Step("Loading Spell Chain Data..."); sSpellMgr.LoadSpellChains(); - sLog.outString("Loading Spell Elixir types..."); + StartupUI::Step("Loading Spell Elixir types..."); sSpellMgr.LoadSpellElixirs(); - sLog.outString("Loading Spell Facing Flags..."); + StartupUI::Step("Loading Spell Facing Flags..."); sSpellMgr.LoadFacingCasterFlags(); - sLog.outString("Loading Spell Learn Skills..."); + StartupUI::Step("Loading Spell Learn Skills..."); sSpellMgr.LoadSpellLearnSkills(); // must be after LoadSpellChains - sLog.outString("Loading Spell Learn Spells..."); + StartupUI::Step("Loading Spell Learn Spells..."); sSpellMgr.LoadSpellLearnSpells(); - sLog.outString("Loading Spell Proc Event conditions..."); + StartupUI::Step("Loading Spell Proc Event conditions..."); sSpellMgr.LoadSpellProcEvents(); - sLog.outString("Loading Spell Bonus Data..."); + StartupUI::Step("Loading Spell Bonus Data..."); sSpellMgr.LoadSpellBonuses(); - sLog.outString("Loading Spell Proc Item Enchant..."); + StartupUI::Step("Loading Spell Proc Item Enchant..."); sSpellMgr.LoadSpellProcItemEnchant(); // must be after LoadSpellChains - sLog.outString("Loading Spell Linked definitions..."); + StartupUI::Step("Loading Spell Linked definitions..."); sSpellMgr.LoadSpellLinked(); // must be after LoadSpellChains - sLog.outString("Loading Aggro Spells Definitions..."); + StartupUI::Step("Loading Aggro Spells Definitions..."); sSpellMgr.LoadSpellThreats(); - sLog.outString("Loading NPC Texts..."); + StartupUI::Step("Loading NPC Texts..."); sObjectMgr.LoadGossipText(); - sLog.outString("Loading Item Random Enchantments Table..."); + StartupUI::Step("Loading Item Random Enchantments Table..."); LoadRandomEnchantmentsTable(); - sLog.outString("Loading Disables..."); // must be before loading quests and items + StartupUI::Step("Loading Disables..."); // must be before loading quests and items DisableMgr::LoadDisables(); - sLog.outString("Loading Item Templates..."); // must be after LoadRandomEnchantmentsTable and LoadPageTexts + StartupUI::Step("Loading Item Templates..."); // must be after LoadRandomEnchantmentsTable and LoadPageTexts sObjectMgr.LoadItemPrototypes(); - sLog.outString("Loading Creature Model Based Info Data..."); + StartupUI::Step("Loading Creature Model Based Info Data..."); sObjectMgr.LoadCreatureModelInfo(); - sLog.outString("Loading Creature Items..."); + StartupUI::Step("Loading Creature Items..."); sObjectMgr.LoadCreatureItemTemplates(); - sLog.outString("Loading Equipment templates..."); + StartupUI::Step("Loading Equipment templates..."); sObjectMgr.LoadEquipmentTemplates(); - sLog.outString("Loading Creature Stats..."); + StartupUI::Step("Loading Creature Stats..."); sObjectMgr.LoadCreatureClassLvlStats(); - sLog.outString("Loading Creature templates..."); + StartupUI::Step("Loading Creature templates..."); sObjectMgr.LoadCreatureTemplates(); - sLog.outString("Loading Creature template spells..."); + StartupUI::Step("Loading Creature template spells..."); sObjectMgr.LoadCreatureTemplateSpells(); - sLog.outString("Loading Creature spells..."); + StartupUI::Step("Loading Creature spells..."); sObjectMgr.LoadCreatureSpells(); - sLog.outString("Loading SpellsScriptTarget..."); + StartupUI::Step("Loading SpellsScriptTarget..."); sSpellMgr.LoadSpellScriptTarget(); // must be after LoadCreatureTemplates and LoadGameobjectInfo - sLog.outString("Loading ItemRequiredTarget..."); + StartupUI::Step("Loading ItemRequiredTarget..."); sObjectMgr.LoadItemRequiredTarget(); - sLog.outString("Loading Reputation Reward Rates..."); + StartupUI::Step("Loading Reputation Reward Rates..."); sObjectMgr.LoadReputationRewardRate(); - sLog.outString("Loading Creature Reputation OnKill Data..."); + StartupUI::Step("Loading Creature Reputation OnKill Data..."); sObjectMgr.LoadReputationOnKill(); - sLog.outString("Loading Reputation Spillover Data..."); + StartupUI::Step("Loading Reputation Spillover Data..."); sObjectMgr.LoadReputationSpilloverTemplate(); - sLog.outString("Loading Points Of Interest Data..."); + StartupUI::Step("Loading Points Of Interest Data..."); sObjectMgr.LoadPointsOfInterest(); - sLog.outString("Loading Pet Create Spells..."); + StartupUI::Step("Loading Pet Create Spells..."); sObjectMgr.LoadPetCreateSpells(); - sLog.outString("Loading Creature Data..."); + StartupUI::Step("Loading Creature Data..."); sObjectMgr.LoadCreatures(); - sLog.outString("Loading Creature Addon Data..."); + StartupUI::Step("Loading Creature Addon Data..."); sObjectMgr.LoadCreatureAddons(); // must be after LoadCreatureTemplates() and LoadCreatures() sLog.outString(">>> Creature Addon Data loaded"); sLog.outString(); - sLog.outString("Loading Gameobject Data..."); + StartupUI::Step("Loading Gameobject Data..."); sObjectMgr.LoadGameObjects(); - sLog.outString("Loading CreatureLinking Data..."); // must be after Creatures + StartupUI::Step("Loading CreatureLinking Data..."); // must be after Creatures sCreatureLinkingMgr.LoadFromDB(); - sLog.outString("Loading Objects Pooling Data..."); + StartupUI::Step("Loading Objects Pooling Data..."); sPoolMgr.LoadFromDB(); - sLog.outString("Loading Weather Data..."); + StartupUI::Step("Loading Weather Data..."); sWeatherMgr.LoadWeatherZoneChances(); - sLog.outString("Loading Quests..."); + StartupUI::Step("Loading Quests..."); sObjectMgr.LoadQuests(); // must be loaded after DBCs, creature_template, item_template, gameobject tables - sLog.outString("Loading Quests Relations..."); + StartupUI::Step("Loading Quests Relations..."); sObjectMgr.LoadQuestRelations(); // must be after quest load sLog.outString(">>> Quests Relations loaded"); sLog.outString(); @@ -606,133 +612,133 @@ void World::SetInitialWorldSettings() sLog.outString("Checking Quest Disables..."); DisableMgr::CheckQuestDisables(); // must be after loading quests - sLog.outString("Loading Game Event Data..."); // must be after sPoolMgr.LoadFromDB and quests to properly load pool events and quests for events + StartupUI::Step("Loading Game Event Data..."); // must be after sPoolMgr.LoadFromDB and quests to properly load pool events and quests for events sGameEventMgr.LoadFromDB(); sLog.outString(">>> Game Event Data loaded"); sLog.outString(); // Load Conditions - sLog.outString("Loading Conditions..."); + StartupUI::Step("Loading Conditions..."); sObjectMgr.LoadConditions(); sLog.outString("Creating map persistent states for non-instanceable maps..."); // must be after PackInstances(), LoadCreatures(), sPoolMgr.LoadFromDB(), sGameEventMgr.LoadFromDB(); sMapPersistentStateMgr.InitWorldMaps(); sLog.outString(); - sLog.outString("Loading Creature Respawn Data..."); // must be after LoadCreatures(), and sMapPersistentStateMgr.InitWorldMaps() + StartupUI::Step("Loading Creature Respawn Data..."); // must be after LoadCreatures(), and sMapPersistentStateMgr.InitWorldMaps() sMapPersistentStateMgr.LoadCreatureRespawnTimes(); - sLog.outString("Loading Gameobject Respawn Data..."); // must be after LoadGameObjects(), and sMapPersistentStateMgr.InitWorldMaps() + StartupUI::Step("Loading Gameobject Respawn Data..."); // must be after LoadGameObjects(), and sMapPersistentStateMgr.InitWorldMaps() sMapPersistentStateMgr.LoadGameobjectRespawnTimes(); - sLog.outString("Loading SpellArea Data..."); // must be after quest load + StartupUI::Step("Loading SpellArea Data..."); // must be after quest load sSpellMgr.LoadSpellAreas(); - sLog.outString("Loading AreaTrigger definitions..."); + StartupUI::Step("Loading AreaTrigger definitions..."); sObjectMgr.LoadAreaTriggerTeleports(); // must be after item template load - sLog.outString("Loading Quest Area Triggers..."); + StartupUI::Step("Loading Quest Area Triggers..."); sObjectMgr.LoadQuestAreaTriggers(); // must be after LoadQuests - sLog.outString("Loading Tavern Area Triggers..."); + StartupUI::Step("Loading Tavern Area Triggers..."); sObjectMgr.LoadTavernAreaTriggers(); - //sLog.outString("Loading AreaTrigger script names..."); + //StartupUI::Step("Loading AreaTrigger script names..."); //sScriptMgr.LoadAreaTriggerScripts(); - //sLog.outString("Loading event id script names..."); + //StartupUI::Step("Loading event id script names..."); //sScriptMgr.LoadEventIdScripts(); - //sLog.outString("Loading spell script names..."); + //StartupUI::Step("Loading spell script names..."); //sScriptMgr.LoadSpellIdScripts(); #ifdef ENABLE_SD3 - sLog.outString("Loading all script bindings..."); + StartupUI::Step("Loading all script bindings..."); sScriptMgr.LoadScriptBinding(); #endif /* ENABLE_SD3 */ - sLog.outString("Loading Graveyard-zone links..."); + StartupUI::Step("Loading Graveyard-zone links..."); sObjectMgr.LoadGraveyardZones(); - sLog.outString("Loading spell target destination coordinates..."); + StartupUI::Step("Loading spell target destination coordinates..."); sSpellMgr.LoadSpellTargetPositions(); - sLog.outString("Loading SpellAffect definitions..."); + StartupUI::Step("Loading SpellAffect definitions..."); sSpellMgr.LoadSpellAffects(); - sLog.outString("Loading spell pet auras..."); + StartupUI::Step("Loading spell pet auras..."); sSpellMgr.LoadSpellPetAuras(); - sLog.outString("Loading Player Create Info & Level Stats..."); + StartupUI::Step("Loading Player Create Info & Level Stats..."); sObjectMgr.LoadPlayerInfo(); sLog.outString(">>> Player Create Info & Level Stats loaded"); sLog.outString(); - sLog.outString("Loading Exploration BaseXP Data..."); + StartupUI::Step("Loading Exploration BaseXP Data..."); sObjectMgr.LoadExplorationBaseXP(); - sLog.outString("Loading Pet Name Parts..."); + StartupUI::Step("Loading Pet Name Parts..."); sObjectMgr.LoadPetNames(); CharacterDatabaseCleaner::CleanDatabase(); sLog.outString(); - sLog.outString("Loading the max pet number..."); + StartupUI::Step("Loading the max pet number..."); sObjectMgr.LoadPetNumber(); - sLog.outString("Loading pet level stats..."); + StartupUI::Step("Loading pet level stats..."); sObjectMgr.LoadPetLevelInfo(); - sLog.outString("Loading Player Corpses..."); + StartupUI::Step("Loading Player Corpses..."); sObjectMgr.LoadCorpses(); - sLog.outString("Loading Loot Tables..."); + StartupUI::Step("Loading Loot Tables..."); LoadLootTables(); sLog.outString(">>> Loot Tables loaded"); sLog.outString(); - sLog.outString("Loading Skill Fishing base level requirements..."); + StartupUI::Step("Loading Skill Fishing base level requirements..."); sObjectMgr.LoadFishingBaseSkillLevel(); - sLog.outString("Loading Gossip scripts..."); + StartupUI::Step("Loading Gossip scripts..."); sScriptMgr.LoadDbScripts(DBS_ON_GOSSIP); // must be before gossip menu options sObjectMgr.LoadGossipMenus(); - sLog.outString("Loading Vendors..."); + StartupUI::Step("Loading Vendors..."); sObjectMgr.LoadVendorTemplates(); // must be after load ItemTemplate sObjectMgr.LoadVendors(); // must be after load CreatureTemplate, VendorTemplate, and ItemTemplate - sLog.outString("Loading Trainers..."); + StartupUI::Step("Loading Trainers..."); sObjectMgr.LoadTrainerTemplates(); // must be after load CreatureTemplate sObjectMgr.LoadTrainers(); // must be after load CreatureTemplate, TrainerTemplate - sLog.outString("Loading Waypoint scripts..."); // before loading from creature_movement + StartupUI::Step("Loading Waypoint scripts..."); // before loading from creature_movement sScriptMgr.LoadDbScripts(DBS_ON_CREATURE_MOVEMENT); - sLog.outString("Loading Waypoints..."); + StartupUI::Step("Loading Waypoints..."); sWaypointMgr.Load(); sLog.outString("Modifying in-memory dbc spell attributes..."); sSpellMgr.ModDBCSpellAttributes(); - sLog.outString("Loading ReservedNames..."); + StartupUI::Step("Loading ReservedNames..."); sObjectMgr.LoadReservedPlayersNames(); - sLog.outString("Loading GameObjects for quests..."); + StartupUI::Step("Loading GameObjects for quests..."); sObjectMgr.LoadGameObjectForQuests(); - sLog.outString("Loading BattleMasters..."); + StartupUI::Step("Loading BattleMasters..."); sBattleGroundMgr.LoadBattleMastersEntry(); - sLog.outString("Loading BattleGround event indexes..."); + StartupUI::Step("Loading BattleGround event indexes..."); sBattleGroundMgr.LoadBattleEventIndexes(); - sLog.outString("Loading GameTeleports..."); + StartupUI::Step("Loading GameTeleports..."); sObjectMgr.LoadGameTele(); ///- Loading localization data - sLog.outString("Loading Localization strings..."); + StartupUI::Step("Loading Localization strings..."); sObjectMgr.LoadCreatureLocales(); // must be after CreatureInfo loading sObjectMgr.LoadGameObjectLocales(); // must be after GameobjectInfo loading sObjectMgr.LoadItemLocales(); // must be after ItemPrototypes loading @@ -745,30 +751,34 @@ void World::SetInitialWorldSettings() sLog.outString(">>> Localization strings loaded"); sLog.outString(); + StartupUI::BeginPhase("Characters and economy"); + ///- Load dynamic data tables from the database - sLog.outString("Loading Auctions..."); + StartupUI::Step("Loading Auctions..."); sAuctionMgr.LoadAuctionItems(); sAuctionMgr.LoadAuctions(); sLog.outString(">>> Auctions loaded"); sLog.outString(); - sLog.outString("Loading Guilds..."); + StartupUI::Step("Loading Guilds..."); sGuildMgr.LoadGuilds(); - sLog.outString("Loading Groups..."); + StartupUI::Step("Loading Groups..."); sObjectMgr.LoadGroups(); sLog.outString("Returning old mails..."); sObjectMgr.ReturnOrDeleteOldMails(false); - sLog.outString("Loading GM tickets..."); + StartupUI::Step("Loading GM tickets..."); sTicketMgr.LoadGMTickets(); + StartupUI::BeginPhase("Scripts"); + #ifdef ENABLE_ELUNA if (sElunaConfig->IsElunaEnabled()) { ///- Run eluna scripts. - sLog.outString("Starting Eluna world state..."); + StartupUI::Step("Starting Eluna world state..."); // use map id -1 for the global Eluna state eluna = new Eluna(nullptr); sLog.outString(); @@ -776,7 +786,7 @@ void World::SetInitialWorldSettings() #endif /*ENABLE_ELUNA*/ ///- Load and initialize DBScripts Engine - sLog.outString("Loading DB-Scripts Engine..."); + StartupUI::Step("Loading DB-Scripts Engine..."); sScriptMgr.LoadDbScripts(DBS_ON_QUEST_START); // must be after load Creature/Gameobject(Template/Data) and QuestTemplate sScriptMgr.LoadDbScripts(DBS_ON_QUEST_END); // must be after load Creature/Gameobject(Template/Data) and QuestTemplate sScriptMgr.LoadDbScripts(DBS_ON_SPELL); // must be after load Creature/Gameobject(Template/Data) @@ -787,17 +797,17 @@ void World::SetInitialWorldSettings() sLog.outString(">>> DB Scripts loaded"); sLog.outString(); - sLog.outString("Loading Scripts text locales..."); // must be after Load*Scripts calls + StartupUI::Step("Loading Scripts text locales..."); // must be after Load*Scripts calls sScriptMgr.LoadDbScriptStrings(); ///- Load and initialize EventAI Scripts - sLog.outString("Loading CreatureEventAI Texts..."); + StartupUI::Step("Loading CreatureEventAI Texts..."); sEventAIMgr.LoadCreatureEventAI_Texts(false); // false, will checked in LoadCreatureEventAI_Scripts - sLog.outString("Loading CreatureEventAI Summons..."); + StartupUI::Step("Loading CreatureEventAI Summons..."); sEventAIMgr.LoadCreatureEventAI_Summons(false); // false, will checked in LoadCreatureEventAI_Scripts - sLog.outString("Loading CreatureEventAI Scripts..."); + StartupUI::Step("Loading CreatureEventAI Scripts..."); sEventAIMgr.LoadCreatureEventAI_Scripts(); sLog.outString("Initializing Scripts..."); @@ -822,8 +832,10 @@ void World::SetInitialWorldSettings() #endif /* ENABLE_SD3 */ sLog.outString(); + StartupUI::BeginPhase("World systems"); + ///- Initialize game time and timers - sLog.outString("Initialize game time and timers"); + StartupUI::Step("Initialize game time and timers"); m_gameTime = time(NULL); m_startTime = m_gameTime; @@ -890,11 +902,11 @@ void World::SetInitialWorldSettings() sOutdoorPvPMgr.InitOutdoorPvP(); // Initialize Warden - sLog.outString("Loading Warden Checks..."); + StartupUI::Step("Loading Warden Checks..."); sWardenCheckMgr->LoadWardenChecks(); sLog.outString(); - sLog.outString("Loading Warden Action Overrides..."); + StartupUI::Step("Loading Warden Action Overrides..."); sWardenCheckMgr->LoadWardenOverrides(); sLog.outString(); @@ -905,7 +917,7 @@ void World::SetInitialWorldSettings() sLog.outString("Starting server Maintenance system..."); InitServerMaintenanceCheck(); - sLog.outString("Loading Honor Standing list..."); + StartupUI::Step("Loading Honor Standing list..."); sObjectMgr.LoadStandingList(); sLog.outString("Starting Game Event system..."); @@ -913,7 +925,7 @@ void World::SetInitialWorldSettings() m_timers[WUPDATE_EVENTS].SetInterval(nextGameEvent); // depend on next event sLog.outString(); - sLog.outString("Loading grids for active creatures and local transports..."); + StartupUI::Step("Loading grids for active creatures and local transports..."); uint32 loadContinentsBegin = getMSTime(); MapManager::LivingWorldStartupStats lwStats = sMapMgr.LoadContinents(); uint32 loadContinentsMs = GetMSTimeDiffToNow(loadContinentsBegin); @@ -921,7 +933,7 @@ void World::SetInitialWorldSettings() lwStats.forcedMaps, lwStats.totalUniqueGrids, lwStats.totalNewlyLoaded, lwStats.totalLocalTransports, loadContinentsMs); sLog.outString(); - sLog.outString("Loading global transports..."); + StartupUI::Step("Loading global transports..."); sMapMgr.LoadTransports(); sLog.outString(); @@ -968,105 +980,130 @@ void World::SetInitialWorldSettings() sPlayerbotAIConfig.Initialize(); #endif - showFooter(); + StartupUI::EndPhase(); uint32 startupDuration = GetMSTimeDiffToNow(startupBegin); - sLog.outString("SERVER STARTUP TIME: %i minutes %i seconds", (startupDuration / 60000), ((startupDuration % 60000) / 1000)); - sLog.outString(); + + // The completion panel says this in a nicer way, but the line is a long-lived + // grep target for ops tooling, so the log file keeps it verbatim. + char startupLine[128]; + snprintf(startupLine, sizeof(startupLine), "SERVER STARTUP TIME: %i minutes %i seconds", + (startupDuration / 60000), ((startupDuration % 60000) / 1000)); + StartupUI::LogOnly(startupLine); + + showFooter(startupDuration); + + ///- World initialization is over: drop the console hooks, so no runtime log + /// line and no reload-time progress bar pays for them. + StartupUI::Shutdown(); +} + +namespace +{ + /// "Eluna, ScriptDev3, Warden" -- or "none" for an empty list. + std::string JoinList(const std::vector& items) + { + std::string joined; + + for (std::size_t i = 0; i < items.size(); ++i) + { + if (i > 0) + { + joined += ", "; + } + joined += items[i]; + } + + return joined.empty() ? std::string("none") : joined; + } } /** - * @brief Prints the startup footer and enabled module summary. + * @brief Draws the completion panel: what this build is, what it is talking to, + * and which optional subsystems came up. + * + * @param startupMs how long world initialization took */ -void World::showFooter() +void World::showFooter(uint32 startupMs) { - std::set modules_; + std::vector enabled; + std::vector disabled; - // ELUNA is either included or disabled + // Eluna and SD3 are either compiled in or not there at all. #ifdef ENABLE_ELUNA - modules_.insert(" Eluna : Enabled"); + enabled.push_back("Eluna"); #endif - // SD3 is either included or disabled #ifdef ENABLE_SD3 - modules_.insert(" ScriptDev3 (SD3) : Enabled"); + enabled.push_back("ScriptDev3"); #endif - // PLAYERBOTS can be included or excluded but also disabled via mangos.conf + // The rest are compiled in but mangosd.conf still decides whether they run. #ifdef ENABLE_PLAYERBOTS - bool playerBotActive = sConfig.GetBoolDefault("PlayerbotAI.DisableBots", true); - if (playerBotActive) + if (sConfig.GetBoolDefault("PlayerbotAI.DisableBots", true)) { - modules_.insert(" PlayerBots : Disabled"); + disabled.push_back("PlayerBots"); } else { - modules_.insert(" PlayerBots : Enabled"); + enabled.push_back("PlayerBots"); } #endif - // Remote Access can be activated / deactivated via mangos.conf - bool raActive = sConfig.GetBoolDefault("Ra.Enable", false); - if (raActive) + if (sConfig.GetBoolDefault("Ra.Enable", false)) { - modules_.insert(" Remote Access (RA) : Enabled"); + enabled.push_back("Remote Access"); } else { - modules_.insert(" Remote Access (RA) : Disabled"); + disabled.push_back("Remote Access"); } - // SOAP can be included or excluded but also disabled via mangos.conf #ifdef ENABLE_SOAP - bool soapActive = sConfig.GetBoolDefault("SOAP.Enabled", false); - if (soapActive) + if (sConfig.GetBoolDefault("SOAP.Enabled", false)) { - modules_.insert(" SOAP : Enabled"); + enabled.push_back("SOAP"); } else { - modules_.insert(" SOAP : Disabled"); + disabled.push_back("SOAP"); } #endif - // Warden is always included, set active or disabled via mangos.conf - bool wardenActive = (sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) || sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)); - if (wardenActive) + if (getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) || getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)) { - modules_.insert(" Warden : Enabled"); + enabled.push_back("Warden"); } else { - modules_.insert(" Warden : Disabled"); + disabled.push_back("Warden"); } - std::string thisClientVersion (EXPECTED_MANGOSD_CLIENT_VERSION); - std::string thisClientBuilds = AcceptableClientBuildsListStr(); + char database[128]; + snprintf(database, sizeof(database), "Rel%s.%s.%s", GitRevision::GetWorldDBVersion(), + GitRevision::GetWorldDBStructure(), GitRevision::GetWorldDBContent()); - std::string sModules; - for (std::set::const_iterator it = modules_.begin(); it != modules_.end(); ++it) + char ready[64]; + if (startupMs >= 60000) + { + snprintf(ready, sizeof(ready), "ready in %um %02us", startupMs / 60000, (startupMs % 60000) / 1000); + } + else { - sModules = sModules + " \n" + *it; + snprintf(ready, sizeof(ready), "ready in %.1fs", startupMs / 1000.0); } - sLog.outString("\n" - "_______________________________________________________\n" - "\n" - " MaNGOS Server: World Initialization Complete\n" - "_______________________________________________________\n" - "\n" - " Server Version : %s\n" - " Eluna Version : %s\n" - " SD3 Version : %s\n" - " Database Version : Rel%s.%s.%s\n" - "\n" - " Supporting Clients : %s\n" - " Builds : %s\n" - "\n" - " Module Status -\n%s\n" - "_______________________________________________________\n", - GitRevision::GetProductVersionStr(), GitRevision::GetDepElunaFullRevision(), GitRevision::GetDepSD3FullRevision(), GitRevision::GetWorldDBVersion(), GitRevision::GetWorldDBStructure(), GitRevision::GetWorldDBContent(), - thisClientVersion.c_str(), thisClientBuilds.c_str(), sModules.c_str()); + std::vector rows; + rows.push_back(StartupUI::Row("server", GitRevision::GetProductVersionStr())); + rows.push_back(StartupUI::Row("eluna", GitRevision::GetDepElunaFullRevision())); + rows.push_back(StartupUI::Row("sd3", GitRevision::GetDepSD3FullRevision())); + rows.push_back(StartupUI::Row("database", database)); + rows.push_back(StartupUI::Row("clients", EXPECTED_MANGOSD_CLIENT_VERSION)); + rows.push_back(StartupUI::Row("builds", AcceptableClientBuildsListStr())); + rows.push_back(StartupUI::Row("enabled", JoinList(enabled))); + rows.push_back(StartupUI::Row("disabled", JoinList(disabled))); + + StartupUI::Panel("World initialization complete", ready, rows); } /** diff --git a/src/game/WorldHandlers/World.h b/src/game/WorldHandlers/World.h index 4b45c61d7e..5eced6c7b5 100644 --- a/src/game/WorldHandlers/World.h +++ b/src/game/WorldHandlers/World.h @@ -477,7 +477,7 @@ typedef UNORDERED_MAP SessionMap; class World { public: - static ACE_Atomic_Op m_worldLoopCounter; + static std::atomic m_worldLoopCounter; World(); ~World(); @@ -520,7 +520,7 @@ class World void SetMotd(const std::string& motd) { m_motd = motd; } /// Get the current Message of the Day const char* GetMotd() const { return m_motd.c_str(); } - void showFooter(); + void showFooter(uint32 startupMs); LocaleConstant GetDefaultDbcLocale() const { return m_defaultDbcLocale; } @@ -835,14 +835,14 @@ class World static uint32 m_visibility_observer_sweep_interval; // CLI command holder to be thread safe - ACE_Based::LockedQueue cliCmdQueue; + MaNGOS::LockedQueue cliCmdQueue; // Player Queue Queue m_QueuedSessions; // sessions that are added async void AddSession_(WorldSession* s); - ACE_Based::LockedQueue addSessQueue; + MaNGOS::LockedQueue addSessQueue; // used versions std::string m_DBVersion; diff --git a/src/game/vmap/MapTree.h b/src/game/vmap/MapTree.h index df776f0359..d8247365d9 100644 --- a/src/game/vmap/MapTree.h +++ b/src/game/vmap/MapTree.h @@ -29,6 +29,12 @@ #include "Utilities/UnorderedMapSet.h" #include "BIH.h" +// Transitional: pulled in here so the Movemap-Generator's MapBuilder.cpp (in the +// Extractor_projects submodule) gets FLT_MAX/INT_MAX without editing that repo. +// Belongs in MapBuilder.cpp; move it there when the extractor submodule is updated. +#include +#include + namespace VMAP { class ModelInstance; diff --git a/src/ipc/BoundedQueue.h b/src/ipc/BoundedQueue.h index 940e86c925..5e04919970 100644 --- a/src/ipc/BoundedQueue.h +++ b/src/ipc/BoundedQueue.h @@ -44,7 +44,7 @@ * CONCURRENCY / INVARIANT: a SINGLE mutex (@ref m_mutex) guards the whole of * every operation - the container mutation AND the count/byte counters move * together under that one lock. The earlier design mutated an - * ACE_Based::LockedQueue (with its own internal mutex) and then bumped + * MaNGOS::LockedQueue (with its own internal mutex) and then bumped * SEPARATE atomic counters OUTSIDE that mutex, so a producer preempted between * the enqueue and the counter increment could be overtaken by a consumer's * decrement, permanently drifting the counters away from the real contents. diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt index d51f39f1c3..1821227a74 100644 --- a/src/ipc/CMakeLists.txt +++ b/src/ipc/CMakeLists.txt @@ -1,4 +1,7 @@ file(GLOB ah_ipc_SRC *.cpp *.h) add_library(ah_ipc STATIC ${ah_ipc_SRC}) target_include_directories(ah_ipc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(ah_ipc PUBLIC shared) # shared brings ace, ByteBuffer, LockedQueue, Config +target_link_libraries(ah_ipc PUBLIC shared) # shared brings ByteBuffer, LockedQueue, Config, Threading +if(WIN32) + target_link_libraries(ah_ipc PUBLIC ws2_32) +endif() diff --git a/src/ipc/IpcChannel.cpp b/src/ipc/IpcChannel.cpp index 86c67c0ba1..bc78ddc78c 100644 --- a/src/ipc/IpcChannel.cpp +++ b/src/ipc/IpcChannel.cpp @@ -20,29 +20,29 @@ */ #include "IpcChannel.h" -#include "IpcOutboundNotifier.h" +#include "IpcServerHandler.h" +#include "IpcClientHandler.h" #include "Log/Log.h" + #include +#include // =========================================================================== // Concurrency model // --------------------------------------------------------------------------- // The facade (IpcServer / IpcClient) is used by the CALLER thread (mangosd's -// world thread, or the child's service loop). The socket handler runs on a -// separate REACTOR thread. To stay race-free and free of use-after-free: +// world thread, or the child's service loop). The socket handler runs its own +// receiver thread. To stay race-free: // -// - All handler-pointer access and all peer().send() / socket I/O happen on -// the reactor thread, never on the caller thread. -// - SendFrame() (caller thread) enqueues the frame into a thread-safe -// outbound queue and wakes the reactor via ACE_Reactor::notify(). The -// reactor-thread notifier (IpcOutboundNotifier::handle_exception) drains -// the queue and performs the actual send through the live handler. -// - Liveness is a std::atomic published by the reactor thread on READY -// and cleared on handle_close(); Connected() just reads the atomic. +// - Liveness / run-id / write-authority are std::atomics on the shared, +// reference-counted IpcLink, published by the receiver thread. +// - SendFrame() copies the live handler shared_ptr from the link under its +// mutex and sends outside the lock; the copied reference keeps the handler +// alive for the duration of the send even if the receiver thread closes +// concurrently, and the handler serialises the socket write internally. // -// The IpcLink object couples the two threads. It is reference-counted and -// outlives both the handler and the reactor thread, so the notifier can always -// touch it safely. +// The IpcLink outlives both the handler and the driver thread, so the reliable +// lane and atomics can always be touched safely. // =========================================================================== // =========================================================================== @@ -53,7 +53,7 @@ IpcServer::IpcServer() : m_inbound(IPC_INBOUND_QUEUE_CAP, IPC_INBOUND_BYTE_CAP), m_link(nullptr), m_thread(nullptr), - m_aceThread(nullptr) + m_worker(nullptr) { } @@ -70,15 +70,13 @@ bool IpcServer::Start(const char* host, uint16 port, const std::string& secret) return false; } - // Create the coupling object (one ref held by the facade); the thread takes - // its own ref in its constructor. + // Create the coupling object (one ref for the facade; the driver thread + // takes its own ref in its constructor). m_link = new IpcServerLink(); m_link->AddRef(); m_thread = new IpcThread(host, port, secret, &m_inbound, m_link); - m_aceThread = new ACE_Based::Thread(m_thread); - // ACE_Based::Thread::start() is called in its constructor when given a - // Runnable (see Threading.h). + m_worker = new MaNGOS::Thread(m_thread); // starts the thread immediately return true; } @@ -88,14 +86,12 @@ void IpcServer::Stop() { m_thread->Stop(); } - if (m_aceThread) + if (m_worker) { - m_aceThread->wait(); - delete m_aceThread; - m_aceThread = nullptr; + m_worker->wait(); + delete m_worker; // joins + drops the last reference, reclaiming m_thread + m_worker = nullptr; } - // m_thread is deleted by ACE_Based reference counting when decReference() - // reaches 0. m_thread = nullptr; if (m_link) @@ -107,47 +103,18 @@ void IpcServer::Stop() bool IpcServer::SendFrame(const IpcMessage& msg) { - if (!m_link) - { - return false; - } - - // Only enqueue while the channel is live; otherwise the frame would sit in - // the queue with no handler to drain it. - if (!m_link->live.load(std::memory_order_acquire)) - { - return false; - } - - // Enqueue on the caller thread (thread-safe queue), then poke the reactor. - if (!m_link->outbound.push(msg)) - { - sLog.outError("IpcServer::SendFrame: outbound queue full" - " - frame 0x%04X dropped", msg.op); - return false; - } - - // Poke the reactor under m_notifyMtx so the notify can never race teardown - // destroying the reactor/notifier. Teardown nulls reactor/notifier under - // this same mutex AFTER joining the reactor thread, so here reactor is - // either still valid (and notify is safe) or already null (we skip). - // ACE_Reactor::notify() is documented thread-safe; the notifier's - // handle_exception() runs on the reactor thread and drains the queue. - std::lock_guard guard(m_link->m_notifyMtx); - - ACE_Reactor* r = m_link->reactor.load(std::memory_order_acquire); - if (!r) + if (!m_link || !m_link->live.load(std::memory_order_acquire)) { return false; } - if (r->notify(m_link->notifier) == -1) + std::shared_ptr h = m_link->GetSendTarget(); + if (!h) { - sLog.outError("IpcServer::SendFrame: reactor notify failed"); return false; } - return true; + return h->SendFrame(msg) == 0; } bool IpcServer::PopInbound(IpcMessage& out) @@ -157,8 +124,6 @@ bool IpcServer::PopInbound(IpcMessage& out) bool IpcServer::PopReliable(IpcMessage& out) { - // The reliable lane lives on the shared, reference-counted link so it - // survives child reconnects (the reactor pushes, the world thread drains). return m_link && m_link->PopReliable(out); } @@ -174,12 +139,18 @@ bool IpcServer::Connected() const void IpcServer::SetRunId(uint32 runId) { - IpcServerHandler::SetPendingRunId(runId); + if (m_link) + { + m_link->runId.store(runId, std::memory_order_release); + } } void IpcServer::SetWriteAuthority(bool on) { - IpcServerHandler::SetPendingWriteAuthority(on); + if (m_link) + { + m_link->writeAuthority.store(on ? 1u : 0u, std::memory_order_release); + } } // =========================================================================== @@ -190,7 +161,7 @@ IpcClient::IpcClient() : m_inbound(IPC_INBOUND_QUEUE_CAP), m_link(nullptr), m_thread(nullptr), - m_aceThread(nullptr) + m_worker(nullptr) { } @@ -210,9 +181,8 @@ bool IpcClient::Connect(const char* host, uint16 port, const std::string& secret m_link = new IpcClientLink(); m_link->AddRef(); - m_thread = new IpcClientThread(host, port, secret, - &m_inbound, m_link); - m_aceThread = new ACE_Based::Thread(m_thread); + m_thread = new IpcClientThread(host, port, secret, &m_inbound, m_link); + m_worker = new MaNGOS::Thread(m_thread); return true; } @@ -222,11 +192,11 @@ void IpcClient::Stop() { m_thread->Stop(); } - if (m_aceThread) + if (m_worker) { - m_aceThread->wait(); - delete m_aceThread; - m_aceThread = nullptr; + m_worker->wait(); + delete m_worker; + m_worker = nullptr; } m_thread = nullptr; @@ -239,41 +209,18 @@ void IpcClient::Stop() bool IpcClient::SendFrame(const IpcMessage& msg) { - if (!m_link) - { - return false; - } - - if (!m_link->live.load(std::memory_order_acquire)) - { - return false; - } - - if (!m_link->outbound.push(msg)) - { - fprintf(stderr, "IpcClient::SendFrame: outbound queue full" - " - frame 0x%04X dropped\n", - static_cast(msg.op)); - return false; - } - - // See IpcServer::SendFrame: notify under m_notifyMtx so it cannot race - // teardown destroying the reactor/notifier. - std::lock_guard guard(m_link->m_notifyMtx); - - ACE_Reactor* r = m_link->reactor.load(std::memory_order_acquire); - if (!r) + if (!m_link || !m_link->live.load(std::memory_order_acquire)) { return false; } - if (r->notify(m_link->notifier) == -1) + std::shared_ptr h = m_link->GetSendTarget(); + if (!h) { - fprintf(stderr, "IpcClient::SendFrame: reactor notify failed\n"); return false; } - return true; + return h->SendFrame(msg) == 0; } bool IpcClient::PopInbound(IpcMessage& out) @@ -293,18 +240,10 @@ bool IpcClient::Connected() const uint32 IpcClient::RunId() const { - if (!m_link) - { - return 0; - } - return m_link->runId.load(std::memory_order_acquire); + return m_link ? m_link->runId.load(std::memory_order_acquire) : 0u; } bool IpcClient::WriteAuthority() const { - if (!m_link) - { - return false; - } - return m_link->writeAuthority.load(std::memory_order_acquire) != 0u; + return m_link && m_link->writeAuthority.load(std::memory_order_acquire) != 0u; } diff --git a/src/ipc/IpcChannel.h b/src/ipc/IpcChannel.h index cb58902f0b..721b8c2727 100644 --- a/src/ipc/IpcChannel.h +++ b/src/ipc/IpcChannel.h @@ -25,8 +25,6 @@ #include "Common.h" #include "IpcMessage.h" #include "IpcThread.h" -#include "IpcServerHandler.h" -#include "IpcClientHandler.h" #include "IpcLink.h" #include "BoundedQueue.h" #include "Threading/Threading.h" @@ -167,7 +165,7 @@ class IpcServer BoundedQueue m_inbound; IpcServerLink* m_link; ///< Shared link (refcounted). IpcThread* m_thread; - ACE_Based::Thread* m_aceThread; + MaNGOS::Thread* m_worker; ///< Server driver thread. // Non-copyable. IpcServer(const IpcServer&); @@ -247,7 +245,7 @@ class IpcClient BoundedQueue m_inbound; IpcClientLink* m_link; ///< Shared link (refcounted). IpcClientThread* m_thread; - ACE_Based::Thread* m_aceThread; + MaNGOS::Thread* m_worker; ///< Client driver thread. // Non-copyable. IpcClient(const IpcClient&); diff --git a/src/ipc/IpcClientHandler.cpp b/src/ipc/IpcClientHandler.cpp index 1ec2c74fa1..29e8b8d7f9 100644 --- a/src/ipc/IpcClientHandler.cpp +++ b/src/ipc/IpcClientHandler.cpp @@ -22,10 +22,7 @@ #include "IpcClientHandler.h" #include "IpcReliable.h" #include "IpcVersion.h" -#include "Log/Log.h" -#include -#include #include #ifdef _WIN32 @@ -36,172 +33,113 @@ #define IPC_GETPID() static_cast(::getpid()) #endif -// --------------------------------------------------------------------------- -// Static context -// --------------------------------------------------------------------------- - -BoundedQueue* IpcClientHandler::s_pendingInbound = nullptr; -std::string IpcClientHandler::s_pendingSecret; -IpcClientLink* IpcClientHandler::s_pendingLink = nullptr; - -void IpcClientHandler::SetPendingContext(BoundedQueue* inbound, - const std::string& secret, - IpcClientLink* link) -{ - s_pendingInbound = inbound; - s_pendingSecret = secret; - s_pendingLink = link; -} - -// --------------------------------------------------------------------------- -// Constructor / destructor -// --------------------------------------------------------------------------- - -IpcClientHandler::IpcClientHandler() - : m_outBuffer(nullptr), +IpcClientHandler::IpcClientHandler(IpcSocket&& sock, + BoundedQueue* inbound, + const std::string& secret, + IpcClientLink* link) + : m_sock(std::move(sock)), m_state(IPC_CLI_WAIT_CONNECT), - m_inbound(nullptr), - m_link(nullptr), + m_secret(secret), + m_inbound(inbound), + m_link(link), m_closing(false) { - reference_counting_policy().value( - ACE_Event_Handler::Reference_Counting_Policy::ENABLED); + if (m_link) + { + m_link->AddRef(); + } } IpcClientHandler::~IpcClientHandler() { - if (m_outBuffer) - { - m_outBuffer->release(); - m_outBuffer = nullptr; - } if (m_link) { m_link->Release(); m_link = nullptr; } - peer().close(); } // --------------------------------------------------------------------------- -// open() - called by ACE_Connector when connection succeeds +// SendHello - initiate the handshake // --------------------------------------------------------------------------- -int IpcClientHandler::open(void* /*connector*/) +int IpcClientHandler::SendHello() { - if (m_outBuffer) - { - return -1; - } - - m_inbound = s_pendingInbound; - m_secret = s_pendingSecret; - if (s_pendingLink) - { - m_link = s_pendingLink; - m_link->AddRef(); - } - - ACE_NEW_RETURN(m_outBuffer, ACE_Message_Block(k_outBufSize), -1); - - if (reactor()->register_handler(this, ACE_Event_Handler::READ_MASK) == -1) - { - fprintf(stderr, "IpcClientHandler::open: register_handler failed\n"); - return -1; - } - - // Publish ourselves into the link as the reactor-thread handler slot - // (runs on the reactor thread). Liveness stays false until the handshake - // completes in ProcessFrame(). - if (m_link) - { - m_link->handler = this; - } - - // Begin the handshake BEFORE releasing our local reference. SendHello() - // touches `this`, so dropping the reference first would risk a use-after- - // free if the reactor concurrently closed the handler. We send first, then - // hand ownership to the reactor. - const int rc = SendHello(); - - // Reactor now holds a reference; release ours. - remove_reference(); - return rc; -} + IpcMessage hello; + hello.op = IPC_HELLO; -// --------------------------------------------------------------------------- -// close() -// --------------------------------------------------------------------------- + // body: uint16 proto, uint32 pid, then secret bytes + hello.body << uint16(IPC_PROTOCOL_VERSION) + << uint32(IPC_GETPID()); + hello.body.append(reinterpret_cast(m_secret.data()), + m_secret.size()); -int IpcClientHandler::close(u_long /*flags*/) -{ - shutdown(); - m_closing = true; - remove_reference(); - return 0; + m_state = IPC_CLI_WAIT_HELLO_ACK; + return SendFrame(hello); } // --------------------------------------------------------------------------- -// handle_input() +// ReceiveLoop // --------------------------------------------------------------------------- -int IpcClientHandler::handle_input(ACE_HANDLE) +void IpcClientHandler::ReceiveLoop(std::atomic& stop) { - if (m_closing) - { - return -1; - } - char buf[4096]; - const ssize_t n = peer().recv(buf, sizeof(buf)); - if (n <= 0) + while (!stop.load(std::memory_order_acquire) && + !m_closing.load(std::memory_order_acquire)) { - if (n == 0 || (errno != EWOULDBLOCK && errno != EAGAIN)) + const std::ptrdiff_t n = m_sock.RecvSome(buf, sizeof(buf), 200); + if (n == -2) { - return -1; + continue; + } + if (n <= 0) + { + break; } - return 0; - } - - m_recvBuf.append(reinterpret_cast(buf), - static_cast(n)); - while (m_recvBuf.rpos() < m_recvBuf.size()) - { - IpcMessage msg; - std::string err; + m_recvBuf.append(reinterpret_cast(buf), + static_cast(n)); - if (!IpcMessage::Decode(m_recvBuf, msg, err)) + bool fatal = false; + while (m_recvBuf.rpos() < m_recvBuf.size()) { - // Only "short header" / "incomplete" are transient (need more - // bytes); any other Decode error is a corrupt stream and fatal. - if (err == "incomplete" || err == "short header") + IpcMessage msg; + std::string err; + + if (!IpcMessage::Decode(m_recvBuf, msg, err)) { + if (err == "incomplete" || err == "short header") + { + break; + } + fprintf(stderr, "IpcClientHandler: framing error: %s\n", + err.c_str()); + fatal = true; break; } - fprintf(stderr, "IpcClientHandler: framing error: %s\n", - err.c_str()); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + if (ProcessFrame(msg) == -1) + { + fatal = true; + break; + } } - if (ProcessFrame(msg) == -1) + CompactRecvBuf(); + + if (fatal) { - return -1; + break; } } - // Compact: drop consumed front bytes so a peer that always leaves a - // trailing partial frame cannot grow m_recvBuf without bound. - CompactRecvBuf(); - - return 0; + OnClose(); } // --------------------------------------------------------------------------- -// CompactRecvBuf() - drop already-consumed front bytes from the reassembly buf +// CompactRecvBuf // --------------------------------------------------------------------------- void IpcClientHandler::CompactRecvBuf() @@ -225,54 +163,12 @@ void IpcClientHandler::CompactRecvBuf() } // --------------------------------------------------------------------------- -// handle_output() -// --------------------------------------------------------------------------- - -int IpcClientHandler::handle_output(ACE_HANDLE) -{ - return FlushOutBuffer(); -} - -// --------------------------------------------------------------------------- -// handle_close() -// --------------------------------------------------------------------------- - -int IpcClientHandler::handle_close(ACE_HANDLE h, ACE_Reactor_Mask) -{ - { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); - m_closing = true; - if (h == ACE_INVALID_HANDLE) - { - peer().close_writer(); - } - } - - // Clear the link so the facade stops reporting us live and the reactor- - // thread notifier stops routing through us. Runs on the reactor thread. - if (m_link) - { - m_link->live.store(false, std::memory_order_release); - if (m_link->handler == this) - { - m_link->handler = nullptr; - } - } - - reactor()->remove_handler(this, - ACE_Event_Handler::DONT_CALL | ACE_Event_Handler::ALL_EVENTS_MASK); - return 0; -} - -// --------------------------------------------------------------------------- -// SendFrame() +// SendFrame // --------------------------------------------------------------------------- int IpcClientHandler::SendFrame(const IpcMessage& msg) { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); - - if (m_closing) + if (m_closing.load(std::memory_order_acquire)) { return -1; } @@ -280,41 +176,20 @@ int IpcClientHandler::SendFrame(const IpcMessage& msg) ByteBuffer wire; msg.Encode(wire); - const size_t len = wire.size(); - if (m_outBuffer->space() < len) + std::lock_guard g(m_sendMtx); + if (m_closing.load(std::memory_order_acquire)) { - fprintf(stderr, "IpcClientHandler::SendFrame: output buffer full\n"); return -1; } - - m_outBuffer->copy(reinterpret_cast(wire.contents()), len); - - if (reactor()->schedule_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) + if (!m_sock.SendAll(wire.contents(), wire.size())) { - fprintf(stderr, "IpcClientHandler::SendFrame:" - " schedule_wakeup WRITE_MASK failed\n"); return -1; } - return 0; } // --------------------------------------------------------------------------- -// IsLive / IsClosing -// --------------------------------------------------------------------------- - -bool IpcClientHandler::IsLive() const -{ - return m_state == IPC_CLI_LIVE; -} - -bool IpcClientHandler::IsClosing() const -{ - return m_closing; -} - -// --------------------------------------------------------------------------- -// InboundFrameAcceptable() - per-opcode size predicate (shared with tests) +// InboundFrameAcceptable - per-opcode size predicate (shared with tests) // --------------------------------------------------------------------------- bool IpcClientHandler::InboundFrameAcceptable(uint16 op, uint32 bodyLen) @@ -328,26 +203,7 @@ bool IpcClientHandler::InboundFrameAcceptable(uint16 op, uint32 bodyLen) } // --------------------------------------------------------------------------- -// SendHello() - initiate the handshake -// --------------------------------------------------------------------------- - -int IpcClientHandler::SendHello() -{ - IpcMessage hello; - hello.op = IPC_HELLO; - - // body: uint16 proto, uint32 pid, then secret bytes - hello.body << uint16(IPC_PROTOCOL_VERSION) - << uint32(IPC_GETPID()); - hello.body.append(reinterpret_cast(m_secret.data()), - m_secret.size()); - - m_state = IPC_CLI_WAIT_HELLO_ACK; - return SendFrame(hello); -} - -// --------------------------------------------------------------------------- -// ProcessFrame() - handshake state machine (client side) +// ProcessFrame - handshake state machine (client side) // --------------------------------------------------------------------------- int IpcClientHandler::ProcessFrame(const IpcMessage& msg) @@ -360,8 +216,7 @@ int IpcClientHandler::ProcessFrame(const IpcMessage& msg) { fprintf(stderr, "IpcClientHandler: expected IPC_HELLO_ACK," " got 0x%04X\n", static_cast(msg.op)); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } // Body: uint32 run-id + (SP-2) uint8 write-authority. A legacy @@ -390,7 +245,7 @@ int IpcClientHandler::ProcessFrame(const IpcMessage& msg) " write-authority %u\n", runId, static_cast(writeAuthority)); fflush(stdout); - // Send IPC_READY + IpcMessage ready; ready.op = IPC_READY; @@ -401,9 +256,10 @@ int IpcClientHandler::ProcessFrame(const IpcMessage& msg) return -1; } - // Publish liveness for the facade. Runs on the reactor thread. + // Publish the live send target and liveness for the facade. if (m_link) { + m_link->SetSendTarget(shared_from_this()); m_link->live.store(true, std::memory_order_release); } @@ -425,9 +281,7 @@ int IpcClientHandler::ProcessFrame(const IpcMessage& msg) break; } // [SP-2 decision 10] Mutation-class frames ride the UNBOUNDED - // reliable lane (never dropped); the facade drains it to exhaustion - // before the bounded queue each pass. Everything else stays on the - // bounded drop-newest queue. Fall back to bounded if no link. + // reliable lane; everything else stays on the bounded queue. if (m_link && IpcIsReliableOpcode(msg.op)) { m_link->PushReliable(msg); @@ -455,53 +309,18 @@ int IpcClientHandler::ProcessFrame(const IpcMessage& msg) } // --------------------------------------------------------------------------- -// FlushOutBuffer() +// OnClose - clear the link and close the socket (idempotent) // --------------------------------------------------------------------------- -int IpcClientHandler::FlushOutBuffer() +void IpcClientHandler::OnClose() { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); + m_closing.store(true, std::memory_order_release); - if (m_closing) - { - return -1; - } - - const size_t sendLen = m_outBuffer->length(); - if (sendLen == 0) + if (m_link) { - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - return 0; + m_link->live.store(false, std::memory_order_release); + m_link->ClearSendTarget(this); } -#ifdef MSG_NOSIGNAL - ssize_t n = peer().send(m_outBuffer->rd_ptr(), sendLen, MSG_NOSIGNAL); -#else - ssize_t n = peer().send(m_outBuffer->rd_ptr(), sendLen); -#endif - - if (n == 0) - { - return -1; - } - else if (n == -1) - { - if (errno == EWOULDBLOCK || errno == EAGAIN) - { - return 0; - } - return -1; - } - else if (n < static_cast(sendLen)) - { - m_outBuffer->rd_ptr(static_cast(n)); - m_outBuffer->crunch(); - return 0; - } - else - { - m_outBuffer->reset(); - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - return 0; - } + m_sock.Close(); } diff --git a/src/ipc/IpcClientHandler.h b/src/ipc/IpcClientHandler.h index d3b7e30d6d..509e80a88a 100644 --- a/src/ipc/IpcClientHandler.h +++ b/src/ipc/IpcClientHandler.h @@ -22,19 +22,17 @@ #ifndef AH_IPC_CLIENT_HANDLER_H #define AH_IPC_CLIENT_HANDLER_H -#include -#include -#include -#include -#include -#include -#include - #include "Common.h" #include "Utilities/ByteBuffer.h" #include "IpcMessage.h" #include "BoundedQueue.h" #include "IpcLink.h" +#include "IpcSocket.h" + +#include +#include +#include +#include /** * @brief Handshake state for the client (child) side of the IPC connection. @@ -49,94 +47,60 @@ enum IpcClientHandshakeState }; /** - * @brief Client-side IPC socket handler. + * @brief Client-side IPC connection handler (child / ah-service side). * - * Modelled on WorldSocket / IpcServerHandler. Used by ACE_Connector on the - * child (ah-service) side. On open(), immediately sends IPC_HELLO to begin - * the handshake. + * Owns the connected socket. SendHello() begins the handshake; ReceiveLoop() + * reassembles frames and dispatches them until close. Symmetric to + * IpcServerHandler. * - * Handshake sequence (client side): - * open() -> send IPC_HELLO { proto, pid, secret } - * recv IPC_HELLO_ACK -> send IPC_READY - * (channel is now live) + * Handshake (client side): + * SendHello() -> send IPC_HELLO { proto, pid, secret } + * recv IPC_HELLO_ACK -> send IPC_READY (channel now live) */ -class IpcClientHandler : public ACE_Svc_Handler +class IpcClientHandler : public std::enable_shared_from_this { public: - typedef ACE_Thread_Mutex LockType; - - // --- static injection before connector fires --- - - /** - * @brief Set shared secret, inbound queue and link before Connect() is - * called. Invoked on the reactor thread before open() fires. - */ - static void SetPendingContext(BoundedQueue* inbound, - const std::string& secret, - IpcClientLink* link); - - // --- ACE framework callbacks --- - - IpcClientHandler(); - virtual ~IpcClientHandler(); - - /// Called by ACE_Connector when the connection is established. - int open(void* connector = 0) override; + IpcClientHandler(IpcSocket&& sock, + BoundedQueue* inbound, + const std::string& secret, + IpcClientLink* link); + ~IpcClientHandler(); - int close(u_long flags = 0) override; - - int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override; - int handle_output(ACE_HANDLE = ACE_INVALID_HANDLE) override; - int handle_close( - ACE_HANDLE = ACE_INVALID_HANDLE, - ACE_Reactor_Mask = ACE_Event_Handler::ALL_EVENTS_MASK) override; + /// Send IPC_HELLO to begin the handshake. Call before ReceiveLoop. + int SendHello(); - // --- Public interface used by IpcClient facade --- + /// Run the blocking receive loop until close / stop. Clears the link. + void ReceiveLoop(std::atomic& stop); /** - * @brief Encode and queue @p msg for send. + * @brief Encode and send @p msg on the socket. Thread-safe. * @return 0 on success, -1 on failure. */ int SendFrame(const IpcMessage& msg); /// True iff a frame with this opcode + body length is acceptable to - /// stage (per-opcode size rule). Shared by ProcessFrame and tests. + /// stage (per-opcode size rule). Shared with tests. static bool InboundFrameAcceptable(uint16 op, uint32 bodyLen); - /// True once the handshake is complete. - bool IsLive() const; - - /// True once handle_close has been called. - bool IsClosing() const; - - /// Expose handshake state for the IpcClient facade polling loop. + bool IsLive() const { return m_state == IPC_CLI_LIVE; } + bool IsClosing() const { return m_closing.load(std::memory_order_acquire); } IpcClientHandshakeState GetState() const { return m_state; } private: - LockType m_outLock; - ACE_Message_Block* m_outBuffer; - static const size_t k_outBufSize = 65536; + IpcSocket m_sock; + std::mutex m_sendMtx; ByteBuffer m_recvBuf; - IpcClientHandshakeState m_state; - std::string m_secret; BoundedQueue* m_inbound; IpcClientLink* m_link; - bool m_closing; - // Static context (reactor thread only). - static BoundedQueue* s_pendingInbound; - static std::string s_pendingSecret; - static IpcClientLink* s_pendingLink; + std::atomic m_closing; - int SendHello(); - int ProcessFrame(const IpcMessage& msg); - int FlushOutBuffer(); + int ProcessFrame(const IpcMessage& msg); void CompactRecvBuf(); + void OnClose(); }; -typedef ACE_Connector IpcConnector; - #endif // AH_IPC_CLIENT_HANDLER_H diff --git a/src/ipc/IpcLink.h b/src/ipc/IpcLink.h index 78abcdc1c0..70d58e2460 100644 --- a/src/ipc/IpcLink.h +++ b/src/ipc/IpcLink.h @@ -24,114 +24,75 @@ #include "Common.h" #include "IpcMessage.h" -#include "BoundedQueue.h" -#include -#include #include #include +#include #include -class IpcServerHandler; -class IpcClientHandler; - -/** - * @brief Capacity of the outbound (caller-thread -> reactor) queue per side. - */ -static const size_t IPC_OUTBOUND_QUEUE_CAP = 256; - /** * @brief Thread-safe coupling object between a facade (caller thread) and its - * reactor thread / live handler. - * - * The whole point of this object is that mangosd's world thread calls - * SendFrame/Connected on the facade while a separate reactor thread runs the - * socket handler. The previous design handed a raw handler pointer across that - * boundary, which is a use-after-free + data race once cross-thread use begins. - * - * Concurrency contract: - * - @ref outbound and @ref live are the ONLY lock-free members the caller - * thread may touch. @ref outbound is internally synchronised (BoundedQueue) - * and @ref live is a std::atomic - both are safe to read/write from any - * thread. - * - @ref handler is owned by, and may only be read/written from, the REACTOR - * thread. The caller thread never dereferences it. - * - @ref reactor and @ref notifier form the notify target. The caller thread - * reads them (to call ACE_Reactor::notify) ONLY while holding - * @ref m_notifyMtx; teardown nulls them ONLY while holding the same mutex, - * and only AFTER the reactor thread has been joined. This serialises the - * caller's notify() against destruction of the reactor/notifier objects, - * closing the teardown TOCTOU/use-after-free. See @ref m_notifyMtx. + * socket handler (which runs a receiver thread). * - * Send path (caller thread): encode into @ref outbound, then take - * @ref m_notifyMtx and, if @ref reactor is non-null, poke the reactor via - * ACE_Reactor::notify(). The notifier's - * handle_exception() runs on the reactor thread, where it drains @ref outbound - * and performs the actual send through @ref handler. + * The facade (IpcServer/IpcClient) is used by mangosd's world thread (or the + * child's service loop) while the socket handler runs on its own thread. The + * link couples them race-free: * - * IMPORTANT: the reactor-thread drain (IpcOutboundNotifier::handle_exception) - * must NEVER take @ref m_notifyMtx. The drain runs on the reactor thread BEFORE - * the teardown join completes; taking the mutex there while teardown waits on - * the join would deadlock. The drain only needs @ref outbound and @ref handler, - * both of which it already uses. + * - @ref live, @ref runId, @ref writeAuthority are std::atomics published by + * the handler thread and read by the caller thread. + * - The send target is a shared_ptr to the live handler, published under + * @ref sendMtx when the handshake completes and cleared under the same + * mutex on close. SendFrame() copies the shared_ptr under the lock and then + * sends outside it; the copied reference keeps the handler alive for the + * duration of the send even if the receiver thread closes concurrently. + * The handler's own SendFrame serialises the socket write with its own mutex. + * - The reliable lane (@ref reliableInbound) is an UNBOUNDED, never-dropped + * queue for mutation-class frames; the handler thread pushes, the caller + * thread drains it to exhaustion before the bounded inbound queue each pass. * - * Lifetime: the link is heap-allocated and reference-counted (one ref held by - * the facade, one by the reactor thread). It outlives both the handler and the - * reactor so the reactor-thread notifier can always touch it safely. + * Lifetime: heap-allocated and reference-counted (one ref held by the facade, + * one by the handler thread), so it outlives both. */ template struct IpcLink { IpcLink() - : outbound(IPC_OUTBOUND_QUEUE_CAP), - live(false), + : live(false), runId(0), writeAuthority(0), handlerActive(false), - handler(nullptr), - reactor(nullptr), - notifier(nullptr), refCount(0) { } - // --- caller-thread-safe members --- - - /// Frames enqueued by the caller thread, drained on the reactor thread. - BoundedQueue outbound; - - /// Published true by the reactor thread on handshake completion, cleared on - /// handle_close(). Read by the caller thread via Connected(). + /// Published true by the handler thread on handshake completion, cleared on + /// close. Read by the caller thread via Connected(). std::atomic live; - /// Per-spawn run-id received in IPC_HELLO_ACK (set by handler, read by - /// IpcClient::RunId()). Zero until the handshake completes. + /// Per-spawn run-id received in IPC_HELLO_ACK (client) / to send in the ACK + /// (server). Zero until the handshake completes. std::atomic runId; - /// [SP-2] Write-authority bit received in IPC_HELLO_ACK (0 until then). + /// [SP-2] Write-authority bit exchanged in IPC_HELLO_ACK (0 until then). std::atomic writeAuthority; - /// [SP-2 decision 10] UNBOUNDED reliable inbound lane for mutation-class - /// frames (IpcIsReliableOpcode) that must never be dropped under inbound - /// pressure - they carry money/item value or their outcome. The reactor - /// thread pushes (PushReliable); the caller/facade thread drains - /// (PopReliable) to EXHAUSTION before the bounded inbound queue each pass, - /// so a browse flood on the bounded queue can neither drop nor starve a - /// value-bearing frame. Genuinely unbounded (no drop policy): the frame - /// rate is naturally bounded (one per player action / worker resolve) and - /// each accepted frame already passed the per-opcode size validation. Its - /// own mutex, independent of the notify/outbound paths. + /// Single-owner guard for the server: the FIRST accepted connection claims + /// it (test-and-set); a second concurrent local connection finds it set and + /// refuses itself without touching the live handler. Cleared by the owner + /// on close. + std::atomic handlerActive; + + // --- reliable inbound lane (unbounded, never dropped) --- + std::deque reliableInbound; std::mutex reliableMx; - /// Push a reliable frame (reactor thread). Never drops. void PushReliable(const IpcMessage& m) { std::lock_guard g(reliableMx); reliableInbound.push_back(m); } - /// Pop the oldest reliable frame (caller thread). False when empty. bool PopReliable(IpcMessage& out) { std::lock_guard g(reliableMx); @@ -144,8 +105,6 @@ struct IpcLink return true; } - /// Discard every queued reliable frame; returns the count discarded. Used - /// by the supervisor purge on child death/respawn (mirrors ClearInbound). size_t ClearReliable() { std::lock_guard g(reliableMx); @@ -154,62 +113,41 @@ struct IpcLink return removed; } - /// Approximate number of queued reliable frames. size_t ReliableSize() { std::lock_guard g(reliableMx); return reliableInbound.size(); } - /// Single-owner guard. Set true (test-and-set) by the FIRST handler that - /// opens on the reactor thread; any additional accepted connection finds - /// it already set and refuses itself WITHOUT touching the live handler, - /// reactor, or routing state. Cleared by the owning handler's - /// handle_close(). Only the loopback child should ever connect; a second - /// concurrent local connection is hostile and must not be able to steal - /// outbound routing by overwriting @ref handler. Atomic because the - /// test-and-set is the ownership decision; in practice all touches are on - /// the reactor thread, but the atomic makes the intent explicit and is - /// free on this path. - std::atomic handlerActive; + // --- send target (guarded shared_ptr to the live handler) --- - // --- reactor-thread-only members --- - - /// Live handler pointer. Set on the reactor thread when the handler goes - /// live; cleared on the reactor thread in handle_close(). NEVER read off - /// the reactor thread. - HandlerT* handler; - - /// The reactor owning the handler. Set once by the reactor thread before it - /// enters run_reactor_event_loop(); read by the caller thread ONLY to call - /// ACE_Reactor::notify(), which is documented thread-safe. - /// - /// This pointer is the publication point: it is stored LAST (after - /// @ref notifier, with release ordering) on the reactor thread, and - /// Connected()/SendFrame() on the caller thread load it with acquire - /// ordering and treat null as "not ready". A non-null acquire-load - /// therefore guarantees @ref notifier is also visible. - /// - /// TEARDOWN: the caller thread's SendFrame() additionally reads this under - /// @ref m_notifyMtx and only calls notify() while the mutex is held, so - /// the notify cannot race the reactor/notifier objects being destroyed. - std::atomic reactor; - - /// The reactor-thread notifier whose handle_exception() drains @ref - /// outbound. Set before @ref reactor is published; passed to - /// ACE_Reactor::notify() by the caller thread (under @ref m_notifyMtx). - /// Lifetime owned by the reactor thread (the IpcThread / IpcClientThread). - ACE_Event_Handler* notifier; - - /// Guards ONLY the (@ref reactor, @ref notifier) validity for the caller - /// thread's notify() call. SendFrame() takes this around the notify; - /// teardown takes it to null @ref reactor + @ref notifier AFTER joining the - /// reactor thread and BEFORE destroying those objects. The reactor-thread - /// drain path must NEVER take this mutex (see the class note above) - it - /// would deadlock against the teardown join. - std::mutex m_notifyMtx; - - // --- reference counting (simple, mutex-free via atomic) --- + void SetSendTarget(const std::shared_ptr& h) + { + std::lock_guard g(sendMtx); + sendTarget = h; + } + + void ClearSendTarget(const HandlerT* who) + { + std::lock_guard g(sendMtx); + // Only the current target clears itself, so a refused/older handler + // cannot wipe the live one. + if (sendTarget.get() == who) + { + sendTarget.reset(); + } + } + + std::shared_ptr GetSendTarget() + { + std::lock_guard g(sendMtx); + return sendTarget; + } + + std::mutex sendMtx; + std::shared_ptr sendTarget; + + // --- reference counting --- std::atomic refCount; @@ -224,6 +162,9 @@ struct IpcLink } }; +class IpcServerHandler; +class IpcClientHandler; + typedef IpcLink IpcServerLink; typedef IpcLink IpcClientLink; diff --git a/src/ipc/IpcOutboundNotifier.h b/src/ipc/IpcOutboundNotifier.h deleted file mode 100644 index 641b9ae578..0000000000 --- a/src/ipc/IpcOutboundNotifier.h +++ /dev/null @@ -1,98 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -#ifndef AH_IPC_OUTBOUND_NOTIFIER_H -#define AH_IPC_OUTBOUND_NOTIFIER_H - -#include "IpcLink.h" -#include "IpcMessage.h" - -#include -#include - -/** - * @brief Reactor-thread drain hook for a side's outbound queue. - * - * Registered with the reactor; the caller thread wakes it via - * ACE_Reactor::notify(this). ACE then invokes handle_exception() ON THE - * REACTOR THREAD, where it is safe to touch the live handler. handle_exception - * drains the link's outbound queue and forwards each frame to the live handler. - * - * @tparam LinkT IpcServerLink or IpcClientLink. - * @tparam HandlerT IpcServerHandler or IpcClientHandler (LinkT::handler type). - */ -template -class IpcOutboundNotifier : public ACE_Event_Handler -{ - public: - explicit IpcOutboundNotifier(LinkT* link) - : m_link(link) - { - m_link->AddRef(); - } - - ~IpcOutboundNotifier() override - { - if (m_link) - { - m_link->Release(); - m_link = nullptr; - } - } - - /** - * @brief Runs on the reactor thread (via ACE_Reactor::notify). - * - * Drains the outbound queue and forwards each frame to the live - * handler. Because this executes on the reactor thread, the handler - * pointer cannot be freed underneath us (handle_close() also runs on - * this thread and is serialised against us). - */ - int handle_exception(ACE_HANDLE /*fd*/ = ACE_INVALID_HANDLE) override - { - if (!m_link) - { - return 0; - } - - HandlerT* h = m_link->handler; // reactor-thread-only read - safe. - - IpcMessage msg; - while (m_link->outbound.pop(msg)) - { - if (h) - { - // Best-effort: a send failure here means the handler is - // closing; the frame is dropped (mirrors a dropped queue). - h->SendFrame(msg); - } - // If no live handler, frames are silently discarded - the - // caller checks Connected() before sending in normal use. - } - - return 0; - } - - private: - LinkT* m_link; -}; - -#endif // AH_IPC_OUTBOUND_NOTIFIER_H diff --git a/src/ipc/IpcProcess.cpp b/src/ipc/IpcProcess.cpp new file mode 100644 index 0000000000..4504511135 --- /dev/null +++ b/src/ipc/IpcProcess.cpp @@ -0,0 +1,198 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "IpcProcess.h" + +#include +#include + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#endif + +#ifdef _WIN32 + +IpcProcess::IpcProcess() : m_pid(INVALID_PID), m_handle(NULL) {} + +IpcProcess::~IpcProcess() +{ + if (m_handle != NULL) + { + CloseHandle(m_handle); + m_handle = NULL; + } +} + +bool IpcProcess::Spawn(const std::string& exePath, + const std::vector& args, + const std::string& envName, + const std::string& envValue) +{ + // Build a single command line: "exe" arg1 "arg with spaces" ... + std::string cmd = "\"" + exePath + "\""; + for (const std::string& a : args) + { + cmd += ' '; + if (a.find_first_of(" \t") != std::string::npos) + { + cmd += '"'; + cmd += a; + cmd += '"'; + } + else + { + cmd += a; + } + } + + // Pass the secret out-of-band: set it in this process's environment just + // for the CreateProcess call (inherited by the child), then remove it. + SetEnvironmentVariableA(envName.c_str(), envValue.c_str()); + + std::vector mutableCmd(cmd.begin(), cmd.end()); + mutableCmd.push_back('\0'); + + STARTUPINFOA si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(pi)); + + const BOOL ok = CreateProcessA( + NULL, mutableCmd.data(), NULL, NULL, FALSE, + CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); + + SetEnvironmentVariableA(envName.c_str(), NULL); + + if (!ok) + { + return false; + } + + CloseHandle(pi.hThread); + m_handle = pi.hProcess; + m_pid = static_cast(pi.dwProcessId); + return true; +} + +bool IpcProcess::Running() const +{ + if (m_handle == NULL) + { + return false; + } + return WaitForSingleObject(m_handle, 0) == WAIT_TIMEOUT; +} + +void IpcProcess::Terminate() +{ + if (m_handle != NULL) + { + TerminateProcess(m_handle, 1); + } +} + +void IpcProcess::Reap() +{ + if (m_handle != NULL) + { + CloseHandle(m_handle); + m_handle = NULL; + } + m_pid = INVALID_PID; +} + +#else // POSIX + +IpcProcess::IpcProcess() : m_pid(INVALID_PID) {} + +IpcProcess::~IpcProcess() {} + +bool IpcProcess::Spawn(const std::string& exePath, + const std::vector& args, + const std::string& envName, + const std::string& envValue) +{ + const pid_t pid = ::fork(); + if (pid < 0) + { + return false; + } + + if (pid == 0) + { + // Child: add the secret to our environment (only the child's), then exec. + ::setenv(envName.c_str(), envValue.c_str(), 1); + + std::vector argv; + argv.reserve(args.size() + 2); + argv.push_back(const_cast(exePath.c_str())); + for (const std::string& a : args) + { + argv.push_back(const_cast(a.c_str())); + } + argv.push_back(nullptr); + + ::execv(exePath.c_str(), argv.data()); + // exec only returns on failure. + ::_exit(127); + } + + m_pid = static_cast(pid); + return true; +} + +bool IpcProcess::Running() const +{ + if (m_pid == INVALID_PID) + { + return false; + } + int status = 0; + const pid_t r = ::waitpid(static_cast(m_pid), &status, WNOHANG); + // 0 => still running; >0 => exited (reaped here); <0 => not our child / gone. + return r == 0; +} + +void IpcProcess::Terminate() +{ + if (m_pid != INVALID_PID) + { + ::kill(static_cast(m_pid), SIGKILL); + } +} + +void IpcProcess::Reap() +{ + if (m_pid != INVALID_PID) + { + int status = 0; + ::waitpid(static_cast(m_pid), &status, WNOHANG); + m_pid = INVALID_PID; + } +} + +#endif diff --git a/src/ipc/IpcProcess.h b/src/ipc/IpcProcess.h new file mode 100644 index 0000000000..cdda6464c6 --- /dev/null +++ b/src/ipc/IpcProcess.h @@ -0,0 +1,88 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef AH_IPC_PROCESS_H +#define AH_IPC_PROCESS_H + +#include "Common.h" + +#include +#include + +#ifdef _WIN32 +#include +#endif + +/** + * @brief Cross-platform spawn/liveness/terminate/reap of a single child process. + * + * Spawns and supervises the AH worker child. The shared secret is passed + * out-of-band in the child's environment (never on argv), so it cannot be read + * from the process command line by another local account. + */ +class IpcProcess +{ + public: + static const uint32 INVALID_PID = 0; + + IpcProcess(); + ~IpcProcess(); + + /** + * @brief Spawn @p exePath with @p args and one extra environment + * variable @p envName=@p envValue (added to the inherited env). + * + * On Windows the child gets its own console (CREATE_NEW_CONSOLE). + * @return true on success; Pid()/Handle() are then valid. + */ + bool Spawn(const std::string& exePath, + const std::vector& args, + const std::string& envName, + const std::string& envValue); + + /// True while the child is still running (non-blocking check). + bool Running() const; + + /// Force-kill the child (SIGKILL / TerminateProcess). Best effort. + void Terminate(); + + /// Release the OS handle / reap the zombie once the child has exited. + void Reap(); + + uint32 Pid() const { return m_pid; } + bool Valid() const { return m_pid != INVALID_PID; } + +#ifdef _WIN32 + /// Child process HANDLE (for Job Object assignment); NULL if none. + HANDLE Handle() const { return m_handle; } +#endif + + private: + uint32 m_pid; +#ifdef _WIN32 + HANDLE m_handle; +#endif + + IpcProcess(const IpcProcess&); + IpcProcess& operator=(const IpcProcess&); +}; + +#endif // AH_IPC_PROCESS_H diff --git a/src/ipc/IpcServerHandler.cpp b/src/ipc/IpcServerHandler.cpp index 8c7652b941..c49956a67e 100644 --- a/src/ipc/IpcServerHandler.cpp +++ b/src/ipc/IpcServerHandler.cpp @@ -24,253 +24,112 @@ #include "IpcVersion.h" #include "Log/Log.h" -#include -#include -#include - -// --------------------------------------------------------------------------- -// Static context injected by IpcThread::run before the acceptor runs. -// All of these are written on the reactor thread before any accept fires. -// --------------------------------------------------------------------------- - -BoundedQueue* IpcServerHandler::s_pendingInbound = nullptr; -std::string IpcServerHandler::s_pendingSecret; -IpcServerLink* IpcServerHandler::s_pendingLink = nullptr; -std::atomic IpcServerHandler::s_pendingRunId(0); -std::atomic IpcServerHandler::s_pendingWriteAuthority(0); - -void IpcServerHandler::SetPendingContext(BoundedQueue* inbound, - const std::string& secret, - IpcServerLink* link) -{ - s_pendingInbound = inbound; - s_pendingSecret = secret; - s_pendingLink = link; -} - -void IpcServerHandler::SetPendingRunId(uint32 runId) -{ - s_pendingRunId.store(runId, std::memory_order_release); -} - -void IpcServerHandler::SetPendingWriteAuthority(bool on) -{ - s_pendingWriteAuthority.store(on ? 1u : 0u, std::memory_order_release); -} - -// --------------------------------------------------------------------------- -// Constructor / destructor -// --------------------------------------------------------------------------- - -IpcServerHandler::IpcServerHandler() - : m_outBuffer(nullptr), +IpcServerHandler::IpcServerHandler(IpcSocket&& sock, + BoundedQueue* inbound, + const std::string& secret, + IpcServerLink* link, + uint32 runId, + uint8 writeAuthority) + : m_sock(std::move(sock)), m_state(IPC_SRV_WAIT_HELLO), - m_inbound(nullptr), - m_link(nullptr), - m_closing(false), - m_isOwner(false), - m_runId(s_pendingRunId.load(std::memory_order_acquire)) - , m_writeAuthority(s_pendingWriteAuthority.load(std::memory_order_acquire)) + m_secret(secret), + m_runId(runId), + m_writeAuthority(writeAuthority), + m_inbound(inbound), + m_link(link), + m_closing(false) { - reference_counting_policy().value( - ACE_Event_Handler::Reference_Counting_Policy::ENABLED); + if (m_link) + { + m_link->AddRef(); + } } IpcServerHandler::~IpcServerHandler() { - if (m_outBuffer) - { - m_outBuffer->release(); - m_outBuffer = nullptr; - } - // Safety net for the open()-failure path: if this handler claimed the - // single-owner guard but failed before going live (e.g. register_handler - // failed), handle_close() never ran, so release ownership here. Normal - // teardown already cleared m_isOwner in handle_close(). - if (m_link && m_isOwner) - { - m_isOwner = false; - m_link->handlerActive.store(false, std::memory_order_release); - } if (m_link) { m_link->Release(); m_link = nullptr; } - peer().close(); } // --------------------------------------------------------------------------- -// open() - called by ACE_Acceptor immediately after accept() +// ReceiveLoop - reassemble and dispatch frames until close / stop // --------------------------------------------------------------------------- -int IpcServerHandler::open(void* /*acceptor*/) +void IpcServerHandler::ReceiveLoop(std::atomic& stop) { - if (m_outBuffer) - { - // Already opened - shouldn't happen. - return -1; - } - - // Grab context injected before the reactor started. - m_inbound = s_pendingInbound; - m_secret = s_pendingSecret; - if (s_pendingLink) - { - m_link = s_pendingLink; - m_link->AddRef(); - } + char buf[4096]; - if (!m_inbound) + while (!stop.load(std::memory_order_acquire) && + !m_closing.load(std::memory_order_acquire)) { - sLog.outError("IpcServerHandler::open: no inbound queue set" - " - call SetPendingContext first"); - return -1; - } - - // SINGLE-OWNER GUARD. This is a one-connection server: only the spawned - // loopback child should ever connect. If a handler is already active, - // a second concurrent connection is hostile - it must NOT be allowed to - // overwrite the live handler/link and steal outbound routing. Atomically - // claim ownership; if another handler already owns the link, refuse this - // connection by returning -1 (ACE then closes us) WITHOUT touching the - // live owner's handler slot, liveness, or reactor state. - if (m_link) - { - bool expected = false; - if (!m_link->handlerActive.compare_exchange_strong( - expected, true, - std::memory_order_acq_rel, std::memory_order_acquire)) + const std::ptrdiff_t n = m_sock.RecvSome(buf, sizeof(buf), 200); + if (n == -2) { - sLog.outError("IpcServerHandler::open: a child connection is" - " already active - refusing additional connection"); - return -1; + continue; // timeout: re-check stop } - m_isOwner = true; - } - - ACE_NEW_RETURN(m_outBuffer, ACE_Message_Block(k_outBufSize), -1); - - // Register for READ events; reactor owns ref after this. - if (reactor()->register_handler(this, ACE_Event_Handler::READ_MASK) == -1) - { - sLog.outError("IpcServerHandler::open: register_handler failed"); - return -1; - } - - sLog.outString("IpcServerHandler: child connected, awaiting handshake"); - - // Publish ourselves into the link as the reactor-thread handler slot. - // This runs ON the reactor thread, so the facade never sees a raw pointer. - if (m_link) - { - m_link->handler = this; - } - - // Reactor now holds a reference; release ours. - remove_reference(); - return 0; -} - -// --------------------------------------------------------------------------- -// close() - ACE close hook -// --------------------------------------------------------------------------- - -int IpcServerHandler::close(u_long /*flags*/) -{ - shutdown(); - m_closing = true; - remove_reference(); - return 0; -} - -// --------------------------------------------------------------------------- -// handle_input() - data available on socket -// --------------------------------------------------------------------------- - -int IpcServerHandler::handle_input(ACE_HANDLE) -{ - if (m_closing) - { - return -1; - } - - // Read into a stack buffer, append to reassembly ByteBuffer. - char buf[4096]; - const ssize_t n = peer().recv(buf, sizeof(buf)); - - if (n <= 0) - { - if (n == 0 || (errno != EWOULDBLOCK && errno != EAGAIN)) + if (n <= 0) { - return -1; // connection closed or hard error + break; // 0 = peer closed, -1 = error } - return 0; - } - m_recvBuf.append(reinterpret_cast(buf), - static_cast(n)); + m_recvBuf.append(reinterpret_cast(buf), + static_cast(n)); - // Decode as many complete frames as possible. - while (m_recvBuf.rpos() < m_recvBuf.size()) - { - // PF2-C: fail-fast on oversize-for-op BEFORE buffering/reassembling - // the body. Once the 8-byte header is present we can read the declared - // body length and the opcode WITHOUT consuming the (possibly still - // partial) body; if the declared length exceeds what the opcode allows, - // close the connection now rather than letting a live child force the - // recv path to buffer up to IPC_MAX_FRAME (~1 MiB) before the - // post-reassembly exact-size check would reject it. The header is - // peeked at absolute positions, so read position is untouched and the - // normal Decode() below still owns frame consumption. - if (m_recvBuf.size() - m_recvBuf.rpos() >= 8) + bool fatal = false; + while (m_recvBuf.rpos() < m_recvBuf.size()) { - if (RejectOversizeForOp()) + // PF2-C: fail-fast on oversize-for-op BEFORE buffering the body. + if (m_recvBuf.size() - m_recvBuf.rpos() >= 8) { - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + if (RejectOversizeForOp()) + { + fatal = true; + break; + } } - } - IpcMessage msg; - std::string err; + IpcMessage msg; + std::string err; - if (!IpcMessage::Decode(m_recvBuf, msg, err)) - { - // The only non-fatal Decode outcomes are "need more bytes" cases - // ("short header" / "incomplete"); every other error ("version - // mismatch", "oversize frame", ...) is a corrupt/hostile stream and - // is fatal. We match the two known transient strings explicitly and - // treat anything else as fatal - without depending on the exact - // wording of the fatal strings. - if (err == "incomplete" || err == "short header") + if (!IpcMessage::Decode(m_recvBuf, msg, err)) { - break; // need more data + // Only "short header" / "incomplete" are transient; every other + // error is a corrupt/hostile stream and is fatal. + if (err == "incomplete" || err == "short header") + { + break; + } + sLog.outError("IpcServerHandler: framing error: %s - closing", + err.c_str()); + fatal = true; + break; } - // Fatal framing error. - sLog.outError("IpcServerHandler: framing error: %s - closing", - err.c_str()); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + if (ProcessFrame(msg) == -1) + { + fatal = true; + break; + } } - if (ProcessFrame(msg) == -1) + // Drop consumed front bytes so a peer that always leaves a trailing + // partial frame cannot make m_recvBuf grow without bound. + CompactRecvBuf(); + + if (fatal) { - return -1; + break; } } - // Compact the reassembly buffer. If fully drained, clear it; otherwise drop - // the consumed front bytes so a peer that always leaves a trailing partial - // frame cannot make m_recvBuf grow without bound. - CompactRecvBuf(); - - return 0; + OnClose(); } // --------------------------------------------------------------------------- -// CompactRecvBuf() - drop already-consumed front bytes from the reassembly buf +// CompactRecvBuf // --------------------------------------------------------------------------- void IpcServerHandler::CompactRecvBuf() @@ -278,7 +137,7 @@ void IpcServerHandler::CompactRecvBuf() const size_t consumed = m_recvBuf.rpos(); if (consumed == 0) { - return; // nothing consumed; leave the (possibly partial) frame in place + return; } if (consumed == m_recvBuf.size()) @@ -287,7 +146,6 @@ void IpcServerHandler::CompactRecvBuf() return; } - // Move the unconsumed tail to the front and reset positions. const size_t remaining = m_recvBuf.size() - consumed; ByteBuffer compacted; compacted.append(m_recvBuf.contents() + consumed, remaining); @@ -295,37 +153,24 @@ void IpcServerHandler::CompactRecvBuf() } // --------------------------------------------------------------------------- -// RejectOversizeForOp() - PF2-C fail-fast on oversize-for-op at header parse +// RejectOversizeForOp - PF2-C fail-fast on oversize-for-op at header parse // --------------------------------------------------------------------------- bool IpcServerHandler::RejectOversizeForOp() { - // Caller guarantees at least 8 header bytes are buffered at rpos(). Peek - // the opcode and declared body length at their absolute wire offsets - // WITHOUT advancing the read position: version @ +0 (Decode validates), - // opcode @ +2, length @ +4. read(pos) does not move rpos(), so the - // subsequent Decode() still sees the full frame. + // Caller guarantees >= 8 header bytes at rpos(). Peek opcode (+2) and body + // length (+4) at absolute offsets WITHOUT advancing rpos, so the subsequent + // Decode() still owns frame consumption. const size_t base = m_recvBuf.rpos(); const uint16 op = m_recvBuf.read(base + 2); const uint32 len = m_recvBuf.read(base + 4); const IpcBodySizeRule rule = IpcExpectedBodySize(op); - - // Unknown opcodes are not rejected here: Decode() caps any body at - // IPC_MAX_FRAME, and ProcessFrame() drops the unknown frame after a tiny - // reassembly. We only fail-fast the case this fix targets: a KNOWN opcode - // declaring a body larger than that opcode can ever legitimately carry, - // which would otherwise force up to IPC_MAX_FRAME of buffering before the - // post-reassembly exact-size check rejects it. if (!rule.known) { - return false; + return false; // Decode caps at IPC_MAX_FRAME; ProcessFrame drops it. } - // maxLen is the exact size (exact rule) or the inclusive max (maxlen rule); - // either way a declared len strictly greater than it can never be valid, so - // reject before reassembly. An undersized exact-rule body is still left to - // the cheap post-reassembly exact-size check (tiny buffering, not a flood). if (len > rule.maxLen) { sLog.outError("IpcServerHandler: opcode 0x%04X declares oversize body" @@ -338,64 +183,12 @@ bool IpcServerHandler::RejectOversizeForOp() } // --------------------------------------------------------------------------- -// handle_output() - socket ready to write -// --------------------------------------------------------------------------- - -int IpcServerHandler::handle_output(ACE_HANDLE) -{ - return FlushOutBuffer(); -} - -// --------------------------------------------------------------------------- -// handle_close() - connection closed or error -// --------------------------------------------------------------------------- - -int IpcServerHandler::handle_close(ACE_HANDLE h, ACE_Reactor_Mask) -{ - { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); - m_closing = true; - if (h == ACE_INVALID_HANDLE) - { - peer().close_writer(); - } - } - - // Clear the link so the facade stops reporting us live and the reactor- - // thread notifier stops routing through us. Runs on the reactor thread, so - // it is serialised against the notifier's drain and our own destruction. - // - // Only the OWNER (the handler that won the single-owner test-and-set in - // open()) clears the live handler slot and releases the single-owner - // guard. A refused second connection never owns the link, so it must not - // touch the live owner's slot or flag here. - if (m_link && m_isOwner) - { - m_link->live.store(false, std::memory_order_release); - if (m_link->handler == this) - { - m_link->handler = nullptr; - } - // Release ownership so the next legitimate child connection can claim - // the link. Done last, after the handler slot is cleared. - m_isOwner = false; - m_link->handlerActive.store(false, std::memory_order_release); - } - - reactor()->remove_handler(this, - ACE_Event_Handler::DONT_CALL | ACE_Event_Handler::ALL_EVENTS_MASK); - return 0; -} - -// --------------------------------------------------------------------------- -// SendFrame() - encode + queue for send +// SendFrame - encode and write directly to the socket (thread-safe) // --------------------------------------------------------------------------- int IpcServerHandler::SendFrame(const IpcMessage& msg) { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); - - if (m_closing) + if (m_closing.load(std::memory_order_acquire)) { return -1; } @@ -403,42 +196,20 @@ int IpcServerHandler::SendFrame(const IpcMessage& msg) ByteBuffer wire; msg.Encode(wire); - const size_t len = wire.size(); - if (m_outBuffer->space() < len) + std::lock_guard g(m_sendMtx); + if (m_closing.load(std::memory_order_acquire)) { - // Buffer full - this is a programming error (frames are small). - sLog.outError("IpcServerHandler::SendFrame: output buffer full"); return -1; } - - m_outBuffer->copy(reinterpret_cast(wire.contents()), len); - - if (reactor()->schedule_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) + if (!m_sock.SendAll(wire.contents(), wire.size())) { - sLog.outError("IpcServerHandler::SendFrame:" - " schedule_wakeup WRITE_MASK failed"); return -1; } - return 0; } // --------------------------------------------------------------------------- -// IsLive / IsClosing -// --------------------------------------------------------------------------- - -bool IpcServerHandler::IsLive() const -{ - return m_state == IPC_SRV_LIVE; -} - -bool IpcServerHandler::IsClosing() const -{ - return m_closing; -} - -// --------------------------------------------------------------------------- -// ProcessFrame() - dispatch a decoded frame through the handshake state machine +// ProcessFrame - handshake state machine (server side) // --------------------------------------------------------------------------- int IpcServerHandler::ProcessFrame(const IpcMessage& msg) @@ -451,8 +222,7 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) { sLog.outError("IpcServerHandler: expected IPC_HELLO," " got 0x%04X - closing", msg.op); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } // IPC_HELLO body: uint16 proto, uint32 pid, string secret @@ -460,8 +230,7 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) { sLog.outError("IpcServerHandler: IPC_HELLO body too short" " - closing"); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } ByteBuffer b; @@ -471,9 +240,7 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) uint32 pid; b >> proto >> pid; - // Remaining bytes are the secret (not null-terminated in the wire - // buffer; extract as raw string). - size_t secretLen = b.size() - b.rpos(); + const size_t secretLen = b.size() - b.rpos(); const char* secretPtr = reinterpret_cast(b.contents() + b.rpos()); std::string secret(secretPtr, secretLen); @@ -483,24 +250,21 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) sLog.outError("IpcServerHandler: proto mismatch" " (got %u, expected %u) - closing", proto, IPC_PROTOCOL_VERSION); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } if (secret != m_secret) { sLog.outError("IpcServerHandler: secret mismatch from pid %u" " - closing", pid); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } sLog.outString("IpcServerHandler: IPC_HELLO OK from pid %u", pid); - // Reply IPC_HELLO_ACK { run-id: per-spawn uuid seed } IpcMessage ack; ack.op = IPC_HELLO_ACK; - ack.body << m_runId << m_writeAuthority; // per-spawn run-id + SP-2 authority + ack.body << m_runId << m_writeAuthority; // run-id + SP-2 authority if (SendFrame(ack) == -1) { return -1; @@ -516,15 +280,15 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) { sLog.outError("IpcServerHandler: expected IPC_READY," " got 0x%04X - closing", msg.op); - return handle_close(ACE_INVALID_HANDLE, - ACE_Event_Handler::ALL_EVENTS_MASK); + return -1; } m_state = IPC_SRV_LIVE; - // Publish liveness for the facade. Runs on the reactor thread. + // Publish the live send target and liveness for the facade. if (m_link) { + m_link->SetSendTarget(shared_from_this()); m_link->live.store(true, std::memory_order_release); } @@ -534,12 +298,9 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) case IPC_SRV_LIVE: { - // The child is UNTRUSTED. Before enqueuing, validate the body - // length against the authoritative per-opcode size rule and - // reject unknown opcodes. This makes every accepted frame tiny, - // so a hostile child cannot enqueue oversize-for-op frames or - // flood bytes (the inbound queue's byte budget is the second - // line of defence; with this validation it should never trip). + // The child is UNTRUSTED: validate the body length against the + // per-opcode rule and reject unknown opcodes, so every accepted + // frame is tiny. const IpcBodySizeRule rule = IpcExpectedBodySize(msg.op); const uint32 bodyLen = static_cast(msg.body.size()); @@ -561,21 +322,14 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) break; } - // PF2-B: stamp this frame with our per-connection run-id BEFORE - // it enters the shared inbound queue. The supervisor drops any - // drained frame whose stamp != its CURRENT run-id, so a frame this - // (possibly dying) connection produces can never be applied under a - // LATER child even if it slips into the queue after a purge. The - // run-id is the same value sent to the child in IPC_HELLO_ACK. + // PF2-B: stamp with our per-connection run-id so the supervisor can + // drop a frame produced by a PRIOR child that slipped into the queue. IpcMessage stamped(msg); stamped.generation = m_runId; - // [SP-2 decision 10] Route mutation-class frames onto the UNBOUNDED - // reliable lane (never dropped under inbound pressure); the facade - // drains it to exhaustion BEFORE the bounded queue each pass so a - // browse flood can never drop a value-bearing frame. Everything else - // stays on the bounded drop-newest queue, charged against its byte - // budget. If the link is absent (legacy path) fall back to bounded. + // [SP-2 decision 10] Mutation-class frames ride the UNBOUNDED + // reliable lane (never dropped); everything else stays on the + // bounded drop-newest queue charged against its byte budget. if (m_link && IpcIsReliableOpcode(stamped.op)) { m_link->PushReliable(stamped); @@ -597,53 +351,19 @@ int IpcServerHandler::ProcessFrame(const IpcMessage& msg) } // --------------------------------------------------------------------------- -// FlushOutBuffer() - write as much of m_outBuffer as the socket will take +// OnClose - clear the link and close the socket (idempotent) // --------------------------------------------------------------------------- -int IpcServerHandler::FlushOutBuffer() +void IpcServerHandler::OnClose() { - ACE_GUARD_RETURN(LockType, g, m_outLock, -1); + m_closing.store(true, std::memory_order_release); - if (m_closing) - { - return -1; - } - - const size_t sendLen = m_outBuffer->length(); - if (sendLen == 0) + if (m_link) { - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - return 0; + m_link->live.store(false, std::memory_order_release); + m_link->ClearSendTarget(this); + m_link->handlerActive.store(false, std::memory_order_release); } -#ifdef MSG_NOSIGNAL - ssize_t n = peer().send(m_outBuffer->rd_ptr(), sendLen, MSG_NOSIGNAL); -#else - ssize_t n = peer().send(m_outBuffer->rd_ptr(), sendLen); -#endif - - if (n == 0) - { - return -1; - } - else if (n == -1) - { - if (errno == EWOULDBLOCK || errno == EAGAIN) - { - return 0; - } - return -1; - } - else if (n < static_cast(sendLen)) - { - m_outBuffer->rd_ptr(static_cast(n)); - m_outBuffer->crunch(); - return 0; - } - else - { - m_outBuffer->reset(); - reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK); - return 0; - } + m_sock.Close(); } diff --git a/src/ipc/IpcServerHandler.h b/src/ipc/IpcServerHandler.h index bb68b903ee..ada2afe40e 100644 --- a/src/ipc/IpcServerHandler.h +++ b/src/ipc/IpcServerHandler.h @@ -22,19 +22,17 @@ #ifndef AH_IPC_SERVER_HANDLER_H #define AH_IPC_SERVER_HANDLER_H -#include -#include -#include -#include -#include -#include -#include - #include "Common.h" #include "Utilities/ByteBuffer.h" #include "IpcMessage.h" #include "BoundedQueue.h" #include "IpcLink.h" +#include "IpcSocket.h" + +#include +#include +#include +#include /** * @brief Handshake state for the server side of the IPC connection. @@ -48,147 +46,64 @@ enum IpcServerHandshakeState }; /** - * @brief Server-side IPC socket handler. + * @brief Server-side IPC connection handler. * - * Modelled on WorldSocket. Registered with the reactor by IpcThread's - * ACE_Acceptor. On handle_input, received bytes are appended to a - * reassembly ByteBuffer and looped through IpcMessage::Decode; complete - * frames are pushed into the shared inbound BoundedQueue. + * Owns the accepted socket and runs a receive loop on the server thread: bytes + * are reassembled into IpcMessage frames and dispatched through the handshake + * state machine, then application frames are pushed onto the shared inbound + * queue (or the reliable lane). SendFrame() writes directly to the socket under + * a mutex, so it is safe to call from the world thread via the facade. * - * The handshake sequence (server side): - * recv IPC_HELLO -> verify proto + secret -> send IPC_HELLO_ACK - * recv IPC_READY -> mark live, log "AH service READY" + * Handshake (server side): + * recv IPC_HELLO -> verify proto + secret -> send IPC_HELLO_ACK + * recv IPC_READY -> mark live * - * This is a 1-connection server; only one IpcServerHandler is ever active. + * This is a 1-connection server; the owning thread guarantees only one handler + * is active at a time (single-owner guard on the link). */ -class IpcServerHandler : public ACE_Svc_Handler +class IpcServerHandler : public std::enable_shared_from_this { public: - typedef ACE_Thread_Mutex LockType; - - // --- static injection used by IpcThread before the acceptor runs --- + IpcServerHandler(IpcSocket&& sock, + BoundedQueue* inbound, + const std::string& secret, + IpcServerLink* link, + uint32 runId, + uint8 writeAuthority); + ~IpcServerHandler(); + + /// Run the blocking receive loop until the peer closes, an error occurs, + /// or @p stop is set. Clears the link on exit. + void ReceiveLoop(std::atomic& stop); /** - * @brief Provide the shared inbound queue, secret and link before any - * connection is accepted. Called once by IpcThread::run on the - * reactor thread before the acceptor fires open(). - * - * @param inbound Inbound queue shared with the IpcServer facade. - * @param secret Shared secret validated against IPC_HELLO. - * @param link Coupling object (outbound queue + liveness + handler - * slot) shared with the facade. May be null in legacy - * paths but is always set by IpcThread. - */ - static void SetPendingContext(BoundedQueue* inbound, - const std::string& secret, - IpcServerLink* link); - - static void SetPendingRunId(uint32 runId); - - /// Set the write-authority bit sent in IPC_HELLO_ACK (supervisor - /// thread, before SpawnChild). SP-2: the worker only becomes the - /// auction write-authority when this is true. - static void SetPendingWriteAuthority(bool on); - - // --- ACE framework callbacks --- - - IpcServerHandler(); - virtual ~IpcServerHandler(); - - /// Called by the acceptor when a new connection is accepted. - int open(void* acceptor = 0) override; - - /// Called when the close(u_long) hook fires. - int close(u_long flags = 0) override; - - /// Called by the reactor when data is available to read. - int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override; - - /// Called by the reactor when the socket is ready to write. - int handle_output(ACE_HANDLE = ACE_INVALID_HANDLE) override; - - /// Called on connection close or error. - int handle_close( - ACE_HANDLE = ACE_INVALID_HANDLE, - ACE_Reactor_Mask = ACE_Event_Handler::ALL_EVENTS_MASK) override; - - // --- Public interface used by IpcServer facade --- - - /** - * @brief Encode and queue @p msg for send; schedule WRITE_MASK. + * @brief Encode and send @p msg on the socket. Thread-safe. * @return 0 on success, -1 on failure. */ int SendFrame(const IpcMessage& msg); - /// True once the handshake is complete. - bool IsLive() const; - - /// True once handle_close has been called. - bool IsClosing() const; + bool IsLive() const { return m_state == IPC_SRV_LIVE; } + bool IsClosing() const { return m_closing.load(std::memory_order_acquire); } private: - // Output buffer and lock (mirrors WorldSocket pattern). - LockType m_outLock; - ACE_Message_Block* m_outBuffer; - static const size_t k_outBufSize = 65536; + IpcSocket m_sock; + std::mutex m_sendMtx; - // Reassembly buffer for incoming raw bytes. ByteBuffer m_recvBuf; - - // Handshake state. IpcServerHandshakeState m_state; - - // Shared secret expected in IPC_HELLO. std::string m_secret; + uint32 m_runId; + uint8 m_writeAuthority; - uint32 m_runId; ///< Per-spawn run-id from handshake. - uint8 m_writeAuthority; ///< Snapshot at ctor. - - // Inbound queue shared with IpcServer facade. BoundedQueue* m_inbound; - - // Coupling object shared with the facade (outbound queue + liveness + - // reactor-thread handler slot). Owned via reference count. IpcServerLink* m_link; - // Closing flag. Only touched on the reactor thread (under m_outLock for - // the close transition); the facade never reads it. - bool m_closing; + std::atomic m_closing; - // True only for the handler that won the single-owner test-and-set in - // open(). A handler refused because another is already active never - // sets this, so its handle_close() does NOT clear the owner's - // handlerActive flag or its handler slot. Reactor-thread only. - bool m_isOwner; - - // --- static context set before first accept (reactor thread only) --- - static BoundedQueue* s_pendingInbound; - static std::string s_pendingSecret; - static IpcServerLink* s_pendingLink; - static std::atomic s_pendingRunId; - static std::atomic s_pendingWriteAuthority; - - // Helpers. - int ProcessFrame(const IpcMessage& msg); - int FlushOutBuffer(); + int ProcessFrame(const IpcMessage& msg); void CompactRecvBuf(); - - /** - * @brief PF2-C: reject an oversize-for-op frame at header parse. - * - * Precondition: at least 8 header bytes are buffered at m_recvBuf's - * current read position. Peeks the opcode + declared body length - * WITHOUT consuming any bytes and, for a KNOWN opcode, returns true - * when the declared length exceeds that opcode's allowed body size - - * so the caller can close the connection before the body is buffered/ - * reassembled. Unknown opcodes and in-range lengths return false (the - * normal Decode()/ProcessFrame() path handles them). - * - * @return true if the frame must be rejected (close the connection). - */ bool RejectOversizeForOp(); + void OnClose(); }; -typedef ACE_Acceptor IpcAcceptor; - #endif // AH_IPC_SERVER_HANDLER_H diff --git a/src/ipc/IpcSocket.cpp b/src/ipc/IpcSocket.cpp new file mode 100644 index 0000000000..dff4103a2f --- /dev/null +++ b/src/ipc/IpcSocket.cpp @@ -0,0 +1,261 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "IpcSocket.h" + +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace +{ +#ifdef _WIN32 + std::atomic g_wsaRefs{0}; + + inline int LastError() { return ::WSAGetLastError(); } + inline bool WouldBlock(int e){ return e == WSAEWOULDBLOCK; } + inline void CloseFd(ipc_socket_t fd) { ::closesocket(fd); } +#else + inline int LastError() { return errno; } + inline bool WouldBlock(int e){ return e == EWOULDBLOCK || e == EAGAIN || e == EINTR; } + inline void CloseFd(ipc_socket_t fd) { ::close(fd); } +#endif + + // Fill a loopback-friendly IPv4 sockaddr for host:port. + bool MakeAddr(const std::string& host, uint16 port, sockaddr_in& addr) + { + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + const char* h = host.empty() ? "127.0.0.1" : host.c_str(); + return ::inet_pton(AF_INET, h, &addr.sin_addr) == 1; + } +} + +bool IpcSocket::GlobalInit() +{ +#ifdef _WIN32 + if (g_wsaRefs.fetch_add(1) == 0) + { + WSADATA data; + if (::WSAStartup(MAKEWORD(2, 2), &data) != 0) + { + g_wsaRefs.fetch_sub(1); + return false; + } + } +#endif + return true; +} + +void IpcSocket::GlobalShutdown() +{ +#ifdef _WIN32 + if (g_wsaRefs.fetch_sub(1) == 1) + { + ::WSACleanup(); + } +#endif +} + +bool IpcSocket::Listen(const std::string& host, uint16 port) +{ + Close(); + + m_fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (m_fd == INVALID) + { + return false; + } + + int yes = 1; + ::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); + + sockaddr_in addr; + if (!MakeAddr(host, port, addr) || + ::bind(m_fd, reinterpret_cast(&addr), sizeof(addr)) != 0 || + ::listen(m_fd, 1) != 0) + { + Close(); + return false; + } + + return true; +} + +int IpcSocket::AcceptOnce(int timeoutMs, IpcSocket& out) +{ + if (m_fd == INVALID) + { + return -1; + } + + fd_set rd; + FD_ZERO(&rd); + FD_SET(m_fd, &rd); + + timeval tv; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; + + const int sel = ::select(static_cast(m_fd) + 1, &rd, nullptr, nullptr, &tv); + if (sel == 0) + { + return 0; // timeout + } + if (sel < 0) + { + return WouldBlock(LastError()) ? 0 : -1; + } + + const ipc_socket_t peer = ::accept(m_fd, nullptr, nullptr); + if (peer == INVALID) + { + return WouldBlock(LastError()) ? 0 : -1; + } + + out = IpcSocket(peer); + return 1; +} + +bool IpcSocket::Connect(const std::string& host, uint16 port) +{ + Close(); + + m_fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (m_fd == INVALID) + { + return false; + } + + sockaddr_in addr; + if (!MakeAddr(host, port, addr) || + ::connect(m_fd, reinterpret_cast(&addr), sizeof(addr)) != 0) + { + Close(); + return false; + } + + return true; +} + +std::ptrdiff_t IpcSocket::RecvSome(void* buf, size_t len, int timeoutMs) +{ + if (m_fd == INVALID) + { + return -1; + } + + fd_set rd; + FD_ZERO(&rd); + FD_SET(m_fd, &rd); + + timeval tv; + tv.tv_sec = timeoutMs / 1000; + tv.tv_usec = (timeoutMs % 1000) * 1000; + + const int sel = ::select(static_cast(m_fd) + 1, &rd, nullptr, nullptr, &tv); + if (sel == 0) + { + return -2; // timeout + } + if (sel < 0) + { + return WouldBlock(LastError()) ? -2 : -1; + } + + const int n = ::recv(m_fd, static_cast(buf), static_cast(len), 0); + if (n > 0) + { + return static_cast(n); + } + if (n == 0) + { + return 0; // peer closed + } + return WouldBlock(LastError()) ? -2 : -1; +} + +bool IpcSocket::SendAll(const void* data, size_t len) +{ + if (m_fd == INVALID) + { + return false; + } + + const char* p = static_cast(data); + size_t sent = 0; + + int flags = 0; +#ifdef MSG_NOSIGNAL + flags = MSG_NOSIGNAL; +#endif + + while (sent < len) + { + const int n = ::send(m_fd, p + sent, static_cast(len - sent), flags); + if (n > 0) + { + sent += static_cast(n); + continue; + } + if (n < 0 && WouldBlock(LastError())) + { + continue; + } + return false; + } + + return true; +} + +void IpcSocket::ShutdownBoth() +{ + if (m_fd != INVALID) + { +#ifdef _WIN32 + ::shutdown(m_fd, SD_BOTH); +#else + ::shutdown(m_fd, SHUT_RDWR); +#endif + } +} + +void IpcSocket::Close() +{ + if (m_fd != INVALID) + { + CloseFd(m_fd); + m_fd = INVALID; + } +} diff --git a/src/ipc/IpcSocket.h b/src/ipc/IpcSocket.h new file mode 100644 index 0000000000..8bf632deea --- /dev/null +++ b/src/ipc/IpcSocket.h @@ -0,0 +1,99 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef AH_IPC_SOCKET_H +#define AH_IPC_SOCKET_H + +#include "Common.h" + +#include +#include + +#ifdef _WIN32 +#include +typedef SOCKET ipc_socket_t; +#else +typedef int ipc_socket_t; +#endif + +/** + * @brief Minimal cross-platform blocking TCP socket for the loopback IPC channel. + * + * One connection per side; both handshake and framing run over a single stream. + * Reads and accepts are made interruptible by polling with a timeout (select), + * so an owning thread can observe a stop flag without a signal or a self-pipe. + */ +class IpcSocket +{ + public: + static const ipc_socket_t INVALID = (ipc_socket_t)(~0); + + /// Reference-counted Winsock startup/cleanup (no-op elsewhere). + static bool GlobalInit(); + static void GlobalShutdown(); + + IpcSocket() : m_fd(INVALID) {} + explicit IpcSocket(ipc_socket_t fd) : m_fd(fd) {} + ~IpcSocket() { Close(); } + + IpcSocket(IpcSocket&& o) noexcept : m_fd(o.m_fd) { o.m_fd = INVALID; } + IpcSocket& operator=(IpcSocket&& o) noexcept + { + if (this != &o) { Close(); m_fd = o.m_fd; o.m_fd = INVALID; } + return *this; + } + + bool Valid() const { return m_fd != INVALID; } + + /// Bind + listen on host:port. Sets SO_REUSEADDR. False on any error. + bool Listen(const std::string& host, uint16 port); + + /** + * @brief Accept a single connection, waiting up to @p timeoutMs. + * @return 1 accepted (out holds the peer), 0 timeout, -1 error. + */ + int AcceptOnce(int timeoutMs, IpcSocket& out); + + /// Blocking connect to host:port. False on any error. + bool Connect(const std::string& host, uint16 port); + + /** + * @brief Receive up to @p len bytes, waiting up to @p timeoutMs. + * @return >0 bytes read, 0 peer closed, -1 error, -2 timeout. + */ + std::ptrdiff_t RecvSome(void* buf, size_t len, int timeoutMs); + + /// Send exactly @p len bytes (loops on partial writes). False on error. + bool SendAll(const void* data, size_t len); + + /// Wake a blocked peer (half-close both directions) without freeing the fd. + void ShutdownBoth(); + + void Close(); + + private: + ipc_socket_t m_fd; + + IpcSocket(const IpcSocket&); + IpcSocket& operator=(const IpcSocket&); +}; + +#endif // AH_IPC_SOCKET_H diff --git a/src/ipc/IpcThread.cpp b/src/ipc/IpcThread.cpp index 32457f2648..7a8fedf4a1 100644 --- a/src/ipc/IpcThread.cpp +++ b/src/ipc/IpcThread.cpp @@ -20,17 +20,16 @@ */ #include "IpcThread.h" +#include "IpcServerHandler.h" #include "IpcClientHandler.h" +#include "IpcSocket.h" #include "Log/Log.h" -#include -#include -#include - #include +#include // =========================================================================== -// IpcThread (server / acceptor side) +// IpcThread (server side) // =========================================================================== IpcThread::IpcThread(const char* host, @@ -43,11 +42,7 @@ IpcThread::IpcThread(const char* host, m_secret(secret), m_inbound(inbound), m_link(link), - m_reactor(nullptr), - m_stopRequested(false), - m_acceptor(nullptr), - m_notifier(nullptr), - m_running(true) + m_stop(false) { if (m_link) { @@ -57,46 +52,6 @@ IpcThread::IpcThread(const char* host, IpcThread::~IpcThread() { - if (m_acceptor) - { - delete m_acceptor; - m_acceptor = nullptr; - } - - // Teardown TOCTOU guard. This destructor runs on the CALLER thread, - // and only AFTER IpcServer::Stop() has joined the reactor thread - // (m_aceThread->wait() returns before ~Thread -> decReference() -> - // this dtor). With the reactor thread gone, take m_notifyMtx and null - // reactor/notifier on the link, THEN release the mutex and destroy - // the objects. A concurrent SendFrame() either (a) ran its notify() - // entirely before us under the mutex (reactor still alive then), or - // (b) blocks on the mutex and afterwards sees reactor == null and - // skips. notify() can therefore never touch a freed reactor/notifier. - // The lock is released before delete, and is NEVER taken across the - // join, so there is no deadlock with the reactor-thread drain path. - if (m_link) - { - std::lock_guard guard(m_link->m_notifyMtx); - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->notifier = nullptr; - } - - // The notifier is purge_pending_notifications()'d and removed from the - // reactor in run() before the reactor is destroyed; safe to delete here. - if (m_notifier) - { - delete m_notifier; - m_notifier = nullptr; - } - // The reactor thread has been joined (see above), so no other thread can - // observe m_reactor now; a relaxed load is sufficient to recover the - // pointer for destruction. - ACE_Reactor* reactor = m_reactor.load(std::memory_order_relaxed); - if (reactor) - { - delete reactor; - m_reactor.store(nullptr, std::memory_order_relaxed); - } if (m_link) { m_link->Release(); @@ -106,116 +61,88 @@ IpcThread::~IpcThread() void IpcThread::run() { - sLog.outString("IpcThread: starting server reactor on %s:%u", - m_host.c_str(), m_port); - - // Build a TP reactor (same as WorldSocketMgr::StartNetwork). - ACE_TP_Reactor* impl = new ACE_TP_Reactor(); - impl->max_notify_iterations(128); - ACE_Reactor* reactor = new ACE_Reactor(impl, 1 /*delete_implementation*/); + IpcSocket::GlobalInit(); - // Publish the reactor (release) so a concurrent Stop() on the caller - // thread can acquire-load it and end the loop. - m_reactor.store(reactor, std::memory_order_release); - - // Create the outbound notifier and bind the reactor onto the link BEFORE - // any accept fires. The notifier owns the send path: the facade enqueues a - // frame and calls reactor->notify(m_notifier), and handle_exception() - // drains the queue on this (the reactor) thread. - if (m_link) + IpcSocket listener; + if (!listener.Listen(m_host, m_port)) { - m_notifier = new IpcServerNotifier(m_link); - m_notifier->reactor(reactor); - m_link->notifier = m_notifier; - // Publishing the reactor pointer last (release) makes Connected()/ - // SendFrame() on the facade a no-op until the notifier is visible. - m_link->reactor.store(reactor, std::memory_order_release); + sLog.outError("IpcThread: listen failed on %s:%u", + m_host.c_str(), m_port); + IpcSocket::GlobalShutdown(); + return; } - // Inject context before the acceptor fires open() on a new handler. - IpcServerHandler::SetPendingContext(m_inbound, m_secret, m_link); - - m_acceptor = new IpcAcceptor(); + sLog.outString("IpcThread: listening on %s:%u", m_host.c_str(), m_port); - ACE_INET_Addr addr(m_port, m_host.c_str()); - if (m_acceptor->open(addr, reactor, ACE_NONBLOCK) == -1) + while (!m_stop.load(std::memory_order_acquire)) { - sLog.outError("IpcThread: acceptor->open() failed on %s:%u - %s", - m_host.c_str(), m_port, ACE_OS::strerror(errno)); - if (m_link) + IpcSocket peer; + const int r = listener.AcceptOnce(200, peer); + if (r == 0) { - m_link->reactor.store(nullptr, std::memory_order_release); + continue; // timeout: re-check stop + } + if (r < 0) + { + if (m_stop.load(std::memory_order_acquire)) + { + break; + } + continue; } - return; - } - // Early-stop guard: if Stop() was requested before we got here, it may have - // ended the loop on a not-yet-running reactor (a no-op). Do not enter the - // loop in that case, or this thread would block forever and hang the later - // wait() at shutdown. - if (m_stopRequested.load(std::memory_order_acquire)) - { - sLog.outString("IpcThread: stop requested before loop entry; exiting"); - if (m_link) + // SINGLE-OWNER GUARD: only one child connection is served at a time. A + // second concurrent local connection is hostile - refuse it without + // touching the live handler. + bool expected = false; + if (!m_link->handlerActive.compare_exchange_strong( + expected, true, + std::memory_order_acq_rel, std::memory_order_acquire)) { - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->live.store(false, std::memory_order_release); + sLog.outError("IpcThread: a child connection is already active" + " - refusing additional connection"); + peer.Close(); + continue; } - return; - } - sLog.outString("IpcThread: listening on %s:%u", m_host.c_str(), m_port); - reactor->run_reactor_event_loop(); - sLog.outString("IpcThread: reactor loop exited"); + const uint32 runId = m_link->runId.load(std::memory_order_acquire); + const uint8 wa = m_link->writeAuthority.load(std::memory_order_acquire); - // Reactor loop has ended. Stop accepting facade notifications and flush any - // still-queued notifications so the reactor never calls the notifier after - // this thread proceeds to teardown. - if (m_link) - { - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->live.store(false, std::memory_order_release); - } - if (m_notifier) - { - reactor->purge_pending_notifications(m_notifier); + sLog.outString("IpcThread: child connected, awaiting handshake"); + + auto handler = std::make_shared( + std::move(peer), m_inbound, m_secret, m_link, runId, wa); + + // Runs until the connection closes or Stop() is requested; clears the + // link (and the single-owner guard) on exit. + handler->ReceiveLoop(m_stop); } + + listener.Close(); + IpcSocket::GlobalShutdown(); + sLog.outString("IpcThread: server loop exited"); } void IpcThread::Stop() { - m_running = false; - // Set the request BEFORE reading the reactor so an early Stop() (before - // run() publishes the reactor) is observed by run()'s pre-loop check and - // cannot be lost. The acquire-load pairs with run()'s release-store. - m_stopRequested.store(true, std::memory_order_release); - ACE_Reactor* reactor = m_reactor.load(std::memory_order_acquire); - if (reactor) - { - reactor->end_reactor_event_loop(); - } + m_stop.store(true, std::memory_order_release); } // =========================================================================== -// IpcClientThread (client / connector side) +// IpcClientThread (client side) // =========================================================================== IpcClientThread::IpcClientThread(const char* host, - uint16 port, - const std::string& secret, - BoundedQueue* inbound, - IpcClientLink* link) + uint16 port, + const std::string& secret, + BoundedQueue* inbound, + IpcClientLink* link) : m_host(host ? host : "127.0.0.1"), m_port(port), m_secret(secret), m_inbound(inbound), m_link(link), - m_reactor(nullptr), - m_stopRequested(false), - m_connector(nullptr), - m_handler(nullptr), - m_notifier(nullptr), - m_running(true), + m_stop(false), m_ready(false) { if (m_link) @@ -226,36 +153,6 @@ IpcClientThread::IpcClientThread(const char* host, IpcClientThread::~IpcClientThread() { - if (m_connector) - { - delete m_connector; - m_connector = nullptr; - } - - // Teardown TOCTOU guard - symmetric to ~IpcThread. Runs on the caller - // thread after IpcClient::Stop() has joined the reactor thread. Null - // reactor/notifier under m_notifyMtx, release, THEN destroy objects. - // See ~IpcThread for the full rationale and the no-deadlock argument. - if (m_link) - { - std::lock_guard guard(m_link->m_notifyMtx); - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->notifier = nullptr; - } - - if (m_notifier) - { - delete m_notifier; - m_notifier = nullptr; - } - // The reactor thread has been joined, so a relaxed load suffices to - // recover the pointer for destruction (see ~IpcThread). - ACE_Reactor* reactor = m_reactor.load(std::memory_order_relaxed); - if (reactor) - { - delete reactor; - m_reactor.store(nullptr, std::memory_order_relaxed); - } if (m_link) { m_link->Release(); @@ -265,114 +162,41 @@ IpcClientThread::~IpcClientThread() void IpcClientThread::run() { - fprintf(stdout, "IpcClientThread: connecting to %s:%u\n", - m_host.c_str(), m_port); - fflush(stdout); - - ACE_TP_Reactor* impl = new ACE_TP_Reactor(); - impl->max_notify_iterations(128); - ACE_Reactor* reactor = new ACE_Reactor(impl, 1); - - // Publish the reactor (release) so a concurrent Stop() can acquire-load it. - m_reactor.store(reactor, std::memory_order_release); + IpcSocket::GlobalInit(); - // Create the outbound notifier and bind the reactor onto the link before - // open() fires. See IpcThread::run for the contract. - if (m_link) - { - m_notifier = new IpcClientNotifier(m_link); - m_notifier->reactor(reactor); - m_link->notifier = m_notifier; - m_link->reactor.store(reactor, std::memory_order_release); - } - - // Inject context before open() fires on the handler. - IpcClientHandler::SetPendingContext(m_inbound, m_secret, m_link); - - m_connector = new IpcConnector(); - m_connector->reactor(reactor); - - // Allocate the handler. ACE_Connector will call open() on success. - ACE_NEW_NORETURN(m_handler, IpcClientHandler()); - if (!m_handler) + IpcSocket sock; + if (!sock.Connect(m_host, m_port)) { - fprintf(stderr, "IpcClientThread: OOM allocating IpcClientHandler\n"); - if (m_link) - { - m_link->reactor.store(nullptr, std::memory_order_release); - } + fprintf(stderr, "IpcClientThread: connect failed to %s:%u\n", + m_host.c_str(), m_port); + IpcSocket::GlobalShutdown(); return; } - ACE_INET_Addr addr(m_port, m_host.c_str()); - if (m_connector->connect(m_handler, addr) == -1) - { - fprintf(stderr, "IpcClientThread: connect() failed: %s\n", - ACE_OS::strerror(errno)); - - // OWNERSHIP: do NOT remove_reference() here - it would be a double-free. - // On EVERY -1 return, ACE_Connector::connect_i() has already called - // sh->close(CLOSE_DURING_NEW_CONNECTION) on the handler. close() is - // virtual and IpcClientHandler overrides it to call remove_reference(), - // which (creation refcount 1 -> 0, no register_handler ref since open() - // never ran) deletes the handler inside that close() call. m_handler is - // therefore already dangling on this path; just null it so nothing else - // touches the freed object. (~IpcClientThread never deletes m_handler.) - m_handler = nullptr; - - if (m_link) - { - m_link->reactor.store(nullptr, std::memory_order_release); - } - return; - } + fprintf(stdout, "IpcClientThread: connected to %s:%u\n", + m_host.c_str(), m_port); + fflush(stdout); - m_ready = true; + auto handler = std::make_shared( + std::move(sock), m_inbound, m_secret, m_link); - // Early-stop guard: if Stop() was requested before we got here, it may - // have ended the loop on a not-yet-running reactor (a no-op). Do not enter - // the loop, or this thread blocks forever and hangs the later wait(). - if (m_stopRequested.load(std::memory_order_acquire)) + if (handler->SendHello() == -1) { - fprintf(stdout, - "IpcClientThread: stop requested before loop entry; exiting\n"); - fflush(stdout); - if (m_link) - { - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->live.store(false, std::memory_order_release); - } + fprintf(stderr, "IpcClientThread: SendHello failed\n"); + IpcSocket::GlobalShutdown(); return; } - fprintf(stdout, "IpcClientThread: connected; starting client reactor\n"); - fflush(stdout); + m_ready.store(true, std::memory_order_release); - reactor->run_reactor_event_loop(); - fprintf(stdout, "IpcClientThread: client reactor loop exited\n"); - fflush(stdout); + handler->ReceiveLoop(m_stop); - // Reactor loop has ended; stop facade notifications and flush pending ones. - if (m_link) - { - m_link->reactor.store(nullptr, std::memory_order_release); - m_link->live.store(false, std::memory_order_release); - } - if (m_notifier) - { - reactor->purge_pending_notifications(m_notifier); - } + IpcSocket::GlobalShutdown(); + fprintf(stdout, "IpcClientThread: client loop exited\n"); + fflush(stdout); } void IpcClientThread::Stop() { - m_running = false; - // Set the request BEFORE reading the reactor so an early Stop() cannot be - // lost (see IpcThread::Stop). The acquire-load pairs with run()'s release. - m_stopRequested.store(true, std::memory_order_release); - ACE_Reactor* reactor = m_reactor.load(std::memory_order_acquire); - if (reactor) - { - reactor->end_reactor_event_loop(); - } + m_stop.store(true, std::memory_order_release); } diff --git a/src/ipc/IpcThread.h b/src/ipc/IpcThread.h index e410d21aa6..83de0f3e97 100644 --- a/src/ipc/IpcThread.h +++ b/src/ipc/IpcThread.h @@ -23,45 +23,23 @@ #define AH_IPC_THREAD_H #include "Threading/Threading.h" -#include "IpcServerHandler.h" -#include "IpcClientHandler.h" #include "IpcLink.h" -#include "IpcOutboundNotifier.h" #include "BoundedQueue.h" #include "IpcMessage.h" -#include -#include - #include #include -typedef IpcOutboundNotifier IpcServerNotifier; -typedef IpcOutboundNotifier IpcClientNotifier; - /** - * @brief Reactor thread that owns the IPC acceptor (server side). - * - * Modelled on SqlDelayThread (ACE_Based::Runnable) and WorldSocketMgr. - * - * run() creates an ACE_TP_Reactor + ACE_Reactor, opens an - * ACE_Acceptor on - * 127.0.0.1:, then calls run_reactor_event_loop() - mirroring - * WorldSocketMgr::StartNetwork lines ~152-164. + * @brief Server-side driver thread: listen, accept one child at a time, and run + * its receive loop. Reconnecting children are accepted in turn. * - * Stop() signals end_reactor_event_loop() so run() returns. + * Stop() sets a flag; the accept and receive loops poll it (with a short select + * timeout) and exit, so no cross-thread socket teardown is needed. */ -class IpcThread : public ACE_Based::Runnable +class IpcThread : public MaNGOS::Runnable { public: - /** - * @param host Bind address (e.g. "127.0.0.1"). - * @param port TCP port to listen on. - * @param secret Shared secret validated against IPC_HELLO. - * @param inbound Shared inbound queue populated by IpcServerHandler. - * @param link Coupling object shared with the IpcServer facade - * (outbound queue + liveness + reactor-thread handler). - */ IpcThread(const char* host, uint16 port, const std::string& secret, @@ -70,10 +48,7 @@ class IpcThread : public ACE_Based::Runnable ~IpcThread() override; - /// ACE_Based::Runnable interface - runs the reactor event loop. void run() override; - - /// Signal the reactor to stop. void Stop(); private: @@ -83,41 +58,16 @@ class IpcThread : public ACE_Based::Runnable BoundedQueue* m_inbound; IpcServerLink* m_link; - /// Published (release) by run() after the reactor is created so Stop() - /// (acquire) can safely end the loop from the caller thread. A plain - /// pointer here would let an immediate Stop() observe null, skip - /// end_reactor_event_loop(), and hang shutdown. - std::atomic m_reactor; - - /// Set by Stop() so an early stop (before run() enters the loop) is - /// never lost: run() checks it right after publishing m_reactor and - /// declines to enter / immediately ends the loop. - std::atomic m_stopRequested; - - IpcAcceptor* m_acceptor; - IpcServerNotifier* m_notifier; - - volatile bool m_running; + std::atomic m_stop; }; /** - * @brief Reactor thread that owns the IPC connector (client side). - * - * Symmetric to IpcThread but uses ACE_Connector instead of ACE_Acceptor. - * On start, injects context into IpcClientHandler, then connects and - * runs the reactor event loop. + * @brief Client-side driver thread: connect, send IPC_HELLO, run the receive + * loop. Symmetric to IpcThread. */ -class IpcClientThread : public ACE_Based::Runnable +class IpcClientThread : public MaNGOS::Runnable { public: - /** - * @param host Server address to connect to. - * @param port Server TCP port. - * @param secret Shared secret sent in IPC_HELLO. - * @param inbound Shared inbound queue populated by IpcClientHandler. - * @param link Coupling object shared with the IpcClient facade - * (outbound queue + liveness + reactor-thread handler). - */ IpcClientThread(const char* host, uint16 port, const std::string& secret, @@ -126,14 +76,10 @@ class IpcClientThread : public ACE_Based::Runnable ~IpcClientThread() override; - /// ACE_Based::Runnable interface. void run() override; - - /// Signal the reactor to stop. void Stop(); - /// True after run() establishes the connection and reactor is live. - bool IsReady() const { return m_ready; } + bool IsReady() const { return m_ready.load(std::memory_order_acquire); } private: std::string m_host; @@ -142,21 +88,8 @@ class IpcClientThread : public ACE_Based::Runnable BoundedQueue* m_inbound; IpcClientLink* m_link; - /// Published (release) by run() after the reactor is created so Stop() - /// (acquire) can safely end the loop from the caller thread. See - /// IpcThread::m_reactor for the rationale. - std::atomic m_reactor; - - /// Set by Stop() so an early stop (before run() enters the loop) is - /// never lost. See IpcThread::m_stopRequested. - std::atomic m_stopRequested; - - IpcConnector* m_connector; - IpcClientHandler* m_handler; - IpcClientNotifier* m_notifier; - - volatile bool m_running; - volatile bool m_ready; + std::atomic m_stop; + std::atomic m_ready; }; #endif // AH_IPC_THREAD_H diff --git a/src/ipc/WorkerSupervisor.cpp b/src/ipc/WorkerSupervisor.cpp index 2a04f720d4..5559c9e52a 100644 --- a/src/ipc/WorkerSupervisor.cpp +++ b/src/ipc/WorkerSupervisor.cpp @@ -29,11 +29,12 @@ #include "Log.h" #include "IpcOpcodes.h" -#include -#include - -#include +#include #include +#include +#include +#include +#include // --------------------------------------------------------------------------- // Constructor / Destructor @@ -51,7 +52,7 @@ WorkerSupervisor::WorkerSupervisor(const std::string& name, , m_secret(secret) , m_botGuid(botGuid) , m_cfgPath(cfgPath) - , m_pid(ACE_INVALID_PID) + , m_pid(IpcProcess::INVALID_PID) , m_lastHeartbeatSent(0) , m_lastHeartbeatAck(0) , m_connectAnchor(0) @@ -66,6 +67,7 @@ WorkerSupervisor::WorkerSupervisor(const std::string& name, // running, never toward a silent stall). , m_childHealthy(false) , m_runId(0) + , m_writeAuthority(false) , m_appDropped(0) #ifdef _WIN32 , m_jobObject(NULL) @@ -145,73 +147,49 @@ bool WorkerSupervisor::Start() bool WorkerSupervisor::SpawnChild() { - // Build command line. + // Build the argument list. // ah-service --port

--botguid --config // // C4: the shared secret is passed OUT-OF-BAND via the AH_SERVICE_SECRET - // environment variable (set below) and is NOT placed on argv, so it cannot - // be read from /proc//cmdline (Linux) or the Win32 process command - // line by any local account. The child reads the env var first and only - // falls back to a manual-testing --secret when the env var is absent. - ACE_Process_Options opts; - - // Buffer large enough for a typical path + args. - char cmdBuf[2048]; - snprintf(cmdBuf, sizeof(cmdBuf), - "\"%s\" --port %u --botguid %u --config \"%s\"", - m_exePath.c_str(), - static_cast(m_port), - static_cast(m_botGuid), - m_cfgPath.c_str()); - - if (opts.command_line("%s", cmdBuf) != 0) - { - sLog.outError("[WorkerSupervisor:%s]" - " ACE_Process_Options::command_line failed", - m_name.c_str()); - return false; + // environment variable and is NOT placed on argv, so it cannot be read from + // /proc//cmdline (Linux) or the Win32 process command line by any local + // account. The child reads the env var first and only falls back to a + // manual-testing --secret when the env var is absent. + std::vector args; + args.push_back("--port"); + args.push_back(std::to_string(static_cast(m_port))); + args.push_back("--botguid"); + args.push_back(std::to_string(static_cast(m_botGuid))); + args.push_back("--config"); + args.push_back(m_cfgPath); + + // For logging: the argument list carries NO secret, so it is safe to log. + std::string cmdLog = m_exePath; + for (const std::string& a : args) + { + cmdLog += ' '; + cmdLog += a; } - // Pass the shared secret out-of-band in the child's environment. setenv() - // here adds to the child's env block only (it does not mutate mangosd's - // own environment). inherit_environment defaults to true so the rest of - // mangosd's environment is preserved for the child. - if (opts.setenv("AH_SERVICE_SECRET", "%s", m_secret.c_str()) != 0) - { - sLog.outError("[WorkerSupervisor:%s]" - " ACE_Process_Options::setenv(AH_SERVICE_SECRET) failed", - m_name.c_str()); - return false; - } - - // The command line carries NO secret, so it is safe to log verbatim. - const char* cmdLog = cmdBuf; - -#ifdef _WIN32 - // CREATE_NEW_CONSOLE: child gets its own console window. - // The child will manage show/hide in Task 5. - opts.creation_flags(CREATE_NEW_CONSOLE); -#endif - // Assign a new per-spawn run-id (monotonically increasing; 0 is never used). + // Arm the run-id + SP-2 write-authority for the IPC_HELLO_ACK before the + // child can connect. ++m_runId; m_ipc.SetRunId(m_runId); + m_ipc.SetWriteAuthority(m_writeAuthority); sLog.outString("[WorkerSupervisor:%s] assigned run-id %u", m_name.c_str(), static_cast(m_runId)); - // Spawn via the singleton ACE_Process_Manager, passing our ACE_Process - // object so we can retrieve the process HANDLE for the Job Object. - pid_t pid = ACE_Process_Manager::instance()->spawn(&m_process, opts); - - if (pid == ACE_INVALID_PID) + // Spawn the child (Windows: CREATE_NEW_CONSOLE + a HANDLE for the Job + // Object; secret injected into the child environment as AH_SERVICE_SECRET). + if (!m_process.Spawn(m_exePath, args, "AH_SERVICE_SECRET", m_secret)) { - sLog.outError("[WorkerSupervisor:%s] ACE_Process_Manager::spawn" - " failed (cmd: %s)", - m_name.c_str(), cmdLog); + sLog.outError("[WorkerSupervisor:%s] spawn failed (cmd: %s)", + m_name.c_str(), cmdLog.c_str()); return false; } - m_pid = pid; + m_pid = m_process.Pid(); m_childExited = false; // C2: arm the connect deadline from this spawn, and reset the @@ -227,7 +205,7 @@ bool WorkerSupervisor::SpawnChild() ClearStagedFrames(); sLog.outString("[WorkerSupervisor:%s] child spawned (pid=%u, cmd: %s)", - m_name.c_str(), static_cast(m_pid), cmdLog); + m_name.c_str(), static_cast(m_pid), cmdLog.c_str()); #ifdef _WIN32 // ------------------------------------------------------------------ @@ -269,12 +247,11 @@ bool WorkerSupervisor::SpawnChild() } else { - // ACE_Process::gethandle() returns the Windows HANDLE for child. - HANDLE hChild = m_process.gethandle(); + HANDLE hChild = m_process.Handle(); if (hChild == INVALID_HANDLE_VALUE || hChild == NULL) { - sLog.outError("[WorkerSupervisor:%s] ACE_Process::gethandle()" - " returned invalid - orphan guard disabled", + sLog.outError("[WorkerSupervisor:%s] child process handle" + " invalid - orphan guard disabled", m_name.c_str()); CloseHandle(m_jobObject); m_jobObject = NULL; @@ -301,8 +278,8 @@ bool WorkerSupervisor::SpawnChild() // Console_InstallParentDeathGuard() (OPEN-2: a distinct signal so SIGTERM // keeps its default-terminate disposition), so the kernel signals the // child when this (the parent) dies. Nothing to arm on the supervisor side - // here. The supervisor's hard-kill uses ACE terminate() -> POSIX - // kill(pid, SIGKILL), which is uncatchable, so it always works regardless. + // here. The supervisor's hard-kill uses kill(pid, SIGKILL), which is + // uncatchable, so it always works regardless. #endif return true; @@ -312,40 +289,21 @@ bool WorkerSupervisor::SpawnChild() // ReapChild (private) // --------------------------------------------------------------------------- -void WorkerSupervisor::ReapChild(pid_t pid) +void WorkerSupervisor::ReapChild() { - if (pid == ACE_INVALID_PID) - { - return; - } + const uint32 pid = m_pid; - // Non-blocking reap of the specific pid so the kernel releases the - // process-table entry (Linux zombie) / OS handle (Windows) before the - // ACE_Process object is reused by the next SpawnChild(). A zero timeout - // means "poll": the child has already exited (process-exit detection) or - // has just been terminate()'d, so the status is immediately available. - ACE_exitcode status = 0; - pid_t reaped = ACE_Process_Manager::instance()->wait( - pid, ACE_Time_Value::zero, &status); + // Release the child's OS handle (Windows) / reap the zombie (Linux) so the + // kernel frees the entry before the next SpawnChild(). Only ever called + // after the child is known dead/terminated, so the status is available. + m_process.Reap(); + m_pid = IpcProcess::INVALID_PID; - if (reaped == pid) - { - DETAIL_LOG("[WorkerSupervisor:%s] reaped child pid=%u (status=%d)", - m_name.c_str(), static_cast(pid), - static_cast(status)); - } - else if (reaped == ACE_INVALID_PID) + if (pid != IpcProcess::INVALID_PID) { - // Already reaped by the process-manager's own SIGCHLD handling, or no - // longer tracked: benign. Logged at detail level only. - DETAIL_LOG("[WorkerSupervisor:%s] child pid=%u already reaped /" - " not tracked", + DETAIL_LOG("[WorkerSupervisor:%s] reaped child pid=%u", m_name.c_str(), static_cast(pid)); } - // reaped == 0 (timeout) cannot normally happen here because we only call - // ReapChild after the child is known dead/terminated; if it did, the next - // SpawnChild()'s ACE_Process reuse still proceeds and the manager reaps it - // on the subsequent exit. } // --------------------------------------------------------------------------- @@ -388,21 +346,18 @@ void WorkerSupervisor::Tick(uint32 gametime) // app frames staged in m_pendingFrames for World::HandleAhInbound. DrainInboundProtocol(); - // Check if child has exited by polling ACE_Process_Manager. - if (!m_childExited && m_pid != ACE_INVALID_PID) + // Check if the child has exited. + if (!m_childExited && m_pid != IpcProcess::INVALID_PID) { - // running() returns 1 if alive, 0 if exited. - if (!m_process.running()) + if (!m_process.Running()) { sLog.outError("[WorkerSupervisor:%s] child process (pid=%u)" " exited unexpectedly", m_name.c_str(), static_cast(m_pid)); - // C5: reap the dead child's process-table entry / handle before - // the ACE_Process is reused, so a flapping child never leaks. - const pid_t deadPid = m_pid; + // C5: reap the dead child's handle / process-table entry so a + // flapping child never leaks. m_childExited = true; - m_pid = ACE_INVALID_PID; - ReapChild(deadPid); + ReapChild(); // Discard the dead child's staged intents: a reconnecting child // must never replay the previous child's stale, half-applied // batch (which would over-post against the resumed in-process bot). @@ -420,13 +375,10 @@ void WorkerSupervisor::Tick(uint32 gametime) " (no ACK for %u s) - marking child dead", m_name.c_str(), static_cast(ackAge)); m_childExited = true; - if (m_pid != ACE_INVALID_PID) + if (m_pid != IpcProcess::INVALID_PID) { - const pid_t deadPid = m_pid; - ACE_Process_Manager::instance()->terminate(m_pid); - m_pid = ACE_INVALID_PID; - // C5: reap the terminated child before the ACE_Process reuse. - ReapChild(deadPid); + m_process.Terminate(); + ReapChild(); } // Discard the dead child's staged intents (see process-exit path). ClearStagedFrames(); @@ -473,13 +425,10 @@ void WorkerSupervisor::Tick(uint32 gametime) static_cast(m_pid), static_cast(WS_CONNECT_DEADLINE_SEC)); m_childExited = true; - if (m_pid != ACE_INVALID_PID) + if (m_pid != IpcProcess::INVALID_PID) { - const pid_t deadPid = m_pid; - ACE_Process_Manager::instance()->terminate(m_pid); - m_pid = ACE_INVALID_PID; - // C5: reap the terminated child before the next spawn. - ReapChild(deadPid); + m_process.Terminate(); + ReapChild(); // reap the terminated child before the next spawn } ClearStagedFrames(); } @@ -852,7 +801,7 @@ void WorkerSupervisor::Shutdown() { break; } - ACE_OS::sleep(ACE_Time_Value(0, 50 * 1000)); // 50 ms + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } if (gotAck) @@ -867,19 +816,17 @@ void WorkerSupervisor::Shutdown() // that was about to exit cleanly). Bounded by the remaining time in // the existing grace window, so the overall shutdown bound is // unchanged. We reap the pid as soon as it exits. - while (time(nullptr) < deadline && m_pid != ACE_INVALID_PID) + while (time(nullptr) < deadline && m_pid != IpcProcess::INVALID_PID) { - if (!m_process.running()) + if (!m_process.Running()) { - const pid_t donePid = m_pid; - m_pid = ACE_INVALID_PID; - ReapChild(donePid); + ReapChild(); sLog.outString("[WorkerSupervisor:%s] child exited cleanly" " after ACK", m_name.c_str()); break; } - ACE_OS::sleep(ACE_Time_Value(0, 50 * 1000)); // 50 ms + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } else @@ -892,12 +839,10 @@ void WorkerSupervisor::Shutdown() // 2. Hard-kill if child is still alive (no ACK, or it did not exit within // the post-ACK grace), then reap so we never leak a handle/zombie. - if (m_pid != ACE_INVALID_PID) + if (m_pid != IpcProcess::INVALID_PID) { - const pid_t deadPid = m_pid; - ACE_Process_Manager::instance()->terminate(m_pid); - m_pid = ACE_INVALID_PID; - ReapChild(deadPid); + m_process.Terminate(); + ReapChild(); } // 3. Stop the IPC server (closes acceptor + reactor thread). diff --git a/src/ipc/WorkerSupervisor.h b/src/ipc/WorkerSupervisor.h index da942b85be..e160a45d74 100644 --- a/src/ipc/WorkerSupervisor.h +++ b/src/ipc/WorkerSupervisor.h @@ -25,9 +25,7 @@ #include "Common.h" #include "IpcChannel.h" #include "IpcMessage.h" - -#include -#include +#include "IpcProcess.h" #include #include @@ -167,6 +165,12 @@ class WorkerSupervisor /// Access the underlying IPC channel. IpcServer& Channel() { return m_ipc; } + /** + * @brief [SP-2] Set the write-authority bit sent to the worker in + * IPC_HELLO_ACK. Call before Start(); applied on every spawn. + */ + void SetWriteAuthority(bool on) { m_writeAuthority = on; } + /** * @brief Drain up to @p maxPerTick application frames into @p out. * @@ -243,15 +247,12 @@ class WorkerSupervisor /** * @brief Reap the dead child's process-table entry / OS handle. * - * The same ACE_Process is reused across restarts, so a flapping child - * would otherwise leak zombies (Linux) or HANDLEs (Windows). This - * waits (non-blocking) on the recorded pid via ACE_Process_Manager so - * the kernel releases the entry before the next SpawnChild(). Safe to - * call when there is nothing to reap. Clears @p pid to ACE_INVALID_PID. - * - * @param pid The child pid to reap (by value; caller clears its copy). + * A flapping child would otherwise leak zombies (Linux) or HANDLEs + * (Windows). Releases the child's OS handle / process-table entry so it + * is freed before the next SpawnChild(). Safe to call with nothing to + * reap; clears m_pid. */ - void ReapChild(pid_t pid); + void ReapChild(); /** * @brief Discard all staged (not-yet-applied) application frames. @@ -283,12 +284,13 @@ class WorkerSupervisor uint32 m_botGuid; std::string m_cfgPath; uint32 m_runId; ///< Per-spawn run-id; incremented on every spawn. + bool m_writeAuthority; ///< [SP-2] authority bit sent in IPC_HELLO_ACK. IpcServer m_ipc; - /// ACE process handle for the spawned child. - ACE_Process m_process; - pid_t m_pid; + /// Handle to the spawned child process. + IpcProcess m_process; + uint32 m_pid; ///< Child pid (for logging); 0 when none. /// Wall-clock timestamps (seconds since epoch) for heartbeat logic. time_t m_lastHeartbeatSent; diff --git a/src/mangosd/AFThread.cpp b/src/mangosd/AFThread.cpp deleted file mode 100644 index eb7e518fa3..0000000000 --- a/src/mangosd/AFThread.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -#include "ace/OS.h" -#include "AFThread.h" -#include "World.h" -#include "Log.h" - -/** - * Initializes the anti-freeze watchdog thread with the configured delay interval. - */ -AntiFreezeThread::AntiFreezeThread(uint32 delay) : delaytime_(delay) -{ - m_loops = 0; - w_loops = 0; - m_lastchange = 0; - w_lastchange = 0; -} - -/** - * Activates the anti-freeze watchdog thread. - */ -int AntiFreezeThread::open(void* unused) -{ - activate(); - return 0; -} - -/** - * Monitors the main and world loops and terminates the server if they become stuck. - */ -int AntiFreezeThread::svc(void) -{ - if (!delaytime_) - { - return 0; - } - - sLog.outString("AntiFreeze Thread started (%u seconds max stuck time)", delaytime_ / 1000); - while (!World::IsStopped()) - { - ACE_OS::sleep(1); - - uint32 curtime = getMSTime(); - - // normal work - if (w_loops != World::m_worldLoopCounter.value()) - { - w_lastchange = curtime; - w_loops = World::m_worldLoopCounter.value(); - } - // possible freeze - else if (getMSTimeDiff(w_lastchange, curtime) > delaytime_) - { - sLog.outError("World Thread hangs, kicking out server!"); - *((uint32 volatile*)NULL) = 0; // bang crash - } - } - - sLog.outString("AntiFreeze Thread stopped."); - return 0; -} - diff --git a/src/mangosd/CMakeLists.txt b/src/mangosd/CMakeLists.txt index 4d4f98a17b..38aa9f1cb8 100644 --- a/src/mangosd/CMakeLists.txt +++ b/src/mangosd/CMakeLists.txt @@ -24,16 +24,13 @@ #Main Files set(SRC_GRP_MAIN - AFThread.cpp - AFThread.h - CliThread.cpp - CliThread.h MangosdTest.cpp MangosdTest.h - RAThread.cpp - RAThread.h - WorldThread.cpp - WorldThread.h + Master.cpp + Master.h + RASession.cpp + RASession.h + Service.h mangosd.cpp ) source_group("Main" FILES ${SRC_GRP_MAIN}) @@ -78,8 +75,9 @@ target_link_libraries(mangosd ah_ipc $<$:gsoap> Threads::Threads - OpenSSL::Crypto - OpenSSL::SSL + mangos_openssl + PRIVATE + mangos_openssl_strict ) install( diff --git a/src/mangosd/CliThread.cpp b/src/mangosd/CliThread.cpp deleted file mode 100644 index 3d4f7144df..0000000000 --- a/src/mangosd/CliThread.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/// \addtogroup mangosd -/// @{ -/// \file - -#include - -#include "CliThread.h" -#include "World.h" -#include "Util.h" -#include "Log.h" - -#ifdef _WIN32 -#include -#endif - -/** - * Prints the interactive mangosd console prompt. - */ -static void prompt(void* callback = NULL, bool status = true) -{ - // Route the prompt through the console writer (verbatim, no newline) so it - // shares the single serialized stdout with bar redraws and log lines and - // cannot overtake queued output -- e.g. the bar frames from a just-finished - // .reload that this same callback follows on the world thread. - sLog.ConsoleEmitRaw("mangos>"); -} - -// Non-blocking keypress detector, when return pressed, return 1, else always return 0 -#if (PLATFORM != PLATFORM_WINDOWS) - -/** - * Checks whether console input is ready without blocking on non-Windows platforms. - */ -static int kb_hit_return() -{ - struct timeval tv; - fd_set fds; - tv.tv_sec = 0; - tv.tv_usec = 0; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv); - return FD_ISSET(STDIN_FILENO, &fds); -} -#endif - -/** - * Initializes the CLI thread with optional console beep support. - */ -CliThread::CliThread(bool beep) : beep_(beep) -{ -} - -/// %Thread start -int CliThread::svc() -{ - ACE_OS::sleep(1); - - if (beep_) - { - sLog.ConsoleEmitRaw("\a"); // \a = Alert (through the writer, single-owner stdout) - } - - prompt(); - - ///- As long as the World is running (no World::m_stopEvent), get the command line and handle it - while (!World::IsStopped()) - { -#if (PLATFORM != PLATFORM_WINDOWS) - while (!kb_hit_return() && !World::IsStopped()) - // With this, we limit CLI to 10 commands/second - { - usleep(100); - } - if (World::IsStopped()) - { - break; - } -#endif - char* command_str = fgets(buffer_, sizeof(buffer_), stdin); - if (command_str != NULL) - { - for (int x = 0; command_str[x]; ++x) - { - if (command_str[x] == '\r' || command_str[x] == '\n') - { - command_str[x] = 0; - break; - } - } - - if (!*command_str) - { - prompt(); - continue; - } - - std::string command; - if (!consoleToUtf8(command_str, command)) // convert from console encoding to utf8 - { - prompt(); - continue; - } - - sWorld.QueueCliCommand(new CliCommandHolder(0, SEC_CONSOLE, NULL, command.c_str(), &utf8print, &prompt)); - } - - else if (feof(stdin)) - { - World::StopNow(SHUTDOWN_EXIT_CODE); - } - } - - return 0; -} - -/** - * Unblocks the CLI thread during server shutdown. - */ -void CliThread::cli_shutdown() -{ -#ifdef _WIN32 - - // send keyboard input to safely unblock the CLI thread, which is blocked on fgets - INPUT_RECORD b; - HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); - - b.EventType = KEY_EVENT; - b.Event.KeyEvent.bKeyDown = TRUE; - b.Event.KeyEvent.dwControlKeyState = 0; - b.Event.KeyEvent.uChar.AsciiChar = '\r'; - b.Event.KeyEvent.wVirtualKeyCode = VK_RETURN; - b.Event.KeyEvent.wRepeatCount = 1; - b.Event.KeyEvent.wVirtualScanCode = 0x1c; - - DWORD numb = 0; - BOOL ret = WriteConsoleInput(hStdIn, &b, 1, &numb); - - wait(); -#endif -} diff --git a/src/mangosd/CliThread.h b/src/mangosd/CliThread.h deleted file mode 100644 index 5116c58714..0000000000 --- a/src/mangosd/CliThread.h +++ /dev/null @@ -1,50 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/// \addtogroup mangosd -/// @{ -/// \file - -#ifndef MANGOS_H_CLITHREAD -#define MANGOS_H_CLITHREAD - -#include "ace/Task.h" - -/** - * @brief Command Line Interface handling thread - * - */ -class CliThread : public ACE_Task_Base -{ - enum { BUFFSIZE = 256 }; - public: - CliThread(bool); - int svc() override; - void cli_shutdown(); - private: - char buffer_[BUFFSIZE]; - bool beep_; -}; -#endif -/// @} diff --git a/src/mangosd/Master.cpp b/src/mangosd/Master.cpp new file mode 100644 index 0000000000..e918628db2 --- /dev/null +++ b/src/mangosd/Master.cpp @@ -0,0 +1,890 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "Master.h" + +#include "AuctionHouseBot.h" +#include "Config/Config.h" +#include "Database/DatabaseEnv.h" +#include "Log.h" +#include "MangosdTest.h" +#include "MapManager.h" +#include "MassMailMgr.h" +#include "ObjectAccessor.h" +#include "ProgressBar.h" +#include "ScriptMgr.h" +#include "SystemConfig.h" +#include "Timer.h" +#include "Util.h" +#include "World.h" +#include "WorkerSupervisor.h" +#include "WorldSocket.h" +#include "WorldSocketMgr.h" + +#ifdef ENABLE_SOAP +#include "SOAP/SoapThread.h" +#endif + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include "ServiceWin32.h" +extern int m_ServiceStatus; +#else +#include "PosixDaemon.h" // detachDaemon() +#include // STDIN_FILENO +#include // select(), fd_set +#endif + +extern DatabaseType WorldDatabase; +extern DatabaseType CharacterDatabase; +extern DatabaseType LoginDatabase; +extern uint32 realmID; + +/// Shortest interval between two world ticks. +constexpr std::chrono::milliseconds WORLD_SLEEP_CONST{50}; + +/// How often the console reader looks for a pending line while idle. Fast enough that a +/// typed command feels instant, slow enough that an idle server is not making syscalls. +constexpr std::chrono::milliseconds CLI_POLL_INTERVAL{100}; + +namespace +{ + /// Forward a fully-built progress-bar redraw to the off-thread console writer + /// (verbatim: no prefix, colour or newline), so the bar shares one serialised stdout + /// with the log lines and cannot tear against them. + void BarConsoleSink(char const* bytes, size_t len) + { + sLog.ConsoleEmitRaw(std::string(bytes, len)); + } + +#ifdef _WIN32 + /// Update the console title with current player/connection counts, only if changed. + void UpdateConsoleTitle(uint32 players, uint32 connections) + { + static std::string s_lastTitle; + char title[128]; + snprintf(title, sizeof(title), "%s (%u Players - %u Connections)", + MANGOS_PACKAGENAME, players, connections); + std::string newTitle(title); + if (s_lastTitle != newTitle) + { + s_lastTitle = newTitle; + SetConsoleTitleA(title); + } + } +#endif + + /** + * @brief RAII handle for one opened database. + * + * Opening the three databases is a ladder, and every rung can fail. Done by hand, each + * failure has to remember to halt the delay threads of everything opened so far — which + * is where the repeated (and easy to get wrong) HaltDelayThread() cascades came from. + * Here, unwinding is automatic, and Release() is called only once every database is up. + */ + class DatabaseGuard + { + public: + + explicit DatabaseGuard(DatabaseType& db) : m_db(&db) {} + + ~DatabaseGuard() + { + if (m_db) + { + m_db->HaltDelayThread(); + } + } + + DatabaseGuard(const DatabaseGuard&) = delete; + DatabaseGuard& operator=(const DatabaseGuard&) = delete; + + /// The database is now owned by the caller; stop tracking it. + void Release() { m_db = nullptr; } + + private: + + DatabaseType* m_db; + }; + + /** + * @brief Open one database and verify its schema version. + */ + bool OpenDatabase(DatabaseType& db, const char* infoKey, const char* connKey, + const char* label, DatabaseTypes versionCheck) + { + const std::string dbstring = sConfig.GetStringDefault(infoKey, ""); + if (dbstring.empty()) + { + sLog.outError("%s not specified in configuration file", label); + return false; + } + + const int nConnections = sConfig.GetIntDefault(connKey, 1); + sLog.outString("%s total connections: %i", label, nConnections + 1); + + if (!db.Initialize(dbstring.c_str(), nConnections)) + { + sLog.outError("Can not connect to %s %s", label, dbstring.c_str()); + return false; + } + + return db.CheckDatabaseVersion(versionCheck); + } +} + +// -- Databases --------------------------------------------------------------------- + +bool Master::StartDatabases() +{ + if (!OpenDatabase(WorldDatabase, "WorldDatabaseInfo", "WorldDatabaseConnections", + "World Database", DATABASE_WORLD)) + { + return false; + } + DatabaseGuard worldGuard(WorldDatabase); + + if (!OpenDatabase(CharacterDatabase, "CharacterDatabaseInfo", "CharacterDatabaseConnections", + "Character Database", DATABASE_CHARACTER)) + { + return false; + } + DatabaseGuard characterGuard(CharacterDatabase); + + if (!OpenDatabase(LoginDatabase, "LoginDatabaseInfo", "LoginDatabaseConnections", + "Login Database", DATABASE_REALMD)) + { + return false; + } + DatabaseGuard loginGuard(LoginDatabase); + + sLog.outString(); + + ///- Get the realm Id from the configuration file + realmID = sConfig.GetIntDefault("RealmID", 0); + if (!realmID) + { + sLog.outError("Realm ID not defined in configuration file"); + return false; + } + + sLog.outString("Realm running as realm ID %d", realmID); + sLog.outString(); + + ///- Clean the database before starting + ClearOnlineAccounts(); + + sWorld.LoadDBVersion(); + sLog.outString("Using World DB: %s", sWorld.GetDBVersion()); + sLog.outString(); + + // Everything is up: the databases are ours to keep, so the guards must not unwind. + loginGuard.Release(); + characterGuard.Release(); + worldGuard.Release(); + + return true; +} + +void Master::StopDatabases() +{ + sLog.outString("[shutdown] halting DB delay threads (Login/Character/World)..."); + + // Reverse of the open order in StartDatabases(), which is also the order the + // DatabaseGuards unwind in on a failed startup. The halts are independent, so this + // costs nothing — but having one teardown order instead of two means the shutdown + // path and the startup-failure path can't drift apart later. + LoginDatabase.HaltDelayThread(); + CharacterDatabase.HaltDelayThread(); + WorldDatabase.HaltDelayThread(); + + sLog.outString("[shutdown] DB delay threads halted"); +} + +void Master::ClearOnlineAccounts() +{ + // Cleanup online status for characters hosted at current realm + /// \todo Only accounts with characters logged on *this* realm should have online + /// status reset. Move the online column from 'account' to 'realmcharacters'? + LoginDatabase.PExecute("UPDATE `account` SET `active_realm_id` = 0, `os` = '' WHERE `active_realm_id` = '%u'", realmID); + + CharacterDatabase.Execute("UPDATE `characters` SET `online` = 0 WHERE `online`<>0"); + + // Battleground instance ids reset at server restart + CharacterDatabase.Execute("UPDATE `character_battleground_data` SET `instance_id` = 0"); +} + +// -- World loop -------------------------------------------------------------------- + +/** + * @brief The world heartbeat. + * + * Runs on the caller's thread (main), so there is no world thread to spawn or join any + * more. Ticks the world at most every WORLD_SLEEP_CONST ms, then unwinds the world in + * order: kick the players, flush their sessions, stop the network, unload the maps. + */ +void Master::WorldLoop() +{ + uint32 realPrevTime = getMSTime(); + + sLog.outString("World Updater started (%lldms min update interval)", + static_cast(WORLD_SLEEP_CONST.count())); + + while (!World::IsStopped()) + { + ++World::m_worldLoopCounter; + + const uint32 realCurrTime = getMSTime(); + const uint32 diff = getMSTimeDiff(realPrevTime, realCurrTime); + + sWorld.Update(diff); + realPrevTime = realCurrTime; + +#ifdef _WIN32 + static uint32 titleUpdateCounter = 0; + if ((++titleUpdateCounter) >= 60) // ~3 seconds at a 50ms tick + { + titleUpdateCounter = 0; + UpdateConsoleTitle(sWorld.GetActiveSessionCount(), WorldSocket::GetOpenConnectionCount()); + } +#endif + + const uint32 executionTimeDiff = getMSTimeDiff(realCurrTime, getMSTime()); + + if (executionTimeDiff > 1000) + { + sLog.outError("Master::WorldLoop: sWorld.Update(diff=%u) took %u ms (loopCounter=%u, sessions=%u)", + diff, executionTimeDiff, World::m_worldLoopCounter.load(), + sWorld.GetActiveSessionCount()); + } + + // Sleep off whatever is left of this tick's budget. + const std::chrono::milliseconds executionTime{executionTimeDiff}; + if (executionTime < WORLD_SLEEP_CONST) + { + std::this_thread::sleep_for(WORLD_SLEEP_CONST - executionTime); + } + +#ifdef _WIN32 + if (m_ServiceStatus == 0) // service stopped + { + World::StopNow(SHUTDOWN_EXIT_CODE); + } + + while (m_ServiceStatus == 2) // service paused + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } +#endif + } + + sLog.outString("[shutdown] world loop stopped; entering world shutdown tail"); + + sLog.outString("[shutdown] KickAll: saving + kicking players..."); + sWorld.KickAll(); // save and kick all players + sLog.outString("[shutdown] KickAll done"); + + sLog.outString("[shutdown] final UpdateSessions..."); + sWorld.UpdateSessions(1); // real players unload requires this call + sLog.outString("[shutdown] final UpdateSessions done"); + + sLog.outString("[shutdown] StopNetwork: closing listener + joining network threads..."); + sWorldSocketMgr.StopNetwork(); + sLog.outString("[shutdown] StopNetwork done"); + + sLog.outString("[shutdown] UnloadAll: unloading maps + MapUpdater teardown..."); + sMapMgr.UnloadAll(); // unload all grids (including locked ones) + sLog.outString("[shutdown] UnloadAll returned"); + + sLog.outString("World Updater stopped"); +} + +// -- Services ---------------------------------------------------------------------- + +namespace +{ + /** + * @brief Watchdog: abort the process if the world loop stops advancing. + * + * A frozen world is worse than a dead one — clients hang, nothing saves — so this + * deliberately crashes the process rather than letting it sit there. + */ + class FreezeDetectorService : public IService + { + public: + + explicit FreezeDetectorService(uint32 maxStuckMs) : m_maxStuckMs(maxStuckMs) {} + + const char* Name() const override { return "freeze detector"; } + + void Start() override + { + m_thread = std::thread([this] { Run(); }); + } + + void Join() override + { + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + private: + + void Run() + { + sLog.outString("AntiFreeze thread started (%u seconds max stuck time)", + m_maxStuckMs / 1000); + + uint32 lastLoops = 0; + uint32 lastChange = 0; + + while (!World::IsStopped()) + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + const uint32 curtime = getMSTime(); + const uint32 loops = World::m_worldLoopCounter.load(); + + if (loops != lastLoops) // normal progress + { + lastLoops = loops; + lastChange = curtime; + } + else if (getMSTimeDiff(lastChange, curtime) > m_maxStuckMs) + { + sLog.outError("World Thread hangs, kicking out server!"); + + // Deliberately die, so the freeze leaves a core/minidump pinned at + // the point it was detected. A volatile null store would be + // undefined behaviour: the optimiser is entitled to delete it + // outright, silently turning the watchdog into a no-op in Release. + // abort() is defined, dumps core on POSIX, and still reaches + // WheatyExceptionReport on Windows via _CALL_REPORTFAULT. + sLog.Flush(); // get the diagnosis to disk first + std::abort(); + } + } + + sLog.outString("AntiFreeze thread stopped"); + } + + uint32 m_maxStuckMs; + std::thread m_thread; + }; + + /// Print the interactive prompt through the console writer, so it shares the single + /// serialised stdout with log lines and progress bars and cannot overtake them. + void CliPrompt(void* /*callbackArg*/ = nullptr, bool /*status*/ = true) + { + sLog.ConsoleEmitRaw("mangos>"); + } + +#if PLATFORM != PLATFORM_WINDOWS + /// Non-blocking check for pending console input. + int kb_hit_return() + { + struct timeval tv; + fd_set fds; + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); + return FD_ISSET(STDIN_FILENO, &fds); + } +#endif + + /** + * @brief Console reader: turns stdin lines into CLI commands queued to the world thread. + * + * The only service that has to override RequestStop(): on Windows it parks inside + * fgets(), which no flag can interrupt, so it has to be woken by hand. + */ + class ConsoleService : public IService + { + public: + + explicit ConsoleService(bool beep) : m_beep(beep) {} + + const char* Name() const override { return "console"; } + + void Start() override + { + m_thread = std::thread([this] { Run(); }); + } + + void RequestStop() override + { +#ifdef _WIN32 + if (!m_thread.joinable()) + { + return; + } + + // The reader is parked in fgets(); feed it a synthetic Return so it wakes, + // sees that the world has stopped, and leaves. (On POSIX it polls instead, + // so it notices the flag on its own and needs nothing here.) + INPUT_RECORD record; + record.EventType = KEY_EVENT; + record.Event.KeyEvent.bKeyDown = TRUE; + record.Event.KeyEvent.dwControlKeyState = 0; + record.Event.KeyEvent.uChar.AsciiChar = '\r'; + record.Event.KeyEvent.wVirtualKeyCode = VK_RETURN; + record.Event.KeyEvent.wRepeatCount = 1; + record.Event.KeyEvent.wVirtualScanCode = 0x1c; + + DWORD written = 0; + WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &record, 1, &written); +#endif + } + + void Join() override + { + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + private: + + void Run() + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + if (m_beep) + { + sLog.ConsoleEmitRaw("\a"); // \a = Alert, via the single-owner stdout + } + + CliPrompt(); + + char buffer[256]; + + while (!World::IsStopped()) + { +#if PLATFORM != PLATFORM_WINDOWS + // Poll rather than block, so shutdown does not have to interrupt a + // parked read. The interval is what bounds the cost: this used to sleep + // 100 *micro*seconds despite claiming to cap the console at ~10 + // commands/second, so an idle server sat here spinning through ~20k + // syscalls/second for its entire uptime. + while (!kb_hit_return() && !World::IsStopped()) + { + std::this_thread::sleep_for(CLI_POLL_INTERVAL); + } + + if (World::IsStopped()) + { + break; + } +#endif + char* command_str = fgets(buffer, sizeof(buffer), stdin); + if (!command_str) + { + if (feof(stdin)) + { + World::StopNow(SHUTDOWN_EXIT_CODE); + } + continue; + } + + for (int x = 0; command_str[x]; ++x) + { + if (command_str[x] == '\r' || command_str[x] == '\n') + { + command_str[x] = 0; + break; + } + } + + if (!*command_str) + { + CliPrompt(); + continue; + } + + std::string command; + if (!consoleToUtf8(command_str, command)) // console encoding to utf8 + { + CliPrompt(); + continue; + } + + sWorld.QueueCliCommand(new CliCommandHolder(0, SEC_CONSOLE, nullptr, + command.c_str(), + &utf8print, &CliPrompt)); + } + } + + bool m_beep; + std::thread m_thread; + }; + + /// The remote-access (telnet) listener. The socket, and its accept loop, belong to + /// RaServer; this only gives it the same lifecycle as everything else. + class RaService : public IService + { + public: + + RaService(uint16 port, const std::string& bindIp) : m_port(port), m_bindIp(bindIp) {} + + const char* Name() const override { return "remote access"; } + + void Start() override { m_server.Start(m_port, m_bindIp); } + + // The listener is not a thread we join; closing it *is* the join. + void Join() override { m_server.Stop(); } + + private: + + uint16 m_port; + std::string m_bindIp; + RaServer m_server; + }; + +#ifdef ENABLE_SOAP + /// The SOAP listener. Its loop polls World::IsStopped() on a 3-second accept timeout, + /// so it needs no wake-up — it leaves on its own within one timeout. + class SoapService : public IService + { + public: + + SoapService(const std::string& ip, uint16 port) : m_ip(ip), m_port(port) {} + + const char* Name() const override { return "SOAP"; } + + void Start() override + { + m_thread = std::thread(SoapThread, m_ip, m_port); + } + + void Join() override + { + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + private: + + std::string m_ip; + uint16 m_port; + std::thread m_thread; + }; +#endif + + /** + * @brief The auction-house worker subprocess (optional, default-off). + * + * Not a thread: the supervisor owns a child process and its IPC link. It gets the + * same lifecycle as everything else so its Shutdown() is ordered by the same rule — + * joined after the world loop has stopped, so no tick can race it. + */ + class AhServiceService : public IService + { + public: + + AhServiceService() : m_supervisor(nullptr) {} + + const char* Name() const override { return "AH service"; } + + void Start() override + { + // SP-1 coordinator authority: the worker is the configured AH read + // authority from here on. Set BEFORE Start() so that even if Start() + // fails (missing exe / port taken / bad bot GUID) and the supervisor is + // torn down below, the read handlers still send "AH unavailable" rather + // than silently reverting to in-process reads. + sWorld.SetAhServiceConfigured(true); + + m_supervisor = new WorkerSupervisor( + "ah-service", + sConfig.GetStringDefault("AH.Service.Path", "service-workers/ah-service/ah-service"), + uint16(sConfig.GetIntDefault("AH.Service.Port", 5760)), + sConfig.GetStringDefault("AH.Service.Secret", "changeme"), + sAuctionBotConfig.GetAHBotId(), + sConfig.GetStringDefault("AH.Service.Config", "ah-service.conf")); + + // SP-2: arm the write-authority bit for the IPC handshake BEFORE Start() -- + // IPC_HELLO_ACK carries {runId, writeAuthority} to the worker (spec + // decision 7: the worker never reads it from its own conf). Applied on + // every child respawn. + m_supervisor->SetWriteAuthority(sWorld.IsAhWriteAuthority()); + + if (!m_supervisor->Start()) + { + sLog.outError("AH service failed to start; falling back to in-process bot"); + delete m_supervisor; + m_supervisor = nullptr; + } + + // Published before the world loop starts, so the first tick already sees it. + sWorld.SetAhSupervisor(m_supervisor); + } + + void Join() override + { + if (!m_supervisor) + { + return; + } + + // Unpublish before destroying: the world loop has stopped, but nothing else + // should be able to observe a dangling supervisor. + sWorld.SetAhSupervisor(nullptr); + m_supervisor->Shutdown(); + delete m_supervisor; + m_supervisor = nullptr; + } + + private: + + WorkerSupervisor* m_supervisor; + }; +} + +void Master::StartService(std::unique_ptr service) +{ + service->Start(); + m_services.push_back(std::move(service)); +} + +void Master::StopServices() +{ + // Ask everyone to stop first, then join. Doing it in one pass instead would mean each + // service is only *told* to stop once the one before it has fully exited, serialising + // shutdowns that could have overlapped. + for (const std::unique_ptr& service : m_services) + { + service->RequestStop(); + } + + // Join in reverse order of start, so a service may rely on anything started before it + // still being alive while it winds down. + while (!m_services.empty()) + { + const std::unique_ptr& service = m_services.back(); + + sLog.outString("[shutdown] joining %s...", service->Name()); + service->Join(); + sLog.outString("[shutdown] %s joined", service->Name()); + + m_services.pop_back(); + } +} + +// -- Run --------------------------------------------------------------------------- + +int Master::Run(const std::string& testMode) +{ + // Register this thread with the MySQL client library BEFORE it issues its first query. + // + // The main thread is a MySQL client thread like any other: it runs every synchronous + // query of the startup load — the schema checks in StartDatabases(), then the thousands + // that SetInitialWorldSettings() fires over the next few minutes. mysql_thread_init() + // has to precede all of them. It used to be called after the whole load had finished, + // which is far too late to mean anything; it survived only because mysql_init() + // initialises the calling thread implicitly, making the explicit call a no-op that + // looked load-bearing. + // + // (No matching ThreadEnd() here on purpose: this thread lives for the whole process, and + // its thread-local block is released by mysql_library_end() when the DatabaseType globals + // are destroyed. The threads that genuinely come and go — the SqlDelayThreads — pair + // their own init/end around run(), which is where it matters.) + WorldDatabase.ThreadStart(); + + ///- Start the databases + if (!StartDatabases()) + { + return 1; + } + + ///- Run an in-process self-test instead of the world, if one was asked for + if (!testMode.empty()) + { + const int rc = RunMangosdTest(testMode); + sLog.outString("mangosd test '%s' exit %d", testMode.c_str(), rc); + sLog.Flush(); + fflush(stdout); + _exit(rc); + } + + // Move console output off the world/map-update threads. Started only after the + // fallible init above, so an early-return error path never leaves a writer thread + // running into stdio teardown — but before SetInitialWorldSettings(), whose spawn + // burst is exactly the hot console path this exists to cover. + sLog.StartConsoleThread(); + + // The writer now owns stdout, so route progress-bar redraws through it too: the bars + // were previously raw printf from the loading thread and could tear against it. Must + // follow StartConsoleThread, since ConsoleEmitRaw falls back to a synchronous write + // whenever the writer is not running. + BarGoLink::SetConsoleSink(&BarConsoleSink); + + ///- Set Realm to Offline, in case a crash happened. Only used once. + LoginDatabase.DirectPExecute("UPDATE `realmlist` SET `realmflags` = `realmflags` | %u WHERE `id` = '%u'", + REALM_FLAG_OFFLINE, realmID); + + ///- Initialize the World + sWorld.SetInitialWorldSettings(); + + // A stop can already have been asked for: signals are hooked before Run(), and the load + // above takes minutes on a cold cache, so Ctrl-C during it is both likely and, until + // now, useless — nothing looked at the flag until the world loop was reached, by which + // point the whole load had been paid for anyway. Bail out here instead of standing up a + // listener and a thread fleet purely to tear them all down on the next line. + if (World::IsStopped()) + { + sLog.outString("[shutdown] stop requested during startup; skipping world run"); + StopDatabases(); + return World::GetExitCode(); + } + +#ifndef _WIN32 + detachDaemon(); +#endif + + // Set the realm flags from configuration, and mark the realm online. + const uint8 recommendedornew = sWorld.getConfig(CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW) + ? REALM_FLAG_NEW_PLAYERS : REALM_FLAG_RECOMMENDED; + const uint8 realmstatus = sWorld.getConfig(CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW_ENABLED) + ? recommendedornew : uint8(REALM_FLAG_NONE); + + std::string builds = AcceptableClientBuildsListStr(); + LoginDatabase.escape_string(builds); + LoginDatabase.DirectPExecute("UPDATE `realmlist` SET `realmflags` = %u, `population` = 0, `realmbuilds` = '%s' WHERE `id` = '%u'", + realmstatus, builds.c_str(), realmID); + + // The server is up: async DB requests are allowed from here (they are forbidden + // during startup, which is why this is not done any earlier). + CharacterDatabase.AllowAsyncTransactions(); + WorldDatabase.AllowAsyncTransactions(); + LoginDatabase.AllowAsyncTransactions(); + + ///- Start the world listener + const std::string bindIp = sConfig.GetStringDefault("BindIP", "0.0.0.0"); + const uint16 worldPort = uint16(sWorld.getConfig(CONFIG_UINT32_PORT_WORLD)); + + if (sWorldSocketMgr.StartNetwork(worldPort, bindIp) == -1) + { + sLog.outError("Failed to start network"); + World::StopNow(ERROR_EXIT_CODE); + StopDatabases(); + return 1; + } + + ///- Start the remote access listener + if (sConfig.GetBoolDefault("Ra.Enable", false)) + { + StartService(std::unique_ptr( + new RaService(uint16(sConfig.GetIntDefault("Ra.Port", 3443)), + sConfig.GetStringDefault("Ra.IP", "0.0.0.0")))); + } + + ///- Start the SOAP listener + if (sConfig.GetBoolDefault("SOAP.Enabled", false)) + { +#ifdef ENABLE_SOAP + StartService(std::unique_ptr( + new SoapService(sConfig.GetStringDefault("SOAP.IP", "127.0.0.1"), + uint16(sConfig.GetIntDefault("SOAP.Port", 7878))))); +#else + sLog.outError("SOAP is enabled but wasn't included during compilation, not activating it."); +#endif + } + + ///- Start the freeze detector + const uint32 stuckTime = uint32(sConfig.GetIntDefault("MaxCoreStuckTime", 0)); + sLog.outString("AntiFreeze: MaxCoreStuckTime = %u, watchdog %s", stuckTime, + stuckTime > 0 ? "ARMED" : "DISABLED"); + if (stuckTime) + { + StartService(std::unique_ptr(new FreezeDetectorService(1000 * stuckTime))); + } + + ///- Start the AH subprocess worker (optional, default-off) + if (sConfig.GetBoolDefault("AH.Service.Enabled", false)) + { + StartService(std::unique_ptr(new AhServiceService())); + } + + ///- Start the console +#ifdef _WIN32 + const bool consoleEnabled = sConfig.GetBoolDefault("Console.Enable", true) && + (m_ServiceStatus == -1); // no console when run as a service +#else + const bool consoleEnabled = sConfig.GetBoolDefault("Console.Enable", true); +#endif + if (consoleEnabled) + { + StartService(std::unique_ptr( + new ConsoleService(sConfig.GetBoolDefault("BeepAtStart", true)))); + } + + ///- Run the world. Returns once World::StopNow() has been called and the world has + /// finished unwinding. + WorldLoop(); + + sLog.outString("[shutdown] world loop returned; joining auxiliary threads"); + + ///- Wind the services down in the reverse of the order they were started. + StopServices(); + + sLog.outString("Halting process..."); + + ///- Set the realm offline again + LoginDatabase.DirectPExecute("UPDATE `realmlist` SET `realmflags` = `realmflags` | %u WHERE `id` = '%u'", + REALM_FLAG_OFFLINE, realmID); + + ///- Clean the account database before leaving + ClearOnlineAccounts(); + + // Send any still-queued mass mails before the DB connections go down. + sMassMailMgr.Update(true); + + StopDatabases(); + + // Unload the script library before it is unloaded automatically: ~ScriptMgr() runs + // too late, being allocated with static storage. + sLog.outString("[shutdown] unloading script library..."); + sScriptMgr.UnloadScriptLibrary(); + sLog.outString("[shutdown] script library unloaded"); + + return World::GetExitCode(); +} +/// @} diff --git a/src/mangosd/Master.h b/src/mangosd/Master.h new file mode 100644 index 0000000000..c84c490a38 --- /dev/null +++ b/src/mangosd/Master.h @@ -0,0 +1,99 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_MASTER +#define MANGOS_H_MASTER + +#include "Common.h" +#include "RASession.h" +#include "Service.h" + +#include +#include +#include + +/** + * @brief Brings the world daemon up, runs it, and takes it back down. + * + * Owns everything with a lifetime: the databases, the listening sockets, and the + * auxiliary threads (freeze detector, console, remote access, SOAP, AH service). + * The world heartbeat itself runs on the caller's thread — Run() only returns once + * the world has stopped and everything else has been joined. + * + * This replaces the WorldThread / CliThread / AntiFreezeThread task objects and the + * hand-wired teardown that used to reap them. + */ +class Master +{ + public: + + Master() = default; + + Master(const Master&) = delete; + Master& operator=(const Master&) = delete; + + /** + * @brief Run the server to completion. + * @param testMode when non-empty, run that -t self-test against the freshly + * opened databases and exit instead of starting the world. + * @return The process exit code. + */ + int Run(const std::string& testMode); + + private: + + /// Open the three databases and check their schema versions. + bool StartDatabases(); + + /// Flush and close the databases (in the reverse order they were opened). + void StopDatabases(); + + /// Reset the online flags left behind by an unclean shutdown. + void ClearOnlineAccounts(); + + /// The world heartbeat. Runs on the calling thread and returns once stopped. + void WorldLoop(); + + /** + * @brief Start an auxiliary service and take ownership of it. + * + * Order matters, and now it is the only thing that has to: services are joined in + * exactly the reverse of the order they were started here, so a service may safely + * depend on anything started before it. + */ + void StartService(std::unique_ptr service); + + /// Ask every service to stop, then join them in reverse order of start. + void StopServices(); + + /// The auxiliary threads: freeze watchdog, console, remote access, SOAP, AH service. + std::vector> m_services; +}; + +#endif +/// @} diff --git a/src/mangosd/RASession.cpp b/src/mangosd/RASession.cpp new file mode 100644 index 0000000000..afc66723fe --- /dev/null +++ b/src/mangosd/RASession.cpp @@ -0,0 +1,323 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#include "RASession.h" + +#include "AccountMgr.h" +#include "Config.h" +#include "Language.h" +#include "Log.h" +#include "ObjectMgr.h" +#include "World.h" + +#include +#include +#include +#include +#include +#include + +RASession::RASession() + : m_closed(false), + m_stage(NONE), + m_accountId(0), + m_accessLevel(SEC_PLAYER), + m_commandsPending(0) +{ + m_secure = sConfig.GetBoolDefault("RA.Secure", true); + m_stricted = sConfig.GetBoolDefault("RA.Stricted", false); + m_minLevel = AccountTypes(sConfig.GetIntDefault("RA.MinLevel", SEC_ADMINISTRATOR)); +} + +RASession::~RASession() +{ + sLog.outRALog("Connection was closed."); +} + +void RASession::Send(const char* message) +{ + if (!message || m_closed.load() || !m_sender) + { + return; + } + + m_sender(reinterpret_cast(message), strlen(message)); +} + +void RASession::Close() +{ + if (m_closed.exchange(true)) + { + return; + } + + if (m_closer) + { + m_closer(); + } +} + +void RASession::onClose() +{ + m_closed.store(true); +} + +std::vector RASession::onConnect() +{ + sLog.outRALog("Incoming connection from %s.", m_address.c_str()); + + Send(sWorld.GetMotd()); + Send("\r\n"); + Send(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); + + return {}; +} + +std::vector RASession::onData(const uint8_t* data, size_t len) +{ + if (m_closed.load()) + { + return {}; + } + + m_input.append(reinterpret_cast(data), len); + + for (;;) + { + const std::string::size_type eol = m_input.find_first_of("\r\n"); + if (eol == std::string::npos) + { + break; + } + + const std::string line = m_input.substr(0, eol); + + // Swallow the whole line terminator, however the client spells it. + std::string::size_type next = m_input.find_first_not_of("\r\n", eol); + m_input.erase(0, next == std::string::npos ? m_input.size() : next); + + HandleLine(line); + + if (m_closed.load()) + { + break; + } + } + + return {}; +} + +void RASession::HandleLine(const std::string& line) +{ + switch (m_stage) + { + case NONE: HandleUsername(line); break; + case LG: HandlePassword(line); break; + case OK: HandleCommand(line); break; + } +} + +void RASession::HandleUsername(const std::string& line) +{ + m_accountId = sAccountMgr.GetId(line); + + ///- If the user is not found, deny access + if (!m_accountId) + { + Send("-No such user.\r\n"); + sLog.outRALog("User %s does not exist.", line.c_str()); + + if (m_secure) + { + Close(); + return; + } + + Send("\r\n"); + Send(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); + return; + } + + m_accessLevel = sAccountMgr.GetSecurity(m_accountId); + + ///- if gmlevel is too low, deny access + if (m_accessLevel < m_minLevel) + { + Send("-Not enough privileges.\r\n"); + sLog.outRALog("User %s has no privilege.", line.c_str()); + + if (m_secure) + { + Close(); + return; + } + + Send("\r\n"); + Send(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); + return; + } + + ///- allow a remotely connected admin to use console-level commands, per config + if (m_accessLevel >= SEC_ADMINISTRATOR && !m_stricted) + { + m_accessLevel = SEC_CONSOLE; + } + + m_stage = LG; + Send(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_PASS)); +} + +void RASession::HandlePassword(const std::string& line) +{ + if (sAccountMgr.CheckPassword(m_accountId, line)) + { + m_stage = OK; + + Send("+Logged in.\r\n"); + sLog.outRALog("User account %u has logged in.", m_accountId); + Send("mangos>"); + return; + } + + ///- Else deny access + Send("-Wrong pass.\r\n"); + sLog.outRALog("User account %u has failed to log in.", m_accountId); + + if (m_secure) + { + Close(); + return; + } + + Send("\r\n"); + Send(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_PASS)); +} + +void RASession::HandleCommand(const std::string& line) +{ + if (line.empty()) + { + Send("mangos>"); + return; + } + + sLog.outRALog("Got '%s' cmd.", line.c_str()); + + if (line.compare(0, 4, "quit") == 0) + { + Close(); + return; + } + + { + // The command runs later, on the world thread, and is handed a bare pointer + // back to us. Keep ourselves alive until it reports finished. + std::lock_guard guard(m_commandLock); + + if (m_commandsPending++ == 0) + { + m_keepAlive = std::static_pointer_cast(shared_from_this()); + } + } + + sWorld.QueueCliCommand(new CliCommandHolder(m_accountId, m_accessLevel, this, line.c_str(), + &RASession::CommandPrint, + &RASession::CommandFinished)); +} + +void RASession::ReleaseCommand() +{ + // Drop the self-reference outside the lock: it may be the last one. + std::shared_ptr expiring; + + { + std::lock_guard guard(m_commandLock); + + if (--m_commandsPending == 0) + { + expiring = std::move(m_keepAlive); + m_keepAlive.reset(); + } + } +} + +/// Command output (world thread). +void RASession::CommandPrint(void* callbackArg, const char* text) +{ + if (!text) + { + return; + } + + static_cast(callbackArg)->Send(text); +} + +/// Command completion (world thread). +void RASession::CommandFinished(void* callbackArg, bool /*success*/) +{ + RASession* session = static_cast(callbackArg); + + session->Send("mangos>"); + session->ReleaseCommand(); +} + +// ── RaServer ───────────────────────────────────────────────────────────────────── + +bool RaServer::Start(uint16_t port, const std::string& bindIp) +{ + if (m_started) + { + return false; + } + + net::SessionFactory factory = []() -> std::shared_ptr + { + return std::make_shared(); + }; + + if (!m_server.start(port, std::move(factory), bindIp)) + { + sLog.outError("RaServer: failed to listen on %s:%u", + (bindIp.empty() ? "0.0.0.0" : bindIp.c_str()), unsigned(port)); + return false; + } + + return m_started = true; +} + +void RaServer::Stop() +{ + if (!m_started) + { + return; + } + + m_server.stop(); + m_started = false; +} +/// @} diff --git a/src/mangosd/RASession.h b/src/mangosd/RASession.h new file mode 100644 index 0000000000..55da7170ac --- /dev/null +++ b/src/mangosd/RASession.h @@ -0,0 +1,133 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_RASESSION +#define MANGOS_H_RASESSION + +#include "Common.h" +#include "SharedDefines.h" + +#include "net/Server.hpp" + +#include +#include +#include +#include +#include +#include + +/** + * One remote-access (telnet) connection: a line-oriented login shell (username, + * password, then commands) whose commands are queued to the world thread exactly as + * the local CLI's are. Runs on the shared networking engine. + */ +class RASession : public net::ISession +{ + public: + + RASession(); + ~RASession() override; + + void setPeerAddress(const std::string& address) override { m_address = address; } + void setSender(net::Sender sender) override { m_sender = std::move(sender); } + void setCloser(net::Closer closer) override { m_closer = std::move(closer); } + + std::vector onConnect() override; + std::vector onData(const uint8_t* data, size_t len) override; + void onClose() override; + + bool closed() const override { return m_closed.load(); } + + private: + + enum Stage + { + NONE, ///< awaiting the username + LG, ///< username accepted; awaiting the password + OK ///< authenticated; accepting commands + }; + + /// Write a line to the peer. Thread-safe (the Sender is a no-op once gone). + void Send(const char* message); + void Close(); + + void HandleLine(const std::string& line); + void HandleUsername(const std::string& line); + void HandlePassword(const std::string& line); + void HandleCommand(const std::string& line); + + /// World-thread callbacks handed to CliCommandHolder. + static void CommandPrint(void* callbackArg, const char* text); + static void CommandFinished(void* callbackArg, bool success); + void ReleaseCommand(); + + std::string m_address; + net::Sender m_sender; + net::Closer m_closer; + + std::atomic m_closed; + + std::string m_input; ///< partial line carried between reads + + Stage m_stage; + uint32 m_accountId; + AccountTypes m_accessLevel; + + bool m_secure; ///< drop the connection on a bad user/password + bool m_stricted; ///< forbid SEC_CONSOLE-only commands remotely + AccountTypes m_minLevel; ///< lowest account level allowed to connect + + // A queued command runs later on the world thread and is handed a bare pointer + // to us; hold a reference to ourselves while any command is outstanding so a + // peer disconnecting mid-command cannot free the session under the callback. + std::mutex m_commandLock; + std::shared_ptr m_keepAlive; + int m_commandsPending; +}; + +/** + * Owns the remote-access listening socket. + */ +class RaServer +{ + public: + + RaServer() : m_started(false) {} + ~RaServer() { Stop(); } + + bool Start(uint16_t port, const std::string& bindIp); + void Stop(); + + private: + + net::Server m_server; + bool m_started; +}; + +#endif +/// @} diff --git a/src/mangosd/RAThread.cpp b/src/mangosd/RAThread.cpp deleted file mode 100644 index 3aedeb3d1b..0000000000 --- a/src/mangosd/RAThread.cpp +++ /dev/null @@ -1,438 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/// \addtogroup mangosd -/// @{ -/// \file - -#include -#include -#include -#include -#include - -#include "RAThread.h" - -#include "AccountMgr.h" -#include "Log.h" -#include "World.h" -#include "Util.h" -#include "Language.h" -#include "Config.h" -#include "ObjectMgr.h" - -class RASocket: protected ACE_Svc_Handler < ACE_SOCK_STREAM, ACE_NULL_SYNCH> -{ - typedef ACE_Svc_Handler < ACE_SOCK_STREAM, ACE_NULL_SYNCH> Base; - - enum { RA_BUFF_SIZE = 8192 }; - - public: - friend class ACE_Acceptor; - - int sendf(const char* msg) - { - ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); - - if (closing_) - { - return -1; - } - - int msgLen = strlen(msg); - - if (msgLen + outputBufferLen > RA_BUFF_SIZE) - { - return -1; - } - - ACE_OS::memcpy(outputBuffer + outputBufferLen, msg, msgLen); - outputBufferLen += msgLen; - - if (!outActive) - { - if (reactor()->schedule_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) - { - sLog.outError("RASocket::sendf error while schedule_wakeup"); - return -1; - } - outActive = true; - } - return 0; - } - - protected: - RASocket(void) : Base(),outBufferLock(), outActive(false), inputBufferLen(0), - outputBufferLen(0), stage(NONE) - { - bSecure = sConfig.GetBoolDefault("RA.Secure", true); - bStricted = sConfig.GetBoolDefault("RA.Stricted", false); - iMinLevel = AccountTypes(sConfig.GetIntDefault("RA.MinLevel", SEC_ADMINISTRATOR)); - reference_counting_policy().value(ACE_Event_Handler::Reference_Counting_Policy::ENABLED); - } - - virtual ~RASocket(void) - { - peer().close(); - sLog.outRALog("Connection was closed."); - } - - int open(void* unused) override - { - if (reactor()->register_handler(this, ACE_Event_Handler::READ_MASK | ACE_Event_Handler::WRITE_MASK) == -1) - { - sLog.outError("RASocket::open: unable to accept connection from client, error = %s", ACE_OS::strerror(errno)); - return -1; - } - - ACE_INET_Addr remote_addr; - - if (peer().get_remote_addr(remote_addr) == -1) - { - sLog.outError("RASocket::open: peer ().get_remote_addr errno = %s", ACE_OS::strerror(errno)); - return -1; - } - - sLog.outRALog("Incoming connection from %s.", remote_addr.get_host_addr()); - - ///- print Motd - sendf(sWorld.GetMotd()); - sendf("\r\n"); - sendf(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); - - return 0; - } - - int close(u_long unused) override - { - if (closing_) - { - return -1; - } - - shutdown(); - - closing_ = true; - - remove_reference(); - return 0; - } - - int handle_input(ACE_HANDLE = ACE_INVALID_HANDLE) override - { - if (closing_) - { - sLog.outError("Called RASocket::handle_input with closing_ = true"); - return -1; - } - - size_t readBytes = peer().recv(inputBuffer + inputBufferLen, RA_BUFF_SIZE - inputBufferLen - 1); - - if (readBytes <= 0) - { - DEBUG_LOG("read %zu bytes in RASocket::handle_input", readBytes); - return -1; - } - - ///- Discard data after line break or line feed - bool gotenter = false; - for (; readBytes > 0 ; --readBytes) - { - char c = inputBuffer[inputBufferLen]; - if (c == '\r' || c == '\n') - { - gotenter = true; - break; - } - ++inputBufferLen; - } - - if (gotenter) - { - inputBuffer[inputBufferLen] = 0; - inputBufferLen = 0; - switch (stage) - { - case NONE: - { - std::string szLogin = inputBuffer; - accId = sAccountMgr.GetId(szLogin); - - ///- If the user is not found, deny access - if (!accId) - { - sendf("-No such user.\r\n"); - sLog.outRALog("User %s does not exist.", szLogin.c_str()); - if (bSecure) - { - handle_output(); - return -1; - } - sendf("\r\n"); - sendf(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); - break; - } - - accAccessLevel = sAccountMgr.GetSecurity(accId); - - ///- if gmlevel is too low, deny access - if (accAccessLevel < iMinLevel) - { - sendf("-Not enough privileges.\r\n"); - sLog.outRALog("User %s has no privilege.", szLogin.c_str()); - if (bSecure) - { - handle_output(); - return -1; - } - sendf("\r\n"); - sendf(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_USER)); - break; - } - - ///- allow by remotely connected admin use console level commands dependent from config setting - if (accAccessLevel >= SEC_ADMINISTRATOR && !bStricted) - { - accAccessLevel = SEC_CONSOLE; - } - - stage = LG; - sendf(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_PASS)); - break; - } - case LG: - { - // login+pass ok - std::string pw = inputBuffer; - - if (sAccountMgr.CheckPassword(accId, pw)) - { - stage = OK; - - sendf("+Logged in.\r\n"); - sLog.outRALog("User account %u has logged in.", accId); - sendf("mangos>"); - } - else - { - ///- Else deny access - sendf("-Wrong pass.\r\n"); - sLog.outRALog("User account %u has failed to log in.", accId); - if (bSecure) - { - handle_output(); - return -1; - } - sendf("\r\n"); - sendf(sObjectMgr.GetMangosStringForDBCLocale(LANG_RA_PASS)); - } - break; - } - case OK: - if (strlen(inputBuffer)) - { - sLog.outRALog("Got '%s' cmd.", inputBuffer); - if (strncmp(inputBuffer, "quit", 4) == 0) - { - return -1; - } - else - { - CliCommandHolder* cmd = new CliCommandHolder(accId, accAccessLevel, this, inputBuffer, - &RASocket::zprint, &RASocket::commandFinished); - sWorld.QueueCliCommand(cmd); - } - } - else - { - sendf("mangos>"); - } - break; - } - } - // no enter yet? wait for next input... - return 0; - } - - int handle_output(ACE_HANDLE h = ACE_INVALID_HANDLE) override - { - ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); - - if (closing_) - { - return -1; - } - - if (!outputBufferLen) - { - if (reactor()->cancel_wakeup(this, ACE_Event_Handler::WRITE_MASK) == -1) - { - return -1; - } - outActive = false; - return 0; - } -#ifdef MSG_NOSIGNAL - ssize_t n = peer().send(outputBuffer, outputBufferLen, MSG_NOSIGNAL); -#else - ssize_t n = peer().send(outputBuffer, outputBufferLen); -#endif // MSG_NOSIGNAL - - if (n <= 0) - { - return -1; - } - - ACE_OS::memmove(outputBuffer, outputBuffer + n, outputBufferLen - n); - - outputBufferLen -= n; - - return 0; - } - - int handle_close(ACE_HANDLE h = ACE_INVALID_HANDLE, - ACE_Reactor_Mask mask = ACE_Event_Handler::ALL_EVENTS_MASK) override - { - if (closing_) - { - return -1; - } - - ACE_GUARD_RETURN(ACE_Thread_Mutex, Guard, outBufferLock, -1); - - closing_ = true; - - if (h == ACE_INVALID_HANDLE) - { - peer().close_writer(); - } - remove_reference(); - return 0; - } - - private: - bool outActive; - - char inputBuffer[RA_BUFF_SIZE]; - uint32 inputBufferLen; - - char outputBuffer[RA_BUFF_SIZE]; - uint32 outputBufferLen; - - uint32 accId; - AccountTypes accAccessLevel; - bool bSecure; /**< kick on wrong pass, non exist. user OR user with no priv. will protect from DOS, bruteforce attacks */ - bool bStricted; /**< not allow execute console only commands (SEC_CONSOLE) remotly */ - AccountTypes iMinLevel; - - enum - { - NONE, // initial value - LG, // only login was entered - OK // both login and pass were given, they were correct and user has enough priv. - } stage; - - ACE_Thread_Mutex outBufferLock; - - static void zprint(void* callbackArg, const char* szText) - { - if (!szText) - { - return; - } - - ((RASocket*)callbackArg)->sendf(szText); - } - - static void commandFinished(void* callbackArg, bool success) - { - RASocket* raSocket = (RASocket*)callbackArg; - raSocket->sendf("mangos>"); - } -}; - -/** - * Initializes the remote access listener thread with the configured host and port. - */ -RAThread::RAThread(uint16 port, const char* host) : listen_addr(port, host) -{ - ACE_Reactor_Impl* imp = 0; - - imp = new ACE_TP_Reactor(); - imp->max_notify_iterations(128); - - m_Reactor = new ACE_Reactor(imp, 1); - m_Acceptor = new RAAcceptor; -} - -/** - * Releases the remote access reactor and acceptor resources. - */ -RAThread::~RAThread() -{ - delete m_Reactor; - delete m_Acceptor; -} - -/** - * Opens the remote access listener socket and activates the thread. - */ -int RAThread::open(void* unused) -{ - if (m_Acceptor->open(listen_addr, m_Reactor, ACE_NONBLOCK) == -1) - { - sLog.outError("MaNGOS RA can not bind to port %d on %s\n", listen_addr.get_port_number(), listen_addr.get_host_addr()); - return -1; - } - activate(); - return 0; -} - -/** - * Runs the remote access reactor event loop until shutdown. - */ -int RAThread::svc() -{ - sLog.outString("Remote Access Thread started (listening on %s:%d)", - listen_addr.get_host_addr(), - listen_addr.get_port_number()); - - while (!m_Reactor->reactor_event_loop_done()) - { - ACE_Time_Value interval(0, 10000); - - if (m_Reactor->run_reactor_event_loop(interval) == -1) - { - break; - } - - if (World::IsStopped()) - { - m_Acceptor->close(); - break; - } - } - sLog.outString("Remote Access Thread stopped"); - return 0; -} diff --git a/src/mangosd/Service.h b/src/mangosd/Service.h new file mode 100644 index 0000000000..ad011cf9ad --- /dev/null +++ b/src/mangosd/Service.h @@ -0,0 +1,77 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2026 MaNGOS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/// \addtogroup mangosd +/// @{ +/// \file + +#ifndef MANGOS_H_SERVICE +#define MANGOS_H_SERVICE + +/** + * @brief One auxiliary thread of the daemon, with a uniform lifecycle. + * + * The freeze watchdog, the console reader, the remote-access listener and SOAP each used to + * carry their own bespoke start and their own bespoke shutdown dance, hand-wired into + * Master::Run() in an order that existed only as the sequence those statements happened to + * appear in. Nobody had written that order down, and nothing enforced it — so the teardown + * was correct by coincidence rather than by construction, and every new background thread + * added another private ritual to remember. + * + * Behind this interface they all start the same way, are asked to stop the same way, and are + * joined in exactly the reverse of the order they were started. + * + * The stop *signal* is not part of this interface: it is global (World::StopNow / + * World::IsStopped), and every service loop already watches it. RequestStop() exists only + * for the services that park in a blocking call and therefore need a nudge before they can + * notice a flag that was set while they were asleep. + */ +class IService +{ + public: + + virtual ~IService() = default; + + /// Name of this service, for the shutdown log. + virtual const char* Name() const = 0; + + /// Bring the service up. Called exactly once. + virtual void Start() = 0; + + /** + * @brief Wake a service that is parked in a blocking call. + * + * The world has already been asked to stop by the time this runs; a service whose + * loop simply polls World::IsStopped() needs nothing here and can take the default. + * Only a service blocked inside a syscall it cannot otherwise leave (the console + * reader, sitting in fgets()) has to override this. + */ + virtual void RequestStop() {} + + /// Block until the service's thread has exited. Called once, after RequestStop(). + virtual void Join() = 0; +}; + +#endif +/// @} diff --git a/src/mangosd/WorldThread.cpp b/src/mangosd/WorldThread.cpp deleted file mode 100644 index 5821f385d0..0000000000 --- a/src/mangosd/WorldThread.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/** \file - \ingroup mangosd - */ - -#include "Common.h" -#include "SystemConfig.h" -#include "WorldSocket.h" -#include "WorldSocketMgr.h" -#include "World.h" -#include "WorldThread.h" -#include "Timer.h" -#include "ObjectAccessor.h" -#include "MapManager.h" -#include "Database/DatabaseEnv.h" - -#include -#include - -#ifdef ENABLE_ELUNA -#include "LuaEngine.h" -#endif /* ENABLE_ELUNA */ - -#define WORLD_SLEEP_CONST 50 - -#ifdef WIN32 -#include "ServiceWin32.h" -extern int m_ServiceStatus; -#include -#endif - -#ifdef _WIN32 -/// Update the console title with current player/connection counts, only if changed. -static void UpdateConsoleTitle(uint32 players, uint32 connections) -{ - static std::string s_lastTitle; - char title[128]; - snprintf(title, sizeof(title), "%s (%u Players - %u Connections)", MANGOS_PACKAGENAME, players, connections); - std::string newTitle(title); - if (s_lastTitle != newTitle) - { - s_lastTitle = newTitle; - SetConsoleTitleA(title); - } -} -#endif - -/** - * Initializes the world thread listener with the configured host and port. - */ -WorldThread::WorldThread(uint16 port, const char* host) : listen_addr(port, host) -{ -} - -/** - * Starts the world socket network listener and activates the world thread. - */ -int WorldThread::open(void* unused) -{ - if (sWorldSocketMgr->StartNetwork(listen_addr) == -1) - { - sLog.outError("Failed to start network"); - Log::WaitBeforeContinueIfNeed(); - World::StopNow(ERROR_EXIT_CODE); - return -1; - } - - activate(); - return 0; -} - -/// Heartbeat for the World -int WorldThread::svc() -{ - uint32 realCurrTime = 0; - uint32 realPrevTime = getMSTime(); - sLog.outString("World Updater Thread started (%dms min update interval)", WORLD_SLEEP_CONST); - - ///- While we have not World::m_stopEvent, update the world - while (!World::IsStopped()) - { - ++World::m_worldLoopCounter; - realCurrTime = getMSTime(); - - uint32 diff = getMSTimeDiff(realPrevTime, realCurrTime); - - sWorld.Update(diff); - realPrevTime = realCurrTime; - -#ifdef _WIN32 - static uint32 titleUpdateCounter = 0; - if ((++titleUpdateCounter) >= 60) // ~3 seconds at WORLD_SLEEP_CONST=50ms - { - titleUpdateCounter = 0; - UpdateConsoleTitle(sWorld.GetActiveSessionCount(), WorldSocket::GetOpenConnectionCount()); - } -#endif - - uint32 executionTimeDiff = getMSTimeDiff(realCurrTime, getMSTime()); - - if (executionTimeDiff > 1000) - { - sLog.outError("WorldThread::svc: sWorld.Update(diff=%u) took %u ms (loopCounter=%u, sessions=%u)", - diff, executionTimeDiff, World::m_worldLoopCounter.value(), sWorld.GetActiveSessionCount()); - } - - // we know exactly how long it took to update the world, if the update took less than WORLD_SLEEP_CONST, sleep for WORLD_SLEEP_CONST - world update time - if (executionTimeDiff < WORLD_SLEEP_CONST) - { - std::this_thread::sleep_for(std::chrono::milliseconds(WORLD_SLEEP_CONST - executionTimeDiff)); - } -#ifdef _WIN32 - if (m_ServiceStatus == 0) // service stopped - { - World::StopNow(SHUTDOWN_EXIT_CODE); - } - - while (m_ServiceStatus == 2) // service paused - { - Sleep(1000); - } -#endif - } - sLog.outString("[shutdown] world loop stopped; entering world-thread shutdown tail"); - sLog.outString("[shutdown] KickAll: saving + kicking players..."); - sWorld.KickAll(); // save and kick all players - sLog.outString("[shutdown] KickAll done"); - sLog.outString("[shutdown] final UpdateSessions..."); - sWorld.UpdateSessions(1); // real players unload required UpdateSessions call - sLog.outString("[shutdown] final UpdateSessions done"); - sLog.outString("[shutdown] StopNetwork: ending reactor + joining network threads..."); - sWorldSocketMgr->StopNetwork(); - sLog.outString("[shutdown] StopNetwork done"); - sLog.outString("[shutdown] UnloadAll: unloading maps + MapUpdater teardown..."); - sMapMgr.UnloadAll(); // unload all grids (including locked in memory) - sLog.outString("[shutdown] UnloadAll returned; world thread exiting"); - - sLog.outString("World Updater Thread stopped"); - return 0; -} diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index 5716e3011a..349595122a 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -819,6 +819,16 @@ SD3ErrorLogFile = "scriptdev3-errors.log" # Default: 1 (true) # 0 (false) # +# Console.Style +# Presentation of the startup output on the console. The log file is the +# same either way. +# Default: "auto" (draw the boxed, coloured loading UI when stdout is an +# interactive console; plain text when it is redirected +# to a file or a pipe, or when running as a service) +# "fancy" (force the loading UI on, for a terminal that cannot be +# detected as one) +# "plain" (never; plain text, as older releases printed it) +# # WaitAtStartupError # After startup error report wait or some time before continue (and possible close console window) # -1 (wait until press) @@ -909,6 +919,7 @@ PetUnsummonAtMount = 0 Event.Announce = 0 BeepAtStart = 1 ShowProgressBars = 1 +Console.Style = "auto" WaitAtStartupError = 10 PlayerCommands = 0 Motd = "Welcome to Mangos Zero" diff --git a/src/mangosd/mangosd.cpp b/src/mangosd/mangosd.cpp index 06c4317851..a62fb8e737 100644 --- a/src/mangosd/mangosd.cpp +++ b/src/mangosd/mangosd.cpp @@ -44,18 +44,14 @@ #include #include -#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3) -# include -# include "Auth/OpenSSLProvider.h" -#endif -#include -#include - +#include +#include "Auth/OpenSSLProvider.h" #include "Common.h" #include "Database/DatabaseEnv.h" #include "Config/Config.h" #include "GitRevision.h" #include "ProgressBar.h" +#include "StartupUI.h" #include "Log.h" #include "SystemConfig.h" #include "AuctionHouseBot.h" @@ -66,17 +62,7 @@ #include "MassMailMgr.h" #include "ScriptMgr.h" -#include "WorldThread.h" -#include "CliThread.h" -#include "AFThread.h" -#include "RAThread.h" -#include "WorkerSupervisor.h" -#include "IpcServerHandler.h" -#include "MangosdTest.h" - -#ifdef ENABLE_SOAP -#include "SOAP/SoapThread.h" -#endif +#include "Master.h" #ifdef _WIN32 #include @@ -98,161 +84,6 @@ DatabaseType CharacterDatabase; ///< Accessor to the DatabaseType LoginDatabase; ///< Accessor to the realm/login database uint32 realmID = 0; ///< Id of the realm - -/** - * @brief Clear online status for realm accounts on startup - * - * Resets the 'online' status for all accounts that were marked as - * connected to this realm. This handles cases where the server - * crashed without properly logging out all players. - * - * Also resets character online status and battleground instance data. - */ -static void clear_online_accounts() -{ - // Cleanup online status for characters hosted at current realm - /// \todo Only accounts with characters logged on *this* realm should have online status reset. Move the online column from 'account' to 'realmcharacters'? - LoginDatabase.PExecute("UPDATE `account` SET `active_realm_id` = 0, `os` = '' WHERE `active_realm_id` = '%u'", realmID); - - CharacterDatabase.Execute("UPDATE `characters` SET `online` = 0 WHERE `online`<>0"); - - // Battleground instance ids reset at server restart - CharacterDatabase.Execute("UPDATE `character_battleground_data` SET `instance_id` = 0"); -} - -/** - * @brief Initialize database connections - * @return true if all databases connected successfully, false otherwise - * - * Connects to three databases: - * - World Database: Contains game data (creatures, items, quests, etc.) - * - Character Database: Contains player character data - * - Login Database: References realm authentication data - * - * Validates database versions and connection counts from configuration. - * On failure, properly cleans up any connections that were established. - */ -static bool start_db() -{ - ///- Get world database info from configuration file - std::string dbstring = sConfig.GetStringDefault("WorldDatabaseInfo", ""); - int nConnections = sConfig.GetIntDefault("WorldDatabaseConnections", 1); - if (dbstring.empty()) - { - sLog.outError("Database not specified in configuration file"); - return false; - } - sLog.outString("World Database total connections: %i", nConnections + 1); - - ///- Initialise the world database - if (!WorldDatabase.Initialize(dbstring.c_str(), nConnections)) - { - sLog.outError("Can not connect to world database %s", dbstring.c_str()); - return false; - } - - ///- Check the World database version - if (!WorldDatabase.CheckDatabaseVersion(DATABASE_WORLD)) - { - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - return false; - } - - dbstring = sConfig.GetStringDefault("CharacterDatabaseInfo", ""); - nConnections = sConfig.GetIntDefault("CharacterDatabaseConnections", 1); - if (dbstring.empty()) - { - sLog.outError("Character Database not specified in configuration file"); - - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - return false; - } - sLog.outString("Character Database total connections: %i", nConnections + 1); - - ///- Initialise the Character database - if (!CharacterDatabase.Initialize(dbstring.c_str(), nConnections)) - { - sLog.outError("Can not connect to Character database %s", dbstring.c_str()); - - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - return false; - } - - ///- Check the Character database version - if (!CharacterDatabase.CheckDatabaseVersion(DATABASE_CHARACTER)) - { - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - CharacterDatabase.HaltDelayThread(); - return false; - } - - ///- Get login database info from configuration file - dbstring = sConfig.GetStringDefault("LoginDatabaseInfo", ""); - nConnections = sConfig.GetIntDefault("LoginDatabaseConnections", 1); - if (dbstring.empty()) - { - sLog.outError("Login database not specified in configuration file"); - - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - CharacterDatabase.HaltDelayThread(); - return false; - } - - ///- Initialise the login database - sLog.outString("Login Database total connections: %i", nConnections + 1); - if (!LoginDatabase.Initialize(dbstring.c_str(), nConnections)) - { - sLog.outError("Can not connect to login database %s", dbstring.c_str()); - - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - CharacterDatabase.HaltDelayThread(); - return false; - } - - ///- Check the Realm database version - if (!LoginDatabase.CheckDatabaseVersion(DATABASE_REALMD)) - { - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - CharacterDatabase.HaltDelayThread(); - LoginDatabase.HaltDelayThread(); - return false; - } - - sLog.outString(); - - ///- Get the realm Id from the configuration file - realmID = sConfig.GetIntDefault("RealmID", 0); - if (!realmID) - { - sLog.outError("Realm ID not defined in configuration file"); - - ///- Wait for already started DB delay threads to end - WorldDatabase.HaltDelayThread(); - CharacterDatabase.HaltDelayThread(); - LoginDatabase.HaltDelayThread(); - return false; - } - - sLog.outString("Realm running as realm ID %d", realmID); - sLog.outString(); - - ///- Clean the database before starting - clear_online_accounts(); - - sWorld.LoadDBVersion(); - - sLog.outString("Using World DB: %s", sWorld.GetDBVersion()); - sLog.outString(); - return true; -} - /// Handle termination signals static void on_signal(int s) { @@ -299,6 +130,8 @@ static void usage(const char* prog) " -v, --version print version and exist\n\r" " -c use config_file as configuration file\n\r" " -a, --ahbot use config_file as ahbot configuration file\n\r" + " --console-demo [