mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2025-07-01 23:35:41 +00:00
451 lines
16 KiB
C++
451 lines
16 KiB
C++
// BeamMP, the BeamNG.drive multiplayer mod.
|
|
// Copyright (C) 2024 BeamMP Ltd., BeamMP team and contributors.
|
|
//
|
|
// BeamMP Ltd. can be contacted by electronic mail via contact@beammp.com.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
// by the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
#include "Common.h"
|
|
|
|
#include "Env.h"
|
|
#include "TConsole.h"
|
|
#include <array>
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <fmt/core.h>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#include "Compat.h"
|
|
#include "CustomAssert.h"
|
|
#include "Http.h"
|
|
|
|
void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) {
|
|
std::unique_lock Lock(mShutdownHandlersMutex);
|
|
if (Handler) {
|
|
mShutdownHandlers.push_front(Handler);
|
|
}
|
|
}
|
|
|
|
void Application::GracefullyShutdown() {
|
|
SetShutdown(true);
|
|
static bool AlreadyShuttingDown = false;
|
|
static uint8_t ShutdownAttempts = 0;
|
|
if (AlreadyShuttingDown) {
|
|
++ShutdownAttempts;
|
|
// hard shutdown at 2 additional tries
|
|
if (ShutdownAttempts == 2) {
|
|
beammp_info("hard shutdown forced by multiple shutdown requests");
|
|
std::exit(0);
|
|
}
|
|
beammp_info("already shutting down!");
|
|
return;
|
|
} else {
|
|
AlreadyShuttingDown = true;
|
|
}
|
|
beammp_trace("waiting for lock release");
|
|
std::unique_lock Lock(mShutdownHandlersMutex);
|
|
beammp_info("please wait while all subsystems are shutting down...");
|
|
for (size_t i = 0; i < mShutdownHandlers.size(); ++i) {
|
|
beammp_info("Subsystem " + std::to_string(i + 1) + "/" + std::to_string(mShutdownHandlers.size()) + " shutting down");
|
|
mShutdownHandlers[i]();
|
|
}
|
|
// std::exit(-1);
|
|
}
|
|
|
|
std::string Application::ServerVersionString() {
|
|
return mVersion.AsString();
|
|
}
|
|
|
|
std::array<uint8_t, 3> Application::VersionStrToInts(const std::string& str) {
|
|
std::array<uint8_t, 3> Version;
|
|
std::stringstream ss(str);
|
|
for (uint8_t& i : Version) {
|
|
std::string Part;
|
|
std::getline(ss, Part, '.');
|
|
std::from_chars(&*Part.begin(), &*Part.begin() + Part.size(), i);
|
|
}
|
|
return Version;
|
|
}
|
|
|
|
TEST_CASE("Application::VersionStrToInts") {
|
|
auto v = Application::VersionStrToInts("1.2.3");
|
|
CHECK(v[0] == 1);
|
|
CHECK(v[1] == 2);
|
|
CHECK(v[2] == 3);
|
|
|
|
v = Application::VersionStrToInts("10.20.30");
|
|
CHECK(v[0] == 10);
|
|
CHECK(v[1] == 20);
|
|
CHECK(v[2] == 30);
|
|
|
|
v = Application::VersionStrToInts("100.200.255");
|
|
CHECK(v[0] == 100);
|
|
CHECK(v[1] == 200);
|
|
CHECK(v[2] == 255);
|
|
}
|
|
|
|
bool Application::IsOutdated(const Version& Current, const Version& Newest) {
|
|
if (Newest.major > Current.major) {
|
|
return true;
|
|
} else if (Newest.major == Current.major && Newest.minor > Current.minor) {
|
|
return true;
|
|
} else if (Newest.major == Current.major && Newest.minor == Current.minor && Newest.patch > Current.patch) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool Application::IsShuttingDown() {
|
|
std::shared_lock Lock(mShutdownMtx);
|
|
return mShutdown;
|
|
}
|
|
|
|
void Application::SleepSafeSeconds(size_t Seconds) {
|
|
// Sleeps for 500 ms, checks if a shutdown occurred, and so forth
|
|
for (size_t i = 0; i < Seconds * 2; ++i) {
|
|
if (Application::IsShuttingDown()) {
|
|
return;
|
|
} else {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Application::IsOutdated (version check)") {
|
|
SUBCASE("Same version") {
|
|
CHECK(!Application::IsOutdated({ 1, 2, 3 }, { 1, 2, 3 }));
|
|
}
|
|
// we need to use over 1-2 digits to test against lexical comparisons
|
|
SUBCASE("Patch outdated") {
|
|
for (uint8_t Patch = 0; Patch < 10; ++Patch) {
|
|
for (uint8_t Minor = 0; Minor < 10; ++Minor) {
|
|
for (uint8_t Major = 0; Major < 10; ++Major) {
|
|
CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major), uint8_t(Minor), uint8_t(Patch + 1) }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SUBCASE("Minor outdated") {
|
|
for (uint8_t Patch = 0; Patch < 10; ++Patch) {
|
|
for (uint8_t Minor = 0; Minor < 10; ++Minor) {
|
|
for (uint8_t Major = 0; Major < 10; ++Major) {
|
|
CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major), uint8_t(Minor + 1), uint8_t(Patch) }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SUBCASE("Major outdated") {
|
|
for (uint8_t Patch = 0; Patch < 10; ++Patch) {
|
|
for (uint8_t Minor = 0; Minor < 10; ++Minor) {
|
|
for (uint8_t Major = 0; Major < 10; ++Major) {
|
|
CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major + 1), uint8_t(Minor), uint8_t(Patch) }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SUBCASE("All outdated") {
|
|
for (uint8_t Patch = 0; Patch < 10; ++Patch) {
|
|
for (uint8_t Minor = 0; Minor < 10; ++Minor) {
|
|
for (uint8_t Major = 0; Major < 10; ++Major) {
|
|
CHECK(Application::IsOutdated({ uint8_t(Major), uint8_t(Minor), uint8_t(Patch) }, { uint8_t(Major + 1), uint8_t(Minor + 1), uint8_t(Patch + 1) }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Application::SetSubsystemStatus(const std::string& Subsystem, Status status) {
|
|
switch (status) {
|
|
case Status::Good:
|
|
beammp_trace("Subsystem '" + Subsystem + "': Good");
|
|
break;
|
|
case Status::Bad:
|
|
beammp_trace("Subsystem '" + Subsystem + "': Bad");
|
|
break;
|
|
case Status::Starting:
|
|
beammp_trace("Subsystem '" + Subsystem + "': Starting");
|
|
break;
|
|
case Status::ShuttingDown:
|
|
beammp_trace("Subsystem '" + Subsystem + "': Shutting down");
|
|
break;
|
|
case Status::Shutdown:
|
|
beammp_trace("Subsystem '" + Subsystem + "': Shutdown");
|
|
break;
|
|
default:
|
|
beammp_assert_not_reachable();
|
|
}
|
|
std::unique_lock Lock(mSystemStatusMapMutex);
|
|
mSystemStatusMap[Subsystem] = status;
|
|
}
|
|
|
|
void Application::SetShutdown(bool Val) {
|
|
std::unique_lock Lock(mShutdownMtx);
|
|
mShutdown = Val;
|
|
}
|
|
|
|
TEST_CASE("Application::SetSubsystemStatus") {
|
|
Application::SetSubsystemStatus("Test", Application::Status::Good);
|
|
auto Map = Application::GetSubsystemStatuses();
|
|
CHECK(Map.at("Test") == Application::Status::Good);
|
|
Application::SetSubsystemStatus("Test", Application::Status::Bad);
|
|
Map = Application::GetSubsystemStatuses();
|
|
CHECK(Map.at("Test") == Application::Status::Bad);
|
|
}
|
|
|
|
void Application::CheckForUpdates() {
|
|
Application::SetSubsystemStatus("UpdateCheck", Application::Status::Starting);
|
|
static bool FirstTime = true;
|
|
// checks current version against latest version
|
|
std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" };
|
|
for (const auto& url : GetBackendUrlsInOrder()) {
|
|
auto Response = Http::GET(url + "/v/s");
|
|
bool Matches = std::regex_match(Response, VersionRegex);
|
|
if (Matches) {
|
|
auto MyVersion = ServerVersion();
|
|
auto RemoteVersion = Version(VersionStrToInts(Response));
|
|
if (IsOutdated(MyVersion, RemoteVersion)) {
|
|
std::string RealVersionString = std::string("v") + RemoteVersion.AsString();
|
|
const std::string DefaultUpdateMsg = "NEW VERSION IS OUT! Please update to the new version ({}) of the BeamMP-Server! Download it here: https://beammp.com/! For a guide on how to update, visit: https://docs.beammp.com/server/server-maintenance/#updating-the-server";
|
|
auto UpdateMsg = Env::Get(Env::Key::PROVIDER_UPDATE_MESSAGE).value_or(DefaultUpdateMsg);
|
|
UpdateMsg = fmt::vformat(std::string_view(UpdateMsg), fmt::make_format_args(RealVersionString));
|
|
beammp_warnf("{}{}{}", ANSI_YELLOW_BOLD, UpdateMsg, ANSI_RESET);
|
|
} else {
|
|
if (FirstTime) {
|
|
beammp_info("Server up-to-date!");
|
|
}
|
|
}
|
|
Application::SetSubsystemStatus("UpdateCheck", Application::Status::Good);
|
|
break;
|
|
} else {
|
|
if (FirstTime) {
|
|
beammp_debug("Failed to fetch version from: " + url);
|
|
beammp_trace("got " + Response);
|
|
Application::SetSubsystemStatus("UpdateCheck", Application::Status::Bad);
|
|
}
|
|
}
|
|
}
|
|
if (Application::GetSubsystemStatuses().at("UpdateCheck") == Application::Status::Bad) {
|
|
if (FirstTime) {
|
|
beammp_warn("Unable to fetch version info from backend.");
|
|
}
|
|
}
|
|
FirstTime = false;
|
|
}
|
|
|
|
// thread name stuff
|
|
|
|
static std::map<std::thread::id, std::string> threadNameMap {};
|
|
static std::mutex ThreadNameMapMutex {};
|
|
|
|
std::string ThreadName(bool DebugModeOverride) {
|
|
auto Lock = std::unique_lock(ThreadNameMapMutex);
|
|
if (DebugModeOverride || Application::Settings.getAsBool(Settings::Key::General_Debug)) {
|
|
auto id = std::this_thread::get_id();
|
|
if (threadNameMap.find(id) != threadNameMap.end()) {
|
|
// found
|
|
return threadNameMap.at(id) + " ";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
TEST_CASE("ThreadName") {
|
|
RegisterThread("MyThread");
|
|
auto OrigDebug = Application::Settings.getAsBool(Settings::Key::General_Debug);
|
|
|
|
// ThreadName adds a space at the end, legacy but we need it still
|
|
SUBCASE("Debug mode enabled") {
|
|
Application::Settings.set(Settings::Key::General_Debug, true);
|
|
CHECK(ThreadName(true) == "MyThread ");
|
|
CHECK(ThreadName(false) == "MyThread ");
|
|
}
|
|
SUBCASE("Debug mode disabled") {
|
|
Application::Settings.set(Settings::Key::General_Debug, false);
|
|
CHECK(ThreadName(true) == "MyThread ");
|
|
CHECK(ThreadName(false) == "");
|
|
}
|
|
// cleanup
|
|
Application::Settings.set(Settings::Key::General_Debug, OrigDebug);
|
|
}
|
|
|
|
void RegisterThread(const std::string& str) {
|
|
std::string ThreadId;
|
|
#ifdef BEAMMP_WINDOWS
|
|
ThreadId = std::to_string(GetCurrentThreadId());
|
|
#elif defined(BEAMMP_APPLE)
|
|
ThreadId = std::to_string(getpid()); // todo: research if 'getpid()' is a valid, posix compliant alternative to 'gettid()'
|
|
#elif defined(BEAMMP_LINUX)
|
|
ThreadId = std::to_string(gettid());
|
|
#elif defined(BEAMMP_FREEBSD)
|
|
ThreadId = std::to_string(getpid());
|
|
#endif
|
|
if (Application::Settings.getAsBool(Settings::Key::General_Debug)) {
|
|
std::ofstream ThreadFile(".Threads.log", std::ios::app);
|
|
ThreadFile << ("Thread \"" + str + "\" is TID " + ThreadId) << std::endl;
|
|
}
|
|
auto Lock = std::unique_lock(ThreadNameMapMutex);
|
|
threadNameMap[std::this_thread::get_id()] = str;
|
|
}
|
|
|
|
TEST_CASE("RegisterThread") {
|
|
RegisterThread("MyThread");
|
|
CHECK(threadNameMap.at(std::this_thread::get_id()) == "MyThread");
|
|
}
|
|
|
|
Version::Version(uint8_t major, uint8_t minor, uint8_t patch)
|
|
: major(major)
|
|
, minor(minor)
|
|
, patch(patch) { }
|
|
|
|
Version::Version(const std::array<uint8_t, 3>& v)
|
|
: Version(v[0], v[1], v[2]) {
|
|
}
|
|
|
|
std::string Version::AsString() {
|
|
return fmt::format("{:d}.{:d}.{:d}", major, minor, patch);
|
|
}
|
|
|
|
TEST_CASE("Version::AsString") {
|
|
CHECK(Version { 0, 0, 0 }.AsString() == "0.0.0");
|
|
CHECK(Version { 1, 2, 3 }.AsString() == "1.2.3");
|
|
CHECK(Version { 255, 255, 255 }.AsString() == "255.255.255");
|
|
}
|
|
|
|
void LogChatMessage(const std::string& name, int id, const std::string& msg) {
|
|
if (Application::Settings.getAsBool(Settings::Key::General_LogChat)) {
|
|
std::stringstream ss;
|
|
ss << ThreadName();
|
|
ss << "[CHAT] ";
|
|
if (id != -1) {
|
|
ss << "(" << id << ") <" << name << "> ";
|
|
} else {
|
|
ss << name << "";
|
|
}
|
|
ss << msg;
|
|
#ifdef DOCTEST_CONFIG_DISABLE
|
|
Application::Console().Write(ss.str());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
std::string GetPlatformAgnosticErrorString() {
|
|
#ifdef BEAMMP_WINDOWS
|
|
// This will provide us with the error code and an error message, all in one.
|
|
int err;
|
|
char msgbuf[256];
|
|
msgbuf[0] = '\0';
|
|
|
|
err = GetLastError();
|
|
|
|
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr,
|
|
err,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
|
msgbuf,
|
|
sizeof(msgbuf),
|
|
nullptr);
|
|
|
|
if (*msgbuf) {
|
|
return std::to_string(GetLastError()) + " - " + std::string(msgbuf);
|
|
} else {
|
|
return std::to_string(GetLastError());
|
|
}
|
|
#elif defined(BEAMMP_LINUX) || defined(BEAMMP_APPLE)
|
|
return std::strerror(errno);
|
|
#else
|
|
return "(no human-readable errors on this platform)";
|
|
#endif
|
|
}
|
|
|
|
// TODO: add unit tests to SplitString
|
|
void SplitString(const std::string& str, const char delim, std::vector<std::string>& out) {
|
|
size_t start;
|
|
size_t end = 0;
|
|
|
|
while ((start = str.find_first_not_of(delim, end)) != std::string::npos) {
|
|
end = str.find(delim, start);
|
|
out.push_back(str.substr(start, end - start));
|
|
}
|
|
}
|
|
static constexpr size_t STARTING_MAX_DECOMPRESSION_BUFFER_SIZE = 15 * 1024 * 1024;
|
|
static constexpr size_t MAX_DECOMPRESSION_BUFFER_SIZE = 30 * 1024 * 1024;
|
|
|
|
std::vector<uint8_t> DeComp(std::span<const uint8_t> input) {
|
|
beammp_debugf("got {} bytes of input data", input.size());
|
|
|
|
// start with a decompression buffer of 5x the input size, clamped to a maximum of 15 MB.
|
|
// this buffer can and will grow, but we don't want to start it too large. A 5x compression ratio
|
|
// is pretty optimistic.
|
|
std::vector<uint8_t> output_buffer(std::min<size_t>(input.size() * 5, STARTING_MAX_DECOMPRESSION_BUFFER_SIZE));
|
|
|
|
uLongf output_size = output_buffer.size();
|
|
|
|
while (true) {
|
|
int res = uncompress(
|
|
reinterpret_cast<Bytef*>(output_buffer.data()),
|
|
&output_size,
|
|
reinterpret_cast<const Bytef*>(input.data()),
|
|
static_cast<uLongf>(input.size()));
|
|
if (res == Z_BUF_ERROR) {
|
|
// We assume that a reasonable maximum size for decompressed packets exists. We want to avoid
|
|
// a client effectively "zip bombing" us by sending a lot of small packets which decompress
|
|
// into huge data.
|
|
// If this limit were to be an issue, this could be made configurable, however clients have a similar
|
|
// limit. For that reason, we just reject packets which decompress into too much data.
|
|
if (output_buffer.size() >= MAX_DECOMPRESSION_BUFFER_SIZE) {
|
|
throw std::runtime_error(fmt::format("decompressed packet size of {} bytes exceeded", MAX_DECOMPRESSION_BUFFER_SIZE));
|
|
}
|
|
// if decompression fails, we double the buffer size (up to the allowed limit) and try again
|
|
output_buffer.resize(std::max<size_t>(output_buffer.size() * 2, MAX_DECOMPRESSION_BUFFER_SIZE));
|
|
beammp_warnf("zlib uncompress() failed, trying with a larger buffer size of {}", output_buffer.size());
|
|
output_size = output_buffer.size();
|
|
} else if (res != Z_OK) {
|
|
beammp_error("zlib uncompress() failed: " + std::to_string(res));
|
|
if (res == Z_DATA_ERROR) {
|
|
throw InvalidDataError {};
|
|
} else {
|
|
throw std::runtime_error("zlib uncompress() failed");
|
|
}
|
|
} else if (res == Z_OK) {
|
|
break;
|
|
}
|
|
}
|
|
output_buffer.resize(output_size);
|
|
return output_buffer;
|
|
}
|
|
|
|
std::vector<uint8_t> Comp(std::span<const uint8_t> input) {
|
|
auto max_size = compressBound(input.size());
|
|
std::vector<uint8_t> output(max_size);
|
|
uLongf output_size = output.size();
|
|
int res = compress(
|
|
reinterpret_cast<Bytef*>(output.data()),
|
|
&output_size,
|
|
reinterpret_cast<const Bytef*>(input.data()),
|
|
static_cast<uLongf>(input.size()));
|
|
if (res != Z_OK) {
|
|
beammp_error("zlib compress() failed: " + std::to_string(res));
|
|
throw std::runtime_error("zlib compress() failed");
|
|
}
|
|
beammp_debug("zlib compressed " + std::to_string(input.size()) + " B to " + std::to_string(output_size) + " B");
|
|
output.resize(output_size);
|
|
return output;
|
|
}
|