From 4d37b6b87925b0ccce94be38df3f598fb628a4c8 Mon Sep 17 00:00:00 2001 From: Marek Kulik Date: Sat, 27 Jan 2024 15:11:29 +0100 Subject: [PATCH] Run ingame server in a separate process --- Client/mods/deathmatch/logic/CClientGame.cpp | 6 +- .../mods/deathmatch/logic/CDynamicLibrary.cpp | 1 + Client/mods/deathmatch/logic/CServer.cpp | 445 ++++++++---------- Client/mods/deathmatch/logic/CServer.h | 58 +-- Server/core/CModManagerImpl.cpp | 5 + Server/core/CModManagerImpl.h | 2 + Server/core/CServerImpl.cpp | 66 ++- Server/core/CServerImpl.h | 5 - Server/launcher/Main.cpp | 1 + Server/mods/deathmatch/CServer.cpp | 5 + Server/mods/deathmatch/CServer.h | 1 + Server/sdk/core/CServerBase.h | 1 + 12 files changed, 273 insertions(+), 323 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 8b4a5852c5..63bd6174be 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -752,7 +752,7 @@ bool CClientGame::StartLocalGame(eServerType Type, const char* szPassword) { m_bWaitingForLocalConnect = true; m_bErrorStartingLocal = true; - g_pCore->ShowMessageBox(_("Error") + _E("CD04"), _("The server is not installed"), MB_ICON_ERROR | MB_BUTTON_OK); + g_pCore->ShowMessageBox(_("Error") + _E("CD60"), _("Could not start the local server. See console for details."), MB_BUTTON_OK | MB_ICON_ERROR); g_pCore->GetModManager()->RequestUnload(); return false; } @@ -1122,8 +1122,8 @@ void CClientGame::DoPulses() // Call debug code if debug mode m_Foo.DoPulse(); - // Output stuff from our internal server eventually - m_Server.DoPulse(); + // Output stuff from our server eventually + m_Server.Pulse(); if (m_pManager->IsGameLoaded() && m_Status == CClientGame::STATUS_JOINED && GetTickCount64_() - m_llLastTransgressionTime > 60000) { diff --git a/Client/mods/deathmatch/logic/CDynamicLibrary.cpp b/Client/mods/deathmatch/logic/CDynamicLibrary.cpp index 69ad0e3072..a6a9c0d762 100644 --- a/Client/mods/deathmatch/logic/CDynamicLibrary.cpp +++ b/Client/mods/deathmatch/logic/CDynamicLibrary.cpp @@ -10,6 +10,7 @@ *****************************************************************************/ #include +#include "CDynamicLibrary.h" CDynamicLibrary::CDynamicLibrary() { diff --git a/Client/mods/deathmatch/logic/CServer.cpp b/Client/mods/deathmatch/logic/CServer.cpp index 3f38eef687..69484c6ad5 100644 --- a/Client/mods/deathmatch/logic/CServer.cpp +++ b/Client/mods/deathmatch/logic/CServer.cpp @@ -1,330 +1,287 @@ /***************************************************************************** * - * PROJECT: Multi Theft Auto v1.0 + * PROJECT: Multi Theft Auto * LICENSE: See LICENSE in the top level directory * FILE: mods/deathmatch/logic/CServer.cpp * PURPOSE: Local server instancing class * - * Multi Theft Auto is available from http://www.multitheftauto.com/ + * Multi Theft Auto is available from https://multitheftauto.com/ * *****************************************************************************/ #include - -static volatile bool g_bIsStarted = false; -extern CCoreInterface* g_pCore; -extern CLocalizationInterface* g_pLocalization; -CCriticalSection CServer::m_OutputCC; -std::list CServer::m_OutputQueue; +#include +#include #ifdef MTA_DEBUG - #define SERVER_DLL_PATH "core_d.dll" + #define SERVER_EXE_PATH "MTA Server_d.exe" #else - #define SERVER_DLL_PATH "core.dll" + #define SERVER_EXE_PATH "MTA Server.exe" #endif -#define ERROR_NO_ERROR 0 -#define ERROR_NO_NETWORK_LIBRARY 1 -#define ERROR_NETWORK_LIBRARY_FAILED 2 -#define ERROR_LOADING_MOD 3 +constexpr UINT PROCESS_FORCEFULLY_TERMINATED = 0x90804050u; static const SFixedArray szServerErrors = {"Server stopped", "Could not load network library", "Loading network library failed", "Error loading mod"}; -CServer::CServer() +static bool IsProcessRunning(HANDLE process) { - assert(!g_bIsStarted); - - // Initialize - m_bIsReady = false; - m_pLibrary = NULL; - m_hThread = INVALID_HANDLE_VALUE; - m_iLastError = ERROR_NO_ERROR; - - m_strServerRoot = CalcMTASAPath("server"); - m_strDLLFile = PathJoin(m_strServerRoot, SERVER_DLL_PATH); + return WaitForSingleObject(process, 0) == WAIT_TIMEOUT; } -CServer::~CServer() +struct PipeHandlePair { - // Make sure the server is stopped - Stop(); + bool AutoClose{}; + HANDLE Read{}; + HANDLE Write{}; - // Make sure the thread handle is closed - if (m_hThread != INVALID_HANDLE_VALUE) + ~PipeHandlePair() { - CloseHandle(m_hThread); + if (AutoClose) + { + CloseHandle(Read); + CloseHandle(Write); + } } -} +}; -void CServer::DoPulse() +bool CServer::Start(const char* configFileName) { - if (IsRunning()) + if (m_isRunning) + return false; + + // Create and use an optional job object to kill the server process automatically when we close the job object. + // This is pretty handy in case the game process unexpectedly crashes and we miss terminating the server process. + HANDLE job = CreateJobObjectW(nullptr, nullptr); + + if (job != nullptr) { - // Make sure the server doesn't happen to be adding anything right now - m_OutputCC.Lock(); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{}; + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; - // Anything to output to console? - if (m_OutputQueue.size() > 0) + if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) { - // Loop through our output queue and echo it to console - std::list::const_iterator iter = m_OutputQueue.begin(); - for (; iter != m_OutputQueue.end(); ++iter) - { - // Echo it - const char* szString = iter->c_str(); - g_pCore->GetConsole()->Echo(szString); - - // Does the message end with "Server started and is ready to accept connections!\n"? - size_t sizeString = iter->length(); - if (sizeString >= 51 && stricmp(szString + sizeString - 51, "Server started and is ready to accept connections!\n") == 0) - { - m_bIsReady = true; - } - } - - // Clear the list - m_OutputQueue.clear(); + g_pCore->GetConsole()->Printf("Server process warning [error: %08x]: failed to setup job object\n", GetLastError()); + + CloseHandle(job); + job = nullptr; } - // Unlock again - m_OutputCC.Unlock(); } - else + + SECURITY_ATTRIBUTES securityAttributes{}; + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.bInheritHandle = TRUE; + + // Create the input pipe for the server process. + PipeHandlePair in; + + if (!CreatePipe(&in.Read, &in.Write, &securityAttributes, 0)) { - // not running, errored? - if (GetLastError() != ERROR_NO_ERROR) - { - Stop(); - } + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to create input pipe\n", GetLastError()); + return false; } -} -bool CServer::Start(const char* szConfig) -{ - // Not already started? - if (!g_bIsStarted) + in.AutoClose = true; + + // Only inherit the read handle to the input pipe. + if (!SetHandleInformation(in.Write, HANDLE_FLAG_INHERIT, 0)) { - m_strConfig = szConfig; + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to modify input pipe\n", GetLastError()); + return false; + } - // Check that the DLL exists - if (!FileExists(m_strDLLFile)) - { - g_pCore->GetConsole()->Printf("Unable to find: '%s'", m_strDLLFile.c_str()); - return false; - } + // Create the output pipe for the server process. + PipeHandlePair out; - // We're now started, but not ready - g_bIsStarted = true; - m_bIsReady = false; + if (!CreatePipe(&out.Read, &out.Write, &securityAttributes, 0)) + { + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to create output pipe\n", GetLastError()); + return false; + } - // Close the previous thread? - if (m_hThread != INVALID_HANDLE_VALUE) - { - CloseHandle(m_hThread); - } + out.AutoClose = true; - // Create a thread to run the server - DWORD dwTemp; - m_hThread = CreateThread(NULL, 0, Thread_EntryPoint, this, 0, &dwTemp); - return m_hThread != NULL; + // Only inherit the write handle to the output pipe. + if (!SetHandleInformation(out.Read, HANDLE_FLAG_INHERIT, 0)) + { + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to modify output pipe\n", GetLastError()); + return false; } - return false; -} + // Create an anonymous event to signal the readyness of the server to accept connections. + HANDLE readyEvent = CreateEventA(&securityAttributes, FALSE, FALSE, nullptr); -bool CServer::IsStarted() -{ - return g_bIsStarted; -} + if (readyEvent == nullptr) + { + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to create ready event\n", GetLastError()); + return false; + } -bool CServer::Stop() -{ - // Started? - if (g_bIsStarted) + // We write the event handle to standard input, which we then extract in CServerImpl::Run (Server Core). + DWORD bytesWritten{}; + + if (!WriteFile(in.Write, &readyEvent, sizeof(readyEvent), &bytesWritten, nullptr) || bytesWritten != sizeof(readyEvent)) { - // Wait for the library to come true or is started to go false - // This is so a call to Start then fast call to Stop will work. Otherwize it might not - // get time to start the server thread before we terminate it and we will end up - // starting it after this call to Stop. - while (g_bIsStarted && !m_pLibrary) - { - Sleep(1); - } + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to write ready event\n", GetLastError()); + CloseHandle(readyEvent); + return false; + } - // Lock - m_CriticalSection.Lock(); + // Create the server process now. + const SString serverRoot = CalcMTASAPath("server"); + const SString serverExePath = PathJoin(serverRoot, SERVER_EXE_PATH); + const SString commandLine("\"%s\" --child-process --config \"%s\"", SERVER_EXE_PATH, configFileName); - // Is the server running? - if (m_pLibrary) - { - // Send the exit message - Send("exit"); - } + STARTUPINFOW startupInfo{}; + startupInfo.cb = sizeof(STARTUPINFOW); + startupInfo.hStdError = out.Write; + startupInfo.hStdOutput = out.Write; + startupInfo.hStdInput = in.Read; + startupInfo.dwFlags = STARTF_USESTDHANDLES; + + PROCESS_INFORMATION processInfo{}; - // Unlock it so we won't deadlock - m_CriticalSection.Unlock(); + if (!CreateProcessW(*FromUTF8(serverExePath), const_cast(*FromUTF8(commandLine)), nullptr, nullptr, TRUE, + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, nullptr, nullptr, &startupInfo, &processInfo)) + { + g_pCore->GetConsole()->Printf("Server process failed to start [error: %08x]: failed to create process\n", GetLastError()); + CloseHandle(readyEvent); + return false; } - // If we have a thread, wait for it to finish - if (m_hThread != INVALID_HANDLE_VALUE) + // Try to assign the project to our job object, if we have one. + if (job != nullptr && !AssignProcessToJobObject(job, processInfo.hProcess)) { - // Let the thread finish - WaitForSingleObject(m_hThread, INFINITE); + CloseHandle(job); + job = nullptr; + } - // If we can get an exit code, see if it's non-zero - DWORD dwExitCode = 0; - if (GetExitCodeThread(m_hThread, &dwExitCode)) - { - // Handle non-zero exit codes - if (dwExitCode != ERROR_NO_ERROR) - { - g_pCore->ShowMessageBox(_("Error") + _E("CD60"), _("Could not start the local server. See console for details."), MB_BUTTON_OK | MB_ICON_ERROR); - g_pCore->GetConsole()->Printf(_("Error: Could not start local server. [%s]"), szServerErrors[GetLastError()]); - } - } + // Close the handles managed by the server process. + CloseHandle(in.Read); + CloseHandle(out.Write); - // Close it - CloseHandle(m_hThread); - m_hThread = INVALID_HANDLE_VALUE; - } + // Close the thread handle, because we don't need it. + CloseHandle(processInfo.hThread); + + in.AutoClose = false; + out.AutoClose = false; - m_iLastError = ERROR_NO_ERROR; + m_stdin = in.Write; + m_stdout = out.Read; + m_isAcceptingConnections = false; + m_isRunning = true; + m_job = job; + m_readyEvent = readyEvent; + m_process = processInfo.hProcess; + m_processId = processInfo.dwProcessId; + g_pCore->GetConsole()->Printf("Server process has started [pid: %lu]\n", m_processId); return true; } -bool CServer::Send(const char* szString) +void CServer::Stop(bool graceful) { - // Server running? - bool bReturn = false; - if (g_bIsStarted) + if (!m_isRunning) + return; + + bool isRunning = IsProcessRunning(m_process); + + if (graceful && isRunning) { - // Wait for the library to come true or is started to go false - while (g_bIsStarted && !m_pLibrary) + DWORD bytesWritten{}; + + // Try to write a normal "exit" command to the server process. + if (WriteFile(m_stdin, "exit\n", 5, &bytesWritten, nullptr) && bytesWritten == 5) { - Sleep(1); + g_pCore->GetConsole()->Printf("Sent 'exit' command to the server to shut it down\n"); + WaitForSingleObject(m_process, 8000); } - - // Lock - m_CriticalSection.Lock(); - - // Are we running the server - if (m_pLibrary) + // Try to send a CTRl+C event to the process console. + else if (AttachConsole(m_processId)) { - // Grab the SendServerCommand function pointer - typedef bool(SendServerCommand_t)(const char*); - SendServerCommand_t* pfnSendServerCommand = reinterpret_cast(m_pLibrary->GetProcedureAddress("SendServerCommand")); - if (pfnSendServerCommand) - { - // Call it with the command - bReturn = pfnSendServerCommand(szString); - } + SetConsoleCtrlHandler(nullptr, true); + GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); + FreeConsole(); + g_pCore->GetConsole()->Printf("Sent 'CTRL+C' event to the server to shut it down\n"); + WaitForSingleObject(m_process, 8000); + SetConsoleCtrlHandler(nullptr, false); } - // Unlock - m_CriticalSection.Unlock(); + isRunning = IsProcessRunning(m_process); } - // Return - return bReturn; -} - -DWORD WINAPI CServer::Thread_EntryPoint(LPVOID pThis) -{ - return reinterpret_cast(pThis)->Thread_Run(); -} + // We either forcefully terminating the server, or the graceful shutdown failed. + if (isRunning) + { + g_pCore->GetConsole()->Printf("Terminating the server forcefully now\n"); -unsigned long CServer::Thread_Run() -{ - // Enter critical section - m_CriticalSection.Lock(); + TerminateProcess(m_process, PROCESS_FORCEFULLY_TERMINATED); + WaitForSingleObject(m_process, 3000); - // Already loaded? Just return or we get a memory leak. - if (m_pLibrary) - { - m_CriticalSection.Unlock(); - return 0; + if (m_job != nullptr) + { + CloseHandle(m_job); + m_job = nullptr; + } } - // Load the DLL - m_pLibrary = new CDynamicLibrary; - if (m_pLibrary->Load(m_strDLLFile)) + if (DWORD exitCode{}; GetExitCodeProcess(m_process, &exitCode)) { - // Grab the entrypoint - typedef int(Main_t)(int, char*[]); - Main_t* pfnEntryPoint = reinterpret_cast(m_pLibrary->GetProcedureAddress("Run")); - if (pfnEntryPoint) - { - // Populate the arguments array - char szArgument1[8]; - strcpy(szArgument1, "-D"); - - char szArgument2[256]; - strncpy(szArgument2, m_strServerRoot, 256); - szArgument2[255] = 0; - - char szArgument3[16]; - strcpy(szArgument3, "--config"); + const char* errorText = "Unknown exit code"; - char szArgument4[64]; - strcpy(szArgument4, m_strConfig); + if (exitCode == PROCESS_FORCEFULLY_TERMINATED) + errorText = "Process was forcefully terminated"; + else if (exitCode < (sizeof(szServerErrors) / sizeof(const char*))) + errorText = szServerErrors[exitCode]; - char szArgument5[8]; - strcpy(szArgument5, "-s"); - - char* szArguments[7]; - szArguments[0] = szArgument1; - szArguments[1] = szArgument2; - szArguments[2] = szArgument3; - szArguments[3] = szArgument4; - szArguments[4] = szArgument5; - - // Give the server our function to output stuff to the console - char szArgument6[32]; - strcpy(szArgument6, "--clientfeedback"); - szArguments[5] = szArgument6; - szArguments[6] = reinterpret_cast(CServer::AddServerOutput); + g_pCore->GetConsole()->Printf("Server process has been terminated [%08X]: %s\n", exitCode, errorText); + } - // We're now running the server - m_CriticalSection.Unlock(); + CloseHandle(m_readyEvent); + CloseHandle(m_stdin); + CloseHandle(m_stdout); + CloseHandle(m_process); - // Call it and grab what it returned - int iReturn = pfnEntryPoint(7, szArguments); + if (m_job != nullptr) + { + CloseHandle(m_job); + m_job = nullptr; + } - m_iLastError = iReturn; + m_isRunning = false; +} - // Lock again - m_CriticalSection.Lock(); +void CServer::Pulse() +{ + if (!m_isRunning) + return; - // Delete the library - delete m_pLibrary; - m_pLibrary = NULL; + // Try to read the process standard output and then write it to the console window. + if (DWORD numBytes{}; PeekNamedPipe(m_stdout, nullptr, 0, nullptr, &numBytes, nullptr) && numBytes > 0) + { + std::array buffer{}; - // Return what the server returned - m_CriticalSection.Unlock(); - g_bIsStarted = false; - return iReturn; + if (ReadFile(m_stdout, buffer.data(), std::min(buffer.size(), numBytes), &numBytes, nullptr) && numBytes > 0) + { + g_pCore->GetConsole()->Printf("%.*s", numBytes, buffer.data()); } } - // Delete the library again - delete m_pLibrary; - m_pLibrary = NULL; - - // Unlock the critialsection and return failed - m_CriticalSection.Unlock(); - g_bIsStarted = false; - return 1; -} - -void CServer::AddServerOutput(const char* szOutput) -{ - // Make sure the client doesn't process the queue right now - m_OutputCC.Lock(); - - // Add the string to the queue - m_OutputQueue.push_back(szOutput); + // Check if the server process was terminated externally. + if (!IsProcessRunning(m_process)) + { + Stop(); + return; + } - // Unlock again - m_OutputCC.Unlock(); + // Check if the server process signalised readyness to accept connections. + if (m_readyEvent != nullptr) + { + if (WaitForSingleObject(m_readyEvent, 0) != WAIT_TIMEOUT) + { + CloseHandle(m_readyEvent); + m_readyEvent = nullptr; + m_isAcceptingConnections = true; + } + } } diff --git a/Client/mods/deathmatch/logic/CServer.h b/Client/mods/deathmatch/logic/CServer.h index e88100ca30..b6f324e799 100644 --- a/Client/mods/deathmatch/logic/CServer.h +++ b/Client/mods/deathmatch/logic/CServer.h @@ -1,59 +1,43 @@ /***************************************************************************** * - * PROJECT: Multi Theft Auto v1.0 + * PROJECT: Multi Theft Auto * LICENSE: See LICENSE in the top level directory * FILE: mods/deathmatch/logic/CServer.h * PURPOSE: Header for server class * - * Multi Theft Auto is available from http://www.multitheftauto.com/ + * Multi Theft Auto is available from https://multitheftauto.com/ * *****************************************************************************/ #pragma once -#include "CDynamicLibrary.h" -#include -#include - -class CServer +class CServer final { public: - CServer(); - ~CServer(); - - void DoPulse(); - - bool Start(const char* szConfig); - bool Stop(); - bool IsStarted(); - bool IsRunning() { return m_pLibrary != NULL; }; - bool IsReady() { return m_bIsReady; }; + ~CServer() { Stop(); } - int GetLastError() { return m_iLastError; }; + bool Start(const char* configFileName); - bool Send(const char* szString); + void Stop(bool graceful = true); - const std::string& GetPassword() { return m_strPassword; }; - void SetPassword(const char* szPassword) { m_strPassword = szPassword; }; + void Pulse(); -private: - static DWORD WINAPI Thread_EntryPoint(LPVOID pThis); - unsigned long Thread_Run(); - - bool m_bIsReady; - HANDLE m_hThread; - CDynamicLibrary* volatile m_pLibrary; - CCriticalSection m_CriticalSection; - SString m_strServerRoot; - SString m_strDLLFile; - SString m_strConfig; + bool IsRunning() const noexcept { return m_isRunning; } - int m_iLastError; + bool IsReady() const noexcept { return m_isRunning && m_isAcceptingConnections; } - std::string m_strPassword; + void SetPassword(const char* password) { m_password = password; } - static CCriticalSection m_OutputCC; - static std::list m_OutputQueue; + const std::string& GetPassword() const noexcept { return m_password; } - static void AddServerOutput(const char* szOutput); +private: + bool m_isRunning{}; + bool m_isAcceptingConnections{}; + HANDLE m_job{}; + HANDLE m_readyEvent{}; + HANDLE m_process{}; + DWORD m_processId{}; + HANDLE m_stdout{}; + HANDLE m_stdin{}; + std::string m_password; }; diff --git a/Server/core/CModManagerImpl.cpp b/Server/core/CModManagerImpl.cpp index 2aa4540621..bd596e78e4 100644 --- a/Server/core/CModManagerImpl.cpp +++ b/Server/core/CModManagerImpl.cpp @@ -148,6 +148,11 @@ void CModManagerImpl::DoPulse() } } +bool CModManagerImpl::IsReadyToAcceptConnections() const noexcept +{ + return (m_pBase != nullptr) && m_pBase->IsReadyToAcceptConnections(); +} + bool CModManagerImpl::IsFinished() { if (m_pBase) diff --git a/Server/core/CModManagerImpl.h b/Server/core/CModManagerImpl.h index da0c765c64..8fae2bbb63 100644 --- a/Server/core/CModManagerImpl.h +++ b/Server/core/CModManagerImpl.h @@ -46,6 +46,8 @@ class CModManagerImpl : public CModManager void DoPulse(); + bool IsReadyToAcceptConnections() const noexcept; + bool IsFinished(); bool PendingWorkToDo(); diff --git a/Server/core/CServerImpl.cpp b/Server/core/CServerImpl.cpp index 45ddd984c3..4a2e7dd08a 100644 --- a/Server/core/CServerImpl.cpp +++ b/Server/core/CServerImpl.cpp @@ -47,6 +47,9 @@ bool IsCursesActive() { return m_wndInput != NULL; } +#else +bool g_isChildProcess = false; +HANDLE g_readyEvent = nullptr; #endif #ifdef WIN32 @@ -57,7 +60,6 @@ CServerImpl::CServerImpl() { #ifdef WIN32 m_pThreadCommandQueue = pThreadCommandQueue; - m_fClientFeedback = NULL; m_hConsole = NULL; #else m_wndMenu = NULL; @@ -122,17 +124,6 @@ void CServerImpl::Printf(const char* szFormat, ...) #endif } - // Eventually feed stuff back to our client if we run inside GTA - #ifdef WIN32 - if (m_fClientFeedback) - { - char szOutput[512]; - szOutput[511] = 0; - VSNPRINTF(szOutput, 511, szFormat, ap); - m_fClientFeedback(szOutput); - } - #endif - va_end(ap); } @@ -177,9 +168,7 @@ int CServerImpl::Run(int iArgumentCount, char* szArguments[]) if (!ParseArguments(iArgumentCount, szArguments)) return 1; -#ifdef WIN32 - if (!m_fClientFeedback) -#else +#ifndef WIN32 if (!g_bNoCrashHandler) #endif { @@ -206,9 +195,7 @@ int CServerImpl::Run(int iArgumentCount, char* szArguments[]) m_hConsole = GetStdHandle(STD_OUTPUT_HANDLE); m_hConsoleInput = GetStdHandle(STD_INPUT_HANDLE); - // If stdout is piped GetConsoleScreenBufferInfo will fail - // ==> check if stdin is piped - if (HasConsole()) + if (!g_isChildProcess && HasConsole()) { // Disable QuickEdit mode to prevent text selection causing server freeze DWORD dwConInMode; @@ -241,7 +228,18 @@ int CServerImpl::Run(int iArgumentCount, char* szArguments[]) { // Enable non-blocking read mode DWORD pipeState = PIPE_NOWAIT; - SetNamedPipeHandleState(GetStdHandle(STD_INPUT_HANDLE), &pipeState, nullptr, nullptr); + SetNamedPipeHandleState(m_hConsoleInput, &pipeState, nullptr, nullptr); + } + + if (g_isChildProcess) + { + DWORD bytesRead{}; + + if (!ReadFile(m_hConsoleInput, &g_readyEvent, sizeof(HANDLE), &bytesRead, nullptr) || bytesRead != sizeof(HANDLE)) + { + Print("ERROR: Failed to read ready-event handle from input (%08x)\n", GetLastError()); + return ERROR_OTHER; + } } #else // support user locales @@ -482,6 +480,13 @@ void CServerImpl::MainLoop() if (m_pModManager->IsFinished()) m_bRequestedQuit = true; + if (g_readyEvent != nullptr && m_pModManager->IsReadyToAcceptConnections()) + { + SetEvent(g_readyEvent); + CloseHandle(g_readyEvent); + g_readyEvent = nullptr; + } + HandlePulseSleep(); } @@ -1071,16 +1076,6 @@ bool CServerImpl::ParseArguments(int iArgumentCount, char* szArguments[]) break; } - // Client feedback pointer? - #ifdef WIN32 - case 'c': - { - m_fClientFeedback = reinterpret_cast(szArguments[i]); - ucNext = 0; - break; - } - #endif - // Nothing we know, proceed default: { @@ -1123,13 +1118,16 @@ bool CServerImpl::ParseArguments(int iArgumentCount, char* szArguments[]) { g_bNoCrashHandler = true; } - - #ifdef WIN32 - else if (strcmp(szArguments[i], "--clientfeedback") == 0) +#ifdef WIN32 + else if (!strcmp(szArguments[i], "--child-process")) { - ucNext = 'c'; + g_isChildProcess = true; + g_bNoTopBar = true; + g_bNoCurses = true; + std::setbuf(stdout, nullptr); + std::setbuf(stderr, nullptr); } - #endif +#endif } } } diff --git a/Server/core/CServerImpl.h b/Server/core/CServerImpl.h index c9955bf562..8a89739786 100644 --- a/Server/core/CServerImpl.h +++ b/Server/core/CServerImpl.h @@ -30,7 +30,6 @@ typedef CXML* (*InitXMLInterface)(const char* szSaveFlagDirectory); typedef CNetServer* (*InitNetServerInterface)(); #ifdef WIN32 -typedef void(FClientFeedback)(const char* szText); constexpr SHORT SCREEN_BUFFER_SIZE = 256; #endif @@ -89,10 +88,6 @@ class CServerImpl : public CServerInterface CModManagerImpl* m_pModManager; CXML* m_pXML; -#ifdef WIN32 - FClientFeedback* m_fClientFeedback; -#endif - SString m_strServerPath; SString m_strServerModPath; diff --git a/Server/launcher/Main.cpp b/Server/launcher/Main.cpp index dc24613491..3c02a939e8 100644 --- a/Server/launcher/Main.cpp +++ b/Server/launcher/Main.cpp @@ -84,6 +84,7 @@ int main(int argc, char* argv[]) printf(" -u Disable output buffering and flush instantly (useful for screenlog)\n"); #ifndef WIN32 printf(" -x Disable simplified crash reports (To allow core dumps)\n"); + printf(" --child-process Run server without output buffering and with a readyness event\n"); #endif printf(" -D [PATH] Use as base directory\n"); printf(" --config [FILE] Alternate mtaserver.conf file\n"); diff --git a/Server/mods/deathmatch/CServer.cpp b/Server/mods/deathmatch/CServer.cpp index a4bca0e9b8..d80f6a4509 100644 --- a/Server/mods/deathmatch/CServer.cpp +++ b/Server/mods/deathmatch/CServer.cpp @@ -96,6 +96,11 @@ void CServer::DoPulse() CLOCK(" Top", " Idle"); } +bool CServer::IsReadyToAcceptConnections() const noexcept +{ + return (m_pGame != nullptr) && m_pGame->IsServerFullyUp(); +} + bool CServer::IsFinished() { if (m_pGame) diff --git a/Server/mods/deathmatch/CServer.h b/Server/mods/deathmatch/CServer.h index f408206f8e..b345df6b66 100644 --- a/Server/mods/deathmatch/CServer.h +++ b/Server/mods/deathmatch/CServer.h @@ -28,6 +28,7 @@ class CServer : public CServerBase void GetTag(char* szInfoTag, int iInfoTag); void HandleInput(char* szCommand); + bool IsReadyToAcceptConnections() const noexcept override; bool IsFinished(); bool PendingWorkToDo(); bool GetSleepIntervals(int& iSleepBusyMs, int& iSleepIdleMs, int& iLogicFpsLimit); diff --git a/Server/sdk/core/CServerBase.h b/Server/sdk/core/CServerBase.h index e4d1c3f615..4dfc073989 100644 --- a/Server/sdk/core/CServerBase.h +++ b/Server/sdk/core/CServerBase.h @@ -24,6 +24,7 @@ class CServerBase virtual void HandleInput(char* szCommand) = 0; virtual void GetTag(char* szInfoTag, int iInfoTag) = 0; + virtual bool IsReadyToAcceptConnections() const noexcept = 0; virtual bool IsFinished() = 0; virtual bool PendingWorkToDo() = 0; virtual bool GetSleepIntervals(int& iSleepBusyMs, int& iSleepIdleMs, int& iLogicFpsLimit) = 0;