Stability Improvements (#489)

1. Issue: Broken QueueThread joining logic which relies on thread being
`joinable` which is unrelated to doing `join`, leading to the case where
the network thread crashes when it reaches the end of scope, due to a
`std::terminate` in the dtor of `std::thread` (throws on destruction
when unjoined). Fix: Using `std::jthread` which uses RAII to join on
destruction.
2. Issue: Slow hardware or overloaded server can cause a client to
disconnect and socket to close before `TNetwork::DisconnectClient` is
called, but `IsDisconnected()` will still be false. In this case,
`.remote_endpoint()` can fail, which throws an exception, which is not
caught and thus `std::terminate`s the server. I observed this, even
though it was only in extremely contrived scenarios, it still happened
and crashed the server. Fix: Wrapping the whole block in a try/catch.
Alternate fix would have been to pass an `error_code` but the result is
the same. This fix is not quite enough though, a later fix resolves the
remaining issue that this causes the `mClientMap` to ignore the
disconnection. Also, all paths that do `if (c.IsDisconnected())` will
fail to decrement the `mClientMap`, which isn't always the right
behavior afaik. Fixed in another fix though.
3. Issue: Connection limiting ("DDoS protection") is broken as
exceptions cause it to not decrement (and slowly fill up the
`mClientMap`) in special cases. Mutexes are locked and unlocked manually
which can (and will) lead to cases where the mutex is locked, an
exception is thrown in the subsequent line (`address().to_string()` can
throw, same with `.remote_endpoint()`, both of which are being called in
the locked context without RAII unlocking). Fix: Replace manual map and
mutex handling with a new class, `TConnectionLimiter`, and an associated
"Guard" object `TConnectionLimiter::TGuard` which uses RAII to correctly
keep track of IP-and-connection-count associations, the way the previous
code was trying to do. This works across exceptions and other weird
issues. Each connection's main thread now owns a guard, which, on
destruction, decrements the counter. This way both the per-IP limits as
well as the global limits are enforced. Also added some stats about this
to the `status` command to ensure that server owners can observe this in
action.
4. Issue: `.address().to_string()` can throw and is called even if
`accept` failed, in `TNetwork::TCPServerMain`'s accept loop. I didn't
observe this and it didn't cause crashes, but I was touching that part
anyway. Fix: Explicitly handle the error case first, then get the IP,
etc.
5. Issue: `ReadWithTimeout` spawned a new async context for each read,
and then ran that context's event loop in a new thread. This means that,
not only did every one of those reads SPAWN A THREAD(!!!), it also
started an io context, which gets an fd, so this made DDoS arguably more
effective, not less.
6. Issue: Client disconnect can race due to being done on multiple
threads (TOCTOU bug). For example `Looper` and a normal disconnect call
can happen at the same time, because they check for `is_open` which can
be true, and then change to false right after the check, causing a
segfault in asio internals. Fix: Added an atomic compare-and-swap (CAS)
mechanic that acts like a lock for the socket disconnect/close, and
adjusted other places that checked `is_open`.
8. Issue: Lua panic calls the panic handler, and if the error supplied
in the panic is not a string, or is otherwise invalid, it will trigger
another panic within the panic handler. This continues and eventually
crashes the program in one of many fun ways. Fix: Use raw lua functions
to check if the top of the stack is a string, and only then print it,
otherwise print that there was a panic and leave it at that.
9. Issue: `error()` crashes the server, due to `sol::error`'s
constructor expecting a `std::string` (`lua_tostring` or `__tostring`
meta method), which doesn't exist if the error is, for example, `nil`. I
reported this to sol2, but it might be an issue only in this older
version we're using. Fix: Fixed as part of the next issue:
10. Issue: `TLuaResult` was used/accessed from multiple threads,
including `sol::object` accessed from multiple threads. This lead to
each access of a `TLuaResult::Result` accessing the Lua stack of that
state (from outside that state's thread, which is unsafe). This
consistently lead to issues and sometimes crashes. Fix: `TLuaResult` now
always marshals results into a detached result variant. This allocates,
but this is unlikely to impact the hot paths, as most results will be
empty or have primitive types. **UPDATE 2026-04-29:** This fix caused
some other issues, so I added a result type that has no result value,
only a status. That seems to help.
11. Issue: HTTP retains a curl handle per thread and never cleans them
up. With one new thread per client, each doing an auth request at least,
this quickly exhausts all file descriptors. This manifests itself as
**dns resolutions failing**, as the server fails to open a socket to
send a DNS query. Fix: The HTTP code now retains a pool of reusable
handles, which clean up automatically via RAII. I tried to build this in
a way that doesn't modify the code too much, so I kept it global and
static.
12. Issue: Crash when accessing an expired `std::weak_ptr<TClient>`.
This can happen when we check for `.expired()`, and then `.lock()`,
which is, of course, another TOCTOU (time of check vs time of use) bug.
What youre SUPPOSED to do instead, is locking, which always returns a
`std::shared_ptr<>`, and then check `std::shared_ptr<>::operator bool`.
An expired `std::weak_ptr` will return a default-constructed
`std::shared_ptr`, which evaluates to `false` when converted to `bool`.
Fix: Replaced all uses of `.expired()` and other such checks with the
correct pattern. This was a lot of search and manual replace :D.

I used LLMs to help with writing unit-tests, but those do not compile
into the final executable anyway. If this is undesired, I'm happy to
remove that code.

---

By creating this pull request, I understand that code that is AI
generated or otherwise automatically generated may be rejected without
further discussion.
I declare that I fully understand all code I pushed into this PR, and
wrote all this code myself and own the rights to this code.
This commit is contained in:
Tixx
2026-05-23 20:47:37 +02:00
committed by GitHub
25 changed files with 1866 additions and 555 deletions
+7 -1
View File
@@ -8,7 +8,7 @@ include(cmake/Vcpkg.cmake) # needs to happen before project()
project(
"BeamMP-Server" # replace this
VERSION 3.3.0
VERSION 3.9.2
)
include(cmake/StandardSettings.cmake)
@@ -39,6 +39,7 @@ set(PRJ_HEADERS
include/TConfig.h
include/TConsole.h
include/THeartbeatThread.h
include/TIoPollThread.h
include/TLuaEngine.h
include/TLuaPlugin.h
include/TNetwork.h
@@ -52,6 +53,8 @@ set(PRJ_HEADERS
include/Settings.h
include/Profiling.h
include/ChronoWrapper.h
include/TConnectionLimiter.h
include/TLuaResult.h
)
# add all source files (.cpp) to this, except the one with main()
set(PRJ_SOURCES
@@ -65,6 +68,7 @@ set(PRJ_SOURCES
src/TConfig.cpp
src/TConsole.cpp
src/THeartbeatThread.cpp
src/TIoPollThread.cpp
src/TLuaEngine.cpp
src/TLuaPlugin.cpp
src/TNetwork.cpp
@@ -78,6 +82,8 @@ set(PRJ_SOURCES
src/Settings.cpp
src/Profiling.cpp
src/ChronoWrapper.cpp
src/TConnectionLimiter.cpp
src/TLuaResult.cpp
)
find_package(Lua REQUIRED)
+2
View File
@@ -15,3 +15,5 @@ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake)
endif()
# ensure SOL2 safeties are all ON
add_compile_definitions(SOL_ALL_SAFETIES_ON=1)
+14 -2
View File
@@ -18,6 +18,7 @@
#pragma once
#include <atomic>
#include <chrono>
#include <memory>
#include <optional>
@@ -68,8 +69,11 @@ public:
std::string GetCarPositionRaw(int Ident);
void SetUDPAddr(const ip::udp::endpoint& Addr) { mUDPAddress = Addr; }
void SetTCPSock(ip::tcp::socket&& CSock) { mSocket = std::move(CSock); }
void Disconnect(std::string_view Reason);
bool IsDisconnected() const { return !mSocket.is_open(); }
// Returns true only for the thread that actually performs socket shutdown/close.
[[nodiscard]] bool Disconnect(std::string_view Reason);
bool IsDisconnected() const {
return mDisconnectState.load(std::memory_order_acquire) != EDisconnectState::Connected;
}
// locks
void DeleteCar(int Ident);
[[nodiscard]] const std::unordered_map<std::string, std::string>& GetIdentifiers() const { return mIdentifiers; }
@@ -106,6 +110,12 @@ public:
[[nodiscard]] const std::vector<uint8_t>& GetMagic() const { return mMagic; }
private:
enum class EDisconnectState {
Connected,
Disconnecting,
Disconnected
};
void InsertVehicle(int ID, const std::string& Data);
TServer& mServer;
@@ -121,6 +131,8 @@ private:
TSetOfVehicleData mVehicleData;
SparseArray<std::string> mVehiclePosition;
std::string mName = "Unknown Client";
// Once disconnect starts, this client is terminal and its socket must be treated as dead.
std::atomic<EDisconnectState> mDisconnectState { EDisconnectState::Connected };
ip::tcp::socket mSocket;
ip::udp::endpoint mUDPAddress {};
int mUnicycleID = -1;
+4
View File
@@ -132,6 +132,10 @@ private:
static inline Version mVersion { 3, 9, 2 };
};
/// Used to static_assert in std::visit
template<class>
inline constexpr bool AlwaysFalseV = false;
void SplitString(std::string const& str, const char delim, std::vector<std::string>& out);
std::string LowerString(std::string str);
+90
View File
@@ -0,0 +1,90 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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/>.
#pragma once
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
class TConnectionLimiter {
public:
struct TStats {
size_t CurrentGlobal = 0;
size_t MaxGlobal = 0;
size_t ActiveIpBuckets = 0;
size_t CurrentMaxPerIp = 0;
size_t MaxPerIp = 0;
size_t SaturatedIpBuckets = 0;
};
class TGuard {
public:
TGuard() = default;
TGuard(TConnectionLimiter* owner, std::string ip);
TGuard(const TGuard&) = delete;
TGuard& operator=(const TGuard&) = delete;
~TGuard() { Release(); }
// not threadsafe
TGuard(TGuard&& other) noexcept { *this = std::move(other); }
// not threadsafe
TGuard& operator=(TGuard&& other) noexcept;
private:
friend class TConnectionLimiter;
// not threadsafe
void Release();
TConnectionLimiter* mOwner { nullptr };
std::string mIp;
};
TConnectionLimiter(size_t maxPerIp, size_t maxGlobal);
[[nodiscard]] std::optional<TGuard> TryAcquire(const std::string& ip);
[[nodiscard]] TStats GetStats();
private:
void Release(const std::string& ip) {
std::unique_lock Lock { mMutex };
auto It = mPerIp.find(ip);
if (It != mPerIp.end()) {
// this guard exists to avoid underflow in case something goes wrong and this gets called too many times
if (It->second > 0)
--It->second;
if (It->second == 0)
mPerIp.erase(It);
}
// this guard exists to avoid underflow in case something goes wrong and this gets called too many times
if (mGlobal > 0)
--mGlobal;
}
const size_t mMaxPerIp;
const size_t mMaxGlobal;
std::mutex mMutex { };
std::unordered_map<std::string, size_t> mPerIp { };
size_t mGlobal = 0;
};
+36
View File
@@ -0,0 +1,36 @@
// 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/>.
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <thread>
class TIoPollThread {
public:
TIoPollThread();
~TIoPollThread();
boost::asio::io_context& IoCtx() noexcept { return mIoCtx; }
private:
boost::asio::io_context mIoCtx;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> mWorkGuard;
std::jthread mThread;
};
+10 -35
View File
@@ -19,13 +19,12 @@
#pragma once
#include "Profiling.h"
#include "TLuaResult.h"
#include "TNetwork.h"
#include "TServer.h"
#include <any>
#include <chrono>
#include <condition_variable>
#include <filesystem>
#include <initializer_list>
#include <list>
#include <lua.hpp>
#include <memory>
@@ -34,6 +33,8 @@
#include <queue>
#include <random>
#include <set>
#include <sol/forward.hpp>
#include <sol/protected_function_result.hpp>
#include <toml.hpp>
#include <unordered_map>
#include <vector>
@@ -58,36 +59,10 @@ namespace fs = std::filesystem;
/**
* std::variant means, that TLuaArgTypes may be one of the Types listed as template args
*/
using TLuaValue = std::variant<std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>;
enum TLuaType {
String = 0,
Int = 1,
Json = 2,
Bool = 3,
StringStringMap = 4,
Float = 5,
};
using TLuaValue = std::variant<std::monostate, std::string, int, JsonString, bool, std::unordered_map<std::string, std::string>, float>;
class TLuaPlugin;
struct TLuaResult {
bool Ready;
bool Error;
std::string ErrorMessage;
sol::object Result { sol::lua_nil };
TLuaStateId StateId;
std::string Function;
std::shared_ptr<std::mutex> ReadyMutex {
std::make_shared<std::mutex>()
};
std::shared_ptr<std::condition_variable> ReadyCondition {
std::make_shared<std::condition_variable>()
};
void MarkAsReady();
void WaitUntilReady();
};
struct TLuaPluginConfig {
static inline const std::string FileName = "PluginConfig.toml";
TLuaStateId StateId;
@@ -166,7 +141,7 @@ public:
const std::optional<std::chrono::high_resolution_clock::duration>& Max = std::nullopt);
void ReportErrors(const std::vector<std::shared_ptr<TLuaResult>>& Results);
bool HasState(TLuaStateId StateId);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaVoidResult> EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(TLuaStateId StateID, const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
void EnsureStateExists(TLuaStateId StateId, const std::string& Name, bool DontCallOnInit = false);
void RegisterEvent(const std::string& EventName, TLuaStateId StateId, const std::string& FunctionName);
@@ -227,9 +202,9 @@ public:
// Debugging functions (slow)
std::unordered_map<std::string /*event name */, std::vector<std::string> /* handlers */> Debug_GetEventsForState(TLuaStateId StateId);
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId);
std::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
std::vector<TLuaResult::DetachedSnapshot> Debug_GetResultsToCheckForState(TLuaStateId StateId);
private:
void CollectAndInitPlugins();
@@ -242,7 +217,7 @@ private:
StateThreadData(const std::string& Name, TLuaStateId StateId, TLuaEngine& Engine);
StateThreadData(const StateThreadData&) = delete;
virtual ~StateThreadData() noexcept { beammp_debug("\"" + mStateId + "\" destroyed"); }
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaVoidResult> EnqueueScript(const TLuaChunk& Script);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName);
[[nodiscard]] std::shared_ptr<TLuaResult> EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy);
void RegisterEvent(const std::string& EventName, const std::string& FunctionName);
@@ -254,7 +229,7 @@ private:
std::vector<std::string> GetStateTableKeys(const std::vector<std::string>& keys);
// Debug functions, slow
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Debug_GetStateExecuteQueue();
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Debug_GetStateExecuteQueue();
std::vector<TLuaEngine::QueuedFunction> Debug_GetStateFunctionQueue();
private:
@@ -279,7 +254,7 @@ private:
TLuaStateId mStateId;
lua_State* mState;
std::thread mThread;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> mStateExecuteQueue;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> mStateExecuteQueue;
std::recursive_mutex mStateExecuteQueueMutex;
std::vector<QueuedFunction> mStateFunctionQueue;
std::mutex mStateFunctionQueueMutex;
+146
View File
@@ -0,0 +1,146 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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/>.
#pragma once
#include "Common.h"
#include <condition_variable>
#include <string>
#include <memory>
#include <sol/sol.hpp>
using TLuaStateId = std::string;
struct TDetachedLuaValue {
using Ptr = std::shared_ptr<TDetachedLuaValue>;
using Array = std::vector<TDetachedLuaValue>;
// This weird Ptr indirection is needed because some implementations of libstc++,
// like the GCC 11 one shipped with ubuntu 22.04, need to know the size of the second
// member of the pairs that make up the elements of such a map. It's fine in vector.
using Object = std::unordered_map<std::string, TDetachedLuaValue::Ptr>;
std::variant<std::monostate, bool, double, int, std::string, Array, Object> V;
};
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value);
/// Result for operations that stay within one Lua state (e.g. script loads).
/// This type intentionally does not store any Lua references or serialized Lua values.
struct TLuaVoidResult {
struct Snapshot {
bool Error;
bool Ready;
std::string ErrorMessage;
TLuaStateId StateId;
};
explicit TLuaVoidResult(TLuaStateId StateId)
: mStateId(std::move(StateId)) {}
/// Marks this result as success & sets it as ready, waking all waiting threads.
void MarkReadySuccess();
/// Marks this result as erroneous & sets it as ready, waking all waiting threads.
void MarkReadyError(sol::protected_function_result Res);
/// Marks this result as erroneous & sets it as ready, waking all waiting threads.
void MarkReadyError(std::string Res);
/// Wait (suspend) until this result is ready.
void WaitUntilReady();
bool IsReady() const;
bool IsError() const;
Snapshot GetSnapshot() const;
private:
mutable std::mutex mMutex {};
bool mReady { false };
bool mError { false };
std::string mErrorMessage;
TLuaStateId mStateId;
std::condition_variable mReadyCondition {};
void SetErrorMessageFromResult(sol::protected_function_result& Res) {
if (Res.valid()) {
beammp_lua_errorf("Error was not an error");
}
if (Res.get_type() == sol::type::string) {
mErrorMessage = Res.get<sol::error>().what();
} else {
mErrorMessage = "(unknown error; error object is not inspectable)";
}
}
void MarkAsReady();
};
struct TLuaResult {
struct DetachedSnapshot {
bool Error;
bool Ready;
std::string ErrorMessage;
/// Serialized Lua result. Has no reference to the Lua state, and is safe to use
/// from any thread that acquires it.
TDetachedLuaValue Result;
TLuaStateId StateId;
std::string Function;
};
TLuaResult(TLuaStateId StateId, std::string FunctionName) : mStateId(StateId), mFunction(std::move(FunctionName)) {}
/// Marks this result as success & sets it as ready, waking all threads waiting on it.
void MarkReadySuccess(sol::object Res);
/// Marks this result as successful and ready without serializing a Lua value.
void MarkReadySuccessNoResult();
/// Marks this result as erroneous & sets it as ready, waking all threads waiting on it.
void MarkReadyError(sol::protected_function_result Res);
/// Marks this result as erroneous & sets it as ready, waking all threads waiting on it.
void MarkReadyError(std::string Res);
/// Wait (suspend) until this result is ready. Use `GetDetachedSnapshot` to get the result.
void WaitUntilReady();
bool IsReady() const;
bool IsError() const;
TLuaStateId OwnerState() const;
void SetOwnerState(TLuaStateId StateId);
/// Returns a snapshot with, if not an error, a serialized version of the result
/// object. This never stores or returns raw Lua references and is safe to use
/// across threads.
DetachedSnapshot GetDetachedSnapshot() const;
private:
mutable std::mutex mMutex {};
bool mReady { false };
bool mError { false };
std::string mErrorMessage;
TDetachedLuaValue mDetachedResult;
TLuaStateId mStateId;
std::string mFunction;
std::condition_variable mReadyCondition {};
TDetachedLuaValue Freeze(const sol::object& o, int depth = 0);
void SetErrorMessageFromResult(sol::protected_function_result& Res) {
if (Res.valid()) {
beammp_lua_errorf("Error was not an error");
}
if (Res.get_type() == sol::type::string) {
mErrorMessage = Res.get<sol::error>().what();
} else {
mErrorMessage = "(unknown error; error object is not inspectable)";
}
}
void MarkAsReady();
};
+6 -5
View File
@@ -20,9 +20,9 @@
#include "BoostAliases.h"
#include "Compat.h"
#include "TConnectionLimiter.h"
#include "TResourceManager.h"
#include "TServer.h"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/udp.hpp>
struct TConnection;
@@ -35,18 +35,20 @@ public:
[[nodiscard]] bool SendLarge(TClient& c, std::vector<uint8_t> Data, bool isSync = false);
[[nodiscard]] bool Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bool isSync = false);
std::shared_ptr<TClient> CreateClient(boost::asio::ip::tcp::socket&& TCPSock);
std::vector<uint8_t> TCPRcv(TClient& c);
std::vector<uint8_t> TCPRcv(TClient& c, bool WithTimeout = false);
void ClientKick(TClient& c, const std::string& R);
void DisconnectClient(const std::weak_ptr<TClient>& c, const std::string& R);
void DisconnectClient(TClient& c, const std::string& R);
[[nodiscard]] bool SyncClient(const std::weak_ptr<TClient>& c);
void Identify(TConnection&& client);
void Identify(TConnection&& client, TConnectionLimiter::TGuard&&);
std::shared_ptr<TClient> Authentication(TConnection&& ClientConnection);
void SyncResources(TClient& c);
[[nodiscard]] bool UDPSend(TClient& Client, std::vector<uint8_t> Data);
void SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self, bool Rel);
void UpdatePlayer(TClient& Client);
boost::system::error_code ReadWithTimeout(boost::asio::ip::tcp::socket& Socket, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout);
boost::system::error_code ReadWithTimeout(TConnection& Connection, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout);
[[nodiscard]] TConnectionLimiter::TStats GetConnectionLimiterStats() { return mConnectionLimiter.GetStats(); }
TResourceManager& ResourceManager() const { return mResourceManager; }
@@ -61,8 +63,7 @@ private:
std::thread mUDPThread;
std::thread mTCPThread;
std::mutex mOpenIDMutex;
std::map<std::string, uint16_t> mClientMap;
std::mutex mClientMapMutex;
TConnectionLimiter mConnectionLimiter;
std::vector<uint8_t> UDPRcvFromClient(boost::asio::ip::udp::endpoint& ClientEndpoint);
void OnConnect(const std::weak_ptr<TClient>& c);
+3 -3
View File
@@ -20,6 +20,7 @@
#include "IThreaded.h"
#include "RWMutex.h"
#include "TIoPollThread.h"
#include "TScopedTimer.h"
#include <functional>
#include <memory>
@@ -50,11 +51,10 @@ public:
const TScopedTimer UptimeTimer;
// asio io context
io_context& IoCtx() { return mIoCtx; }
io_context& IoCtx() { return mIoCtxPoller.IoCtx(); }
private:
io_context mIoCtx {};
TIoPollThread mIoCtxPoller;
TClientSet mClients;
mutable RWMutex mClientsMutex;
static void ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Network);
+11 -3
View File
@@ -76,7 +76,13 @@ std::string TClient::GetCarPositionRaw(int Ident) {
}
}
void TClient::Disconnect(std::string_view Reason) {
bool TClient::Disconnect(std::string_view Reason) {
// Do not remove this guard: concurrent close() on the same socket can crash in Asio internals.
EDisconnectState Expected = EDisconnectState::Connected;
if (!mDisconnectState.compare_exchange_strong(Expected, EDisconnectState::Disconnecting, std::memory_order_acq_rel, std::memory_order_acquire)) {
return false;
}
beammp_debugf("Disconnecting client {} for reason: {}", GetID(), Reason);
boost::system::error_code ec;
if (mSocket.is_open()) {
@@ -91,6 +97,9 @@ void TClient::Disconnect(std::string_view Reason) {
} else {
beammp_debug("Socket is already closed.");
}
// Terminal state: this client must not perform TCP IO after this point.
mDisconnectState.store(EDisconnectState::Disconnected, std::memory_order_release);
return true;
}
void TClient::SetCarPosition(int Ident, const std::string& Data) {
@@ -169,8 +178,7 @@ std::optional<std::weak_ptr<TClient>> GetClient(TServer& Server, int ID) {
std::optional<std::weak_ptr<TClient>> MaybeClient { std::nullopt };
Server.ForEachClient([&](std::weak_ptr<TClient> CPtr) -> bool {
ReadLock Lock(Server.GetClientMutex());
if (!CPtr.expired()) {
auto C = CPtr.lock();
if (auto C = CPtr.lock()) {
if (C->GetID() == ID) {
MaybeClient = CPtr;
return false;
+1
View File
@@ -31,6 +31,7 @@
#include <sstream>
#include <thread>
#include "TLuaResult.h"
#include "Compat.h"
#include "CustomAssert.h"
#include "Http.h"
+107 -11
View File
@@ -18,15 +18,12 @@
#include "Http.h"
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "LuaAPI.h"
#include <curl/easy.h>
#include <map>
#include <nlohmann/json.hpp>
#include <random>
#include <stdexcept>
using json = nlohmann::json;
@@ -37,17 +34,107 @@ static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void*
return size * nmemb;
}
struct CurlDeleter {
void operator()(CURL* c) const noexcept {
if (c)
curl_easy_cleanup(c);
}
};
static std::mutex gCurlPoolMutex;
static std::map<CURL*, bool> gCurlPool; // false = free, true = in use
constexpr size_t MAX_CURL_POOL_SIZE = 128;
static CURL* AcquireCurl() {
std::unique_lock Lock(gCurlPoolMutex);
for (auto& [Curl, InUse] : gCurlPool) {
if (!InUse) {
InUse = true;
curl_easy_reset(Curl);
return Curl;
}
}
// none found, if we're under the limit add one!
if (gCurlPool.size() >= MAX_CURL_POOL_SIZE) {
beammp_debugf("Ran out of curl handles for network requests, skipping this request");
return nullptr;
}
CURL* Curl = curl_easy_init();
if (!Curl) {
// failed, ignore
return nullptr;
}
gCurlPool[Curl] = true;
return Curl;
}
static void ReleaseCurl(CURL* curl) {
if (!curl) {
return;
}
std::unique_lock Lock(gCurlPoolMutex);
auto It = gCurlPool.find(curl);
if (It != gCurlPool.end()) {
It->second = false;
}
}
/// RAII container for curl handles to ensure correct release
class CurlLease {
private:
CURL* mHandle = nullptr;
public:
CurlLease() : mHandle(AcquireCurl()) {}
~CurlLease() {
ReleaseCurl(mHandle);
}
CurlLease(const CurlLease&) = delete;
CurlLease& operator=(const CurlLease&) = delete;
CurlLease(CurlLease&& other) noexcept
: mHandle(other.mHandle) {
other.mHandle = nullptr;
}
CurlLease& operator=(CurlLease&& other) noexcept {
if (this != &other) {
ReleaseCurl(mHandle);
mHandle = other.mHandle;
other.mHandle = nullptr;
}
return *this;
}
CURL* GetHandle() const noexcept {
return mHandle;
}
};
std::string Http::GET(const std::string& url, unsigned int* status) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
CurlLease Lease{};
CURL* curl = Lease.GetHandle();
if (curl) {
CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret));
// ensure we dont keep connections open for long
curl_easy_setopt(curl, CURLOPT_MAXCONNECTS, 8L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_MAXAGE_CONN, 60L);
curl_easy_setopt(curl, CURLOPT_MAXLIFETIME_CONN, 300L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0;
@@ -71,15 +158,16 @@ std::string Http::GET(const std::string& url, unsigned int* status) {
std::string Http::POST(const std::string& url, const std::string& body, const std::string& ContentType, unsigned int* status, const std::map<std::string, std::string>& headers) {
std::string Ret;
static thread_local CURL* curl = curl_easy_init();
CurlLease Lease{};
CURL* curl = Lease.GetHandle();
if (curl) {
CURLcode res;
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&Ret);
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&Ret));
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
struct curl_slist* list = nullptr;
@@ -90,7 +178,15 @@ std::string Http::POST(const std::string& url, const std::string& body, const st
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10); // seconds
// ensure we dont keep connections open for long
curl_easy_setopt(curl, CURLOPT_MAXCONNECTS, 8L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_MAXAGE_CONN, 60L);
curl_easy_setopt(curl, CURLOPT_MAXLIFETIME_CONN, 300L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = 0;
+108 -74
View File
@@ -143,22 +143,23 @@ static inline std::pair<bool, std::string> InternalTriggerClientEvent(int Player
return { true, "" };
} else {
auto MaybeClient = GetClient(LuaAPI::MP::Engine->Server(), PlayerID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
auto c = MaybeClient.value().lock();
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
if (!c->IsSyncing() && !c->IsSynced()) {
return { false, "Player hasn't joined yet" };
}
if (!c->IsSyncing() && !c->IsSynced()) {
return { false, "Player hasn't joined yet" };
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
return { false, "Respond failed, dropping client" };
}
return { true, "" };
}
}
if (!LuaAPI::MP::Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_lua_errorf("Respond failed, dropping client {}", PlayerID);
LuaAPI::MP::Engine->Network().ClientKick(*c, "Disconnected after failing to receive packets");
return { false, "Respond failed, dropping client" };
}
return { true, "" };
beammp_lua_errorf("TriggerClientEvent invalid Player ID '{}'", PlayerID);
return { false, "Invalid Player ID" };
}
}
@@ -169,13 +170,15 @@ std::pair<bool, std::string> LuaAPI::MP::TriggerClientEvent(int PlayerID, const
std::pair<bool, std::string> LuaAPI::MP::DropPlayer(int ID, std::optional<std::string> MaybeReason) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (!MaybeClient || MaybeClient.value().expired()) {
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
return { false, "Player does not exist" };
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
return { true, "" };
}
}
auto c = MaybeClient.value().lock();
LuaAPI::MP::Engine->Network().ClientKick(*c, MaybeReason.value_or("No reason"));
return { true, "" };
beammp_lua_errorf("Tried to drop client with id {}, who doesn't exist", ID);
return { false, "Player does not exist" };
}
std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::string& Message, const bool& LogChat) {
@@ -189,21 +192,26 @@ std::pair<bool, std::string> LuaAPI::MP::SendChatMessage(int ID, const std::stri
Result.first = true;
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player still syncing data";
return Result;
}
if (LogChat) {
LogChatMessage("<Server> (to \"" + c->GetName() + "\")", -1, Message);
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send chat message back to sender (id {}) - did the sender disconnect?", ID);
// TODO: should we return an error here?
}
Result.first = true;
} else {
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Player still syncing data";
return Result;
Result.second = "Invalid Player ID";
}
if (LogChat) {
LogChatMessage("<Server> (to \"" + c->GetName() + "\")", -1, Message);
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send chat message back to sender (id {}) - did the sender disconnect?", ID);
// TODO: should we return an error here?
}
Result.first = true;
} else {
beammp_lua_error("SendChatMessage invalid argument [1] invalid ID");
Result.first = false;
@@ -223,18 +231,23 @@ std::pair<bool, std::string> LuaAPI::MP::SendNotification(int ID, const std::str
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
if (auto c = MaybeClient.value().lock()) {
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
Result.second = "Invalid Player ID";
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send notification to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("SendNotification invalid argument [1] invalid ID");
Result.first = false;
@@ -265,18 +278,23 @@ std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::s
} else {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient) {
auto c = MaybeClient.value().lock();
if (!c->IsSynced()) {
if (auto c = MaybeClient.value().lock()) {
if (!c->IsSynced()) {
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send confirmation dialog to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
Result.first = false;
Result.second = "Player is not synced yet";
return Result;
Result.second = "Invalid Player ID";
}
if (!Engine->Network().Respond(*c, StringToVector(Packet), true)) {
beammp_errorf("Failed to send confirmation dialog to player (id {}) - did the player disconnect?", ID);
Result.first = false;
Result.second = "Failed to send packet";
}
Result.first = true;
} else {
beammp_lua_error("ConfirmationDialog invalid argument [1] invalid ID");
Result.first = false;
@@ -290,22 +308,27 @@ std::pair<bool, std::string> LuaAPI::MP::ConfirmationDialog(int ID, const std::s
std::pair<bool, std::string> LuaAPI::MP::RemoveVehicle(int PID, int VID) {
std::pair<bool, std::string> Result;
auto MaybeClient = GetClient(Engine->Server(), PID);
if (!MaybeClient || MaybeClient.value().expired()) {
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
c->DeleteCar(VID);
Result.first = true;
} else {
Result.first = false;
Result.second = "Vehicle does not exist";
}
} else {
beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false;
Result.second = "Invalid Player ID";
}
} else {
beammp_lua_error("RemoveVehicle invalid Player ID");
Result.first = false;
Result.second = "Invalid Player ID";
return Result;
}
auto c = MaybeClient.value().lock();
if (c->GetCarData(VID) != nlohmann::detail::value_t::null) {
std::string Destroy = "Od:" + std::to_string(PID) + "-" + std::to_string(VID);
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onVehicleDeleted", "", PID, VID));
Engine->Network().SendToAll(nullptr, StringToVector(Destroy), true, true);
c->DeleteCar(VID);
Result.first = true;
} else {
Result.first = false;
Result.second = "Vehicle does not exist";
}
return Result;
}
@@ -412,20 +435,22 @@ void LuaAPI::MP::Sleep(size_t Ms) {
bool LuaAPI::MP::IsPlayerConnected(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsUDPConnected();
} else {
return false;
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
return c->IsUDPConnected();
}
}
return false;
}
bool LuaAPI::MP::IsPlayerGuest(int ID) {
auto MaybeClient = GetClient(Engine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->IsGuest();
} else {
return false;
if (MaybeClient) {
if (auto c = MaybeClient.value().lock()) {
return c->IsGuest();
}
}
return false;
}
void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
@@ -440,7 +465,16 @@ void LuaAPI::MP::PrintRaw(sol::variadic_args Args) {
}
int LuaAPI::PanicHandler(lua_State* State) {
beammp_lua_error("PANIC: " + sol::stack::get<std::string>(State, 1));
// panic path: use raw lua c api only; sol2 conversions can raise lua_error
// and re-enter panic recursively.
const int ErrorType = lua_type(State, -1);
if (ErrorType == LUA_TSTRING) {
const char* Message = lua_tostring(State, -1);
beammp_lua_error(std::string("PANIC: ") + (Message ? Message : "(null)"));
} else {
const char* ErrorTypeName = lua_typename(State, ErrorType);
beammp_lua_error(std::string("PANIC: non-string error object (") + (ErrorTypeName ? ErrorTypeName : "unknown") + ")");
}
return 0;
}
+96
View File
@@ -0,0 +1,96 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 "TConnectionLimiter.h"
#include <mutex>
#include <optional>
#include "Common.h"
TConnectionLimiter::TConnectionLimiter(size_t maxPerIp, size_t maxGlobal)
: mMaxPerIp(maxPerIp)
, mMaxGlobal(maxGlobal) {
if (maxPerIp == 0) {
beammp_errorf("Max connections count per IP is set to zero; the server would reject ALL connections");
throw std::runtime_error("Invalid maximum connections per IP setting");
}
if (maxGlobal == 0) {
beammp_errorf("Max connection count is set to zero; the server would reject ALL connections");
throw std::runtime_error("Invalid maximum connections setting");
}
}
std::optional<TConnectionLimiter::TGuard> TConnectionLimiter::TryAcquire(const std::string& ip) {
std::unique_lock Lock { mMutex };
if (mGlobal >= mMaxGlobal) return std::nullopt;
// `It` is the inserted element (so if insertion worked, its 0), or its the element that
// was already there, in which case we must check the ip connection limit
auto [It, _] = mPerIp.try_emplace(ip, 0);
if (It->second >= mMaxPerIp) {
return std::nullopt;
}
// now increment the counter finally
++It->second;
++mGlobal;
// RAII guard will drop the count once destructed
return TGuard(this, ip);
}
TConnectionLimiter::TStats TConnectionLimiter::GetStats() {
std::unique_lock Lock { mMutex };
TStats Stats;
Stats.CurrentGlobal = mGlobal;
Stats.MaxGlobal = mMaxGlobal;
Stats.ActiveIpBuckets = mPerIp.size();
Stats.MaxPerIp = mMaxPerIp;
for (const auto& [_, Count] : mPerIp) {
if (Count > Stats.CurrentMaxPerIp) {
Stats.CurrentMaxPerIp = Count;
}
if (Count >= mMaxPerIp) {
++Stats.SaturatedIpBuckets;
}
}
return Stats;
}
TConnectionLimiter::TGuard::TGuard(TConnectionLimiter* owner, std::string ip)
: mOwner(owner)
, mIp(std::move(ip)) {
beammp_debugf("Acquired connection guard for {}", mIp);
}
TConnectionLimiter::TGuard& TConnectionLimiter::TGuard::operator=(TGuard&& other) noexcept {
// identity check
if (this != &other) {
Release();
mOwner = other.mOwner;
mIp = std::move(other.mIp);
other.mOwner = nullptr;
// setting this so its obvious when this happens, instead of being UB or empty string
other.mIp = "<moved-from>";
}
return *this;
}
void TConnectionLimiter::TGuard::Release() {
if (mOwner) {
mOwner->Release(mIp);
mOwner = nullptr;
beammp_debugf("Released connection guard for {}", mIp);
}
}
+39 -30
View File
@@ -22,9 +22,9 @@
#include "Client.h"
#include "CustomAssert.h"
#include "Http.h"
#include "LuaAPI.h"
#include "TLuaEngine.h"
#include "Http.h"
#include <ctime>
#include <lua.hpp>
@@ -296,8 +296,7 @@ void TConsole::Command_NetTest(const std::string& cmd, const std::vector<std::st
unsigned int status = 0;
std::string T = Http::GET(
Application::GetServerCheckUrl() + "/api/v2/beammp/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)), &status
);
Application::GetServerCheckUrl() + "/api/v2/beammp/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_Port)), &status);
beammp_debugf("Status and response from Server Check API: {0}, {1}", status, T);
@@ -334,10 +333,9 @@ void TConsole::Command_Kick(const std::string&, const std::vector<std::string>&
return StringStartsWith(Name1, Name2) || StringStartsWith(Name2, Name1);
};
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
if (NameCompare(locked->GetName(), Name)) {
mLuaEngine->Network().ClientKick(*locked, Reason);
if (auto Locked = Client.lock()) {
if (NameCompare(Locked->GetName(), Name)) {
mLuaEngine->Network().ClientKick(*Locked, Reason);
Kicked = true;
return false;
}
@@ -356,7 +354,7 @@ std::tuple<std::string, std::vector<std::string>> TConsole::ParseCommand(const s
// It correctly splits arguments, including respecting single and double quotes, as well as backticks
auto End_i = CommandWithArgs.find_first_of(' ');
std::string Command = CommandWithArgs.substr(0, End_i);
std::string ArgsStr {};
std::string ArgsStr { };
if (End_i != std::string::npos) {
ArgsStr = CommandWithArgs.substr(End_i);
}
@@ -566,11 +564,10 @@ void TConsole::Command_List(const std::string&, const std::vector<std::string>&
std::stringstream ss;
ss << std::left << std::setw(25) << "Name" << std::setw(6) << "ID" << std::setw(6) << "Cars" << std::endl;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
ss << std::left << std::setw(25) << locked->GetName()
<< std::setw(6) << locked->GetID()
<< std::setw(6) << locked->GetCarCount() << "\n";
if (auto Locked = Client.lock()) {
ss << std::left << std::setw(25) << Locked->GetName()
<< std::setw(6) << Locked->GetID()
<< std::setw(6) << Locked->GetCarCount() << "\n";
}
return true;
});
@@ -593,8 +590,7 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
size_t MissedPacketQueueSum = 0;
int LargestSecondsSinceLastPing = 0;
mLuaEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto Locked = Client.lock();
if (auto Locked = Client.lock()) {
CarCount += Locked->GetCarCount();
ConnectedCount += Locked->IsUDPConnected() ? 1 : 0;
GuestCount += Locked->IsGuest() ? 1 : 0;
@@ -613,11 +609,11 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
size_t SystemsBad = 0;
size_t SystemsShuttingDown = 0;
size_t SystemsShutdown = 0;
std::string SystemsBadList {};
std::string SystemsGoodList {};
std::string SystemsStartingList {};
std::string SystemsShuttingDownList {};
std::string SystemsShutdownList {};
std::string SystemsBadList { };
std::string SystemsGoodList { };
std::string SystemsStartingList { };
std::string SystemsShuttingDownList { };
std::string SystemsShutdownList { };
auto Statuses = Application::GetSubsystemStatuses();
for (const auto& NameStatusPair : Statuses) {
switch (NameStatusPair.second) {
@@ -653,6 +649,7 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
SystemsShutdownList = SystemsShutdownList.substr(0, SystemsShutdownList.size() - 2);
auto ElapsedTime = mLuaEngine->Server().UptimeTimer.GetElapsedTime();
auto ConnectionLimiterStats = mLuaEngine->Network().GetConnectionLimiterStats();
Status << "BeamMP-Server Status:\n"
<< "\tTotal Players: " << mLuaEngine->Server().ClientCount() << "\n"
@@ -667,6 +664,11 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
<< "\t\tStates: " << mLuaEngine->GetLuaStateCount() << "\n"
<< "\t\tEvent timers: " << mLuaEngine->GetTimedEventsCount() << "\n"
<< "\t\tEvent handlers: " << mLuaEngine->GetRegisteredEventHandlerCount() << "\n"
<< "\tConnection limiter:\n"
<< "\t\tActive/Max global: " << ConnectionLimiterStats.CurrentGlobal << "/" << ConnectionLimiterStats.MaxGlobal << "\n"
<< "\t\tActive IP buckets: " << ConnectionLimiterStats.ActiveIpBuckets << "\n"
<< "\t\tHighest single IP load: " << ConnectionLimiterStats.CurrentMaxPerIp << "/" << ConnectionLimiterStats.MaxPerIp << "\n"
<< "\t\tSaturated IP buckets: " << ConnectionLimiterStats.SaturatedIpBuckets << "\n"
<< "\tSubsystems:\n"
<< "\t\tGood/Starting/Bad: " << SystemsGood << "/" << SystemsStarting << "/" << SystemsBad << "\n"
<< "\t\tShutting down/Shut down: " << SystemsShuttingDown << "/" << SystemsShutdown << "\n"
@@ -683,9 +685,13 @@ void TConsole::Command_Status(const std::string&, const std::vector<std::string>
void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
auto FutureIsNonNil =
[](const std::shared_ptr<TLuaResult>& Future) {
if (!Future->Error && Future->Result.valid()) {
auto Type = Future->Result.get_type();
return Type != sol::type::lua_nil && Type != sol::type::none;
if (!Future->IsError()) {
auto Snapshot = Future->GetDetachedSnapshot();
if (Snapshot.Result.V.valueless_by_exception() || std::get_if<std::monostate>(&Snapshot.Result.V) != nullptr) {
// no value contained
return false;
}
return true;
}
return false;
};
@@ -695,7 +701,7 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
TLuaEngine::WaitForAll(Futures, std::chrono::seconds(5));
size_t Count = 0;
for (auto& Future : Futures) {
if (!Future->Error) {
if (!Future->IsError()) {
++Count;
}
}
@@ -713,14 +719,16 @@ void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) {
std::stringstream Reply;
if (NonNilFutures.size() > 1) {
for (size_t i = 0; i < NonNilFutures.size(); ++i) {
Reply << NonNilFutures[i]->StateId << ": \n"
<< LuaAPI::LuaToString(NonNilFutures[i]->Result);
auto Snapshot = NonNilFutures[i]->GetDetachedSnapshot();
Reply << Snapshot.StateId << ": \n"
<< Snapshot.Result;
if (i < NonNilFutures.size() - 1) {
Reply << "\n";
}
}
} else {
Reply << LuaAPI::LuaToString(NonNilFutures[0]->Result);
auto Snapshot = NonNilFutures[0]->GetDetachedSnapshot();
Reply << Snapshot.Result;
}
Application::Console().WriteRaw(Reply.str());
}
@@ -801,8 +809,9 @@ void TConsole::InitializeCommandline() {
} else {
auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(TrimmedCmd), "", "" });
Future->WaitUntilReady();
if (Future->Error) {
beammp_lua_error("error in " + mStateId + ": " + Future->ErrorMessage);
if (Future->IsError()) {
auto Snapshot = Future->GetSnapshot();
beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage);
}
}
} else {
@@ -834,7 +843,7 @@ void TConsole::InitializeCommandline() {
if (!mLuaEngine) {
beammp_info("Lua not started yet, please try again in a second");
} else {
std::string prefix {}; // stores non-table part of input
std::string prefix { }; // stores non-table part of input
for (size_t i = stub.length(); i > 0; i--) { // separate table from input
if (!std::isalnum(stub[i - 1]) && stub[i - 1] != '_' && stub[i - 1] != '.') {
prefix = stub.substr(0, i);
+2 -2
View File
@@ -181,8 +181,8 @@ std::string THeartbeatThread::GetPlayers() {
std::string Return;
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
Return += ClientPtr.lock()->GetName() + ";";
if (auto Client = ClientPtr.lock()) {
Return += Client->GetName() + ";";
}
return true;
});
+37
View File
@@ -0,0 +1,37 @@
// 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 "TIoPollThread.h"
TIoPollThread::TIoPollThread()
: mWorkGuard(boost::asio::make_work_guard(mIoCtx))
, mThread([this](std::stop_token StopToken) {
while (!StopToken.stop_requested()) {
try {
mIoCtx.run();
break;
} catch (...) {
mIoCtx.restart();
}
}
}) { }
TIoPollThread::~TIoPollThread() {
mWorkGuard.reset();
mIoCtx.stop();
}
+335 -194
View File
@@ -20,20 +20,26 @@
#include "Client.h"
#include "Common.h"
#include "CustomAssert.h"
#include "Env.h"
#include "Http.h"
#include "LuaAPI.h"
#include "Env.h"
#include "Profiling.h"
#include "TLuaPlugin.h"
#include "TLuaResult.h"
#include "sol/object.hpp"
#include <chrono>
#include <condition_variable>
#include <fmt/core.h>
#include <mutex>
#include <nlohmann/json.hpp>
#include <random>
#include <sol/types.hpp>
#include <thread>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <variant>
TLuaEngine* LuaAPI::MP::Engine;
@@ -57,12 +63,6 @@ TLuaEngine::TLuaEngine()
IThreaded::Start();
}
TEST_CASE("TLuaEngine ctor & dtor") {
Application::Settings.set(Settings::Key::General_ResourceFolder, "beammp_server_test_resources");
TLuaEngine engine;
Application::GracefullyShutdown();
}
void TLuaEngine::operator()() {
RegisterThread("LuaEngine");
// lua engine main thread
@@ -74,8 +74,9 @@ void TLuaEngine::operator()() {
auto Futures = TriggerEvent("onInit", "");
WaitForAll(Futures, std::chrono::seconds(5));
for (const auto& Future : Futures) {
if (Future->Error && Future->ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error("Calling \"onInit\" on \"" + Future->StateId + "\" failed: " + Future->ErrorMessage);
auto Snapshot = Future->GetDetachedSnapshot();
if (Snapshot.Error && Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error("Calling \"onInit\" on \"" + Snapshot.StateId + "\" failed: " + Snapshot.ErrorMessage);
}
}
@@ -85,10 +86,11 @@ void TLuaEngine::operator()() {
std::unique_lock Lock(mResultsToCheckMutex);
if (!mResultsToCheck.empty()) {
mResultsToCheck.remove_if([](const std::shared_ptr<TLuaResult>& Ptr) -> bool {
if (Ptr->Ready) {
if (Ptr->Error) {
if (Ptr->ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error(Ptr->Function + ": " + Ptr->ErrorMessage);
if (Ptr->IsReady()) {
auto Snapshot = Ptr->GetDetachedSnapshot();
if (Snapshot.Error) {
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage);
}
}
return true;
@@ -113,7 +115,7 @@ void TLuaEngine::operator()() {
std::unique_lock StateLock(mLuaStatesMutex);
std::unique_lock Lock2(mResultsToCheckMutex);
for (auto& Handler : Handlers) {
auto Res = mLuaStates[Timer.StateId]->EnqueueFunctionCallFromCustomEvent(Handler, {}, Timer.EventName, Timer.Strategy);
auto Res = mLuaStates[Timer.StateId]->EnqueueFunctionCallFromCustomEvent(Handler, { }, Timer.EventName, Timer.Strategy);
if (Res) {
mResultsToCheck.push_back(Res);
mResultsToCheckCond.notify_one();
@@ -128,7 +130,13 @@ void TLuaEngine::operator()() {
}
}
}
if (mLuaStates.empty()) {
bool StatesEmpty = false;
{
std::unique_lock Lock(mLuaStatesMutex);
StatesEmpty = mLuaStates.empty();
}
if (StatesEmpty) {
beammp_trace("No Lua states, event loop running extremely sparsely");
Application::SleepSafeSeconds(10);
} else {
@@ -201,8 +209,8 @@ std::unordered_map<std::string /* event name */, std::vector<std::string> /* han
return Result;
}
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) {
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> Result;
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::Debug_GetStateExecuteQueueForState(TLuaStateId StateId) {
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> Result;
std::unique_lock Lock(mLuaStatesMutex);
Result = mLuaStates.at(StateId)->Debug_GetStateExecuteQueue();
return Result;
@@ -215,16 +223,16 @@ std::vector<TLuaEngine::QueuedFunction> TLuaEngine::Debug_GetStateFunctionQueueF
return Result;
}
std::vector<TLuaResult> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) {
std::vector<TLuaResult::DetachedSnapshot> TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) {
std::unique_lock Lock(mResultsToCheckMutex);
auto ResultsToCheckCopy = mResultsToCheck;
Lock.unlock();
std::vector<TLuaResult> Result;
std::vector<TLuaResult::DetachedSnapshot> Result;
while (!ResultsToCheckCopy.empty()) {
auto ResultToCheck = std::move(ResultsToCheckCopy.front());
ResultsToCheckCopy.pop_front();
if (ResultToCheck->StateId == StateId) {
Result.push_back(*ResultToCheck);
if (ResultToCheck->OwnerState() == StateId) {
Result.push_back(ResultToCheck->GetDetachedSnapshot());
}
}
return Result;
@@ -255,7 +263,7 @@ std::vector<std::string> TLuaEngine::StateThreadData::GetStateTableKeys(const st
auto globals = mStateView.globals();
sol::table current = globals;
std::vector<std::string> Result {};
std::vector<std::string> Result { };
for (const auto& [key, value] : current) {
std::string s = key.as<std::string>();
@@ -311,27 +319,30 @@ void TLuaEngine::WaitForAll(std::vector<std::shared_ptr<TLuaResult>>& Results, c
size_t ms = 0;
std::set<std::string> WarnedResults;
while (!Result->Ready && !Cancelled) {
while (!Result->IsReady() && !Cancelled) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
ms += 10;
if (Max.has_value() && std::chrono::milliseconds(ms) > Max.value()) {
beammp_trace("'" + Result->Function + "' in '" + Result->StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
auto Snapshot = Result->GetDetachedSnapshot();
beammp_trace("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' did not finish executing in time (took: " + std::to_string(ms) + "ms).");
Cancelled = true;
} else if (ms > 1000 * 60) {
auto ResultId = Result->StateId + "_" + Result->Function;
auto Snapshot = Result->GetDetachedSnapshot();
auto ResultId = Snapshot.StateId + "_" + Snapshot.Function;
if (WarnedResults.count(ResultId) == 0) {
WarnedResults.insert(ResultId);
beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state.");
beammp_lua_warn("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' is taking very long. The event it's handling is too important to discard the result of this handler, but may block this event and possibly the whole lua state.");
}
}
}
auto Snapshot = Result->GetDetachedSnapshot();
if (Cancelled) {
beammp_lua_warn("'" + Result->Function + "' in '" + Result->StateId + "' failed to execute in time and was not waited for. It may still finish executing at a later time.");
beammp_lua_warn("'" + Snapshot.Function + "' in '" + Snapshot.StateId + "' failed to execute in time and was not waited for. It may still finish executing at a later time.");
LuaAPI::MP::Engine->ReportErrors({ Result });
} else if (Result->Error) {
if (Result->ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error(Result->Function + ": " + Result->ErrorMessage);
} else if (Snapshot.Error) {
if (Snapshot.ErrorMessage != BeamMPFnNotFoundError) {
beammp_lua_error(Snapshot.Function + ": " + Snapshot.ErrorMessage);
}
}
}
@@ -351,7 +362,7 @@ bool TLuaEngine::HasState(TLuaStateId StateId) {
return mLuaStates.find(StateId) != mLuaStates.end();
}
std::shared_ptr<TLuaResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) {
std::shared_ptr<TLuaVoidResult> TLuaEngine::EnqueueScript(TLuaStateId StateID, const TLuaChunk& Script) {
std::unique_lock Lock(mLuaStatesMutex);
return mLuaStates.at(StateID)->EnqueueScript(Script);
}
@@ -431,10 +442,11 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name,
mLuaStates[StateId] = std::move(DataPtr);
RegisterEvent("onInit", StateId, "onInit");
if (!DontCallOnInit) {
auto Res = EnqueueFunctionCall(StateId, "onInit", {}, "onInit");
auto Res = EnqueueFunctionCall(StateId, "onInit", { }, "onInit");
Res->WaitUntilReady();
if (Res->Error && Res->ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Res->ErrorMessage);
auto Snapshot = Res->GetDetachedSnapshot();
if (Snapshot.Error && Snapshot.ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Snapshot.ErrorMessage);
}
}
}
@@ -446,6 +458,7 @@ void TLuaEngine::RegisterEvent(const std::string& EventName, TLuaStateId StateId
}
std::set<std::string> TLuaEngine::GetEventHandlersForState(const std::string& EventName, TLuaStateId StateId) {
std::unique_lock Lock(mLuaEventsMutex);
return mLuaEvents[EventName][StateId];
}
@@ -453,7 +466,7 @@ std::vector<sol::object> TLuaEngine::StateThreadData::JsonStringToArray(JsonStri
auto LocalTable = Lua_JsonDecode(Str.value).as<std::vector<sol::object>>();
for (auto& value : LocalTable) {
if (value.is<std::string>() && value.as<std::string>() == BEAMMP_INTERNAL_NIL) {
value = sol::object {};
value = sol::object { };
}
}
return LocalTable;
@@ -495,15 +508,16 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
auto Fn = mStateView[Handler];
if (Fn.valid()) {
auto LuaResult = Fn(LocalArgs);
auto Result = std::make_shared<TLuaResult>();
auto Result = std::make_shared<TLuaResult>(mStateId, Handler);
if (LuaResult.valid()) {
Result->Error = false;
Result->Result = LuaResult;
try {
Result->MarkReadySuccess(LuaResult);
} catch (const std::exception& e) {
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
}
} else {
Result->Error = true;
Result->ErrorMessage = "Function result in TriggerGlobalEvent was invalid";
Result->MarkReadyError("Function result in TriggerGlobalEvent was invalid");
}
Result->MarkAsReady();
Return.push_back(Result);
}
}
@@ -511,26 +525,60 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
sol::table AsyncEventReturn = StateView.create_table();
AsyncEventReturn["ReturnValueImpl"] = Return;
AsyncEventReturn.set_function("IsDone",
[&](const sol::table& Self) -> bool {
[](const sol::table& Self) -> bool {
auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl");
for (const auto& Value : Vector) {
if (!Value->Ready) {
if (!Value->IsReady()) {
return false;
}
}
return true;
});
AsyncEventReturn.set_function("GetResults",
[&](const sol::table& Self) -> sol::table {
sol::state_view StateView(mState);
[](const sol::table& Self, sol::this_state State) -> sol::table {
sol::state_view StateView(State);
sol::table Result = StateView.create_table();
auto Vector = Self.get<std::vector<std::shared_ptr<TLuaResult>>>("ReturnValueImpl");
auto DetachedToLuaObject = [&StateView](const auto& SelfConvert, const TDetachedLuaValue& value) -> sol::object {
return std::visit([&StateView, &SelfConvert](auto&& arg) -> sol::object {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, TDetachedLuaValue::Array>) {
sol::table Table = StateView.create_table(static_cast<int>(arg.size()), 0);
size_t i = 1;
for (const auto& Elem : arg) {
Table.set(i, SelfConvert(SelfConvert, Elem));
++i;
}
return sol::make_object(StateView, Table);
} else if constexpr (std::is_same_v<T, TDetachedLuaValue::Object>) {
sol::table Table = StateView.create_table();
for (const auto& [Key, Elem] : arg) {
Table.set(Key, SelfConvert(SelfConvert, *Elem));
}
return sol::make_object(StateView, Table);
}
else if constexpr (std::is_same_v<T, bool>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, double>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, int>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, std::string>)
return sol::make_object(StateView, arg);
else if constexpr (std::is_same_v<T, std::monostate>)
return sol::make_object(StateView, sol::lua_nil_t());
else
static_assert(AlwaysFalseV<T>, "non-exhaustive visitor!");
}, value.V);
};
int i = 1;
for (const auto& Value : Vector) {
if (!Value->Ready) {
if (!Value->IsReady()) {
return sol::lua_nil;
}
Result.set(i, Value->Result);
auto Snapshot = Value->GetDetachedSnapshot();
Result.set(i, DetachedToLuaObject(DetachedToLuaObject, Snapshot.Result));
++i;
}
return Result;
@@ -550,8 +598,13 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
Result.set(i, FnRet);
++i;
} else {
sol::error Err = FnRet;
beammp_lua_error(std::string("TriggerLocalEvent: ") + Err.what());
std::string ErrStr;
if (FnRet.get_type() == sol::type::string) {
ErrStr = FnRet.get<sol::error>().what();
} else {
ErrStr = "(unknown error; error object is not inspectable)";
}
beammp_lua_errorf("TriggerLocalEvent: {}", ErrStr);
}
}
}
@@ -560,37 +613,37 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerLocalEvent(const std::string&
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerIdentifiers(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
auto IDs = MaybeClient.value().lock()->GetIdentifiers();
if (IDs.empty()) {
return sol::lua_nil;
if (MaybeClient) {
if (std::shared_ptr<TClient> Locked = MaybeClient.value().lock()) {
auto IDs = Locked->GetIdentifiers();
if (IDs.empty()) {
return sol::lua_nil;
}
sol::table Result = mStateView.create_table();
for (const auto& Pair : IDs) {
Result.set(Pair.first, Pair.second);
}
return Result;
}
sol::table Result = mStateView.create_table();
for (const auto& Pair : IDs) {
Result[Pair.first] = Pair.second;
}
return Result;
} else {
return sol::lua_nil;
}
return sol::lua_nil;
}
std::variant<std::string, sol::nil_t> TLuaEngine::StateThreadData::Lua_GetPlayerRole(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient) {
return MaybeClient.value().lock()->GetRoles();
} else {
return sol::nil;
if (auto Locked = MaybeClient.value().lock()) {
return Locked->GetRoles();
}
}
return sol::nil;
}
sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
sol::table Result = mStateView.create_table();
mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
Result[locked->GetID()] = locked->GetName();
if (auto Locked = Client.lock()) {
Result[Locked->GetID()] = Locked->GetName();
}
return true;
});
@@ -600,10 +653,9 @@ sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
int TLuaEngine::StateThreadData::Lua_GetPlayerIDByName(const std::string& Name) {
int Id = -1;
mEngine->mServer->ForEachClient([&Id, &Name](std::weak_ptr<TClient> Client) -> bool {
if (!Client.expired()) {
auto locked = Client.lock();
if (locked->GetName() == Name) {
Id = locked->GetID();
if (auto Locked = Client.lock()) {
if (Locked->GetName() == Name) {
Id = Locked->GetID();
return false;
}
}
@@ -640,60 +692,59 @@ sol::table TLuaEngine::StateThreadData::Lua_FS_ListDirectories(const std::string
std::string TLuaEngine::StateThreadData::Lua_GetPlayerName(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
return MaybeClient.value().lock()->GetName();
} else {
return "";
if (MaybeClient) {
if (auto Locked = MaybeClient.value().lock()) {
return Locked->GetName();
}
}
return "";
}
sol::table TLuaEngine::StateThreadData::Lua_GetPlayerVehicles(int ID) {
auto MaybeClient = GetClient(mEngine->Server(), ID);
if (MaybeClient && !MaybeClient.value().expired()) {
auto Client = MaybeClient.value().lock();
TClient::TSetOfVehicleData VehicleData;
{ // Vehicle Data Lock Scope
auto LockedData = Client->GetAllCars();
VehicleData = *LockedData.VehicleData;
} // End Vehicle Data Lock Scope
if (VehicleData.empty()) {
return sol::lua_nil;
if (MaybeClient) {
if (auto Client = MaybeClient.value().lock()) {
TClient::TSetOfVehicleData VehicleData;
{ // Vehicle Data Lock Scope
auto LockedData = Client->GetAllCars();
VehicleData = *LockedData.VehicleData;
} // End Vehicle Data Lock Scope
if (VehicleData.empty()) {
return sol::lua_nil;
}
sol::state_view StateView(mState);
sol::table Result = StateView.create_table();
for (const auto& v : VehicleData) {
Result[v.ID()] = v.DataAsPacket(Client->GetRoles(), Client->GetName(), Client->GetID()).substr(3);
}
return Result;
}
sol::state_view StateView(mState);
sol::table Result = StateView.create_table();
for (const auto& v : VehicleData) {
Result[v.ID()] = v.DataAsPacket(Client->GetRoles(), Client->GetName(), Client->GetID()).substr(3);
}
return Result;
} else
return sol::lua_nil;
}
return sol::lua_nil;
}
std::pair<sol::table, std::string> TLuaEngine::StateThreadData::Lua_GetPositionRaw(int PID, int VID) {
std::pair<sol::table, std::string> Result;
auto MaybeClient = GetClient(mEngine->Server(), PID);
if (MaybeClient && !MaybeClient.value().expired()) {
auto Client = MaybeClient.value().lock();
std::string VehiclePos = Client->GetCarPositionRaw(VID);
if (MaybeClient) {
if (auto Client = MaybeClient.value().lock()) {
std::string VehiclePos = Client->GetCarPositionRaw(VID);
if (VehiclePos.empty()) {
// return std::make_tuple(sol::lua_nil, sol::make_object(StateView, "Vehicle not found"));
Result.second = "Vehicle not found";
if (VehiclePos.empty()) {
Result.second = "Vehicle not found";
return Result;
}
sol::table t = Lua_JsonDecode(VehiclePos);
if (t == sol::lua_nil) {
Result.second = "Packet decode failed";
}
Result.first = t;
return Result;
}
sol::table t = Lua_JsonDecode(VehiclePos);
if (t == sol::lua_nil) {
Result.second = "Packet decode failed";
}
// return std::make_tuple(Result, sol::make_object(StateView, sol::lua_nil));
Result.first = t;
return Result;
} else {
// return std::make_tuple(sol::lua_nil, sol::make_object(StateView, "Client expired"));
Result.second = "No such player";
return Result;
}
Result.second = "No such player";
return Result;
}
sol::table TLuaEngine::StateThreadData::Lua_HttpCreateConnection(const std::string& host, uint16_t port) {
@@ -734,22 +785,31 @@ static bool mDisableMPSet = [] {
static auto GetSettingName = [](int id) -> const char* {
switch (id) {
case 0: return "Debug";
case 1: return "Private";
case 2: return "MaxCars";
case 3: return "MaxPlayers";
case 4: return "Map";
case 5: return "Name";
case 6: return "Description";
case 7: return "InformationPacket";
default: return "Unknown";
case 0:
return "Debug";
case 1:
return "Private";
case 2:
return "MaxCars";
case 3:
return "MaxPlayers";
case 4:
return "Map";
case 5:
return "Name";
case 6:
return "Description";
case 7:
return "InformationPacket";
default:
return "Unknown";
}
};
static void JsonDecodeRecursive(sol::state_view& StateView, sol::table& table, const std::string& left, const nlohmann::json& right) {
switch (right.type()) {
case nlohmann::detail::value_t::null:
AddToTable(table, left, sol::lua_nil_t {});
AddToTable(table, left, sol::lua_nil_t { });
return;
case nlohmann::detail::value_t::object: {
auto value = table.create();
@@ -897,12 +957,9 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
beammp_lua_error("SendNotification expects 2, 3 or 4 arguments.");
}
});
MPTable.set_function("ConfirmationDialog", sol::overload(
&LuaAPI::MP::ConfirmationDialog,
[&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) {
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
}
));
MPTable.set_function("ConfirmationDialog", sol::overload(&LuaAPI::MP::ConfirmationDialog, [&](const int& ID, const std::string& Title, const std::string& Body, const sol::table& Buttons, const std::string& InteractionID) {
LuaAPI::MP::ConfirmationDialog(ID, Title, Body, Buttons, InteractionID);
}));
MPTable.set_function("GetPlayers", [&]() -> sol::table {
return Lua_GetPlayers();
});
@@ -1073,15 +1130,16 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
Start();
}
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
std::shared_ptr<TLuaVoidResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
std::unique_lock Lock(mStateExecuteQueueMutex);
auto Result = std::make_shared<TLuaResult>();
auto Result = std::make_shared<TLuaVoidResult>(mStateId);
mStateExecuteQueue.push({ Script, Result });
return Result;
}
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName, CallStrategy Strategy) {
// TODO: Document all this
std::unique_lock Lock(mStateFunctionQueueMutex);
decltype(mStateFunctionQueue)::iterator Iter = mStateFunctionQueue.end();
if (Strategy == CallStrategy::BestEffort) {
Iter = std::find_if(mStateFunctionQueue.begin(), mStateFunctionQueue.end(),
@@ -1090,10 +1148,7 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
});
}
if (Iter == mStateFunctionQueue.end()) {
auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = FunctionName;
std::unique_lock Lock(mStateFunctionQueueMutex);
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
mStateFunctionQueueCond.notify_all();
return Result;
@@ -1103,9 +1158,7 @@ std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCallFrom
}
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueFunctionCall(const std::string& FunctionName, const std::vector<TLuaValue>& Args, const std::string& EventName) {
auto Result = std::make_shared<TLuaResult>();
Result->StateId = mStateId;
Result->Function = FunctionName;
auto Result = std::make_shared<TLuaResult>(mStateId, FunctionName);
std::unique_lock Lock(mStateFunctionQueueMutex);
mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName });
mStateFunctionQueueCond.notify_all();
@@ -1154,14 +1207,14 @@ void TLuaEngine::StateThreadData::operator()() {
sol::state_view StateView(mState);
auto Res = StateView.safe_script(*S.first.Content, sol::script_pass_on_error, S.first.FileName);
if (Res.valid()) {
S.second->Error = false;
S.second->Result = std::move(Res);
// Script-load completion should not serialize the script's return value.
// A loaded chunk may legally return non-serializable Lua values such as
// functions or function tables. For this reason, we don't pass anything
// to the result here.
S.second->MarkReadySuccess();
} else {
S.second->Error = true;
sol::error Err = Res;
S.second->ErrorMessage = Err.what();
S.second->MarkReadyError(std::move(Res));
}
S.second->MarkAsReady();
}
}
{ // StateFunctionQueue Scope
@@ -1178,7 +1231,7 @@ void TLuaEngine::StateThreadData::operator()() {
auto& Result = TheQueuedFunction.Result;
auto Args = TheQueuedFunction.Args;
// TODO: Use TheQueuedFunction.EventName for errors, warnings, etc
Result->StateId = mStateId;
Result->SetOwnerState(mStateId);
sol::state_view StateView(mState);
auto Fn = StateView[FnName];
if (Fn.valid() && Fn.get_type() == sol::type::function) {
@@ -1187,49 +1240,45 @@ void TLuaEngine::StateThreadData::operator()() {
if (Arg.valueless_by_exception()) {
continue;
}
switch (Arg.index()) {
case TLuaType::String:
LuaArgs.push_back(sol::make_object(StateView, std::get<std::string>(Arg)));
break;
case TLuaType::Int:
LuaArgs.push_back(sol::make_object(StateView, std::get<int>(Arg)));
break;
case TLuaType::Json: {
auto LocalArgs = JsonStringToArray(std::get<JsonString>(Arg));
LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end());
break;
}
case TLuaType::Bool:
LuaArgs.push_back(sol::make_object(StateView, std::get<bool>(Arg)));
break;
case TLuaType::StringStringMap: {
auto Map = std::get<std::unordered_map<std::string, std::string>>(Arg);
auto Table = StateView.create_table();
for (const auto& [k, v] : Map) {
Table[k] = v;
std::visit([&LuaArgs, &StateView, this](const auto& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::string>) {
LuaArgs.push_back(sol::make_object(StateView, arg));
} else if constexpr (std::is_same_v<T, int>) {
LuaArgs.push_back(sol::make_object(StateView, arg));
} else if constexpr (std::is_same_v<T, bool>) {
LuaArgs.push_back(sol::make_object(StateView, arg));
} else if constexpr (std::is_same_v<T, JsonString>) {
auto LocalArgs = JsonStringToArray(arg);
LuaArgs.insert(LuaArgs.end(), LocalArgs.begin(), LocalArgs.end());
} else if constexpr (std::is_same_v<T, std::monostate>) {
beammp_lua_error("Unknown argument type, passed as nil");
LuaArgs.push_back(sol::lua_nil_t());
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, std::string>>) {
auto Table = StateView.create_table();
for (const auto& [k, v] : arg) {
Table[k] = v;
}
LuaArgs.push_back(Table);
} else if constexpr (std::is_same_v<T, float>) {
LuaArgs.push_back(sol::make_object(StateView, arg));
} else {
static_assert(AlwaysFalseV<T>, "unhandled variant");
}
LuaArgs.push_back(sol::make_object(StateView, Table));
break;
}
default:
beammp_error("Unknown argument type, passed as nil");
break;
}
}, Arg);
}
auto Res = Fn(sol::as_args(LuaArgs));
if (Res.valid()) {
Result->Error = false;
Result->Result = std::move(Res);
try {
Result->MarkReadySuccess(std::move(Res));
} catch (const std::exception& e) {
Result->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
}
} else {
Result->Error = true;
sol::error Err = Res;
Result->ErrorMessage = Err.what();
Result->MarkReadyError(std::move(Res));
}
Result->MarkAsReady();
} else {
Result->Error = true;
Result->ErrorMessage = BeamMPFnNotFoundError; // special error kind that we can ignore later
Result->MarkAsReady();
Result->MarkReadyError(BeamMPFnNotFoundError);
}
auto ProfEnd = prof::now();
auto ProfDuration = prof::duration(ProfStart, ProfEnd);
@@ -1239,7 +1288,7 @@ void TLuaEngine::StateThreadData::operator()() {
}
}
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() {
std::queue<std::pair<TLuaChunk, std::shared_ptr<TLuaVoidResult>>> TLuaEngine::StateThreadData::Debug_GetStateExecuteQueue() {
std::unique_lock Lock(mStateExecuteQueueMutex);
return mStateExecuteQueue;
}
@@ -1282,21 +1331,6 @@ void TLuaEngine::StateThreadData::AddPath(const fs::path& Path) {
mPaths.push(Path);
}
void TLuaResult::MarkAsReady() {
{
std::lock_guard<std::mutex> readyLock(*this->ReadyMutex);
this->Ready = true;
}
this->ReadyCondition->notify_all();
}
void TLuaResult::WaitUntilReady() {
std::unique_lock readyLock(*this->ReadyMutex);
// wait if not ready yet
if (!this->Ready)
this->ReadyCondition->wait(readyLock);
}
TLuaChunk::TLuaChunk(std::shared_ptr<std::string> Content, std::string FileName, std::string PluginPath)
: Content(Content)
, FileName(FileName)
@@ -1311,3 +1345,110 @@ bool TLuaEngine::TimedEvent::Expired() {
void TLuaEngine::TimedEvent::Reset() {
LastCompletion = std::chrono::high_resolution_clock::now();
}
TEST_CASE("TLuaEngine ctor & dtor") {
Application::Settings.set(Settings::Key::General_ResourceFolder, "beammp_server_test_resources");
TLuaEngine engine;
const TLuaStateId StateId = "lua_event_contract_test";
engine.EnsureStateExists(StateId, "LuaEventContractTest", true);
// LLM generated test code
auto Script = std::make_shared<std::string>(R"(
function onPlayerAuth(playerName, playerRole, isGuest, identifiers)
if type(playerName) ~= "string" then return "on:bad-playerName-type:" .. type(playerName) end
if type(playerRole) ~= "string" then return "on:bad-playerRole-type:" .. type(playerRole) end
if type(isGuest) ~= "boolean" then return "on:bad-isGuest-type:" .. type(isGuest) end
if type(identifiers) ~= "table" then return "on:bad-identifiers-type:" .. type(identifiers) end
return "on:" .. playerName .. ":" .. playerRole .. ":" .. tostring(isGuest) .. ":" .. tostring(identifiers.ip) .. ":" .. tostring(identifiers.beammp)
end
function postPlayerAuth(isDenied, reason, playerName, playerRole, isGuest, identifiers)
if type(isDenied) ~= "boolean" then return "post:bad-isDenied-type:" .. type(isDenied) end
if type(reason) ~= "string" then return "post:bad-reason-type:" .. type(reason) end
if type(playerName) ~= "string" then return "post:bad-playerName-type:" .. type(playerName) end
if type(playerRole) ~= "string" then return "post:bad-playerRole-type:" .. type(playerRole) end
if type(isGuest) ~= "boolean" then return "post:bad-isGuest-type:" .. type(isGuest) end
if type(identifiers) ~= "table" then return "post:bad-identifiers-type:" .. type(identifiers) end
return "post:" .. tostring(isDenied) .. ":" .. reason .. ":" .. playerName .. ":" .. playerRole .. ":" .. tostring(isGuest) .. ":" .. tostring(identifiers.ip) .. ":" .. tostring(identifiers.beammp)
end
function arrayBoundaryHandler()
return { "first", "second", [4] = true }
end
function verifyArrayBoundaryRoundtrip()
local pending = MP.TriggerGlobalEvent("arrayBoundaryEvent")
if not pending:IsDone() then
return "not_done"
end
local results = pending:GetResults()
if type(results) ~= "table" then
return "bad_results_type:" .. type(results)
end
if type(results[1]) ~= "table" then
return "bad_item_type:" .. type(results[1])
end
local arr = results[1]
return tostring(arr[1]) .. "|" .. tostring(arr[2]) .. "|" .. tostring(arr[4]) .. "|" .. tostring(arr[3] == nil)
end
MP.RegisterEvent("onPlayerAuth", "onPlayerAuth")
MP.RegisterEvent("postPlayerAuth", "postPlayerAuth")
MP.RegisterEvent("arrayBoundaryEvent", "arrayBoundaryHandler")
)");
auto LoadResult = engine.EnqueueScript(StateId, TLuaChunk(Script, "event_contract.lua", "beammp_server_test_resources/Server/LuaEventContractTest"));
LoadResult->WaitUntilReady();
auto LoadSnapshot = LoadResult->GetSnapshot();
CHECK(!LoadSnapshot.Error);
const std::unordered_map<std::string, std::string> Identifiers {
{"ip", "410.0.24.1"},
{"beammp", "123456"},
};
auto OnPlayerAuthResults = engine.TriggerEvent(
"onPlayerAuth", "",
std::string("guest8133569"),
std::string("USER"),
true,
Identifiers);
REQUIRE(OnPlayerAuthResults.size() == 1);
TLuaEngine::WaitForAll(OnPlayerAuthResults);
auto OnPlayerAuthSnapshot = OnPlayerAuthResults.front()->GetDetachedSnapshot();
CHECK(!OnPlayerAuthSnapshot.Error);
const auto* OnPlayerAuthValue = std::get_if<std::string>(&OnPlayerAuthSnapshot.Result.V);
REQUIRE(OnPlayerAuthValue != nullptr);
CHECK(*OnPlayerAuthValue == "on:guest8133569:USER:true:410.0.24.1:123456");
auto PostPlayerAuthResults = engine.TriggerEvent(
"postPlayerAuth", "",
false,
std::string(""),
std::string("guest8133569"),
std::string("USER"),
true,
Identifiers);
REQUIRE(PostPlayerAuthResults.size() == 1);
TLuaEngine::WaitForAll(PostPlayerAuthResults);
auto PostPlayerAuthSnapshot = PostPlayerAuthResults.front()->GetDetachedSnapshot();
CHECK(!PostPlayerAuthSnapshot.Error);
const auto* PostPlayerAuthValue = std::get_if<std::string>(&PostPlayerAuthSnapshot.Result.V);
REQUIRE(PostPlayerAuthValue != nullptr);
CHECK(*PostPlayerAuthValue == "post:false::guest8133569:USER:true:410.0.24.1:123456");
auto ArrayRoundtrip = engine.EnqueueFunctionCall(StateId, "verifyArrayBoundaryRoundtrip", {}, "verifyArrayBoundaryRoundtrip");
ArrayRoundtrip->WaitUntilReady();
auto ArrayRoundtripSnapshot = ArrayRoundtrip->GetDetachedSnapshot();
CHECK(!ArrayRoundtripSnapshot.Error);
const auto* ArrayRoundtripValue = std::get_if<std::string>(&ArrayRoundtripSnapshot.Result.V);
REQUIRE(ArrayRoundtripValue != nullptr);
CHECK(*ArrayRoundtripValue == "first|second|true|true");
Application::GracefullyShutdown();
}
+4 -3
View File
@@ -44,7 +44,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
std::transform(secondStr.begin(), secondStr.end(), secondStr.begin(), ::tolower);
return firstStr < secondStr;
});
std::vector<std::pair<fs::path, std::shared_ptr<TLuaResult>>> ResultsToCheck;
std::vector<std::pair<fs::path, std::shared_ptr<TLuaVoidResult>>> ResultsToCheck;
for (const auto& Entry : Entries) {
// read in entire file
try {
@@ -63,8 +63,9 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
}
for (auto& Result : ResultsToCheck) {
Result.second->WaitUntilReady();
if (Result.second->Error) {
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Result.second->ErrorMessage);
auto Snapshot = Result.second->GetSnapshot();
if (Snapshot.Error) {
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage);
}
}
}
+425
View File
@@ -0,0 +1,425 @@
// BeamMP, the BeamNG.drive multiplayer mod.
// Copyright (C) 2026 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 "TLuaResult.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <limits>
#include <sol/unsafe_function_result.hpp>
#include <stdexcept>
#include <thread>
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value) {
std::visit([&os](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, TDetachedLuaValue::Array>) {
size_t i = 0;
for (const auto& val : arg) {
if (i > 0) {
os << ", ";
}
os << val;
++i;
}
} else if constexpr (std::is_same_v<T, TDetachedLuaValue::Object>) {
size_t i = 0;
for (const auto& [key, val] : arg) {
if (i > 0) {
os << ", ";
}
os << key << "=" << val;
++i;
}
} else if constexpr (std::is_same_v<T, bool>)
os << (arg ? "true" : "false");
else if constexpr (std::is_same_v<T, double>)
os << arg;
else if constexpr (std::is_same_v<T, int>)
os << arg;
else if constexpr (std::is_same_v<T, std::string>)
os << arg;
else if constexpr (std::is_same_v<T, std::monostate>)
// monostate means no result value
os << "";
else
static_assert(AlwaysFalseV<T>, "non-exhaustive visitor!");
},
value.V);
return os;
}
void TLuaVoidResult::MarkReadySuccess() {
std::unique_lock Lock(mMutex);
mError = false;
mErrorMessage.clear();
MarkAsReady();
}
void TLuaVoidResult::MarkReadyError(sol::protected_function_result Res) {
std::unique_lock Lock(mMutex);
mError = true;
SetErrorMessageFromResult(Res);
MarkAsReady();
}
void TLuaVoidResult::MarkReadyError(std::string Res) {
std::unique_lock Lock(mMutex);
mError = true;
mErrorMessage = std::move(Res);
MarkAsReady();
}
bool TLuaVoidResult::IsReady() const {
std::unique_lock Lock(mMutex);
return mReady;
}
bool TLuaVoidResult::IsError() const {
std::unique_lock Lock(mMutex);
return mError;
}
TLuaVoidResult::Snapshot TLuaVoidResult::GetSnapshot() const {
std::unique_lock Lock(mMutex);
Snapshot snapshot {
.Error = mError,
.Ready = mReady,
.ErrorMessage = mErrorMessage,
.StateId = mStateId,
};
return snapshot;
}
void TLuaVoidResult::MarkAsReady() {
mReady = true;
mReadyCondition.notify_all();
}
void TLuaVoidResult::WaitUntilReady() {
std::unique_lock Lock(mMutex);
while (!mReady)
mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50),
[this] {
return mReady;
});
}
void TLuaResult::MarkReadySuccess(sol::object Res) {
std::unique_lock Lock(mMutex);
mError = false;
mDetachedResult = Freeze(Res);
MarkAsReady();
}
void TLuaResult::MarkReadySuccessNoResult() {
std::unique_lock Lock(mMutex);
mError = false;
mDetachedResult = { { std::monostate { } } };
MarkAsReady();
}
void TLuaResult::MarkReadyError(sol::protected_function_result Res) {
std::unique_lock Lock(mMutex);
mError = true;
SetErrorMessageFromResult(Res);
MarkAsReady();
}
void TLuaResult::MarkReadyError(std::string Res) {
std::unique_lock Lock(mMutex);
mError = true;
mErrorMessage = std::move(Res);
MarkAsReady();
}
bool TLuaResult::IsReady() const {
std::unique_lock Lock(mMutex);
return mReady;
}
bool TLuaResult::IsError() const {
std::unique_lock Lock(mMutex);
return mError;
}
TLuaStateId TLuaResult::OwnerState() const {
std::unique_lock Lock(mMutex);
// copy
return mStateId;
}
void TLuaResult::SetOwnerState(TLuaStateId StateId) {
std::unique_lock Lock(mMutex);
mStateId = std::move(StateId);
}
TLuaResult::DetachedSnapshot TLuaResult::GetDetachedSnapshot() const {
std::unique_lock Lock(mMutex);
DetachedSnapshot snapshot {
.Error = mError,
.Ready = mReady,
.ErrorMessage = mErrorMessage,
.Result = mDetachedResult,
.StateId = mStateId,
.Function = mFunction,
};
return snapshot;
}
TDetachedLuaValue TLuaResult::Freeze(const sol::object& o, int depth) {
if (depth > 64)
throw std::runtime_error("max depth (64) reached");
switch (o.get_type()) {
case sol::type::lua_nil:
return { { std::monostate { } } };
case sol::type::boolean:
return { { o.as<bool>() } };
case sol::type::number: {
if (o.is<int>()) {
return { { o.as<int>() } };
} else {
return { { o.as<double>() } };
}
}
case sol::type::string:
return { { o.as<std::string>() } };
case sol::type::table: {
TDetachedLuaValue::Object ObjectOut;
// numeric stuff is for arrays
std::vector<std::pair<size_t, TDetachedLuaValue>> NumericEntries;
bool HasNonStringOrNumericKey = false;
size_t MaxNumericIndex = 0;
for (auto&& [k, v] : o.as<sol::table>()) {
if (k.is<std::string>()) {
ObjectOut.emplace(k.as<std::string>(), std::make_shared<TDetachedLuaValue>(std::move(Freeze(v, depth + 1))));
continue;
}
// Thanks to lua handling arrays in a weird way, and because they can also be sparse, this weird code is needed.
if (k.get_type() == sol::type::number) {
const double NumericKey = k.as<double>();
const size_t CandidateIndex = static_cast<size_t>(NumericKey);
// unsure if we need to do this, or if we can do the same int check we do in other places, but this works well
const bool IsPositiveInteger = std::isfinite(NumericKey)
&& NumericKey >= 1.0
&& std::fabs(NumericKey - static_cast<double>(CandidateIndex)) <= std::numeric_limits<double>::epsilon();
if (IsPositiveInteger) {
const auto Index = CandidateIndex;
MaxNumericIndex = std::max(MaxNumericIndex, Index);
NumericEntries.emplace_back(Index, Freeze(v, depth + 1));
continue;
}
}
HasNonStringOrNumericKey = true;
}
// Pure numeric-keyed tables are reconstructed as Lua arrays (1-based indexes).
// This preserves array semantics across this serialization boundary :^)
if (!NumericEntries.empty() && ObjectOut.empty() && !HasNonStringOrNumericKey) {
TDetachedLuaValue::Array ArrayOut(MaxNumericIndex);
for (const auto& [Index, Value] : NumericEntries) {
if (Index > 0 && Index <= ArrayOut.size()) {
ArrayOut[Index - 1] = Value;
}
}
return { { std::move(ArrayOut) } };
}
return { { std::move(ObjectOut) } };
}
default:
throw std::runtime_error("unsupported Lua type for cross-thread snapshot");
}
}
void TLuaResult::MarkAsReady() {
mReady = true;
mReadyCondition.notify_all();
}
void TLuaResult::WaitUntilReady() {
std::unique_lock Lock(mMutex);
while (!mReady)
mReadyCondition.wait_for(Lock, std::chrono::milliseconds(50),
[this] {
return mReady;
});
}
TEST_CASE("TLuaInStateResult MarkReadyError(string) marks ready and wakes waiters") {
TLuaVoidResult result("state_local");
std::atomic<bool> waiterDone { false };
auto waiter = std::thread([&] {
result.WaitUntilReady();
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK_FALSE(waiterDone.load(std::memory_order_acquire));
result.MarkReadyError(std::string("boom"));
waiter.join();
CHECK(result.IsReady());
const auto snapshot = result.GetSnapshot();
CHECK(snapshot.Ready);
CHECK(snapshot.Error);
CHECK(snapshot.ErrorMessage == "boom");
CHECK(snapshot.StateId == "state_local");
}
TEST_CASE("TLuaInStateResult MarkReadySuccess clears error state") {
TLuaVoidResult result("state_local_success");
result.MarkReadySuccess();
CHECK(result.IsReady());
CHECK_FALSE(result.IsError());
const auto snapshot = result.GetSnapshot();
CHECK(snapshot.Ready);
CHECK_FALSE(snapshot.Error);
CHECK(snapshot.ErrorMessage.empty());
}
TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") {
TLuaResult result("state_a", "fn_a");
std::atomic<bool> waiterDone { false };
auto waiter = std::thread([&] {
result.WaitUntilReady();
waiterDone.store(true, std::memory_order_release);
});
std::this_thread::sleep_for(std::chrono::milliseconds(20));
CHECK_FALSE(waiterDone.load(std::memory_order_acquire));
result.MarkReadyError(std::string("boom"));
waiter.join();
CHECK(result.IsReady());
const auto snapshot = result.GetDetachedSnapshot();
CHECK(snapshot.Ready);
CHECK(snapshot.Error);
CHECK(snapshot.ErrorMessage == "boom");
CHECK(snapshot.StateId == "state_a");
CHECK(snapshot.Function == "fn_a");
}
TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") {
sol::state lua;
TLuaResult result("state_table", "fn_table");
lua.open_libraries(sol::lib::base);
auto outer = lua.create_table();
auto inner = lua.create_table();
inner["k"] = std::string("v");
outer["flag"] = true;
outer["msg"] = std::string("hello");
outer["inner"] = inner;
outer[1] = std::string("ignored_numeric_key");
result.MarkReadySuccess(sol::make_object(lua.lua_state(), outer));
const auto detached = result.GetDetachedSnapshot();
CHECK(detached.Ready);
CHECK_FALSE(detached.Error);
const auto* object = std::get_if<TDetachedLuaValue::Object>(&detached.Result.V);
REQUIRE(object != nullptr);
CHECK(object->contains("flag"));
CHECK(object->contains("msg"));
CHECK(object->contains("inner"));
CHECK_FALSE(object->contains("1"));
const auto* flag = std::get_if<bool>(&object->at("flag")->V);
REQUIRE(flag != nullptr);
CHECK(*flag);
const auto* msg = std::get_if<std::string>(&object->at("msg")->V);
REQUIRE(msg != nullptr);
CHECK(*msg == "hello");
const auto* innerObj = std::get_if<TDetachedLuaValue::Object>(&object->at("inner")->V);
REQUIRE(innerObj != nullptr);
REQUIRE(innerObj->contains("k"));
const auto* innerValue = std::get_if<std::string>(&innerObj->at("k")->V);
REQUIRE(innerValue != nullptr);
CHECK(*innerValue == "v");
}
TEST_CASE("TLuaResult detached snapshot preserves numeric array tables") {
sol::state lua;
TLuaResult result("state_array", "fn_array");
lua.open_libraries(sol::lib::base);
auto arr = lua.create_table();
arr[1] = std::string("a");
arr[2] = 42;
arr[4] = true; // keep sparse indexes
result.MarkReadySuccess(sol::make_object(lua.lua_state(), arr));
const auto detached = result.GetDetachedSnapshot();
CHECK(detached.Ready);
CHECK_FALSE(detached.Error);
const auto* array = std::get_if<TDetachedLuaValue::Array>(&detached.Result.V);
REQUIRE(array != nullptr);
REQUIRE(array->size() == 4);
const auto* v1 = std::get_if<std::string>(&(*array)[0].V);
REQUIRE(v1 != nullptr);
CHECK(*v1 == "a");
const auto* v2 = std::get_if<int>(&(*array)[1].V);
REQUIRE(v2 != nullptr);
CHECK(*v2 == 42);
CHECK(std::holds_alternative<std::monostate>((*array)[2].V));
const auto* v4 = std::get_if<bool>(&(*array)[3].V);
REQUIRE(v4 != nullptr);
CHECK(*v4);
}
TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value") {
sol::state lua;
TLuaResult result("state_fn", "fn_fn");
lua.open_libraries(sol::lib::base);
lua["f"] = [] { return 1; };
const sol::table globals = lua.globals();
const sol::protected_function fn = globals.get<sol::protected_function>("f");
const sol::object fnObj = sol::make_object(lua.lua_state(), fn);
CHECK_THROWS_AS(result.MarkReadySuccess(fnObj), std::runtime_error);
CHECK_FALSE(result.IsReady());
}
TEST_CASE("TLuaResult MarkReadySuccessNoResult stores monostate") {
TLuaResult result("state_empty", "fn_empty");
result.MarkReadySuccessNoResult();
CHECK(result.IsReady());
const auto snapshot = result.GetDetachedSnapshot();
CHECK_FALSE(snapshot.Error);
CHECK(std::holds_alternative<std::monostate>(snapshot.Result.V));
}
+336 -166
View File
@@ -20,6 +20,7 @@
#include "Client.h"
#include "Common.h"
#include "LuaAPI.h"
#include "TConnectionLimiter.h"
#include "THeartbeatThread.h"
#include "TLuaEngine.h"
#include "TScopedTimer.h"
@@ -27,20 +28,26 @@
#include <CustomAssert.h>
#include <Http.h>
#include <array>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/address_v4.hpp>
#include <boost/asio/ip/address_v6.hpp>
#include <boost/asio/ip/v6_only.hpp>
#include <boost/asio/socket_base.hpp>
#include <boost/asio/steady_timer.hpp>
#include <condition_variable>
#include <cstring>
#include <memory>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <variant>
#include <zlib.h>
typedef boost::asio::detail::socket_option::integer<SOL_SOCKET, SO_RCVTIMEO> rcv_timeout_option;
static constexpr uint8_t MAX_CONCURRENT_CONNECTIONS = 10;
static constexpr uint8_t MAX_GLOBAL_CONNECTIONS = 128;
static constexpr uint8_t READ_TIMEOUT_S = 10; //seconds
static constexpr uint8_t READ_TIMEOUT_S = 10; // seconds
std::vector<uint8_t> StringToVector(const std::string& Str) {
return std::vector<uint8_t>(Str.data(), Str.data() + Str.size());
@@ -55,11 +62,40 @@ static void CompressProperly(std::vector<uint8_t>& Data) {
Data = CombinedData;
}
// for unit-tests, otherwise unused
static bool OpenLoopbackSocketPair(io_context& IoCtx, ip::tcp::socket& ClientSocket, ip::tcp::socket& ServerSocket, boost::system::error_code& Ec) {
ip::tcp::acceptor Acceptor(IoCtx);
Acceptor.open(ip::tcp::v4(), Ec);
if (Ec) {
return true;
}
Acceptor.bind(ip::tcp::endpoint(ip::address_v4::loopback(), 0), Ec);
if (Ec) {
return true;
}
Acceptor.listen(socket_base::max_listen_connections, Ec);
if (Ec) {
return true;
}
const auto Port = Acceptor.local_endpoint(Ec).port();
if (Ec) {
return true;
}
ClientSocket.connect(ip::tcp::endpoint(ip::address_v4::loopback(), Port), Ec);
if (Ec) {
return true;
}
Acceptor.accept(ServerSocket, Ec);
return true;
}
TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager)
: mServer(Server)
, mPPSMonitor(PPSMonitor)
, mUDPSock(Server.IoCtx())
, mResourceManager(ResourceManager) {
, mResourceManager(ResourceManager)
, mConnectionLimiter(MAX_CONCURRENT_CONNECTIONS, MAX_GLOBAL_CONNECTIONS) {
Application::SetSubsystemStatus("TCPNetwork", Application::Status::Starting);
Application::SetSubsystemStatus("UDPNetwork", Application::Status::Starting);
Application::RegisterShutdownHandler([&] {
@@ -116,13 +152,13 @@ void TNetwork::UDPServerMain() {
+ std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + (" Clients"));
while (!Application::IsShuttingDown()) {
try {
boost::asio::ip::udp::endpoint remote_client_ep {};
boost::asio::ip::udp::endpoint remote_client_ep { };
std::vector<uint8_t> Data = UDPRcvFromClient(remote_client_ep);
if (Data.empty()) {
continue;
}
if (Data.size() == 1 && Data.at(0) == 'P') {
mUDPSock.send_to(boost::asio::const_buffer("P", 1), remote_client_ep, {}, ec);
mUDPSock.send_to(boost::asio::const_buffer("P", 1), remote_client_ep, { }, ec);
// ignore errors
(void)ec;
continue;
@@ -136,14 +172,14 @@ void TNetwork::UDPServerMain() {
std::shared_ptr<TClient> Client;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
Client = ClientPtr.lock();
if (auto Locked = ClientPtr.lock()) {
Client = std::move(Locked);
} else
return true;
}
if (Client->GetID() == ID) {
if (Client->GetUDPAddr() == boost::asio::ip::udp::endpoint {} && !Client->IsUDPConnected() && !Client->GetMagic().empty()) {
if (Client->GetUDPAddr() == boost::asio::ip::udp::endpoint { } && !Client->IsUDPConnected() && !Client->GetMagic().empty()) {
if (Data.size() != 66) {
beammp_debugf("Invalid size for UDP value. IP: {} ID: {}", remote_client_ep.address().to_string(), ID);
return false;
@@ -156,7 +192,7 @@ void TNetwork::UDPServerMain() {
return false;
}
Client->SetMagic({});
Client->SetMagic({ });
Client->SetUDPAddr(remote_client_ep);
Client->SetIsUDPConnected(true);
return false;
@@ -208,7 +244,7 @@ void TNetwork::TCPServerMain() {
beammp_warnf("WARNING: On FreeBSD, for IPv4 to work, you must run `sysctl net.inet6.ip6.v6only=0`!");
beammp_debugf("This is due to an annoying detail in the *BSDs: In the name of security, unsetting the IPV6_V6ONLY option does not work by default (but does not fail???), as it allows IPv4 mapped IPv6 like ::ffff:127.0.0.1, which they deem a security issue. For more information, see RFC 2553, section 3.7.");
#endif
socket_base::linger LingerOpt {};
socket_base::linger LingerOpt { };
LingerOpt.enabled(false);
Listener.set_option(LingerOpt, ec);
if (ec) {
@@ -234,28 +270,27 @@ void TNetwork::TCPServerMain() {
beammp_debug("shutdown during TCP wait for accept loop");
break;
}
boost::asio::ip::tcp::endpoint ClientEp;
boost::asio::ip::tcp::socket ClientSocket = Acceptor.accept(ClientEp, ec);
std::string ClientIP = ClientEp.address().to_string();
if (!ec) {
mClientMapMutex.lock();
if (mClientMap[ClientIP] >= MAX_CONCURRENT_CONNECTIONS) {
beammp_debugf("The connection was rejected for {}, as it had {} concurrent connections.", ClientIP, mClientMap[ClientIP]);
}
else if (mClientMap.size() >= MAX_GLOBAL_CONNECTIONS) {
beammp_debugf("The connection was rejected for {}, as there are {} global connections.", ClientIP, mClientMap.size());
}
else {
TConnection Conn { std::move(ClientSocket), ClientEp };
std::thread ID(&TNetwork::Identify, this, std::move(Conn));
ID.detach(); // TODO: Add to a queue and attempt to join periodically
mClientMap[ClientIP]++;
}
mClientMapMutex.unlock();
}
else {
boost::asio::ip::tcp::socket ClientSocket = Acceptor.accept(ec);
if (ec) {
beammp_errorf("Failed to accept() new client: {}", ec.message());
continue;
}
boost::asio::ip::tcp::endpoint ClientEp = ClientSocket.remote_endpoint(ec);
if (ec) {
beammp_errorf("Accepted socket but failed to query remote endpoint for IP address: {}", ec.message());
continue;
}
std::string ClientIP = ClientEp.address().to_string();
auto MaybeGuard = mConnectionLimiter.TryAcquire(ClientIP);
if (!MaybeGuard.has_value()) {
beammp_debugf("Connection rejected for {} due to the global or concurrent connection limit", ClientIP);
continue;
}
// move-swap to avoid copy ctor (deleted)
auto Guard = std::move(MaybeGuard.value());
TConnection Conn { std::move(ClientSocket), ClientEp };
std::thread ID(&TNetwork::Identify, this, std::move(Conn), std::move(Guard));
ID.detach(); // TODO: Add to a queue and attempt to join periodically
} catch (const std::exception& e) {
beammp_errorf("Exception in accept routine: {}", e.what());
}
@@ -267,7 +302,7 @@ void TNetwork::TCPServerMain() {
#include "Json.h"
namespace json = rapidjson;
void TNetwork::Identify(TConnection&& RawConnection) {
void TNetwork::Identify(TConnection&& RawConnection, TConnectionLimiter::TGuard&& Guard) {
RegisterThreadAuto();
char Code;
@@ -276,17 +311,8 @@ void TNetwork::Identify(TConnection&& RawConnection) {
// TODO: is this right?!
beammp_debug("Error occured reading code");
RawConnection.Socket.shutdown(socket_base::shutdown_both, ec);
mClientMapMutex.lock();
{
std::string ClientIP = RawConnection.SockAddr.address().to_string();
if (mClientMap[ClientIP] > 0) {
mClientMap[ClientIP]--;
}
if (mClientMap[ClientIP] == 0) {
mClientMap.erase(ClientIP);
}
}
mClientMapMutex.unlock();
// TODO: is this right too?
RawConnection.Socket.close(ec);
return;
}
std::shared_ptr<TClient> Client { nullptr };
@@ -317,17 +343,6 @@ void TNetwork::Identify(TConnection&& RawConnection) {
beammp_errorf("Error during handling of code {} - client left in invalid state, closing socket: {}", Code, e.what());
boost::system::error_code ec;
RawConnection.Socket.shutdown(boost::asio::socket_base::shutdown_both, ec);
mClientMapMutex.lock();
{
std::string ClientIP = RawConnection.SockAddr.address().to_string();
if (mClientMap[ClientIP] > 0) {
mClientMap[ClientIP]--;
}
if (mClientMap[ClientIP] == 0) {
mClientMap.erase(ClientIP);
}
}
mClientMapMutex.unlock();
if (ec) {
beammp_debugf("Failed to shutdown client socket: {}", ec.message());
}
@@ -338,8 +353,6 @@ void TNetwork::Identify(TConnection&& RawConnection) {
}
}
std::string HashPassword(const std::string& str) {
std::stringstream ret;
unsigned char* hash = SHA256(reinterpret_cast<const unsigned char*>(str.c_str()), str.length(), nullptr);
@@ -367,7 +380,12 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
beammp_info("Identifying new ClientConnection...");
auto Data = TCPRcv(*Client);
auto Data = TCPRcv(*Client, true);
if (Data.empty()) {
beammp_debug("Authentication failed: did not receive version packet");
ClientKick(*Client, "Connection closed during version handshake");
return nullptr;
}
constexpr std::string_view VC = "VC";
if (Data.size() > 3 && std::equal(Data.begin(), Data.begin() + VC.size(), VC.begin(), VC.end())) {
@@ -389,7 +407,12 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
// TODO: handle
}
Data = TCPRcv(*Client);
Data = TCPRcv(*Client, true);
if (Data.empty()) {
beammp_debug("Authentication failed: did not receive auth key packet");
ClientKick(*Client, "Connection closed during authentication");
return nullptr;
}
if (Data.size() > 50) {
ClientKick(*Client, "Invalid Key (too long)!");
@@ -397,11 +420,15 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
}
std::string Key(reinterpret_cast<const char*>(Data.data()), Data.size());
if (Key.empty()) {
ClientKick(*Client, "Invalid Key (empty)!");
return nullptr;
}
std::string AuthKey = Application::Settings.getAsString(Settings::Key::General_AuthKey);
std::string ClientIp = Client->GetIdentifiers().at("ip");
nlohmann::json AuthReq {};
std::string AuthResStr {};
nlohmann::json AuthReq { };
std::string AuthResStr { };
try {
AuthReq = nlohmann::json {
{ "key", Key },
@@ -453,8 +480,8 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
std::shared_ptr<TClient> Cl;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
Cl = ClientPtr.lock();
if (auto Locked = ClientPtr.lock()) {
Cl = std::move(Locked);
} else
return true;
}
@@ -472,23 +499,31 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
bool BypassLimit = false;
for (const auto& Result : Futures) {
if (!Result->Error && Result->Result.is<int>()) {
auto Res = Result->Result.as<int>();
auto Snapshot = Result->GetDetachedSnapshot();
if (!Snapshot.Error) {
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt != nullptr) {
auto Res = *MaybeInt;
if (Res == 1) {
NotAllowed = true;
break;
} else if (Res == 2) {
BypassLimit = true;
if (Res == 1) {
NotAllowed = true;
break;
} else if (Res == 2) {
BypassLimit = true;
}
}
}
}
std::string Reason;
bool NotAllowedWithReason = std::any_of(Futures.begin(), Futures.end(),
[&Reason](const std::shared_ptr<TLuaResult>& Result) -> bool {
if (!Result->Error && Result->Result.is<std::string>()) {
Reason = Result->Result.as<std::string>();
return true;
auto Snapshot = Result->GetDetachedSnapshot();
if (!Snapshot.Error) {
const std::string* MaybeStr = std::get_if<std::string>(&Snapshot.Result.V);
if (MaybeStr != nullptr) {
Reason = *MaybeStr;
return true;
}
}
return false;
});
@@ -499,8 +534,8 @@ std::shared_ptr<TClient> TNetwork::Authentication(TConnection&& RawConnection) {
}
if (!NotAllowed && !NotAllowedWithReason && mServer.ClientCount() >= size_t(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) && !BypassLimit) {
NotAllowedWithReason = true;
Reason = "Server full!";
NotAllowedWithReason = true;
Reason = "Server full!";
}
if (NotAllowedWithReason) {
@@ -565,29 +600,33 @@ bool TNetwork::TCPSend(TClient& c, const std::vector<uint8_t>& Data, bool IsSync
return true;
}
std::vector<uint8_t> TNetwork::TCPRcv(TClient& c) {
std::vector<uint8_t> TNetwork::TCPRcv(TClient& c, bool WithTimeout) {
if (c.IsDisconnected()) {
beammp_error("Client disconnected, cancelling TCPRcv");
return {};
return { };
}
int32_t Header {};
int32_t Header { };
auto& Sock = c.GetTCPSock();
boost::system::error_code ec;
std::array<uint8_t, sizeof(Header)> HeaderData;
boost::asio::read(Sock, boost::asio::buffer(HeaderData), ec);
if (WithTimeout) {
ec = ReadWithTimeout(Sock, HeaderData.data(), HeaderData.size(), std::chrono::seconds(READ_TIMEOUT_S));
} else {
boost::asio::read(Sock, boost::asio::buffer(HeaderData), ec);
}
if (ec) {
// TODO: handle this case (read failed)
beammp_debugf("TCPRcv: Reading header failed: {}", ec.message());
return {};
return { };
}
Header = *reinterpret_cast<int32_t*>(HeaderData.data());
if (Header < 0) {
ClientKick(c, "Invalid packet - header negative");
beammp_errorf("Client {} send negative TCP header, ignoring packet", c.GetID());
return {};
return { };
}
std::vector<uint8_t> Data;
@@ -599,13 +638,23 @@ std::vector<uint8_t> TNetwork::TCPRcv(TClient& c) {
} else {
ClientKick(c, "Header size limit exceeded");
beammp_warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header larger than expected - assuming malicious intent and disconnecting the client.");
return {};
return { };
}
std::size_t N = 0;
if (WithTimeout) {
if (!Data.empty()) {
ec = ReadWithTimeout(Sock, Data.data(), Data.size(), std::chrono::seconds(READ_TIMEOUT_S));
if (!ec) {
N = Data.size();
}
}
} else {
N = boost::asio::read(Sock, boost::asio::buffer(Data), ec);
}
auto N = boost::asio::read(Sock, boost::asio::buffer(Data), ec);
if (ec) {
// TODO: handle this case properly
beammp_debugf("TCPRcv: Reading data failed: {}", ec.message());
return {};
return { };
}
if (N != Header) {
@@ -617,7 +666,7 @@ std::vector<uint8_t> TNetwork::TCPRcv(TClient& c) {
Data.erase(Data.begin(), Data.begin() + ABG.size());
try {
return DeComp(Data);
} catch (const InvalidDataError& ) {
} catch (const InvalidDataError&) {
beammp_errorf("Failed to decompress packet from a client. The receive failed and the client may be disconnected as a result");
// return empty -> error
return std::vector<uint8_t>();
@@ -639,35 +688,26 @@ void TNetwork::ClientKick(TClient& c, const std::string& R) {
DisconnectClient(c, "Kicked");
}
void TNetwork::DisconnectClient(const std::weak_ptr<TClient> &c, const std::string &R)
{
void TNetwork::DisconnectClient(const std::weak_ptr<TClient>& c, const std::string& R) {
if (auto locked = c.lock()) {
DisconnectClient(*locked, R);
}
else {
} else {
beammp_debugf("Tried to disconnect a non existant client with reason: {}", R);
}
}
void TNetwork::DisconnectClient(TClient &c, const std::string &R)
{
if (c.IsDisconnected()) return;
std::string ClientIP = c.GetTCPSock().remote_endpoint().address().to_string();
mClientMapMutex.lock();
if (mClientMap[ClientIP] > 0) {
mClientMap[ClientIP]--;
}
if (mClientMap[ClientIP] == 0) {
mClientMap.erase(ClientIP);
}
mClientMapMutex.unlock();
c.Disconnect(R);
void TNetwork::DisconnectClient(TClient& c, const std::string& R) {
// Keep this unconditional; TClient::Disconnect() is the single-winner guard.
(void)c.Disconnect(R);
}
void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
RegisterThreadAuto();
while (!c.expired()) {
while (true) {
auto Client = c.lock();
if (!Client) {
break;
}
if (Client->IsDisconnected()) {
beammp_debug("client is disconnected, breaking client loop");
break;
@@ -675,7 +715,7 @@ void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
if (!Client->IsSyncing() && Client->IsSynced() && Client->MissedPacketQueueSize() != 0) {
// debug("sending " + std::to_string(Client->MissedPacketQueueSize()) + " queued packets");
while (Client->MissedPacketQueueSize() > 0) {
std::vector<uint8_t> QData {};
std::vector<uint8_t> QData { };
{ // locked context
std::unique_lock lock(Client->MissedPacketQueueMutex());
if (Client->MissedPacketQueueSize() <= 0) {
@@ -701,20 +741,29 @@ void TNetwork::Looper(const std::weak_ptr<TClient>& c) {
}
void TNetwork::TCPClient(const std::weak_ptr<TClient>& c) {
// TODO: the c.expired() might cause issues here, remove if you end up here with your debugger
if (c.expired() || !c.lock()->GetTCPSock().is_open()) {
if (auto Client = c.lock()) {
if (Client->IsDisconnected()) {
mServer.RemoveClient(c);
return;
}
} else {
mServer.RemoveClient(c);
return;
}
OnConnect(c);
RegisterThread("(" + std::to_string(c.lock()->GetID()) + ") \"" + c.lock()->GetName() + "\"");
if (auto Client = c.lock()) {
RegisterThread("(" + std::to_string(Client->GetID()) + ") \"" + Client->GetName() + "\"");
} else {
return;
}
std::thread QueueSync(&TNetwork::Looper, this, c);
std::jthread QueueSync(&TNetwork::Looper, this, c);
while (true) {
if (c.expired())
break;
auto Client = c.lock();
if (!Client) {
break;
}
if (Client->IsDisconnected()) {
beammp_debug("client status < 0, breaking client loop");
break;
@@ -735,11 +784,7 @@ void TNetwork::TCPClient(const std::weak_ptr<TClient>& c) {
}
}
if (QueueSync.joinable())
QueueSync.join();
if (!c.expired()) {
auto Client = c.lock();
if (auto Client = c.lock()) {
OnDisconnect(c);
} else {
beammp_warn("client expired in TCPClient, should never happen");
@@ -750,8 +795,7 @@ void TNetwork::UpdatePlayer(TClient& Client) {
std::string Packet = ("Ss") + std::to_string(mServer.ClientCount()) + "/" + std::to_string(Application::Settings.getAsInt(Settings::Key::General_MaxPlayers)) + ":";
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
auto c = ClientPtr.lock();
if (auto c = ClientPtr.lock()) {
Packet += c->GetName() + ",";
}
return true;
@@ -761,43 +805,144 @@ void TNetwork::UpdatePlayer(TClient& Client) {
//(void)Respond(Client, Packet, true);
}
boost::system::error_code TNetwork::ReadWithTimeout(TConnection& Connection, void *Buf, size_t Len, std::chrono::steady_clock::duration Timeout)
{
io_context TimerIO;
steady_timer Timer(TimerIO);
Timer.expires_after(Timeout);
static boost::system::error_code ReadSocketWithTimeout(
boost::asio::ip::tcp::socket& socket,
void* buffer,
std::size_t length,
std::chrono::steady_clock::duration timeout);
std::atomic<bool> TimedOut = false;
boost::system::error_code TNetwork::ReadWithTimeout(TConnection& Connection, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout) {
return ReadWithTimeout(Connection.Socket, Buf, Len, Timeout);
}
Timer.async_wait([&](const boost::system::error_code& ec) {
if (!ec) {
TimedOut = true;
Connection.Socket.cancel();
}
});
std::thread TimerThread([&]() { TimerIO.run(); });
boost::system::error_code TNetwork::ReadWithTimeout(boost::asio::ip::tcp::socket& Socket, void* Buf, size_t Len, std::chrono::steady_clock::duration Timeout) {
return ReadSocketWithTimeout(Socket, Buf, Len, Timeout);
}
boost::system::error_code ReadEc;
boost::asio::read(Connection.Socket, boost::asio::buffer(Buf, Len), ReadEc);
static boost::system::error_code ReadSocketWithTimeout(
boost::asio::ip::tcp::socket& Socket,
void* Buffer,
std::size_t Length,
std::chrono::steady_clock::duration Timeout) {
namespace asio = boost::asio;
using boost::system::error_code;
TimerIO.stop();
TimerThread.join();
struct TTimeoutState {
explicit TTimeoutState(const boost::asio::any_io_executor& Executor)
: Timer(Executor) { }
if (TimedOut.load()) {
return error::timed_out; // synthesize a clean timeout error
}
return ReadEc; //Succes!
asio::steady_timer Timer;
std::promise<std::pair<error_code, std::size_t>> Promise;
std::atomic_bool Completed { false };
};
auto State = std::make_shared<TTimeoutState>(Socket.get_executor());
auto Future = State->Promise.get_future();
asio::async_read(
Socket,
asio::buffer(Buffer, Length),
[State](error_code ec, std::size_t n) {
if (!State->Completed.exchange(true)) {
State->Timer.cancel();
State->Promise.set_value({ ec, n });
}
});
State->Timer.expires_after(Timeout);
State->Timer.async_wait(
[State, &Socket](error_code ec) {
if (ec == asio::error::operation_aborted)
return;
if (!State->Completed.exchange(true)) {
error_code IgnoredEc;
Socket.cancel(IgnoredEc);
State->Promise.set_value({ asio::error::timed_out, 0 });
}
});
auto [ec, NRead] = Future.get();
return ec;
}
TEST_CASE("ReadSocketWithTimeout returns timed_out when peer sends no data") {
TIoPollThread TimerThread;
boost::system::error_code Ec;
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
REQUIRE(!Ec);
uint8_t ReadByte = 0;
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, &ReadByte, 1, std::chrono::milliseconds(50));
CHECK(ReadEc == error::timed_out);
}
TEST_CASE("ReadSocketWithTimeout reads small payload") {
TIoPollThread TimerThread;
boost::system::error_code Ec;
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
REQUIRE(!Ec);
const std::array<uint8_t, 2> Sent { 'O', 'K' };
boost::asio::write(ClientSocket, boost::asio::buffer(Sent), Ec);
REQUIRE(!Ec);
std::array<uint8_t, 2> Received { };
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, Received.data(), Received.size(), std::chrono::milliseconds(200));
CHECK(!ReadEc);
CHECK(Received == Sent);
}
TEST_CASE("ReadSocketWithTimeout reads large payload") {
TIoPollThread TimerThread;
boost::system::error_code Ec;
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
REQUIRE(!Ec);
constexpr size_t PacketSize = 2 * 1024 * 1024;
std::vector<uint8_t> Sent(PacketSize, uint8_t(0x7A));
boost::asio::write(ClientSocket, boost::asio::buffer(Sent), Ec);
REQUIRE(!Ec);
std::vector<uint8_t> Received(PacketSize);
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, Received.data(), Received.size(), std::chrono::seconds(2));
CHECK(!ReadEc);
CHECK(Received == Sent);
}
TEST_CASE("ReadSocketWithTimeout can timeout then retry successfully") {
TIoPollThread TimerThread;
boost::system::error_code Ec;
ip::tcp::socket ClientSocket(TimerThread.IoCtx());
ip::tcp::socket ServerSocket(TimerThread.IoCtx());
OpenLoopbackSocketPair(TimerThread.IoCtx(), ClientSocket, ServerSocket, Ec);
REQUIRE(!Ec);
uint8_t Received = 0;
CHECK(ReadSocketWithTimeout(ServerSocket, &Received, 1, std::chrono::milliseconds(20)) == error::timed_out);
const uint8_t Sent = 0x42;
boost::asio::write(ClientSocket, boost::asio::buffer(&Sent, 1), Ec);
REQUIRE(!Ec);
const auto ReadEc = ReadSocketWithTimeout(ServerSocket, &Received, 1, std::chrono::milliseconds(200));
CHECK(!ReadEc);
CHECK(Received == Sent);
}
void TNetwork::OnDisconnect(const std::weak_ptr<TClient>& ClientPtr) {
std::shared_ptr<TClient> LockedClientPtr { nullptr };
try {
LockedClientPtr = ClientPtr.lock();
} catch (const std::exception&) {
auto LockedClientPtr = ClientPtr.lock();
if (!LockedClientPtr) {
beammp_warn("Client expired in OnDisconnect, this is unexpected");
return;
}
beammp_assert(LockedClientPtr != nullptr);
TClient& c = *LockedClientPtr;
beammp_info(c.GetName() + (" Connection Terminated"));
std::string Packet;
@@ -828,8 +973,7 @@ int TNetwork::OpenID() {
found = true;
mServer.ForEachClient([&](const std::weak_ptr<TClient>& ClientPtr) -> bool {
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
auto c = ClientPtr.lock();
if (auto c = ClientPtr.lock()) {
if (c->GetID() == ID) {
found = false;
ID++;
@@ -842,9 +986,11 @@ int TNetwork::OpenID() {
}
void TNetwork::OnConnect(const std::weak_ptr<TClient>& c) {
beammp_assert(!c.expired());
beammp_info("Client connected");
auto LockedClient = c.lock();
if (!LockedClient) {
return;
}
beammp_info("Client connected");
LockedClient->SetID(OpenID());
beammp_info("Assigned ID " + std::to_string(LockedClient->GetID()) + " to " + LockedClient->GetName());
LuaAPI::MP::Engine->ReportErrors(LuaAPI::MP::Engine->TriggerEvent("onPlayerConnecting", "", LockedClient->GetID()));
@@ -876,6 +1022,7 @@ void TNetwork::SyncResources(TClient& c) {
while (!c.IsDisconnected()) {
Data = TCPRcv(c);
if (Data.empty()) {
DisconnectClient(c, "TCPRcv failed during resource sync");
break;
}
constexpr std::string_view Done = "Done";
@@ -951,9 +1098,9 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) {
#if defined(BEAMMP_LINUX)
#include <cerrno>
#include <cstring>
#include <signal.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <signal.h>
#endif
void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name) {
TScopedTimer timer(fmt::format("Download of '{}' for client {}", Name, c.GetID()));
@@ -968,17 +1115,40 @@ void TNetwork::SendFileToClient(TClient& c, size_t Size, const std::string& Name
// native handle, needed in order to make native syscalls with it
int socket = c.GetTCPSock().native_handle();
ssize_t ret = 0;
auto ToSendTotal = Size;
auto Start = 0;
while (ret < ssize_t(ToSendTotal)) {
auto SysOffset = off_t(Start + size_t(ret));
ret = sendfile(socket, fd, &SysOffset, ToSendTotal - size_t(ret));
if (ret < 0) {
beammp_errorf("Failed to send mod '{}' to client {}: {}", Name, c.GetID(), std::strerror(errno));
const auto ToSendTotal = Size;
size_t TotalSent = 0;
while (TotalSent < ToSendTotal) {
off_t SysOffset = off_t(TotalSent);
const ssize_t SentNow = sendfile(socket, fd, &SysOffset, ToSendTotal - TotalSent);
if (SentNow > 0) {
TotalSent += size_t(SentNow);
continue;
}
if (SentNow == 0) {
beammp_errorf("Failed to send mod '{}' to client {}: sendfile returned 0 before all bytes were sent", Name, c.GetID());
::close(fd);
DisconnectClient(c, "sendfile returned 0 during mod download");
return;
}
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN
#if EWOULDBLOCK != EAGAIN
|| errno == EWOULDBLOCK
#endif
) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
beammp_errorf("Failed to send mod '{}' to client {}: {}", Name, c.GetID(), std::strerror(errno));
::close(fd);
DisconnectClient(c, "sendfile failed during mod download");
return;
}
::close(fd);
#else
std::ifstream f(Name.c_str(), std::ios::binary);
@@ -1047,10 +1217,10 @@ bool TNetwork::Respond(TClient& c, const std::vector<uint8_t>& MSG, bool Rel, bo
}
bool TNetwork::SyncClient(const std::weak_ptr<TClient>& c) {
if (c.expired()) {
auto LockedClient = c.lock();
if (!LockedClient) {
return false;
}
auto LockedClient = c.lock();
if (LockedClient->IsSynced())
return true;
// Syncing, later set isSynced
@@ -1069,8 +1239,8 @@ bool TNetwork::SyncClient(const std::weak_ptr<TClient>& c) {
std::shared_ptr<TClient> client;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
client = ClientPtr.lock();
if (auto Locked = ClientPtr.lock()) {
client = std::move(Locked);
} else
return true;
}
@@ -1107,14 +1277,14 @@ void TNetwork::SendToAll(TClient* c, const std::vector<uint8_t>& Data, bool Self
char C = Data.at(0);
bool ret = true;
mServer.ForEachClient([&](std::weak_ptr<TClient> ClientPtr) -> bool {
std::shared_ptr<TClient> Client;
try {
std::shared_ptr<TClient> Client { nullptr };
{
ReadLock Lock(mServer.GetClientMutex());
Client = ClientPtr.lock();
} catch (const std::exception&) {
// continue
beammp_warn("Client expired, shouldn't happen - if a client disconnected recently, you can ignore this");
return true;
if (auto Locked = ClientPtr.lock()) {
Client = std::move(Locked);
} else {
return true;
}
}
if (Self || Client.get() != c) {
if (Client->IsSynced() || Client->IsSyncing()) {
@@ -1169,12 +1339,12 @@ bool TNetwork::UDPSend(TClient& Client, std::vector<uint8_t> Data) {
}
std::vector<uint8_t> TNetwork::UDPRcvFromClient(boost::asio::ip::udp::endpoint& ClientEndpoint) {
std::array<char, 1024> Ret {};
std::array<char, 1024> Ret { };
boost::system::error_code ec;
const auto Rcv = mUDPSock.receive_from(boost::asio::mutable_buffer(Ret.data(), Ret.size()), ClientEndpoint, 0, ec);
if (ec) {
beammp_errorf("UDP recvfrom() failed: {}", ec.message());
return {};
return { };
}
beammp_assert(Rcv <= Ret.size());
return std::vector<uint8_t>(Ret.begin(), Ret.begin() + Rcv);
+3 -3
View File
@@ -55,8 +55,8 @@ void TPPSMonitor::operator()() {
std::shared_ptr<TClient> c;
{
ReadLock Lock(mServer.GetClientMutex());
if (!ClientPtr.expired()) {
c = ClientPtr.lock();
if (auto Locked = ClientPtr.lock()) {
c = std::move(Locked);
} else
return true;
}
@@ -76,7 +76,7 @@ void TPPSMonitor::operator()() {
return true;
});
for (auto& ClientToKick : TimedOutClients) {
ClientToKick->Disconnect("Timeout");
Network().DisconnectClient(*ClientToKick, "Timeout");
}
TimedOutClients.clear();
if (C == 0 || mInternalPPS == 0) {
+3 -2
View File
@@ -69,8 +69,9 @@ void TPluginMonitor::operator()() {
auto StateID = mEngine->GetStateIDForPlugin(fs::path(Pair.first).parent_path());
auto Res = mEngine->EnqueueScript(StateID, Chunk);
Res->WaitUntilReady();
if (Res->Error) {
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Res->ErrorMessage);
if (Res->IsError()) {
auto Snapshot = Res->GetSnapshot();
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Snapshot.ErrorMessage);
} else {
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
mEngine->ReportErrors(mEngine->TriggerEvent("onFileChanged", "", Pair.first));
+41 -21
View File
@@ -127,19 +127,15 @@ TServer::TServer(const std::vector<std::string_view>& Arguments) {
}
void TServer::RemoveClient(const std::weak_ptr<TClient>& WeakClientPtr) {
std::shared_ptr<TClient> LockedClientPtr { nullptr };
try {
LockedClientPtr = WeakClientPtr.lock();
} catch (const std::exception&) {
// silently fail, as there's nothing to do
auto LockedClientPtr = WeakClientPtr.lock();
if (!LockedClientPtr) {
return;
}
beammp_assert(LockedClientPtr != nullptr);
TClient& Client = *LockedClientPtr;
beammp_debug("removing client " + Client.GetName() + " (" + std::to_string(ClientCount()) + ")");
Client.ClearCars();
WriteLock Lock(mClientsMutex);
mClients.erase(WeakClientPtr.lock());
mClients.erase(LockedClientPtr);
}
void TServer::ForEachClient(const std::function<bool(std::weak_ptr<TClient>)>& Fn) {
@@ -167,14 +163,16 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
try {
Packet = DeComp(Packet);
} catch (const InvalidDataError& ) {
auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID());
Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)");
if (auto LockedClient = Client.lock()) {
beammp_errorf("Failed to decompress packet from client {}. The client sent invalid data and will now be disconnected.", LockedClient->GetID());
Network.ClientKick(*LockedClient, "Sent invalid compressed packet (this is likely a bug on your end)");
}
return;
} catch (const std::runtime_error& e) {
auto LockedClient = Client.lock();
beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what());
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
if (auto LockedClient = Client.lock()) {
beammp_errorf("Failed to decompress packet from client {}: {}. The server might be out of RAM! The client will now be disconnected.", LockedClient->GetID(), e.what());
Network.ClientKick(*LockedClient, "Decompression failed (likely a server-side problem)");
}
return;
}
}
@@ -182,10 +180,10 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
return;
}
if (Client.expired()) {
auto LockedClient = Client.lock();
if (!LockedClient) {
return;
}
auto LockedClient = Client.lock();
std::any Res;
char Code = Packet.at(0);
@@ -224,7 +222,7 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
case 'p':
if (!Network.Respond(*LockedClient, StringToVector("p"), false)) {
// failed to send
LockedClient->Disconnect("Failed to send ping");
Network.DisconnectClient(*LockedClient, "Failed to send ping");
} else {
Network.UpdatePlayer(*LockedClient);
}
@@ -265,9 +263,15 @@ void TServer::GlobalParser(const std::weak_ptr<TClient>& Client, std::vector<uin
LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1));
bool Rejected = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Elem) {
return !Elem->Error
&& Elem->Result.is<int>()
&& bool(Elem->Result.as<int>());
auto Snapshot = Elem->GetDetachedSnapshot();
if (Snapshot.Error) {
return false;
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return bool(*MaybeInt);
});
if (!Rejected) {
std::string SanitizedPacket = fmt::format("C:{}: {}", LockedClient->GetName(), Message);
@@ -380,7 +384,15 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
TLuaEngine::WaitForAll(Futures);
bool ShouldntSpawn = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
auto Snapshot = Result->GetDetachedSnapshot();
if (Snapshot.Error) {
return false;
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return *MaybeInt != 0;
});
bool SpawnConfirmed = false;
@@ -417,7 +429,15 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ
TLuaEngine::WaitForAll(Futures);
bool ShouldntAllow = std::any_of(Futures.begin(), Futures.end(),
[](const std::shared_ptr<TLuaResult>& Result) {
return !Result->Error && Result->Result.is<int>() && Result->Result.as<int>() != 0;
auto Snapshot = Result->GetDetachedSnapshot();
if (Snapshot.Error) {
return false;
}
const int* MaybeInt = std::get_if<int>(&Snapshot.Result.V);
if (MaybeInt == nullptr) {
return false;
}
return *MaybeInt != 0;
});
auto FoundPos = Packet.find('{');