refactor Lua result handling for safety

this massively improves thread safety and cleanly serializes accesses
into the lua engine's result objects where accesses before were
extremely unsafe and could access a corrupt/invalid stack.

this fixes various obscure crashes related to accessing results,
without changing any observable behavior.
This commit is contained in:
Lion Kortlepel
2026-04-18 18:16:16 +02:00
parent 0c864665bb
commit b3e8d86cef
11 changed files with 498 additions and 133 deletions
+2
View File
@@ -54,6 +54,7 @@ set(PRJ_HEADERS
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
@@ -82,6 +83,7 @@ set(PRJ_SOURCES
src/Profiling.cpp
src/ChronoWrapper.cpp
src/TConnectionLimiter.cpp
src/TLuaResult.cpp
)
find_package(Lua REQUIRED)
+2 -32
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>
@@ -72,35 +71,6 @@ enum TLuaType {
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 SetErrorMessageFromResult(sol::protected_function_result& Res) {
if (Res.valid()) {
beammp_lua_errorf("Error was not an error");
}
if (Res.get_type() == sol::type::string) {
ErrorMessage = Res.get<sol::error>().what();
} else {
ErrorMessage = "(unknown error; error object is not inspectable)";
}
}
void MarkAsReady();
void WaitUntilReady();
};
struct TLuaPluginConfig {
static inline const std::string FileName = "PluginConfig.toml";
TLuaStateId StateId;
@@ -242,7 +212,7 @@ public:
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::vector<QueuedFunction> Debug_GetStateFunctionQueueForState(TLuaStateId StateId);
std::vector<TLuaResult> Debug_GetResultsToCheckForState(TLuaStateId StateId);
std::vector<TLuaResult::DetachedSnapshot> Debug_GetResultsToCheckForState(TLuaStateId StateId);
private:
void CollectAndInitPlugins();
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "Common.h"
#include <condition_variable>
#include <string>
#include <sol/sol.hpp>
using TLuaStateId = std::string;
struct TDetachedLuaValue {
using Array = std::vector<TDetachedLuaValue>;
using Object = std::unordered_map<std::string, TDetachedLuaValue>;
std::variant<std::monostate, bool, double, int, std::string, Array, Object> V;
};
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value);
struct TLuaResult {
struct Snapshot {
bool Error;
bool Ready;
std::string ErrorMessage;
/// CAUTION: Accessing this object causes the Lua stack of the owning state
/// to be accessed. Only call this from the owning Lua state. Otherwise,
/// use `DetachedSnapshot`.
sol::object Result;
TLuaStateId StateId;
std::string Function;
};
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 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 `GetSnapshot` to get the results.
void WaitUntilReady();
bool IsReady() const;
bool IsError() const;
TLuaStateId OwnerState() const;
void SetOwnerState(TLuaStateId StateId);
/// To get the result or error, while in the owning state, use this function.
/// Pass your lua state ID so that the function can verify the safety of the access.
/// If you have no state ID, use `GetDetachedSnapshot`.
/// Accesses the Lua state directly when using the result.
Snapshot GetSnapshot(TLuaStateId CallingState) const;
/// Returns a snapshot with, if not an error, a serialized version of the result
/// object. In contrast to `GetSnapshot`, this does not access the Lua stack when
/// accessing `.Result`.
DetachedSnapshot GetDetachedSnapshot() const;
private:
mutable std::mutex mMutex {};
bool mReady { false };
bool mError;
std::string mErrorMessage;
sol::object mResult { sol::lua_nil };
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();
};
+39
View File
@@ -31,6 +31,7 @@
#include <sstream>
#include <thread>
#include "TLuaResult.h"
#include "Compat.h"
#include "CustomAssert.h"
#include "Http.h"
@@ -373,6 +374,44 @@ std::string GetPlatformAgnosticErrorString() {
return "(no human-readable errors on this platform)";
#endif
}
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, std::vector<TDetachedLuaValue>>) {
size_t i = 0;
for (auto val : arg) {
if (i > 0) {
os << ", ";
}
os << val;
}
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, TDetachedLuaValue>>) {
size_t i = 0;
for (auto [key, val] : arg) {
if (i > 0) {
os << ", ";
}
os << key << "=" << val;
}
} 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(false, "non-exhaustive visitor!");
},
value.V);
return os;
}
// TODO: add unit tests to SplitString
void SplitString(const std::string& str, const char delim, std::vector<std::string>& out) {
+17 -10
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>
@@ -689,9 +689,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;
};
@@ -701,7 +705,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;
}
}
@@ -719,14 +723,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());
}
@@ -807,8 +813,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->GetDetachedSnapshot();
beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage);
}
}
} else {
+84 -72
View File
@@ -20,20 +20,25 @@
#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 <unordered_map>
#include <variant>
TLuaEngine* LuaAPI::MP::Engine;
@@ -74,8 +79,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 +91,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;
@@ -128,6 +135,7 @@ void TLuaEngine::operator()() {
}
}
}
std::unique_lock Lock(mLuaStatesMutex);
if (mLuaStates.empty()) {
beammp_trace("No Lua states, event loop running extremely sparsely");
Application::SleepSafeSeconds(10);
@@ -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;
@@ -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);
}
}
}
@@ -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->GetSnapshot(StateId);
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];
}
@@ -495,15 +508,12 @@ 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;
Result->MarkReadySuccess(LuaResult);
} 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 +521,55 @@ 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;
});
TLuaStateId StateId = mStateId;
AsyncEventReturn.set_function("GetResults",
[&](const sol::table& Self) -> sol::table {
sol::state_view StateView(mState);
[StateId](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");
int i = 1;
for (const auto& Value : Vector) {
if (!Value->Ready) {
if (!Value->IsReady()) {
return sol::lua_nil;
}
Result.set(i, Value->Result);
// event results from this state are valid unserialized
if (Value->OwnerState() == StateId) {
auto Snapshot = Value->GetSnapshot(StateId);
Result.set(i, Snapshot.Result);
} else {
// event result from another state, goes through serialization boundary
auto Snapshot = Value->GetDetachedSnapshot();
std::visit([i, &Result](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::vector<TDetachedLuaValue>>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, std::unordered_map<std::string, TDetachedLuaValue>>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, bool>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, double>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, int>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, std::string>)
Result.set(i, arg);
else if constexpr (std::is_same_v<T, std::monostate>)
// monostate means no result value
Result.set(i, sol::lua_nil_t());
else
static_assert(false, "non-exhaustive visitor!");
}, Snapshot.Result.V);
}
++i;
}
return Result;
@@ -589,7 +628,6 @@ std::variant<std::string, sol::nil_t> TLuaEngine::StateThreadData::Lua_GetPlayer
}
}
sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() {
sol::table Result = mStateView.create_table();
mEngine->Server().ForEachClient([&](std::weak_ptr<TClient> Client) -> bool {
@@ -1080,13 +1118,15 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI
std::shared_ptr<TLuaResult> TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) {
std::unique_lock Lock(mStateExecuteQueueMutex);
auto Result = std::make_shared<TLuaResult>();
// explicitly passing empty string as there's no single function being called here
auto Result = std::make_shared<TLuaResult>(mStateId, std::string());
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(),
@@ -1095,10 +1135,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;
@@ -1108,9 +1145,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();
@@ -1159,13 +1194,10 @@ 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);
S.second->MarkReadySuccess(std::move(Res));
} else {
S.second->Error = true;
S.second->SetErrorMessageFromResult(Res);
S.second->MarkReadyError(std::move(Res));
}
S.second->MarkAsReady();
}
}
{ // StateFunctionQueue Scope
@@ -1182,7 +1214,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) {
@@ -1222,17 +1254,12 @@ void TLuaEngine::StateThreadData::operator()() {
}
auto Res = Fn(sol::as_args(LuaArgs));
if (Res.valid()) {
Result->Error = false;
Result->Result = std::move(Res);
Result->MarkReadySuccess(std::move(Res));
} else {
Result->Error = true;
Result->SetErrorMessageFromResult(Res);
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);
@@ -1285,21 +1312,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)
+3 -2
View File
@@ -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->GetDetachedSnapshot();
if (Snapshot.Error) {
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage);
}
}
}
+210
View File
@@ -0,0 +1,210 @@
#include "TLuaResult.h"
#include <atomic>
#include <chrono>
#include <stdexcept>
#include <thread>
void TLuaResult::MarkReadySuccess(sol::object Res) {
std::unique_lock Lock(mMutex);
mError = false;
mResult = Res;
mDetachedResult = Freeze(Res);
MarkAsReady();
}
void TLuaResult::MarkReadyError(sol::protected_function_result Res) {
std::unique_lock Lock(mMutex);
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::Snapshot TLuaResult::GetSnapshot(TLuaStateId CallingState) const {
std::unique_lock Lock(mMutex);
if (CallingState != mStateId) {
throw std::logic_error("Tried to get snapshot from non-owning state (use a detached snapshot instead)");
}
Snapshot snapshot {
.Error = mError,
.Ready = mReady,
.ErrorMessage = mErrorMessage,
.Result = mResult,
.StateId = mStateId,
.Function = mFunction,
};
return snapshot;
}
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 out;
for (auto&& [k, v] : o.as<sol::table>()) {
if (!k.is<std::string>())
continue; // no numeric-key handling, don't need it
out.emplace(k.as<std::string>(), Freeze(v, depth + 1));
}
return { { std::move(out) } };
}
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("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 GetSnapshot enforces owner state id") {
TLuaResult result("owner_state", "fn_owner");
sol::state lua;
lua.open_libraries(sol::lib::base);
result.MarkReadySuccess(sol::make_object(lua.lua_state(), std::string("ok")));
CHECK_NOTHROW(result.GetSnapshot("owner_state"));
CHECK_THROWS_AS(result.GetSnapshot("different_state"), std::logic_error);
}
TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") {
TLuaResult result("state_table", "fn_table");
sol::state lua;
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 MarkReadySuccess throws on unsupported Lua function value") {
TLuaResult result("state_fn", "fn_fn");
sol::state lua;
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());
}
+19 -10
View File
@@ -40,6 +40,7 @@
#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;
@@ -507,23 +508,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;
});
+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->GetDetachedSnapshot();
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));
+27 -5
View File
@@ -265,9 +265,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 +386,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 +431,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('{');