From 71fd1e3a621360cc22a90e6067ea91ebdd670371 Mon Sep 17 00:00:00 2001 From: kuwacom Date: Sun, 8 Mar 2026 15:22:00 +0900 Subject: [PATCH] add: significantly improved the dedicated server logging system - add ServerLogManager to Minecraft.Server as the single entry point for dedicated-server log output - forward CMinecraftApp logger output to the server logger when running with g_Win64DedicatedServer - add named network logs for incoming, accepted, rejected, and disconnected connections - cache connection metadata by smallId so player name and remote IP remain available for disconnect logs - keep Minecraft.Client changes minimal by using lightweight hook points and handling log orchestration on the server side --- CMakeLists.txt | 1 + Minecraft.Client/Common/Console_Utils.cpp | 36 +- Minecraft.Client/Common/Consoles_App.cpp | 117 +++--- Minecraft.Client/PendingConnection.cpp | 35 ++ Minecraft.Client/PlayerConnection.cpp | 42 +- Minecraft.Client/PlayerConnection.h | 5 + .../Windows64/Network/WinsockNetLayer.cpp | 57 ++- Minecraft.Server/Minecraft.Server.vcxproj | 2 + .../Minecraft.Server.vcxproj.filters | 6 + Minecraft.Server/ServerLogManager.cpp | 377 ++++++++++++++++++ Minecraft.Server/ServerLogManager.h | 118 ++++++ Minecraft.Server/Windows64/ServerMain.cpp | 4 + 12 files changed, 731 insertions(+), 69 deletions(-) create mode 100644 Minecraft.Server/ServerLogManager.cpp create mode 100644 Minecraft.Server/ServerLogManager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c308bfcb6..6652bc490 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,7 @@ list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/FileUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/StringUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerLogger.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerLogManager.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/ServerProperties.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/vendor/linenoise/linenoise.c" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/WorldManager.cpp" diff --git a/Minecraft.Client/Common/Console_Utils.cpp b/Minecraft.Client/Common/Console_Utils.cpp index cb0f1b583..9a64dbea3 100644 --- a/Minecraft.Client/Common/Console_Utils.cpp +++ b/Minecraft.Client/Common/Console_Utils.cpp @@ -1,21 +1,32 @@ #include "stdafx.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "..\..\Minecraft.Server\ServerLogManager.h" +#endif //-------------------------------------------------------------------------------------- // Name: DebugSpewV() // Desc: Internal helper function //-------------------------------------------------------------------------------------- #ifndef _CONTENT_PACKAGE -static VOID DebugSpewV( const CHAR* strFormat, const va_list pArgList ) +static VOID DebugSpewV( const CHAR* strFormat, va_list pArgList ) { #if defined __PS3__ || defined __ORBIS__ || defined __PSVITA__ - assert(0); + assert(0); #else - CHAR str[2048]; - // Use the secure CRT to avoid buffer overruns. Specify a count of - // _TRUNCATE so that too long strings will be silently truncated - // rather than triggering an error. - _vsnprintf_s( str, _TRUNCATE, strFormat, pArgList ); - OutputDebugStringA( str ); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Dedicated server routes legacy debug spew through ServerLogger to preserve CLI prompt handling. + if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs()) + { + ServerRuntime::ServerLogManager::ForwardClientDebugSpewLogV(strFormat, pArgList); + return; + } +#endif + CHAR str[2048]; + // Use the secure CRT to avoid buffer overruns. Specify a count of + // _TRUNCATE so that too long strings will be silently truncated + // rather than triggering an error. + _vsnprintf_s( str, _TRUNCATE, strFormat, pArgList ); + OutputDebugStringA( str ); #endif } #endif @@ -31,10 +42,9 @@ VOID CDECL DebugPrintf( const CHAR* strFormat, ... ) #endif { #ifndef _CONTENT_PACKAGE - va_list pArgList; - va_start( pArgList, strFormat ); - DebugSpewV( strFormat, pArgList ); - va_end( pArgList ); + va_list pArgList; + va_start( pArgList, strFormat ); + DebugSpewV( strFormat, pArgList ); + va_end( pArgList ); #endif } - diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 43cf73e15..81e22360c 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -38,6 +38,9 @@ #include "GameRules\ConsoleSchematicFile.h" #include "..\User.h" #include "..\..\Minecraft.World\LevelData.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "..\..\Minecraft.Server\ServerLogManager.h" +#endif #include "..\..\Minecraft.World\net.minecraft.world.entity.player.h" #include "..\EntityRenderDispatcher.h" #include "..\..\Minecraft.World\compression.h" @@ -240,12 +243,21 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...) { #ifndef _FINAL_BUILD - char buf[1024]; - va_list ap; - va_start(ap, szFormat); - vsnprintf(buf, sizeof(buf), szFormat, ap); - va_end(ap); - OutputDebugStringA(buf); + va_list ap; + va_start(ap, szFormat); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe. + if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs()) + { + ServerRuntime::ServerLogManager::ForwardClientAppDebugLogV(szFormat, ap); + va_end(ap); + return; + } +#endif + char buf[1024]; + vsnprintf(buf, sizeof(buf), szFormat, ap); + va_end(ap); + OutputDebugStringA(buf); #endif } @@ -253,53 +265,62 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...) void CMinecraftApp::DebugPrintf(int user, const char *szFormat, ...) { #ifndef _FINAL_BUILD - if(user == USER_NONE) - return; - char buf[1024]; - va_list ap; - va_start(ap, szFormat); - vsnprintf(buf, sizeof(buf), szFormat, ap); - va_end(ap); + if(user == USER_NONE) + return; + va_list ap; + va_start(ap, szFormat); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Dedicated server routes client debug spew through ServerLogger so CLI output stays prompt-safe. + if (ServerRuntime::ServerLogManager::ShouldForwardClientDebugLogs()) + { + ServerRuntime::ServerLogManager::ForwardClientUserDebugLogV(user, szFormat, ap); + va_end(ap); + return; + } +#endif + char buf[1024]; + vsnprintf(buf, sizeof(buf), szFormat, ap); + va_end(ap); #ifdef __PS3__ - unsigned int writelen; - sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen ); + unsigned int writelen; + sys_tty_write(SYS_TTYP_USER1 + ( user - 1 ), buf, strlen(buf), &writelen ); #elif defined __PSVITA__ - switch(user) - { - case 0: - { - SceUID tty2 = sceIoOpen("tty2:", SCE_O_WRONLY, 0); - if(tty2>=0) - { - std::string string1(buf); - sceIoWrite(tty2, string1.c_str(), string1.length()); - sceIoClose(tty2); - } - } - break; - case 1: - { - SceUID tty3 = sceIoOpen("tty3:", SCE_O_WRONLY, 0); - if(tty3>=0) - { - std::string string1(buf); - sceIoWrite(tty3, string1.c_str(), string1.length()); - sceIoClose(tty3); - } - } - break; - default: - OutputDebugStringA(buf); - break; - } + switch(user) + { + case 0: + { + SceUID tty2 = sceIoOpen("tty2:", SCE_O_WRONLY, 0); + if(tty2>=0) + { + std::string string1(buf); + sceIoWrite(tty2, string1.c_str(), string1.length()); + sceIoClose(tty2); + } + } + break; + case 1: + { + SceUID tty3 = sceIoOpen("tty3:", SCE_O_WRONLY, 0); + if(tty3>=0) + { + std::string string1(buf); + sceIoWrite(tty3, string1.c_str(), string1.length()); + sceIoClose(tty3); + } + } + break; + default: + OutputDebugStringA(buf); + break; + } #else - OutputDebugStringA(buf); + OutputDebugStringA(buf); #endif #ifndef _XBOX - if(user == USER_UI) - { - ui.logDebugString(buf); - } + if(user == USER_UI) + { + ui.logDebugString(buf); + } #endif #endif } diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 1210d8486..4c26cfa32 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -14,6 +14,10 @@ #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\SharedConstants.h" #include "Settings.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "..\Minecraft.Server\ServerLogManager.h" +#include "..\Minecraft.World\Socket.h" +#endif // #ifdef __PS3__ // #include "PS3\Network\NetworkPlayerSony.h" // #endif @@ -24,6 +28,25 @@ Random *PendingConnection::random = new Random(); bool g_bRejectDuplicateNames = true; #endif +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +namespace +{ + static unsigned char GetPendingConnectionSmallId(Connection *connection) + { + if (connection != NULL) + { + Socket *socket = connection->getSocket(); + if (socket != NULL) + { + return socket->getSmallId(); + } + } + + return 0; + } +} +#endif + PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id) { // 4J - added initialisers @@ -196,10 +219,16 @@ void PendingConnection::handleLogin(shared_ptr packet) } else if (bannedXuid) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid); +#endif disconnect(DisconnectPacket::eDisconnect_Banned); } else if (duplicateXuid) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateXuid); +#endif // if same XUID already in use by another player so disconnect this one. app.DebugPrintf("Rejecting duplicate xuid for name: %ls\n", name.c_str()); disconnect(DisconnectPacket::eDisconnect_Banned); @@ -219,6 +248,9 @@ void PendingConnection::handleLogin(shared_ptr packet) } if (nameTaken) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateName); +#endif app.DebugPrintf("Rejecting duplicate name: %ls\n", name.c_str()); disconnect(DisconnectPacket::eDisconnect_Banned); } @@ -276,6 +308,9 @@ void PendingConnection::handleAcceptedLogin(shared_ptr packet) shared_ptr playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid); if (playerEntity != NULL) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name); +#endif server->getPlayers()->placeNewPlayer(connection, playerEntity, packet); connection = NULL; // We've moved responsibility for this over to the new PlayerConnection, NULL so we don't delete our reference to it here in our dtor } diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index 9404a5d68..6c810b3ed 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -34,9 +34,13 @@ // 4J Added #include "..\Minecraft.World\net.minecraft.world.item.crafting.h" #include "Options.h" +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "..\Minecraft.Server\ServerLogManager.h" +#endif Random PlayerConnection::random; + PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr player) { // 4J - added initialisers @@ -66,6 +70,13 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti m_offlineXUID = INVALID_XUID; m_onlineXUID = INVALID_XUID; m_bHasClientTickedOnce = false; + m_logSmallId = 0; + + // Cache the first valid transport smallId because disconnect teardown can clear it before the server logger runs. + if (this->connection != NULL && this->connection->getSocket() != NULL) + { + m_logSmallId = this->connection->getSocket()->getSmallId(); + } setShowOnMaps(app.GetGameHostOption(eGameHostOption_Gamertags)!=0?true:false); } @@ -76,6 +87,17 @@ PlayerConnection::~PlayerConnection() DeleteCriticalSection(&done_cs); } +unsigned char PlayerConnection::getLogSmallId() +{ + // Fall back to the live socket only while the cached value is still empty. + if (m_logSmallId == 0 && connection != NULL && connection->getSocket() != NULL) + { + m_logSmallId = connection->getSocket()->getSmallId(); + } + + return m_logSmallId; +} + void PlayerConnection::tick() { if( done ) return; @@ -118,6 +140,13 @@ void PlayerConnection::disconnect(DisconnectPacket::eDisconnectReason reason) return; } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnPlayerDisconnected( + getLogSmallId(), + (player != NULL) ? player->name : std::wstring(), + reason, + true); +#endif app.DebugPrintf("PlayerConnection disconect reason: %d\n", reason ); player->disconnect(); @@ -538,7 +567,18 @@ void PlayerConnection::handleUseItem(shared_ptr packet) void PlayerConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects) { EnterCriticalSection(&done_cs); - if( done ) return; + if( done ) + { + LeaveCriticalSection(&done_cs); + return; + } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnPlayerDisconnected( + getLogSmallId(), + (player != NULL) ? player->name : std::wstring(), + reason, + false); +#endif // logger.info(player.name + " lost connection: " + reason); // 4J-PB - removed, since it needs to be localised in the language the client is in //server->players->broadcastAll( shared_ptr( new ChatPacket(L"�e" + player->name + L" left the game.") ) ); diff --git a/Minecraft.Client/PlayerConnection.h b/Minecraft.Client/PlayerConnection.h index ff6093a34..0284bc6a2 100644 --- a/Minecraft.Client/PlayerConnection.h +++ b/Minecraft.Client/PlayerConnection.h @@ -37,6 +37,7 @@ private: int dropSpamTickCount; bool m_bHasClientTickedOnce; + unsigned char m_logSmallId; public: PlayerConnection(MinecraftServer *server, Connection *connection, shared_ptr player); @@ -45,6 +46,10 @@ public: void disconnect(DisconnectPacket::eDisconnectReason reason); private: + /** + * Returns the stable network smallId used by dedicated-server logging and refreshes it from the live socket when possible + */ + unsigned char getLogSmallId(); double xLastOk, yLastOk, zLastOk; bool synched; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index ed1853796..139f01dd1 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -11,11 +11,14 @@ #if defined(MINECRAFT_SERVER_BUILD) #include "..\..\..\Minecraft.Server\Access\Access.h" +#include "..\..\..\Minecraft.Server\ServerLogManager.h" #endif #include "..\..\..\Minecraft.World\DisconnectPacket.h" #include "..\..\Minecraft.h" #include "..\4JLibs\inc\4J_Profile.h" +#include + static bool RecvExact(SOCKET sock, BYTE* buf, int len); #if defined(MINECRAFT_SERVER_BUILD) @@ -457,6 +460,7 @@ void WinsockNetLayer::ClearSocketForSmallId(BYTE smallId) LeaveCriticalSection(&s_smallIdToSocketLock); } + // Send reject handshake: sentinel 0xFF + DisconnectPacket wire format (1 byte id 255 + 4 byte big-endian reason). Then caller closes socket. static void SendRejectWithReason(SOCKET clientSocket, DisconnectPacket::eDisconnectReason reason) { @@ -544,12 +548,15 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY, (const char*)&noDelay, sizeof(noDelay)); #if defined(MINECRAFT_SERVER_BUILD) + std::string remoteIp; + const bool hasRemoteIp = TryGetNumericRemoteIp(remoteAddress, &remoteIp); + const char *remoteIpForLog = hasRemoteIp ? remoteIp.c_str() : "unknown"; if (g_Win64DedicatedServer) { - std::string remoteIp; - if (TryGetNumericRemoteIp(remoteAddress, &remoteIp) && ServerRuntime::Access::IsIpBanned(remoteIp)) + ServerRuntime::ServerLogManager::OnIncomingTcpConnection(remoteIpForLog); + if (hasRemoteIp && ServerRuntime::Access::IsIpBanned(remoteIp)) { - app.DebugPrintf("Win64 LAN: Rejecting banned ip %s\n", remoteIp.c_str()); + ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_BannedIp); SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_Banned); closesocket(clientSocket); continue; @@ -560,7 +567,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) extern QNET_STATE _iQNetStubState; if (_iQNetStubState != QNET_STATE_GAME_PLAY) { - app.DebugPrintf("Win64 LAN: Rejecting connection, game not ready\n"); +#if defined(MINECRAFT_SERVER_BUILD) + if (g_Win64DedicatedServer) + { + ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_GameNotReady); + } + else +#endif + { + app.DebugPrintf("Win64 LAN: Rejecting connection, game not ready\n"); + } closesocket(clientSocket); continue; } @@ -568,7 +584,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager; if (g_pPlatformNetworkManager != NULL && !g_pPlatformNetworkManager->CanAcceptMoreConnections()) { - app.DebugPrintf("Win64 LAN: Rejecting connection, server at max players\n"); +#if defined(MINECRAFT_SERVER_BUILD) + if (g_Win64DedicatedServer) + { + ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_ServerFull); + } + else +#endif + { + app.DebugPrintf("Win64 LAN: Rejecting connection, server at max players\n"); + } SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_ServerFull); closesocket(clientSocket); continue; @@ -588,7 +613,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) else { LeaveCriticalSection(&s_freeSmallIdLock); - app.DebugPrintf("Win64 LAN: Server full, rejecting connection\n"); +#if defined(MINECRAFT_SERVER_BUILD) + if (g_Win64DedicatedServer) + { + ServerRuntime::ServerLogManager::OnRejectedTcpConnection(remoteIpForLog, ServerRuntime::ServerLogManager::eTcpRejectReason_ServerFull); + } + else +#endif + { + app.DebugPrintf("Win64 LAN: Server full, rejecting connection\n"); + } SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_ServerFull); closesocket(clientSocket); continue; @@ -615,7 +649,16 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param) int connIdx = (int)s_connections.size() - 1; LeaveCriticalSection(&s_connectionsLock); - app.DebugPrintf("Win64 LAN: Client connected, assigned smallId=%d\n", assignedSmallId); +#if defined(MINECRAFT_SERVER_BUILD) + if (g_Win64DedicatedServer) + { + ServerRuntime::ServerLogManager::OnAcceptedTcpConnection(assignedSmallId, remoteIpForLog); + } + else +#endif + { + app.DebugPrintf("Win64 LAN: Client connected, assigned smallId=%d\n", assignedSmallId); + } EnterCriticalSection(&s_smallIdToSocketLock); s_smallIdToSocket[assignedSmallId] = clientSocket; diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index a72a6e17e..16522ce93 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -126,6 +126,7 @@ + @@ -692,6 +693,7 @@ + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index 7fb5bac7a..1df361a9e 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -24,6 +24,9 @@ Server + + Server + Server @@ -599,6 +602,9 @@ Server + + Server + Server diff --git a/Minecraft.Server/ServerLogManager.cpp b/Minecraft.Server/ServerLogManager.cpp new file mode 100644 index 000000000..554cfedeb --- /dev/null +++ b/Minecraft.Server/ServerLogManager.cpp @@ -0,0 +1,377 @@ +#include "stdafx.h" + +#include "ServerLogManager.h" + +#include "Common\StringUtils.h" +#include "ServerLogger.h" + +#include +#include + +extern bool g_Win64DedicatedServer; + +namespace ServerRuntime +{ + namespace ServerLogManager + { + namespace + { + /** + * **!! This information is managed solely for logging purposes, but it is questionable from a liability perspective, so it will eventually need to be separated !!** + * + * Tracks the remote IP and accepted player name associated with one `smallId` + * 1つのsmallIdに紐づく接続IPとプレイヤー名を保持する + */ + struct ConnectionLogEntry + { + std::string remoteIp; + std::string playerName; + }; + + /** + * Owns the shared connection cache used by hook points running on different threads + * 複数スレッドのhookから共有される接続キャッシュを保持する + */ + struct ServerLogState + { + std::mutex stateLock; + std::array entries; + }; + + ServerLogState g_serverLogState; + + static bool IsDedicatedServerLoggingEnabled() + { + return g_Win64DedicatedServer; + } + + static void ResetConnectionLogEntry(ConnectionLogEntry *entry) + { + if (entry == NULL) + { + return; + } + + entry->remoteIp.clear(); + entry->playerName.clear(); + } + + static std::string NormalizeRemoteIp(const char *ip) + { + if (ip == NULL || ip[0] == 0) + { + return std::string("unknown"); + } + + return std::string(ip); + } + + static std::string NormalizePlayerName(const std::wstring &playerName) + { + std::string playerNameUtf8 = StringUtils::WideToUtf8(playerName); + if (playerNameUtf8.empty()) + { + playerNameUtf8 = ""; + } + + return playerNameUtf8; + } + + // Default to the main app channel when the caller does not provide a source tag. + static const char *NormalizeClientLogSource(const char *source) + { + if (source == NULL || source[0] == 0) + { + return "app"; + } + + return source; + } + + static void EmitClientDebugLogLine(const char *source, const std::string &line) + { + if (line.empty()) + { + return; + } + + LogDebugf("client", "[%s] %s", NormalizeClientLogSource(source), line.c_str()); + } + + // Split one debug payload into individual lines so each line becomes a prompt-safe server log entry. + static void ForwardClientDebugMessage(const char *source, const char *message) + { + if (message == NULL || message[0] == 0) + { + return; + } + + const char *cursor = message; + while (*cursor != 0) + { + const char *lineStart = cursor; + while (*cursor != 0 && *cursor != '\r' && *cursor != '\n') + { + ++cursor; + } + + // Split multi-line client debug output into prompt-safe server log entries. + if (cursor > lineStart) + { + EmitClientDebugLogLine(source, std::string(lineStart, (size_t)(cursor - lineStart))); + } + + while (*cursor == '\r' || *cursor == '\n') + { + ++cursor; + } + } + } + + // Share the same formatting path for app, user, and legacy debug-spew forwards. + static void ForwardFormattedClientDebugLogV(const char *source, const char *format, va_list args) + { + if (!IsDedicatedServerLoggingEnabled() || format == NULL || format[0] == 0) + { + return; + } + + char messageBuffer[2048] = {}; + vsnprintf_s(messageBuffer, sizeof(messageBuffer), _TRUNCATE, format, args); + ForwardClientDebugMessage(source, messageBuffer); + } + + static const char *TcpRejectReasonToString(ETcpRejectReason reason) + { + switch (reason) + { + case eTcpRejectReason_BannedIp: return "banned-ip"; + case eTcpRejectReason_GameNotReady: return "game-not-ready"; + case eTcpRejectReason_ServerFull: return "server-full"; + default: return "unknown"; + } + } + + static const char *LoginRejectReasonToString(ELoginRejectReason reason) + { + switch (reason) + { + case eLoginRejectReason_BannedXuid: return "banned-xuid"; + case eLoginRejectReason_DuplicateXuid: return "duplicate-xuid"; + case eLoginRejectReason_DuplicateName: return "duplicate-name"; + default: return "unknown"; + } + } + + static const char *DisconnectReasonToString(DisconnectPacket::eDisconnectReason reason) + { + switch (reason) + { + case DisconnectPacket::eDisconnect_None: return "none"; + case DisconnectPacket::eDisconnect_Quitting: return "quitting"; + case DisconnectPacket::eDisconnect_Closed: return "closed"; + case DisconnectPacket::eDisconnect_LoginTooLong: return "login-too-long"; + case DisconnectPacket::eDisconnect_IllegalStance: return "illegal-stance"; + case DisconnectPacket::eDisconnect_IllegalPosition: return "illegal-position"; + case DisconnectPacket::eDisconnect_MovedTooQuickly: return "moved-too-quickly"; + case DisconnectPacket::eDisconnect_NoFlying: return "no-flying"; + case DisconnectPacket::eDisconnect_Kicked: return "kicked"; + case DisconnectPacket::eDisconnect_TimeOut: return "timeout"; + case DisconnectPacket::eDisconnect_Overflow: return "overflow"; + case DisconnectPacket::eDisconnect_EndOfStream: return "end-of-stream"; + case DisconnectPacket::eDisconnect_ServerFull: return "server-full"; + case DisconnectPacket::eDisconnect_OutdatedServer: return "outdated-server"; + case DisconnectPacket::eDisconnect_OutdatedClient: return "outdated-client"; + case DisconnectPacket::eDisconnect_UnexpectedPacket: return "unexpected-packet"; + case DisconnectPacket::eDisconnect_ConnectionCreationFailed: return "connection-creation-failed"; + case DisconnectPacket::eDisconnect_NoMultiplayerPrivilegesHost: return "no-multiplayer-privileges-host"; + case DisconnectPacket::eDisconnect_NoMultiplayerPrivilegesJoin: return "no-multiplayer-privileges-join"; + case DisconnectPacket::eDisconnect_NoUGC_AllLocal: return "no-ugc-all-local"; + case DisconnectPacket::eDisconnect_NoUGC_Single_Local: return "no-ugc-single-local"; + case DisconnectPacket::eDisconnect_ContentRestricted_AllLocal: return "content-restricted-all-local"; + case DisconnectPacket::eDisconnect_ContentRestricted_Single_Local: return "content-restricted-single-local"; + case DisconnectPacket::eDisconnect_NoUGC_Remote: return "no-ugc-remote"; + case DisconnectPacket::eDisconnect_NoFriendsInGame: return "no-friends-in-game"; + case DisconnectPacket::eDisconnect_Banned: return "banned"; + case DisconnectPacket::eDisconnect_NotFriendsWithHost: return "not-friends-with-host"; + case DisconnectPacket::eDisconnect_NATMismatch: return "nat-mismatch"; + default: return "unknown"; + } + } + } + + // Only forward client-side debug output while the process is running as the dedicated server. + bool ShouldForwardClientDebugLogs() + { + return IsDedicatedServerLoggingEnabled(); + } + + void ForwardClientAppDebugLogV(const char *format, va_list args) + { + ForwardFormattedClientDebugLogV("app", format, args); + } + + void ForwardClientUserDebugLogV(int user, const char *format, va_list args) + { + char source[32] = {}; + _snprintf_s(source, sizeof(source), _TRUNCATE, "app:user=%d", user); + ForwardFormattedClientDebugLogV(source, format, args); + } + + void ForwardClientDebugSpewLogV(const char *format, va_list args) + { + ForwardFormattedClientDebugLogV("debug-spew", format, args); + } + + // Clear every cached connection slot during startup so stale metadata never leaks into future logs. + void Initialize() + { + std::lock_guard stateLock(g_serverLogState.stateLock); + for (size_t index = 0; index < g_serverLogState.entries.size(); ++index) + { + ResetConnectionLogEntry(&g_serverLogState.entries[index]); + } + } + + // Reuse Initialize as the shutdown cleanup path because both operations wipe the cache. + void Shutdown() + { + Initialize(); + } + + // Log the raw socket arrival before a smallId is assigned so early rejects still have an IP in the logs. + void OnIncomingTcpConnection(const char *ip) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + const std::string remoteIp = NormalizeRemoteIp(ip); + LogInfof("network", "incoming tcp connection from %s", remoteIp.c_str()); + } + + // TCP rejects happen before connection state is cached, so log directly from the supplied remote IP. + void OnRejectedTcpConnection(const char *ip, ETcpRejectReason reason) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + const std::string remoteIp = NormalizeRemoteIp(ip); + LogWarnf("network", "rejected tcp connection from %s: reason=%s", remoteIp.c_str(), TcpRejectReasonToString(reason)); + } + + // Cache the accepted remote IP immediately so later login and disconnect logs can reuse it. + void OnAcceptedTcpConnection(unsigned char smallId, const char *ip) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + const std::string remoteIp = NormalizeRemoteIp(ip); + { + std::lock_guard stateLock(g_serverLogState.stateLock); + ConnectionLogEntry &entry = g_serverLogState.entries[smallId]; + ResetConnectionLogEntry(&entry); + entry.remoteIp = remoteIp; + } + + LogInfof("network", "accepted tcp connection from %s as smallId=%u", remoteIp.c_str(), (unsigned)smallId); + } + + // Once login succeeds, bind the resolved player name onto the cached transport entry. + void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + const std::string playerNameUtf8 = NormalizePlayerName(playerName); + std::string remoteIp("unknown"); + { + std::lock_guard stateLock(g_serverLogState.stateLock); + ConnectionLogEntry &entry = g_serverLogState.entries[smallId]; + entry.playerName = playerNameUtf8; + if (!entry.remoteIp.empty()) + { + remoteIp = entry.remoteIp; + } + } + + LogInfof("network", "accepted player login: name=\"%s\" ip=%s smallId=%u", playerNameUtf8.c_str(), remoteIp.c_str(), (unsigned)smallId); + } + + // Read the cached IP for the rejection log, then clear the slot because the player never fully joined. + void OnRejectedPlayerLogin(unsigned char smallId, const std::wstring &playerName, ELoginRejectReason reason) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + const std::string playerNameUtf8 = NormalizePlayerName(playerName); + std::string remoteIp("unknown"); + { + std::lock_guard stateLock(g_serverLogState.stateLock); + ConnectionLogEntry &entry = g_serverLogState.entries[smallId]; + if (!entry.remoteIp.empty()) + { + remoteIp = entry.remoteIp; + } + ResetConnectionLogEntry(&entry); + } + + LogWarnf("network", "rejected login from %s: name=\"%s\" reason=%s", remoteIp.c_str(), playerNameUtf8.c_str(), LoginRejectReasonToString(reason)); + } + + // Disconnect logging is the final consumer of cached metadata, so it also clears the slot afterward. + void OnPlayerDisconnected( + unsigned char smallId, + const std::wstring &playerName, + DisconnectPacket::eDisconnectReason reason, + bool initiatedByServer) + { + if (!IsDedicatedServerLoggingEnabled()) + { + return; + } + + std::string playerNameUtf8 = NormalizePlayerName(playerName); + std::string remoteIp("unknown"); + { + // Copy state under lock and emit the log after unlocking so CLI output never blocks connection bookkeeping. + std::lock_guard stateLock(g_serverLogState.stateLock); + ConnectionLogEntry &entry = g_serverLogState.entries[smallId]; + if (!entry.remoteIp.empty()) + { + remoteIp = entry.remoteIp; + } + if (playerNameUtf8 == "" && !entry.playerName.empty()) + { + playerNameUtf8 = entry.playerName; + } + ResetConnectionLogEntry(&entry); + } + + LogInfof( + "network", + "%s: name=\"%s\" ip=%s smallId=%u reason=%s", + initiatedByServer ? "disconnecting player" : "player disconnected", + playerNameUtf8.c_str(), + remoteIp.c_str(), + (unsigned)smallId, + DisconnectReasonToString(reason)); + } + + // Provide explicit cache cleanup for paths that terminate without going through disconnect logging. + void ClearConnection(unsigned char smallId) + { + std::lock_guard stateLock(g_serverLogState.stateLock); + ResetConnectionLogEntry(&g_serverLogState.entries[smallId]); + } + } +} diff --git a/Minecraft.Server/ServerLogManager.h b/Minecraft.Server/ServerLogManager.h new file mode 100644 index 000000000..9425fc5c6 --- /dev/null +++ b/Minecraft.Server/ServerLogManager.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include + +#include "..\Minecraft.World\DisconnectPacket.h" + +namespace ServerRuntime +{ + namespace ServerLogManager + { + /** + * Identifies why the dedicated server rejected a TCP connection before login completed + * ログイン完了前にTCP接続を拒否した理由 + */ + enum ETcpRejectReason + { + eTcpRejectReason_BannedIp = 0, + eTcpRejectReason_GameNotReady, + eTcpRejectReason_ServerFull + }; + + /** + * Identifies why the dedicated server rejected a player during login validation + * ログイン検証中にプレイヤーを拒否した理由 + */ + enum ELoginRejectReason + { + eLoginRejectReason_BannedXuid = 0, + eLoginRejectReason_DuplicateXuid, + eLoginRejectReason_DuplicateName + }; + + /** + * Returns `true` when client-side debug logs should be redirected into the dedicated server logger + * dedicated server時にclient側デバッグログを転送すかどうか + */ + bool ShouldForwardClientDebugLogs(); + + /** + * Formats and forwards `CMinecraftApp::DebugPrintf` output through the dedicated server logger + * CMinecraftApp::DebugPrintf の出力を専用サーバーロガーへ転送 + */ + void ForwardClientAppDebugLogV(const char *format, va_list args); + + /** + * Formats and forwards `CMinecraftApp::DebugPrintf(int user, ...)` output through the dedicated server logger + * CMinecraftApp::DebugPrintf(int user, ...) の出力を専用サーバーロガーへ転送 + */ + void ForwardClientUserDebugLogV(int user, const char *format, va_list args); + + /** + * Formats and forwards legacy `DebugSpew` output through the dedicated server logger + * 従来の DebugSpew 出力を専用サーバーロガーへ転送 + */ + void ForwardClientDebugSpewLogV(const char *format, va_list args); + + /** + * Clears cached connection metadata before the dedicated server starts accepting players + * 接続ログ管理用のキャッシュを初期化 + */ + void Initialize(); + + /** + * Releases cached connection metadata after the dedicated server stops + * 接続ログ管理用のキャッシュを停止時に破棄 + */ + void Shutdown(); + + /** + * **Log Incoming TCP Connection** + * + * Emits a named log for a raw TCP accept before smallId assignment finishes + * smallId割り当て前のTCP接続を記録 + */ + void OnIncomingTcpConnection(const char *ip); + + /** + * Emits a named log for a TCP connection rejected before login starts + * ログイン開始前に拒否したTCP接続を記録 + */ + void OnRejectedTcpConnection(const char *ip, ETcpRejectReason reason); + + /** + * Stores the remote IP for the assigned smallId and logs the accepted transport connection + * 割り当て済みsmallIdに対接続IPを保存して記録 + */ + void OnAcceptedTcpConnection(unsigned char smallId, const char *ip); + + /** + * Associates a player name with the connection and emits the accepted login log + * 接続にプレイヤー名を関連付けてログイン成功を記録 + */ + void OnAcceptedPlayerLogin(unsigned char smallId, const std::wstring &playerName); + + /** + * Emits a named login rejection log and clears cached metadata for that smallId + * ログイン拒否を記録し対象smallIdのキャッシュを破棄 + */ + void OnRejectedPlayerLogin(unsigned char smallId, const std::wstring &playerName, ELoginRejectReason reason); + + /** + * Emits a named disconnect log using cached connection metadata and then clears that entry + * 接続キャッシュを使って切断ログを出しその後で破棄 + */ + void OnPlayerDisconnected( + unsigned char smallId, + const std::wstring &playerName, + DisconnectPacket::eDisconnectReason reason, + bool initiatedByServer); + + /** + * Removes any remembered IP or player name for the specified smallId + * 指定smallIdに紐づく接続キャッシュを消去 + */ + void ClearConnection(unsigned char smallId); + } +} \ No newline at end of file diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index e906f5a8e..5d1cc6161 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -8,6 +8,7 @@ #include "..\Access\Access.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" +#include "..\ServerLogManager.h" #include "..\ServerProperties.h" #include "..\WorldManager.h" #include "..\Console\ServerCli.h" @@ -369,6 +370,8 @@ int main(int argc, char **argv) g_Win64DedicatedServerPort = config.port; strncpy_s(g_Win64DedicatedServerBindIP, sizeof(g_Win64DedicatedServerBindIP), config.bindIP, _TRUNCATE); g_Win64DedicatedServerLanAdvertise = serverProperties.lanAdvertise; + LogStartupStep("initializing server log manager"); + ServerRuntime::ServerLogManager::Initialize(); LogStartupStep("initializing dedicated access control"); if (!ServerRuntime::Access::Initialize(".")) { @@ -653,6 +656,7 @@ int main(int argc, char **argv) WinsockNetLayer::Shutdown(); g_NetworkManager.Terminate(); + ServerRuntime::ServerLogManager::Shutdown(); CleanupDevice();