mirror of
https://github.com/BeamMP/BeamMP-Server.git
synced 2026-07-12 17:54:04 +00:00
fix lua result serialization causing various issues
for example, loading a library which returns a table of functions would fail with an inexplicable (to the user) error about serialization. This is now fixed. We no longer use a single result type, instead there are void results and normal results, and the former simply never has to worry about the result. This was likely causing even more issues; if the `sol::object` was populated before, it hitting the dtor in another path would, again, like we've seen before, corrupt the lua stack.
This commit is contained in:
@@ -141,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);
|
||||
@@ -202,7 +202,7 @@ 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::DetachedSnapshot> Debug_GetResultsToCheckForState(TLuaStateId StateId);
|
||||
|
||||
@@ -217,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);
|
||||
@@ -229,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:
|
||||
@@ -254,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;
|
||||
|
||||
+47
-16
@@ -37,19 +37,54 @@ struct TDetachedLuaValue {
|
||||
};
|
||||
std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value);
|
||||
|
||||
struct TLuaResult {
|
||||
/// 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;
|
||||
/// 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;
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -66,33 +101,29 @@ struct TLuaResult {
|
||||
|
||||
/// 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 `GetSnapshot` to get the results.
|
||||
/// 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);
|
||||
|
||||
/// 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`.
|
||||
/// 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;
|
||||
bool mError { false };
|
||||
std::string mErrorMessage;
|
||||
sol::object mResult { sol::lua_nil };
|
||||
TDetachedLuaValue mDetachedResult;
|
||||
TLuaStateId mStateId;
|
||||
std::string mFunction;
|
||||
|
||||
+1
-1
@@ -810,7 +810,7 @@ void TConsole::InitializeCommandline() {
|
||||
auto Future = mLuaEngine->EnqueueScript(mStateId, { std::make_shared<std::string>(TrimmedCmd), "", "" });
|
||||
Future->WaitUntilReady();
|
||||
if (Future->IsError()) {
|
||||
auto Snapshot = Future->GetDetachedSnapshot();
|
||||
auto Snapshot = Future->GetSnapshot();
|
||||
beammp_lua_error("error in " + mStateId + ": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
+79
-44
@@ -209,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;
|
||||
@@ -362,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);
|
||||
}
|
||||
@@ -444,7 +444,7 @@ void TLuaEngine::EnsureStateExists(TLuaStateId StateId, const std::string& Name,
|
||||
if (!DontCallOnInit) {
|
||||
auto Res = EnqueueFunctionCall(StateId, "onInit", { }, "onInit");
|
||||
Res->WaitUntilReady();
|
||||
auto Snapshot = Res->GetSnapshot(StateId);
|
||||
auto Snapshot = Res->GetDetachedSnapshot();
|
||||
if (Snapshot.Error && Snapshot.ErrorMessage != TLuaEngine::BeamMPFnNotFoundError) {
|
||||
beammp_lua_error("Calling \"onInit\" on \"" + StateId + "\" failed: " + Snapshot.ErrorMessage);
|
||||
}
|
||||
@@ -534,45 +534,50 @@ sol::table TLuaEngine::StateThreadData::Lua_TriggerGlobalEvent(const std::string
|
||||
}
|
||||
return true;
|
||||
});
|
||||
TLuaStateId StateId = mStateId;
|
||||
AsyncEventReturn.set_function("GetResults",
|
||||
[StateId](const sol::table& Self, sol::this_state State) -> sol::table {
|
||||
[](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->IsReady()) {
|
||||
return sol::lua_nil;
|
||||
}
|
||||
// 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, TDetachedLuaValue::Array>)
|
||||
Result.set(i, arg);
|
||||
else if constexpr (std::is_same_v<T, TDetachedLuaValue::Object>)
|
||||
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(AlwaysFalseV<T>, "non-exhaustive visitor!");
|
||||
}, Snapshot.Result.V);
|
||||
}
|
||||
auto Snapshot = Value->GetDetachedSnapshot();
|
||||
Result.set(i, DetachedToLuaObject(DetachedToLuaObject, Snapshot.Result));
|
||||
|
||||
++i;
|
||||
}
|
||||
@@ -1125,10 +1130,9 @@ 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);
|
||||
// explicitly passing empty string as there's no single function being called here
|
||||
auto Result = std::make_shared<TLuaResult>(mStateId, std::string());
|
||||
auto Result = std::make_shared<TLuaVoidResult>(mStateId);
|
||||
mStateExecuteQueue.push({ Script, Result });
|
||||
return Result;
|
||||
}
|
||||
@@ -1203,11 +1207,11 @@ 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()) {
|
||||
try {
|
||||
S.second->MarkReadySuccess(std::move(Res));
|
||||
} catch (const std::exception& e) {
|
||||
S.second->MarkReadyError(fmt::format("Call was successful, but result could not be serialized"));
|
||||
}
|
||||
// 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->MarkReadyError(std::move(Res));
|
||||
}
|
||||
@@ -1284,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;
|
||||
}
|
||||
@@ -1369,13 +1373,36 @@ function postPlayerAuth(isDenied, reason, playerName, playerRole, isGuest, ident
|
||||
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->GetDetachedSnapshot();
|
||||
auto LoadSnapshot = LoadResult->GetSnapshot();
|
||||
CHECK(!LoadSnapshot.Error);
|
||||
|
||||
const std::unordered_map<std::string, std::string> Identifiers {
|
||||
@@ -1415,5 +1442,13 @@ MP.RegisterEvent("postPlayerAuth", "postPlayerAuth")
|
||||
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();
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,7 +63,7 @@ TLuaPlugin::TLuaPlugin(TLuaEngine& Engine, const TLuaPluginConfig& Config, const
|
||||
}
|
||||
for (auto& Result : ResultsToCheck) {
|
||||
Result.second->WaitUntilReady();
|
||||
auto Snapshot = Result.second->GetDetachedSnapshot();
|
||||
auto Snapshot = Result.second->GetSnapshot();
|
||||
if (Snapshot.Error) {
|
||||
beammp_lua_error("Failed: \"" + Result.first.string() + "\": " + Snapshot.ErrorMessage);
|
||||
}
|
||||
|
||||
+184
-31
@@ -17,8 +17,11 @@
|
||||
// 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>
|
||||
@@ -64,16 +67,79 @@ std::ostream& operator<<(std::ostream& os, const TDetachedLuaValue& value) {
|
||||
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;
|
||||
mResult = Res;
|
||||
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;
|
||||
@@ -109,21 +175,6 @@ void TLuaResult::SetOwnerState(TLuaStateId StateId) {
|
||||
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 {
|
||||
@@ -155,13 +206,45 @@ TDetachedLuaValue TLuaResult::Freeze(const sol::object& o, int depth) {
|
||||
case sol::type::string:
|
||||
return { { o.as<std::string>() } };
|
||||
case sol::type::table: {
|
||||
TDetachedLuaValue::Object out;
|
||||
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>())
|
||||
continue; // no numeric-key handling, don't need it
|
||||
out.emplace(k.as<std::string>(), std::make_shared<TDetachedLuaValue>(std::move(Freeze(v, depth + 1))));
|
||||
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;
|
||||
}
|
||||
return { { std::move(out) } };
|
||||
// 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");
|
||||
@@ -181,6 +264,41 @@ void TLuaResult::WaitUntilReady() {
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -205,16 +323,6 @@ TEST_CASE("TLuaResult MarkReadyError(string) marks ready and wakes waiters") {
|
||||
CHECK(snapshot.Function == "fn_a");
|
||||
}
|
||||
|
||||
TEST_CASE("TLuaResult GetSnapshot enforces owner state id") {
|
||||
sol::state lua;
|
||||
TLuaResult result("owner_state", "fn_owner");
|
||||
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") {
|
||||
sol::state lua;
|
||||
TLuaResult result("state_table", "fn_table");
|
||||
@@ -258,6 +366,41 @@ TEST_CASE("TLuaResult detached snapshot freezes nested string-keyed tables") {
|
||||
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");
|
||||
@@ -270,3 +413,13 @@ TEST_CASE("TLuaResult MarkReadySuccess throws on unsupported Lua function value"
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ void TPluginMonitor::operator()() {
|
||||
auto Res = mEngine->EnqueueScript(StateID, Chunk);
|
||||
Res->WaitUntilReady();
|
||||
if (Res->IsError()) {
|
||||
auto Snapshot = Res->GetDetachedSnapshot();
|
||||
auto Snapshot = Res->GetSnapshot();
|
||||
beammp_lua_errorf("Error while hot-reloading \"{}\": {}", Pair.first, Snapshot.ErrorMessage);
|
||||
} else {
|
||||
mEngine->ReportErrors(mEngine->TriggerLocalEvent(StateID, "onInit"));
|
||||
|
||||
Reference in New Issue
Block a user