From 1bc12ba2b3bc2c4fbe8fad4797cf33266eb13f4b Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Thu, 23 Jul 2020 00:34:43 +0100 Subject: Streamline startup sequence * Clean up cRoot & main * Move some OS-specifics into OSSupport --- src/BlockEntities/CommandBlockEntity.cpp | 2 +- src/Globals.h | 8 +- src/OSSupport/CMakeLists.txt | 2 + src/OSSupport/MiniDumpWriter.h | 139 ++++++++ src/OSSupport/StartAsService.h | 173 +++++++++ src/OverridesSettingsRepository.cpp | 4 +- src/OverridesSettingsRepository.h | 4 +- src/RCONServer.cpp | 2 +- src/Root.cpp | 370 +++++++++---------- src/Root.h | 55 ++- src/Server.cpp | 48 ++- src/Server.h | 21 +- src/main.cpp | 590 +++++++++---------------------- 13 files changed, 737 insertions(+), 681 deletions(-) create mode 100644 src/OSSupport/MiniDumpWriter.h create mode 100644 src/OSSupport/StartAsService.h diff --git a/src/BlockEntities/CommandBlockEntity.cpp b/src/BlockEntities/CommandBlockEntity.cpp index ad78c4ea3..6cc83ff68 100644 --- a/src/BlockEntities/CommandBlockEntity.cpp +++ b/src/BlockEntities/CommandBlockEntity.cpp @@ -203,7 +203,7 @@ void cCommandBlockEntity::Execute() { cServer * Server = cRoot::Get()->GetServer(); LOGD("cCommandBlockEntity: Executing command %s", m_Command.c_str()); - Server->ExecuteConsoleCommand(RealCommand, CmdBlockOutCb); + Server->QueueExecuteConsoleCommand(RealCommand, CmdBlockOutCb); } else { diff --git a/src/Globals.h b/src/Globals.h index c2f117e23..7fc9addf6 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -280,23 +280,23 @@ template class SizeChecker; #endif // Pretty much the same as ASSERT() but stays in Release builds - #define VERIFY(x) (!!(x) || ( LOGERROR("Verification failed: %s, file %s, line %i", #x, __FILE__, __LINE__), exit(1), 0)) + #define VERIFY(x) (!!(x) || ( LOGERROR("Verification failed: %s, file %s, line %i", #x, __FILE__, __LINE__), std::abort(), 0)) #else // TEST_GLOBALS #ifdef _DEBUG - #define ASSERT(x) ( !!(x) || ( LOGERROR("Assertion failed: %s, file %s, line %i", #x, __FILE__, __LINE__), PrintStackTrace(), assert(0), 0)) + #define ASSERT(x) ( !!(x) || ( LOGERROR("Assertion failed: %s, file %s, line %i", #x, __FILE__, __LINE__), std::abort(), 0)) #else #define ASSERT(x) #endif // Pretty much the same as ASSERT() but stays in Release builds - #define VERIFY(x) (!!(x) || ( LOGERROR("Verification failed: %s, file %s, line %i", #x, __FILE__, __LINE__), PrintStackTrace(), exit(1), 0)) + #define VERIFY(x) (!!(x) || ( LOGERROR("Verification failed: %s, file %s, line %i", #x, __FILE__, __LINE__), std::abort(), 0)) #endif // else TEST_GLOBALS /** Use to mark code that should be impossible to reach. */ -#define UNREACHABLE(x) do { FLOGERROR("Hit unreachable code: {0}, file {1}, line {2}", #x, __FILE__, __LINE__); PrintStackTrace(); std::terminate(); } while (false) +#define UNREACHABLE(x) do { FLOGERROR("Hit unreachable code: {0}, file {1}, line {2}", #x, __FILE__, __LINE__); std::abort(); } while (false) diff --git a/src/OSSupport/CMakeLists.txt b/src/OSSupport/CMakeLists.txt index 1b853c202..42ce6934d 100644 --- a/src/OSSupport/CMakeLists.txt +++ b/src/OSSupport/CMakeLists.txt @@ -28,12 +28,14 @@ target_sources( HostnameLookup.h IPLookup.h IsThread.h + MiniDumpWriter.h Network.h NetworkLookup.h NetworkSingleton.h Queue.h ServerHandleImpl.h StackTrace.h + StartAsService.h TCPLinkImpl.h UDPEndpointImpl.h WinStackWalker.h diff --git a/src/OSSupport/MiniDumpWriter.h b/src/OSSupport/MiniDumpWriter.h new file mode 100644 index 000000000..e5cd1a0ac --- /dev/null +++ b/src/OSSupport/MiniDumpWriter.h @@ -0,0 +1,139 @@ + +#pragma once + + + + + +/** Flags to control minidump contents on supported platforms. */ +enum class MiniDumpFlags +{ + WithDataSegments, + WithFullMemory +}; + + + + + +#if defined(_WIN32) && !defined(_WIN64) && defined(_MSC_VER) // 32-bit Windows app compiled in MSVC + +#include + + + + + +/** Windows 32-bit stuff: +When the server crashes, create a "dump file" containing the callstack of each thread and some variables; +let the user send us that crash file for analysis */ +class MiniDumpWriter +{ + typedef BOOL(WINAPI *pMiniDumpWriteDump)( + HANDLE hProcess, + DWORD ProcessId, + HANDLE hFile, + MINIDUMP_TYPE DumpType, + PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + PMINIDUMP_CALLBACK_INFORMATION CallbackParam + ); + +public: + + MiniDumpWriter() + { + // Magic code to produce dump-files on Windows if the server crashes: + + m_DbgHelp = LoadLibrary(L"DBGHELP.DLL"); + if (m_DbgHelp == INVALID_HANDLE_VALUE) + { + return; + } + + s_WriteMiniDump = (pMiniDumpWriteDump)GetProcAddress(m_DbgHelp, "MiniDumpWriteDump"); + if (s_WriteMiniDump != nullptr) + { + ASSERT(swprintf(s_DumpFileName, ARRAYCOUNT(s_DumpFileName), L"crash_mcs_%x.dmp", GetCurrentProcessId()) > 0); + SetUnhandledExceptionFilter(LastChanceExceptionFilter); + } + + // End of dump-file magic + } + + void AddDumpFlags(const MiniDumpFlags a_Flags) + { + switch (a_Flags) + { + case MiniDumpFlags::WithDataSegments: + { + s_DumpFlags = static_cast(s_DumpFlags | MINIDUMP_TYPE::MiniDumpWithDataSegs); + break; + } + case MiniDumpFlags::WithFullMemory: + { + s_DumpFlags = static_cast(s_DumpFlags | MINIDUMP_TYPE::MiniDumpWithFullMemory); + break; + } + } + } + + ~MiniDumpWriter() + { + FreeLibrary(m_DbgHelp); + } + +private: + + /** This function gets called just before the "program executed an illegal instruction and will be terminated" or similar. + Its purpose is to create the crashdump using the dbghlp DLLs */ + static LONG WINAPI LastChanceExceptionFilter(__in struct _EXCEPTION_POINTERS * a_ExceptionInfo) + { + char * newStack = &s_ExceptionStack[sizeof(s_ExceptionStack) - 1]; + char * oldStack; + + // Use the substitute stack: + // This code is the reason why we don't support 64-bit (yet) + _asm + { + mov oldStack, esp + mov esp, newStack + } + + MINIDUMP_EXCEPTION_INFORMATION ExcInformation; + ExcInformation.ThreadId = GetCurrentThreadId(); + ExcInformation.ExceptionPointers = a_ExceptionInfo; + ExcInformation.ClientPointers = 0; + + // Write the dump file: + HANDLE dumpFile = CreateFile(s_DumpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + s_WriteMiniDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, s_DumpFlags, (a_ExceptionInfo) ? &ExcInformation : nullptr, nullptr, nullptr); + CloseHandle(dumpFile); + + // Revert to old stack: + _asm + { + mov esp, oldStack + } + + return 0; + } + + HINSTANCE m_DbgHelp; + + static inline pMiniDumpWriteDump s_WriteMiniDump; // The function in dbghlp DLL that creates dump files + static inline wchar_t s_DumpFileName[MAX_PATH]; // Filename of the dump file; hes to be created before the dump handler kicks in + static inline char s_ExceptionStack[128 * 1024]; // Substitute stack, just in case the handler kicks in because of "insufficient stack space" + static inline MINIDUMP_TYPE s_DumpFlags = MiniDumpNormal; // By default dump only the stack and some helpers +}; + +#else + +struct MiniDumpWriter +{ + void AddDumpFlags(const MiniDumpFlags) + { + } +}; + +#endif diff --git a/src/OSSupport/StartAsService.h b/src/OSSupport/StartAsService.h new file mode 100644 index 000000000..472184367 --- /dev/null +++ b/src/OSSupport/StartAsService.h @@ -0,0 +1,173 @@ + +#pragma once + + + + + +#ifdef _WIN32 + +#include + + + + + +class cStartAsService +{ +public: + + /** Make a Windows service. */ + template + static bool MakeIntoService() + { + SERVICE_TABLE_ENTRY ServiceTable[] = + { + { g_ServiceName, (LPSERVICE_MAIN_FUNCTION)serviceMain }, + { nullptr, nullptr } + }; + + if (StartServiceCtrlDispatcher(ServiceTable) == FALSE) + { + throw std::system_error(GetLastError(), std::system_category()); + } + + return true; + } + +private: + + /** Set the internal status of the service */ + static void serviceSetState(DWORD acceptedControls, DWORD newState, DWORD exitCode) + { + SERVICE_STATUS serviceStatus = {}; + serviceStatus.dwCheckPoint = 0; + serviceStatus.dwControlsAccepted = acceptedControls; + serviceStatus.dwCurrentState = newState; + serviceStatus.dwServiceSpecificExitCode = 0; + serviceStatus.dwServiceType = SERVICE_WIN32; + serviceStatus.dwWaitHint = 0; + serviceStatus.dwWin32ExitCode = exitCode; + + if (SetServiceStatus(g_StatusHandle, &serviceStatus) == FALSE) + { + LOGERROR("SetServiceStatus() failed\n"); + } + } + + /** Handle stop events from the Service Control Manager */ + static void WINAPI serviceCtrlHandler(DWORD CtrlCode) + { + if (CtrlCode == SERVICE_CONTROL_STOP) + { + std::raise(SIGINT); + serviceSetState(0, SERVICE_STOP_PENDING, 0); + } + } + + /* Startup logic for running as a service */ + template + static void WINAPI serviceMain(DWORD argc, TCHAR *argv[]) + { + wchar_t applicationFilename[MAX_PATH]; + wchar_t applicationDirectory[MAX_PATH]; + + // Get this binary's file path: + if (GetModuleFileName(nullptr, applicationFilename, std::size(applicationFilename)) == 0) + { + serviceSetState(0, SERVICE_STOPPED, GetLastError()); + return; + } + + const auto LastComponent = wcsrchr(applicationFilename, L'\\'); + if (LastComponent == nullptr) + { + serviceSetState(0, SERVICE_STOPPED, E_UNEXPECTED); + return; + } + + const auto LengthToLastComponent = LastComponent - applicationFilename; + + // Strip off the filename, keep only the path: + wcsncpy(applicationDirectory, applicationFilename, LengthToLastComponent); + applicationDirectory[LengthToLastComponent] = L'\0'; // Make sure new path is null terminated + + // Services are run by the SCM, and inherit its working directory - usually System32. + // Set the working directory to the same location as the binary. + if (SetCurrentDirectory(applicationDirectory) == FALSE) + { + serviceSetState(0, SERVICE_STOPPED, GetLastError()); + return; + } + + + g_StatusHandle = RegisterServiceCtrlHandler(g_ServiceName, serviceCtrlHandler); + if (g_StatusHandle == nullptr) + { + OutputDebugStringA("RegisterServiceCtrlHandler() failed\n"); + serviceSetState(0, SERVICE_STOPPED, GetLastError()); + return; + } + + serviceSetState(SERVICE_ACCEPT_STOP, SERVICE_RUNNING, 0); + + char MultibyteArgV0[MAX_PATH]; + char * MultibyteArgV[] = { MultibyteArgV0 }; + + const auto OutputSize = std::size(MultibyteArgV0); + const auto TranslateResult = wcstombs(MultibyteArgV0, argv[0], OutputSize); + + if (TranslateResult == static_cast(-1)) + { + // Translation failed entirely (!): + MultibyteArgV0[0] = '\0'; + } + else if (TranslateResult == OutputSize) + { + // Output too small: + MultibyteArgV0[OutputSize - 1] = '\0'; + } + + const auto Result = MainFunction(1, MultibyteArgV, true); + const auto Return = (Result == EXIT_SUCCESS) ? S_OK : E_FAIL; + + serviceSetState(0, SERVICE_STOPPED, Return); + } + + static inline SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; + static inline HANDLE g_ServiceThread = INVALID_HANDLE_VALUE; + static inline wchar_t g_ServiceName[] = L"Cuberite"; +}; + +#else + +struct cStartAsService +{ + /** Make a UNIX daemon. */ + template + static bool MakeIntoService() + { + pid_t pid = fork(); + + // fork() returns a negative value on error. + if (pid < 0) + { + throw std::system_error(errno, std::system_category()); + } + + // Check if we are the parent or child process. Parent stops here. + if (pid > 0) + { + return true; + } + + // Child process now goes quiet, running in the background. + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + return false; + } +}; + +#endif diff --git a/src/OverridesSettingsRepository.cpp b/src/OverridesSettingsRepository.cpp index f4f56eac4..d07c39c73 100644 --- a/src/OverridesSettingsRepository.cpp +++ b/src/OverridesSettingsRepository.cpp @@ -2,9 +2,9 @@ #include "Globals.h" #include "OverridesSettingsRepository.h" -cOverridesSettingsRepository::cOverridesSettingsRepository(std::unique_ptr a_Main, std::unique_ptr a_Overrides) : +cOverridesSettingsRepository::cOverridesSettingsRepository(std::unique_ptr a_Main, cSettingsRepositoryInterface & a_Overrides) : m_Main(std::move(a_Main)), - m_Overrides(std::move(a_Overrides)) + m_Overrides(&a_Overrides) { } diff --git a/src/OverridesSettingsRepository.h b/src/OverridesSettingsRepository.h index 05260f711..fdb2d7d17 100644 --- a/src/OverridesSettingsRepository.h +++ b/src/OverridesSettingsRepository.h @@ -7,7 +7,7 @@ class cOverridesSettingsRepository : public cSettingsRepositoryInterface { public: - cOverridesSettingsRepository(std::unique_ptr a_Main, std::unique_ptr a_Overrides); + cOverridesSettingsRepository(std::unique_ptr a_Main, cSettingsRepositoryInterface & a_Overrides); virtual ~cOverridesSettingsRepository() override = default; @@ -44,7 +44,7 @@ public: private: std::unique_ptr m_Main; - std::unique_ptr m_Overrides; + cSettingsRepositoryInterface * m_Overrides; }; diff --git a/src/RCONServer.cpp b/src/RCONServer.cpp index dbce003b3..64ddfb4eb 100644 --- a/src/RCONServer.cpp +++ b/src/RCONServer.cpp @@ -295,7 +295,7 @@ bool cRCONServer::cConnection::ProcessPacket(UInt32 a_RequestID, UInt32 a_Packet AString cmd(a_Payload, a_PayloadLength); LOGD("RCON command from %s: \"%s\"", m_IPAddress.c_str(), cmd.c_str()); - cRoot::Get()->ExecuteConsoleCommand(cmd, *(new cRCONCommandOutput(*this, a_RequestID))); + cRoot::Get()->QueueExecuteConsoleCommand(cmd, *(new cRCONCommandOutput(*this, a_RequestID))); // Send an empty response: SendResponse(a_RequestID, RCON_PACKET_RESPONSE, 0, nullptr); diff --git a/src/Root.cpp b/src/Root.cpp index 4b40fd983..4291c5779 100644 --- a/src/Root.cpp +++ b/src/Root.cpp @@ -46,7 +46,10 @@ -cRoot * cRoot::s_Root = nullptr; +extern bool g_RunAsService; +decltype(cRoot::s_Root) cRoot::s_Root; +decltype(cRoot::s_NextState) cRoot::s_NextState; +decltype(cRoot::s_StopEvent) cRoot::s_StopEvent; @@ -65,7 +68,7 @@ cRoot::cRoot(void) : { Temporary::RegisterAllBlockHandlers(m_BlockTypeRegistry); s_Root = this; - m_InputThreadRunFlag.clear(); + TransitionNextState(NextState::Run); } @@ -81,85 +84,28 @@ cRoot::~cRoot() -void cRoot::InputThread(cRoot & a_Params) +bool cRoot::Run(cSettingsRepositoryInterface & a_OverridesRepo) { - cLogCommandOutputCallback Output; - AString Command; - - while (a_Params.m_InputThreadRunFlag.test_and_set() && std::cin.good()) - { - #ifndef _WIN32 - timeval Timeout{ 0, 0 }; - Timeout.tv_usec = 100 * 1000; // 100 msec - - fd_set ReadSet; - FD_ZERO(&ReadSet); - FD_SET(STDIN_FILENO, &ReadSet); - - if (select(STDIN_FILENO + 1, &ReadSet, nullptr, nullptr, &Timeout) <= 0) - { - // Don't call getline because there's nothing to read - continue; - } - #endif - - std::getline(std::cin, Command); - - if (!a_Params.m_InputThreadRunFlag.test_and_set()) - { - // Already shutting down, can't execute commands - break; - } - - if (!Command.empty()) - { - // Execute and clear command string when submitted - a_Params.ExecuteConsoleCommand(TrimString(Command), Output); - } - } - - // We have come here because the std::cin has received an EOF / a terminate signal has been sent, and the server is still running - // Ignore this when running as a service, cin was never opened in that case - if (!std::cin.good() && !m_RunAsService) - { - // Stop the server: - a_Params.QueueExecuteConsoleCommand("stop"); - } -} - - - - - -void cRoot::Start(std::unique_ptr a_OverridesRepo) -{ - #ifdef _WIN32 - HMENU ConsoleMenu = GetSystemMenu(GetConsoleWindow(), FALSE); - EnableMenuItem(ConsoleMenu, SC_CLOSE, MF_GRAYED); // Disable close button when starting up; it causes problems with our CTRL-CLOSE handling - #endif - - auto consoleLogListener = MakeConsoleListener(m_RunAsService); + auto consoleLogListener = MakeConsoleListener(g_RunAsService); auto consoleAttachment = cLogger::GetInstance().AttachListener(std::move(consoleLogListener)); cLogger::cAttachment fileAttachment; - if (!a_OverridesRepo->HasValue("Server","DisableLogFile")) + if (!a_OverridesRepo.HasValue("Server","DisableLogFile")) { auto fileLogListenerRet = MakeFileListener(); if (!fileLogListenerRet.first) { - m_TerminateEventRaised = true; - LOGERROR("Failed to open log file, aborting"); - return; + throw std::runtime_error("failed to open log file"); } fileAttachment = cLogger::GetInstance().AttachListener(std::move(fileLogListenerRet.second)); } LOG("--- Started Log ---"); - #ifdef BUILD_ID - LOG("Cuberite " BUILD_SERIES_NAME " build id: " BUILD_ID); - LOG("from commit id: " BUILD_COMMIT_ID " built at: " BUILD_DATETIME); - #endif +#ifdef BUILD_ID + LOG("Cuberite " BUILD_SERIES_NAME " (id: " BUILD_ID ")"); + LOG("from commit " BUILD_COMMIT_ID " built at: " BUILD_DATETIME); +#endif cDeadlockDetect dd; auto BeginTime = std::chrono::steady_clock::now(); @@ -172,9 +118,9 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) LOG("Reading server config..."); m_SettingsFilename = "settings.ini"; - if (a_OverridesRepo->HasValue("Server","ConfigFile")) + if (a_OverridesRepo.HasValue("Server","ConfigFile")) { - m_SettingsFilename = a_OverridesRepo->GetValue("Server","ConfigFile"); + m_SettingsFilename = a_OverridesRepo.GetValue("Server","ConfigFile"); } auto IniFile = std::make_unique(); @@ -187,7 +133,7 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) IniFile->AddHeaderComment(" Most of the settings here can be configured using the webadmin interface, if enabled in webadmin.ini"); } - auto settingsRepo = std::make_unique(std::move(IniFile), std::move(a_OverridesRepo)); + auto settingsRepo = std::make_unique(std::move(IniFile), a_OverridesRepo); LOG("Starting server..."); @@ -200,8 +146,7 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) if (!m_Server->InitServer(*settingsRepo, ShouldAuthenticate)) { settingsRepo->Flush(); - LOGERROR("Failure starting server, aborting..."); - return; + throw std::runtime_error("failure starting server"); } m_WebAdmin = new cWebAdmin(); @@ -245,32 +190,13 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) { m_WebAdmin->Start(); - LOGD("Starting InputThread..."); - try - { - m_InputThreadRunFlag.test_and_set(); - m_InputThread = std::thread(InputThread, std::ref(*this)); - } - catch (std::system_error & a_Exception) - { - LOGERROR("cRoot::Start (std::thread) error %i: could not construct input thread; %s", a_Exception.code().value(), a_Exception.what()); - } - LOG("Startup complete, took %ldms!", static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - BeginTime).count())); // Save the current time m_StartTime = std::chrono::steady_clock::now(); - #ifdef _WIN32 - EnableMenuItem(ConsoleMenu, SC_CLOSE, MF_ENABLED); // Re-enable close button - #endif - - for (;;) - { - m_StopEvent.Wait(); - - break; - } + HandleInput(); + s_StopEvent.Wait(); // Stop the server: m_WebAdmin->Stop(); @@ -278,13 +204,6 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) LOG("Shutting down server..."); m_Server->Shutdown(); } // if (m_Server->Start() - else - { - cRoot::m_TerminateEventRaised = true; - #ifdef _WIN32 - EnableMenuItem(ConsoleMenu, SC_CLOSE, MF_ENABLED); // Re-enable close button - #endif - } delete m_MojangAPI; m_MojangAPI = nullptr; @@ -313,75 +232,28 @@ void cRoot::Start(std::unique_ptr a_OverridesRepo) LOG("Cleaning up..."); delete m_Server; m_Server = nullptr; - m_InputThreadRunFlag.clear(); - #ifdef _WIN32 - DWORD Length; - INPUT_RECORD Record - { - KEY_EVENT, - { - { - TRUE, - 1, - VK_RETURN, - static_cast(MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC)), - { { VK_RETURN } }, - 0 - } - } - }; - - // Can't kill the input thread since it breaks cin (getline doesn't block / receive input on restart) - // Apparently no way to unblock getline - // Only thing I can think of for now - if (WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &Record, 1, &Length) == 0) - { - LOGWARN("Couldn't notify the input thread; the server will hang before shutdown!"); - m_TerminateEventRaised = true; - m_InputThread.detach(); - } - else - { - m_InputThread.join(); - } - #else - m_InputThread.join(); - #endif - - if (m_TerminateEventRaised) - { - LOG("Shutdown successful!"); - } - else - { - LOG("Shutdown successful - restarting..."); - } + LOG("Shutdown successful!"); LOG("--- Stopped Log ---"); + + return s_NextState == NextState::Restart; } -void cRoot::StopServer() +void cRoot::Stop() { - // Kick all players from the server with custom disconnect message + TransitionNextState(NextState::Stop); +} - bool SentDisconnect = false; - cRoot::Get()->ForEachPlayer([&](cPlayer & a_Player) - { - a_Player.GetClientHandlePtr()->Kick(m_Server->GetShutdownMessage()); - SentDisconnect = true; - return false; - } - ); - if (SentDisconnect) - { - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - m_TerminateEventRaised = true; - m_StopEvent.Set(); - m_InputThreadRunFlag.clear(); + + + + +void cRoot::Restart() +{ + TransitionNextState(NextState::Restart); } @@ -607,58 +479,39 @@ bool cRoot::ForEachWorld(cWorldListCallback a_Callback) -void cRoot::TickCommands(void) -{ - // Execute any pending commands: - cCommandQueue PendingCommands; - { - cCSLock Lock(m_CSPendingCommands); - std::swap(PendingCommands, m_PendingCommands); - } - for (cCommandQueue::iterator itr = PendingCommands.begin(), end = PendingCommands.end(); itr != end; ++itr) - { - ExecuteConsoleCommand(itr->m_Command, *(itr->m_Output)); - } -} - - - - - void cRoot::QueueExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) { - // Put the command into a queue (Alleviates FS #363): - cCSLock Lock(m_CSPendingCommands); - m_PendingCommands.emplace_back(a_Cmd, &a_Output); -} - - - - - -void cRoot::QueueExecuteConsoleCommand(const AString & a_Cmd) -{ - // Put the command into a queue (Alleviates FS #363): - cCSLock Lock(m_CSPendingCommands); - m_PendingCommands.push_back(cCommand(a_Cmd, new cLogCommandDeleteSelfOutputCallback)); -} - - + const auto KickPlayers = [this] + { + // Kick all players from the server with custom disconnect message + bool SentDisconnect = false; + cRoot::Get()->ForEachPlayer( + [&](cPlayer & a_Player) + { + a_Player.GetClientHandlePtr()->Kick(m_Server->GetShutdownMessage()); + SentDisconnect = true; + return false; + } + ); + if (SentDisconnect) + { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }; -void cRoot::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) -{ // Some commands are built-in: if (a_Cmd == "stop") { - StopServer(); + KickPlayers(); + cRoot::Stop(); return; } else if (a_Cmd == "restart") { - m_StopEvent.Set(); - m_InputThreadRunFlag.clear(); + KickPlayers(); + cRoot::Restart(); return; } @@ -670,6 +523,16 @@ void cRoot::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback +void cRoot::QueueExecuteConsoleCommand(const AString & a_Cmd) +{ + // Put the command into a queue (Alleviates FS #363): + QueueExecuteConsoleCommand(a_Cmd, *new cLogCommandDeleteSelfOutputCallback); +} + + + + + void cRoot::KickUser(int a_ClientID, const AString & a_Reason) { m_Server->KickUser(a_ClientID, a_Reason); @@ -1068,3 +931,110 @@ AStringVector cRoot::GetPlayerTabCompletionMultiWorld(const AString & a_Text) ); return Results; } + + + + + +void cRoot::HandleInput() +{ + if (g_RunAsService) + { + // Ignore input when running as a service, cin was never opened in that case: + return; + } + + cLogCommandOutputCallback Output; + AString Command; + + while (s_NextState == NextState::Run) + { +#ifndef _WIN32 + timeval Timeout{ 0, 0 }; + Timeout.tv_usec = 100 * 1000; // 100 msec + + fd_set ReadSet; + FD_ZERO(&ReadSet); + FD_SET(STDIN_FILENO, &ReadSet); + + if (select(STDIN_FILENO + 1, &ReadSet, nullptr, nullptr, &Timeout) <= 0) + { + // Don't call getline because there's nothing to read + continue; + } +#endif + + if (!std::getline(std::cin, Command)) + { + cRoot::Stop(); + return; + } + + if (s_NextState != NextState::Run) + { + // Already shutting down, can't execute commands + break; + } + + if (!Command.empty()) + { + // Execute and clear command string when submitted + QueueExecuteConsoleCommand(TrimString(Command), Output); + } + } +} + + + + + +void cRoot::TransitionNextState(NextState a_NextState) +{ + { + auto Current = s_NextState.load(); + do + { + // Stopping is final, so stops override restarts: + if (Current == NextState::Stop) + { + return; + } + } + while (!s_NextState.compare_exchange_strong(Current, a_NextState)); + } + + if (s_NextState == NextState::Run) + { + return; + } + + s_StopEvent.Set(); + +#ifdef WIN32 + if (g_RunAsService) + { + return; + } + + DWORD Length; + INPUT_RECORD Record + { + KEY_EVENT, + { + { + TRUE, + 1, + VK_RETURN, + static_cast(MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC)), + { { VK_RETURN } }, + 0 + } + } + }; + + // Can't kill the input thread since it breaks cin (getline doesn't block / receive input on restart) + // Apparently no way to unblock getline apart from CancelIoEx, but xoft wants Windows XP support + // Only thing I can think of for now. + VERIFY(WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &Record, 1, &Length) == TRUE); +#endif +} diff --git a/src/Root.h b/src/Root.h index 2610d28b8..1f5354c71 100644 --- a/src/Root.h +++ b/src/Root.h @@ -53,19 +53,20 @@ public: static cRoot * Get() { return s_Root; } // tolua_end - static bool m_TerminateEventRaised; - static bool m_RunAsService; - /** which ini file to load settings from, default is settings.ini */ AString m_SettingsFilename; cRoot(void); ~cRoot(); - void Start(std::unique_ptr a_OverridesRepo); + /** Run the server. Returns true if we should restart, false to quit. */ + bool Run(cSettingsRepositoryInterface & a_OverridesRepo); + + /** Interrupts the server and stops it, as if "/stop" typed in the console. */ + static void Stop(); - /** Stops the server, as if "/stop" was typed in the console. */ - void StopServer(); + /** Interrupts the server and restarts it, as if "/restart" was typed in the console. */ + static void Restart(); // tolua_begin cServer * GetServer(void) { return m_Server; } @@ -127,18 +128,12 @@ public: */ void QueueExecuteConsoleCommand(const AString & a_Cmd); // tolua_export - /** Executes a console command through the cServer class; does special handling for "stop" and "restart". */ - void ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output); - /** Kicks the user, no matter in what world they are. Used from cAuthenticator */ void KickUser(int a_ClientID, const AString & a_Reason); /** Called by cAuthenticator to auth the specified user */ void AuthenticateUser(int a_ClientID, const AString & a_Name, const cUUID & a_UUID, const Json::Value & a_Properties); - /** Executes commands queued in the command queue */ - void TickCommands(void); - /** Returns the number of chunks loaded */ size_t GetTotalChunkCount(void); // tolua_export @@ -195,31 +190,28 @@ public: // tolua_end private: - class cCommand - { - public: - cCommand(const AString & a_Command, cCommandOutputCallback * a_Output) : - m_Command(a_Command), - m_Output(a_Output) - { - } - AString m_Command; - cCommandOutputCallback * m_Output; - } ; + /** States that the global cRoot can be in. + You can transition freely between Run and Restart, but the server unconditionally terminates in Stop. */ + enum class NextState + { + Run, + Restart, + Stop + }; typedef std::map WorldMap; - typedef std::vector cCommandQueue; + + /** Blocking reads and processes console input. */ + void HandleInput(); + + /** Performs run state transition, enforcing guarantees about state transitions. */ + static void TransitionNextState(NextState a_NextState); cWorld * m_pDefaultWorld; WorldMap m_WorldsByName; - cCriticalSection m_CSPendingCommands; - cCommandQueue m_PendingCommands; - - std::thread m_InputThread; - cEvent m_StopEvent; - std::atomic_flag m_InputThreadRunFlag; + static cEvent s_StopEvent; cServer * m_Server; cMonsterConfig * m_MonsterConfig; @@ -254,5 +246,6 @@ private: static cRoot * s_Root; - static void InputThread(cRoot & a_Params); + /** Indicates the next action of cRoot, whether to run, stop or restart. */ + static std::atomic s_NextState; }; // tolua_export diff --git a/src/Server.cpp b/src/Server.cpp index d9a0ced27..a1dedf333 100644 --- a/src/Server.cpp +++ b/src/Server.cpp @@ -94,7 +94,7 @@ void cServer::cTickThread::Execute(void) { auto NowTime = std::chrono::steady_clock::now(); auto msec = std::chrono::duration_cast(NowTime - LastTime).count(); - m_ShouldTerminate = !m_Server.Tick(static_cast(msec)); + m_Server.Tick(static_cast(msec)); auto TickTime = std::chrono::steady_clock::now() - NowTime; if (TickTime < msPerTick) @@ -118,7 +118,6 @@ cServer::cServer(void) : m_PlayerCount(0), m_ClientViewDistance(0), m_bIsConnected(false), - m_bRestarting(false), m_RCONServer(*this), m_MaxPlayers(0), m_bIsHardcore(false), @@ -331,27 +330,16 @@ cTCPLink::cCallbacksPtr cServer::OnConnectionAccepted(const AString & a_RemoteIP -bool cServer::Tick(float a_Dt) +void cServer::Tick(float a_Dt) { // Send the tick to the plugins, as well as let the plugin manager reload, if asked to (issue #102): cPluginManager::Get()->Tick(a_Dt); - // Let the Root process all the queued commands: - cRoot::Get()->TickCommands(); + // Process all the queued commands: + TickCommands(); // Tick all clients not yet assigned to a world: TickClients(a_Dt); - - if (!m_bRestarting) - { - return true; - } - else - { - m_bRestarting = false; - m_RestartEvent.Set(); - return false; - } } @@ -445,6 +433,17 @@ bool cServer::Command(cClientHandle & a_Client, AString & a_Cmd) +void cServer::QueueExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) +{ + // Put the command into a queue (Alleviates FS #363): + cCSLock Lock(m_CSPendingCommands); + m_PendingCommands.emplace_back(a_Cmd, &a_Output); +} + + + + + void cServer::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) { AStringVector split = StringSplit(a_Cmd, " "); @@ -647,8 +646,7 @@ void cServer::Shutdown(void) m_ServerHandles.clear(); // Notify the tick thread and wait for it to terminate: - m_bRestarting = true; - m_RestartEvent.Wait(); + m_TickThread.Stop(); cRoot::Get()->SaveAllChunks(); @@ -706,3 +704,17 @@ void cServer::AuthenticateUser(int a_ClientID, const AString & a_Name, const cUU +void cServer::TickCommands(void) +{ + decltype(m_PendingCommands) PendingCommands; + { + cCSLock Lock(m_CSPendingCommands); + std::swap(PendingCommands, m_PendingCommands); + } + + // Execute any pending commands: + for (const auto & Command : PendingCommands) + { + ExecuteConsoleCommand(Command.first, *Command.second); + } +} diff --git a/src/Server.h b/src/Server.h index 208955237..5ac7fc998 100644 --- a/src/Server.h +++ b/src/Server.h @@ -100,8 +100,10 @@ public: bool Command(cClientHandle & a_Client, AString & a_Cmd); - /** Executes the console command, sends output through the specified callback */ - void ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output); + /** Queues a console command for execution through the cServer class. + The command will be executed in the server tick thread. + The command's output will be written to the a_Output callback. */ + void QueueExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output); /** Lists all available console commands and their helpstrings */ void PrintHelp(const AStringVector & a_Split, cCommandOutputCallback & a_Output); @@ -201,12 +203,13 @@ private: /** Number of players currently playing in the server. */ std::atomic_size_t m_PlayerCount; + cCriticalSection m_CSPendingCommands; + std::vector> m_PendingCommands; + int m_ClientViewDistance; // The default view distance for clients; settable in Settings.ini bool m_bIsConnected; // true - connected false - not connected - std::atomic m_bRestarting; - /** The private key used for the assymetric encryption start in the protocols */ cRsaPrivateKey m_PrivateKey; @@ -229,7 +232,6 @@ private: bool m_bAllowMultiLogin; cTickThread m_TickThread; - cEvent m_RestartEvent; /** The server ID used for client authentication */ AString m_ServerID; @@ -264,6 +266,9 @@ private: cServer(void); + /** Executes the console command, sends output through the specified callback. */ + void ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output); + /** Get the Forge mods registered for a given protocol, for modification */ AStringMap & RegisteredForgeMods(const UInt32 a_Protocol); @@ -274,10 +279,14 @@ private: Returns the cClientHandle reinterpreted as cTCPLink callbacks. */ cTCPLink::cCallbacksPtr OnConnectionAccepted(const AString & a_RemoteIPAddress); - bool Tick(float a_Dt); + void Tick(float a_Dt); /** Ticks the clients in m_Clients, manages the list in respect to removing clients */ void TickClients(float a_Dt); + + /** Executes commands queued in the command queue. */ + void TickCommands(void); + }; // tolua_export diff --git a/src/main.cpp b/src/main.cpp index 4d59fea0e..4a9f22f7d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,63 +1,38 @@ #include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules -#include "Root.h" -#include "tclap/CmdLine.h" - -#include -#include -#include - - - -#ifdef _MSC_VER - #include -#endif // _MSC_VER - -#include "OSSupport/NetworkSingleton.h" #include "BuildInfo.h" #include "Logger.h" - #include "MemorySettingsRepository.h" +#include "OSSupport/NetworkSingleton.h" +#include "OSSupport/MiniDumpWriter.h" +#include "OSSupport/StartAsService.h" +#include "Root.h" +#include "tclap/CmdLine.h" - - - -// Forward declarations to satisfy Clang's -Wmissing-variable-declarations: -extern bool g_ShouldLogCommIn; -extern bool g_ShouldLogCommOut; - +#include +#include -/** If something has told the server to stop; checked periodically in cRoot */ -bool cRoot::m_TerminateEventRaised = false; -/** If set to true, the protocols will log each player's incoming (C->S) communication to a per-connection logfile */ +/** If set to true, the protocols will log each player's incoming (C->S) communication to a per-connection logfile. */ bool g_ShouldLogCommIn; -/** If set to true, the protocols will log each player's outgoing (S->C) communication to a per-connection logfile */ +/** If set to true, the protocols will log each player's outgoing (S->C) communication to a per-connection logfile. */ bool g_ShouldLogCommOut; -/** If set to true, binary will attempt to run as a service on Windows */ -bool cRoot::m_RunAsService = false; +/** If set to true, binary will attempt to run as a service. */ +bool g_RunAsService; +/** Global that registers itself as a last chance exception handler to write a minidump on crash. */ +MiniDumpWriter g_MiniDumpWriter; -#if defined(_WIN32) - SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; - HANDLE g_ServiceThread = INVALID_HANDLE_VALUE; - #define SERVICE_NAME L"CuberiteService" -#endif - - - - -#ifndef _DEBUG // Because SIG_DFL or SIG_IGN could be NULL instead of nullptr, we need to disable the Clang warning here #ifdef __clang__ #pragma clang diagnostic push @@ -69,119 +44,64 @@ bool cRoot::m_RunAsService = false; static void NonCtrlHandler(int a_Signal) { LOGD("Terminate event raised from std::signal"); - cRoot::Get()->QueueExecuteConsoleCommand("stop"); switch (a_Signal) { case SIGSEGV: { - std::signal(SIGSEGV, SIG_DFL); - LOGERROR(" D: | Cuberite has encountered an error and needs to close"); - LOGERROR("Details | SIGSEGV: Segmentation fault"); - #ifdef BUILD_ID - LOGERROR("Cuberite " BUILD_SERIES_NAME " build id: " BUILD_ID); - LOGERROR("from commit id: " BUILD_COMMIT_ID " built at: " BUILD_DATETIME); - #endif PrintStackTrace(); - abort(); + + LOGERROR("Failure report: \n\n" + " :( | Cuberite has encountered an error and needs to close\n" + " | SIGSEGV: Segmentation fault\n" + " |\n" +#ifdef BUILD_ID + " | Cuberite " BUILD_SERIES_NAME " (id: " BUILD_ID ")\n" + " | from commit " BUILD_COMMIT_ID "\n"); +#endif + + std::signal(SIGSEGV, SIG_DFL); + return; } case SIGABRT: - #ifdef SIGABRT_COMPAT +#ifdef SIGABRT_COMPAT case SIGABRT_COMPAT: - #endif +#endif { - std::signal(a_Signal, SIG_DFL); - LOGERROR(" D: | Cuberite has encountered an error and needs to close"); - LOGERROR("Details | SIGABRT: Server self-terminated due to an internal fault"); - #ifdef BUILD_ID - LOGERROR("Cuberite " BUILD_SERIES_NAME " build id: " BUILD_ID); - LOGERROR("from commit id: " BUILD_COMMIT_ID " built at: " BUILD_DATETIME); - #endif PrintStackTrace(); - abort(); + + LOGERROR("Failure report: \n\n" + " :( | Cuberite has encountered an error and needs to close\n" + " | SIGABRT: Server self-terminated due to an internal fault\n" + " |\n" +#ifdef BUILD_ID + " | Cuberite " BUILD_SERIES_NAME " (id: " BUILD_ID ")\n" + " | from commit " BUILD_COMMIT_ID "\n"); +#endif + + std::signal(SIGSEGV, SIG_DFL); + return; } case SIGINT: case SIGTERM: { - std::signal(a_Signal, SIG_IGN); // Server is shutting down, wait for it... - break; + // Server is shutting down, wait for it... + cRoot::Stop(); + return; } - default: break; +#ifdef SIGPIPE + case SIGPIPE: + { + // Ignore (PR #2487) + return; + } +#endif } } #ifdef __clang__ #pragma clang diagnostic pop #endif // __clang__ -#endif // _DEBUG - - - - - -#if defined(_WIN32) && !defined(_WIN64) && defined(_MSC_VER) -//////////////////////////////////////////////////////////////////////////////// -// Windows 32-bit stuff: when the server crashes, create a "dump file" containing the callstack of each thread and some variables; let the user send us that crash file for analysis - -typedef BOOL (WINAPI *pMiniDumpWriteDump)( - HANDLE hProcess, - DWORD ProcessId, - HANDLE hFile, - MINIDUMP_TYPE DumpType, - PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, - PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, - PMINIDUMP_CALLBACK_INFORMATION CallbackParam -); - -static pMiniDumpWriteDump g_WriteMiniDump; // The function in dbghlp DLL that creates dump files - -static wchar_t g_DumpFileName[MAX_PATH]; // Filename of the dump file; hes to be created before the dump handler kicks in -static char g_ExceptionStack[128 * 1024]; // Substitute stack, just in case the handler kicks in because of "insufficient stack space" -static MINIDUMP_TYPE g_DumpFlags = MiniDumpNormal; // By default dump only the stack and some helpers - - - - - -/** This function gets called just before the "program executed an illegal instruction and will be terminated" or similar. -Its purpose is to create the crashdump using the dbghlp DLLs -*/ -static LONG WINAPI LastChanceExceptionFilter(__in struct _EXCEPTION_POINTERS * a_ExceptionInfo) -{ - char * newStack = &g_ExceptionStack[sizeof(g_ExceptionStack) - 1]; - char * oldStack; - - // Use the substitute stack: - // This code is the reason why we don't support 64-bit (yet) - _asm - { - mov oldStack, esp - mov esp, newStack - } - - MINIDUMP_EXCEPTION_INFORMATION ExcInformation; - ExcInformation.ThreadId = GetCurrentThreadId(); - ExcInformation.ExceptionPointers = a_ExceptionInfo; - ExcInformation.ClientPointers = 0; - - // Write the dump file: - HANDLE dumpFile = CreateFile(g_DumpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); - g_WriteMiniDump(GetCurrentProcess(), GetCurrentProcessId(), dumpFile, g_DumpFlags, (a_ExceptionInfo) ? &ExcInformation : nullptr, nullptr, nullptr); - CloseHandle(dumpFile); - - // Print the stack trace for the basic debugging: - PrintStackTrace(); - - // Revert to old stack: - _asm - { - mov esp, oldStack - } - - return 0; -} - -#endif // _WIN32 && !_WIN64 @@ -191,10 +111,12 @@ static LONG WINAPI LastChanceExceptionFilter(__in struct _EXCEPTION_POINTERS * a // Handle CTRL events in windows, including console window close static BOOL CtrlHandler(DWORD fdwCtrlType) { - cRoot::Get()->QueueExecuteConsoleCommand("stop"); + cRoot::Stop(); LOGD("Terminate event raised from the Windows CtrlHandler"); - std::this_thread::sleep_for(std::chrono::seconds(10)); // Delay as much as possible to try to get the server to shut down cleanly - 10 seconds given by Windows + // Delay as much as possible to try to get the server to shut down cleanly - 10 seconds given by Windows + std::this_thread::sleep_for(std::chrono::seconds(10)); + // Returning from main() automatically aborts this handler thread return TRUE; @@ -206,350 +128,186 @@ static BOOL CtrlHandler(DWORD fdwCtrlType) //////////////////////////////////////////////////////////////////////////////// -// UniversalMain - Main startup logic for both standard running and as a service +// ParseArguments - Read the startup arguments and store into a settings object -static void UniversalMain(std::unique_ptr a_OverridesRepo) +static cMemorySettingsRepository ParseArguments(int argc, char ** argv) { - // Initialize logging subsystem: - cLogger::InitiateMultithreading(); - - // Initialize LibEvent: - cNetworkSingleton::Get().Initialise(); - - try + // Parse the comand line args: + TCLAP::CmdLine cmd("Cuberite"); + TCLAP::ValueArg slotsArg ("s", "max-players", "Maximum number of slots for the server to use, overrides setting in setting.ini", false, -1, "number", cmd); + TCLAP::ValueArg confArg ("c", "config-file", "Config file to use", false, "settings.ini", "string", cmd); + TCLAP::MultiArg portsArg ("p", "port", "The port number the server should listen to", false, "port", cmd); + TCLAP::SwitchArg commLogArg ("", "log-comm", "Log server client communications to file", cmd); + TCLAP::SwitchArg commLogInArg ("", "log-comm-in", "Log inbound server client communications to file", cmd); + TCLAP::SwitchArg commLogOutArg ("", "log-comm-out", "Log outbound server client communications to file", cmd); + TCLAP::SwitchArg crashDumpFull ("", "crash-dump-full", "Crashdumps created by the server will contain full server memory", cmd); + TCLAP::SwitchArg crashDumpGlobals("", "crash-dump-globals", "Crashdumps created by the server will contain the global variables' values", cmd); + TCLAP::SwitchArg noBufArg ("", "no-output-buffering", "Disable output buffering", cmd); + TCLAP::SwitchArg noFileLogArg ("", "no-log-file", "Disable logging to file", cmd); + TCLAP::SwitchArg runAsServiceArg ("d", "service", "Run as a service on Windows, or daemon on UNIX like systems", cmd); + cmd.parse(argc, argv); + + // Copy the parsed args' values into a settings repository: + cMemorySettingsRepository repo; + if (confArg.isSet()) { - cRoot Root; - Root.Start(std::move(a_OverridesRepo)); + AString conf_file = confArg.getValue(); + repo.AddValue("Server", "ConfigFile", conf_file); } - catch (const fmt::format_error & exc) + if (slotsArg.isSet()) { - cRoot::m_TerminateEventRaised = true; - FLOGERROR("Formatting exception: {0}", exc.what()); + int slots = slotsArg.getValue(); + repo.AddValue("Server", "MaxPlayers", static_cast(slots)); } - catch (const std::exception & exc) + if (portsArg.isSet()) { - cRoot::m_TerminateEventRaised = true; - LOGERROR("Standard exception: %s", exc.what()); + for (auto port: portsArg.getValue()) + { + repo.AddValue("Server", "Ports", std::to_string(port)); + } } - catch (...) + if (noFileLogArg.getValue()) { - cRoot::m_TerminateEventRaised = true; - LOGERROR("Unknown exception!"); + repo.AddValue("Server", "DisableLogFile", true); } - - // Shutdown all of LibEvent: - cNetworkSingleton::Get().Terminate(); -} - - - - - -#if defined(_WIN32) // Windows service support. -//////////////////////////////////////////////////////////////////////////////// -// serviceWorkerThread: Keep the service alive - -static DWORD WINAPI serviceWorkerThread(LPVOID lpParam) -{ - UNREFERENCED_PARAMETER(lpParam); - - while (!cRoot::m_TerminateEventRaised) + if (commLogArg.getValue()) { - // Do the normal startup - UniversalMain(std::make_unique()); + g_ShouldLogCommIn = true; + g_ShouldLogCommOut = true; } - - return ERROR_SUCCESS; -} - - - - - -//////////////////////////////////////////////////////////////////////////////// -// serviceSetState: Set the internal status of the service - -static void serviceSetState(DWORD acceptedControls, DWORD newState, DWORD exitCode) -{ - SERVICE_STATUS serviceStatus = {}; - serviceStatus.dwCheckPoint = 0; - serviceStatus.dwControlsAccepted = acceptedControls; - serviceStatus.dwCurrentState = newState; - serviceStatus.dwServiceSpecificExitCode = 0; - serviceStatus.dwServiceType = SERVICE_WIN32; - serviceStatus.dwWaitHint = 0; - serviceStatus.dwWin32ExitCode = exitCode; - - if (SetServiceStatus(g_StatusHandle, &serviceStatus) == FALSE) + else { - LOGERROR("SetServiceStatus() failed\n"); + g_ShouldLogCommIn = commLogInArg.getValue(); + g_ShouldLogCommOut = commLogOutArg.getValue(); } -} - - - - -//////////////////////////////////////////////////////////////////////////////// -// serviceCtrlHandler: Handle stop events from the Service Control Manager - -static void WINAPI serviceCtrlHandler(DWORD CtrlCode) -{ - switch (CtrlCode) + if (noBufArg.getValue()) { - case SERVICE_CONTROL_STOP: - { - cRoot::Get()->QueueExecuteConsoleCommand("stop"); - serviceSetState(0, SERVICE_STOP_PENDING, 0); - break; - } - default: - { - break; - } + setvbuf(stdout, nullptr, _IONBF, 0); } -} + repo.SetReadOnly(); - - - -//////////////////////////////////////////////////////////////////////////////// -// serviceMain: Startup logic for running as a service - -static void WINAPI serviceMain(DWORD argc, TCHAR *argv[]) -{ - wchar_t applicationFilename[MAX_PATH]; - wchar_t applicationDirectory[MAX_PATH]; - - GetModuleFileName(nullptr, applicationFilename, sizeof(applicationFilename)); // This binary's file path. - - // Strip off the filename, keep only the path: - wcsncpy_s(applicationDirectory, sizeof(applicationDirectory), applicationFilename, (wcsrchr(applicationFilename, '\\') - applicationFilename)); - applicationDirectory[wcslen(applicationDirectory)] = '\0'; // Make sure new path is null terminated - - // Services are run by the SCM, and inherit its working directory - usually System32. - // Set the working directory to the same location as the binary. - SetCurrentDirectory(applicationDirectory); - - g_StatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME, serviceCtrlHandler); - if (g_StatusHandle == nullptr) + if (runAsServiceArg.getValue()) { - OutputDebugStringA("RegisterServiceCtrlHandler() failed\n"); - serviceSetState(0, SERVICE_STOPPED, GetLastError()); - return; + g_RunAsService = true; } - serviceSetState(SERVICE_ACCEPT_STOP, SERVICE_RUNNING, 0); - - DWORD ThreadID; - g_ServiceThread = CreateThread(nullptr, 0, serviceWorkerThread, nullptr, 0, &ThreadID); - if (g_ServiceThread == nullptr) + // Apply the CrashDump flags for platforms that support them: + if (crashDumpGlobals.getValue()) { - OutputDebugStringA("CreateThread() failed\n"); - serviceSetState(0, SERVICE_STOPPED, GetLastError()); - return; + g_MiniDumpWriter.AddDumpFlags(MiniDumpFlags::WithDataSegments); + } + if (crashDumpFull.getValue()) + { + g_MiniDumpWriter.AddDumpFlags(MiniDumpFlags::WithFullMemory); } - WaitForSingleObject(g_ServiceThread, INFINITE); // Wait here for a stop signal. - - CloseHandle(g_ServiceThread); - serviceSetState(0, SERVICE_STOPPED, 0); + return repo; } -#endif // Windows service support. -static std::unique_ptr ParseArguments(int argc, char ** argv) +//////////////////////////////////////////////////////////////////////////////// +// UniversalMain - Main startup logic for both standard running and as a service + +static int UniversalMain(int argc, char * argv[], bool RunningAsService) { - try + // Initialize logging subsystem: + cLogger::InitiateMultithreading(); + + struct NetworkRAII { - // Parse the comand line args: - TCLAP::CmdLine cmd("Cuberite"); - TCLAP::ValueArg slotsArg ("s", "max-players", "Maximum number of slots for the server to use, overrides setting in setting.ini", false, -1, "number", cmd); - TCLAP::ValueArg confArg ("c", "config-file", "Config file to use", false, "settings.ini", "string", cmd); - TCLAP::MultiArg portsArg ("p", "port", "The port number the server should listen to", false, "port", cmd); - TCLAP::SwitchArg commLogArg ("", "log-comm", "Log server client communications to file", cmd); - TCLAP::SwitchArg commLogInArg ("", "log-comm-in", "Log inbound server client communications to file", cmd); - TCLAP::SwitchArg commLogOutArg ("", "log-comm-out", "Log outbound server client communications to file", cmd); - TCLAP::SwitchArg crashDumpFull ("", "crash-dump-full", "Crashdumps created by the server will contain full server memory", cmd); - TCLAP::SwitchArg crashDumpGlobals("", "crash-dump-globals", "Crashdumps created by the server will contain the global variables' values", cmd); - TCLAP::SwitchArg noBufArg ("", "no-output-buffering", "Disable output buffering", cmd); - TCLAP::SwitchArg noFileLogArg ("", "no-log-file", "Disable logging to file", cmd); - TCLAP::SwitchArg runAsServiceArg ("d", "service", "Run as a service on Windows, or daemon on UNIX like systems", cmd); - cmd.parse(argc, argv); - - // Copy the parsed args' values into a settings repository: - auto repo = std::make_unique(); - if (confArg.isSet()) + NetworkRAII() { - AString conf_file = confArg.getValue(); - repo->AddValue("Server", "ConfigFile", conf_file); + // Initialize LibEvent: + cNetworkSingleton::Get().Initialise(); } - if (slotsArg.isSet()) + + ~NetworkRAII() { - int slots = slotsArg.getValue(); - repo->AddValue("Server", "MaxPlayers", static_cast(slots)); + // Shutdown all of LibEvent: + cNetworkSingleton::Get().Terminate(); } - if (portsArg.isSet()) + }; + + try + { + // Make sure g_RunAsService is set correctly before checking it's value + auto Settings = ParseArguments(argc, argv); + + // Attempt to run as a service + if (!RunningAsService && g_RunAsService) { - for (auto port: portsArg.getValue()) + // This will either fork or call UniversalMain again: + if (cStartAsService::MakeIntoService<&UniversalMain>()) { - repo->AddValue("Server", "Ports", std::to_string(port)); + return EXIT_SUCCESS; } } - if (noFileLogArg.getValue()) - { - repo->AddValue("Server", "DisableLogFile", true); - } - if (commLogArg.getValue()) - { - g_ShouldLogCommIn = true; - g_ShouldLogCommOut = true; - } - else - { - g_ShouldLogCommIn = commLogInArg.getValue(); - g_ShouldLogCommOut = commLogOutArg.getValue(); - } - if (noBufArg.getValue()) - { - setvbuf(stdout, nullptr, _IONBF, 0); - } - repo->SetReadOnly(); - // Set the service flag directly to cRoot: - if (runAsServiceArg.getValue()) + while (true) { - cRoot::m_RunAsService = true; - } + NetworkRAII LibEvent; + cRoot Root; - // Apply the CrashDump flags for platforms that support them: - #if defined(_WIN32) && !defined(_WIN64) && defined(_MSC_VER) // 32-bit Windows app compiled in MSVC - if (crashDumpGlobals.getValue()) + if (!Root.Run(Settings)) { - g_DumpFlags = static_cast(g_DumpFlags | MiniDumpWithDataSegs); + break; } - if (crashDumpFull.getValue()) - { - g_DumpFlags = static_cast(g_DumpFlags | MiniDumpWithFullMemory); - } - #endif // 32-bit Windows app compiled in MSVC + } - return repo; + return EXIT_SUCCESS; } - catch (const TCLAP::ArgException & exc) + catch (const fmt::format_error & Oops) { - fmt::print("Error reading command line {0} for arg {1}", exc.error(), exc.argId()); - return std::make_unique(); + std::cerr << "Formatting exception: " << Oops.what() << '\n'; + } + catch (const TCLAP::ArgException & Oops) + { + std::cerr << fmt::sprintf("Error reading command line {} for argument {}\n", Oops.error(), Oops.argId()); + } + catch (const std::exception & Oops) + { + std::cerr << "Standard exception: " << Oops.what() << '\n'; + } + catch (...) + { + std::cerr << "Unknown exception!\n"; } -} + return EXIT_FAILURE; +} -//////////////////////////////////////////////////////////////////////////////// -// main: int main(int argc, char ** argv) { - // Magic code to produce dump-files on Windows if the server crashes: - #if defined(_WIN32) && !defined(_WIN64) && defined(_MSC_VER) // 32-bit Windows app compiled in MSVC - HINSTANCE hDbgHelp = LoadLibrary(L"DBGHELP.DLL"); - g_WriteMiniDump = (pMiniDumpWriteDump)GetProcAddress(hDbgHelp, "MiniDumpWriteDump"); - if (g_WriteMiniDump != nullptr) - { - _snwprintf_s(g_DumpFileName, ARRAYCOUNT(g_DumpFileName), _TRUNCATE, L"crash_mcs_%x.dmp", GetCurrentProcessId()); - SetUnhandledExceptionFilter(LastChanceExceptionFilter); - } - #endif // 32-bit Windows app compiled in MSVC - // End of dump-file magic - - - #if defined(_DEBUG) && defined(_MSC_VER) - _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); - - // _X: The simple built-in CRT leak finder - simply break when allocating the Nth block ({N} is listed in the leak output) - // Only useful when the leak is in the same sequence all the time - // _CrtSetBreakAlloc(85950); - - #endif // _DEBUG && _MSC_VER - - #ifndef _DEBUG - std::signal(SIGSEGV, NonCtrlHandler); - std::signal(SIGTERM, NonCtrlHandler); - std::signal(SIGINT, NonCtrlHandler); - std::signal(SIGABRT, NonCtrlHandler); - #ifdef SIGABRT_COMPAT - std::signal(SIGABRT_COMPAT, NonCtrlHandler); - #endif // SIGABRT_COMPAT - #endif - - - #ifdef __unix__ - std::signal(SIGPIPE, SIG_IGN); - #endif - - #ifdef _WIN32 - if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE)) - { - LOGERROR("Could not install the Windows CTRL handler!"); - } - #endif - - // Make sure m_RunAsService is set correctly before checking it's value - ParseArguments(argc, argv); - - // Attempt to run as a service - if (cRoot::m_RunAsService) - { - #if defined(_WIN32) // Windows service. - SERVICE_TABLE_ENTRY ServiceTable[] = - { - { SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION)serviceMain }, - { nullptr, nullptr } - }; +#if !defined(NDEBUG) && defined(_MSC_VER) + _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); - if (StartServiceCtrlDispatcher(ServiceTable) == FALSE) - { - LOGERROR("Attempted, but failed, service startup."); - return GetLastError(); - } - #else // UNIX daemon. - pid_t pid = fork(); + // _X: The simple built-in CRT leak finder - simply break when allocating the Nth block ({N} is listed in the leak output) + // Only useful when the leak is in the same sequence all the time + // _CrtSetBreakAlloc(85950); - // fork() returns a negative value on error. - if (pid < 0) - { - LOGERROR("Could not fork process."); - return EXIT_FAILURE; - } +#endif // _DEBUG && _MSC_VER - // Check if we are the parent or child process. Parent stops here. - if (pid > 0) - { - return EXIT_SUCCESS; - } + std::signal(SIGSEGV, NonCtrlHandler); + std::signal(SIGTERM, NonCtrlHandler); + std::signal(SIGINT, NonCtrlHandler); + std::signal(SIGABRT, NonCtrlHandler); +#ifdef SIGABRT_COMPAT + std::signal(SIGABRT_COMPAT, NonCtrlHandler); +#endif +#ifdef SIGPIPE + std::signal(SIGPIPE, SIG_IGN); +#endif - // Child process now goes quiet, running in the background. - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); +#ifdef _WIN32 + VERIFY(SetConsoleCtrlHandler(reinterpret_cast(CtrlHandler), TRUE) == TRUE); +#endif - while (!cRoot::m_TerminateEventRaised) - { - UniversalMain(ParseArguments(argc, argv)); - } - #endif - } - else - { - while (!cRoot::m_TerminateEventRaised) - { - // Not running as a service, do normal startup - UniversalMain(ParseArguments(argc, argv)); - } - } - return EXIT_SUCCESS; + return UniversalMain(argc, argv, false); } -- cgit v1.2.3