diff --git a/CMakeLists.txt b/CMakeLists.txt index 00ca619..b2a7686 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/include/TLuaEngine.h b/include/TLuaEngine.h index b11a928..1aee7e8 100644 --- a/include/TLuaEngine.h +++ b/include/TLuaEngine.h @@ -19,13 +19,12 @@ #pragma once #include "Profiling.h" +#include "TLuaResult.h" #include "TNetwork.h" #include "TServer.h" -#include #include #include #include -#include #include #include #include @@ -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 ReadyMutex { - std::make_shared() - }; - std::shared_ptr ReadyCondition { - std::make_shared() - }; - - 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().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 /* handlers */> Debug_GetEventsForState(TLuaStateId StateId); std::queue>> Debug_GetStateExecuteQueueForState(TLuaStateId StateId); std::vector Debug_GetStateFunctionQueueForState(TLuaStateId StateId); - std::vector Debug_GetResultsToCheckForState(TLuaStateId StateId); + std::vector Debug_GetResultsToCheckForState(TLuaStateId StateId); private: void CollectAndInitPlugins(); diff --git a/include/TLuaResult.h b/include/TLuaResult.h new file mode 100644 index 0000000..81ee223 --- /dev/null +++ b/include/TLuaResult.h @@ -0,0 +1,92 @@ +#pragma once + +#include "Common.h" +#include +#include +#include + +using TLuaStateId = std::string; + +struct TDetachedLuaValue { + using Array = std::vector; + using Object = std::unordered_map; + std::variant 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().what(); + } else { + mErrorMessage = "(unknown error; error object is not inspectable)"; + } + } + + void MarkAsReady(); +}; diff --git a/src/Common.cpp b/src/Common.cpp index 1785b9e..7fff4c6 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -31,6 +31,7 @@ #include #include +#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; + if constexpr (std::is_same_v>) { + size_t i = 0; + for (auto val : arg) { + if (i > 0) { + os << ", "; + } + os << val; + } + } else if constexpr (std::is_same_v>) { + size_t i = 0; + for (auto [key, val] : arg) { + if (i > 0) { + os << ", "; + } + os << key << "=" << val; + } + } else if constexpr (std::is_same_v) + os << (arg ? "true" : "false"); + else if constexpr (std::is_same_v) + os << arg; + else if constexpr (std::is_same_v) + os << arg; + else if constexpr (std::is_same_v) + os << arg; + else if constexpr (std::is_same_v) + // 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& out) { diff --git a/src/TConsole.cpp b/src/TConsole.cpp index 317ec16..5e41f82 100644 --- a/src/TConsole.cpp +++ b/src/TConsole.cpp @@ -22,9 +22,9 @@ #include "Client.h" #include "CustomAssert.h" +#include "Http.h" #include "LuaAPI.h" #include "TLuaEngine.h" -#include "Http.h" #include #include @@ -689,9 +689,13 @@ void TConsole::Command_Status(const std::string&, const std::vector void TConsole::RunAsCommand(const std::string& cmd, bool IgnoreNotACommand) { auto FutureIsNonNil = [](const std::shared_ptr& 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(&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(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 { diff --git a/src/TLuaEngine.cpp b/src/TLuaEngine.cpp index 7b33284..27f5e12 100644 --- a/src/TLuaEngine.cpp +++ b/src/TLuaEngine.cpp @@ -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 #include #include +#include #include #include +#include #include #include +#include +#include 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& 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::Debug_GetStateFunctionQueueF return Result; } -std::vector TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) { +std::vector TLuaEngine::Debug_GetResultsToCheckForState(TLuaStateId StateId) { std::unique_lock Lock(mResultsToCheckMutex); auto ResultsToCheckCopy = mResultsToCheck; Lock.unlock(); - std::vector Result; + std::vector 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>& Results, c size_t ms = 0; std::set 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 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(); + auto Result = std::make_shared(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>>("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>>("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; + if constexpr (std::is_same_v>) + Result.set(i, arg); + else if constexpr (std::is_same_v>) + Result.set(i, arg); + else if constexpr (std::is_same_v) + Result.set(i, arg); + else if constexpr (std::is_same_v) + Result.set(i, arg); + else if constexpr (std::is_same_v) + Result.set(i, arg); + else if constexpr (std::is_same_v) + Result.set(i, arg); + else if constexpr (std::is_same_v) + // 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 TLuaEngine::StateThreadData::Lua_GetPlayer } } - sol::table TLuaEngine::StateThreadData::Lua_GetPlayers() { sol::table Result = mStateView.create_table(); mEngine->Server().ForEachClient([&](std::weak_ptr Client) -> bool { @@ -1080,13 +1118,15 @@ TLuaEngine::StateThreadData::StateThreadData(const std::string& Name, TLuaStateI std::shared_ptr TLuaEngine::StateThreadData::EnqueueScript(const TLuaChunk& Script) { std::unique_lock Lock(mStateExecuteQueueMutex); - auto Result = std::make_shared(); + // explicitly passing empty string as there's no single function being called here + auto Result = std::make_shared(mStateId, std::string()); mStateExecuteQueue.push({ Script, Result }); return Result; } std::shared_ptr TLuaEngine::StateThreadData::EnqueueFunctionCallFromCustomEvent(const std::string& FunctionName, const std::vector& 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 TLuaEngine::StateThreadData::EnqueueFunctionCallFrom }); } if (Iter == mStateFunctionQueue.end()) { - auto Result = std::make_shared(); - Result->StateId = mStateId; - Result->Function = FunctionName; - std::unique_lock Lock(mStateFunctionQueueMutex); + auto Result = std::make_shared(mStateId, FunctionName); mStateFunctionQueue.push_back({ FunctionName, Result, Args, EventName }); mStateFunctionQueueCond.notify_all(); return Result; @@ -1108,9 +1145,7 @@ std::shared_ptr TLuaEngine::StateThreadData::EnqueueFunctionCallFrom } std::shared_ptr TLuaEngine::StateThreadData::EnqueueFunctionCall(const std::string& FunctionName, const std::vector& Args, const std::string& EventName) { - auto Result = std::make_shared(); - Result->StateId = mStateId; - Result->Function = FunctionName; + auto Result = std::make_shared(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 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 Content, std::string FileName, std::string PluginPath) : Content(Content) , FileName(FileName) diff --git a/src/TLuaPlugin.cpp b/src/TLuaPlugin.cpp index 50620be..0d558ac 100644 --- a/src/TLuaPlugin.cpp +++ b/src/TLuaPlugin.cpp @@ -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); } } } diff --git a/src/TLuaResult.cpp b/src/TLuaResult.cpp new file mode 100644 index 0000000..253a478 --- /dev/null +++ b/src/TLuaResult.cpp @@ -0,0 +1,210 @@ +#include "TLuaResult.h" +#include +#include +#include +#include + +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() } }; + case sol::type::number: { + if (o.is()) { + return { { o.as() } }; + } else { + return { { o.as() } }; + } + } + case sol::type::string: + return { { o.as() } }; + case sol::type::table: { + TDetachedLuaValue::Object out; + for (auto&& [k, v] : o.as()) { + if (!k.is()) + continue; // no numeric-key handling, don't need it + out.emplace(k.as(), 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 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(&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(&object->at("flag").V); + REQUIRE(flag != nullptr); + CHECK(*flag); + + const auto* msg = std::get_if(&object->at("msg").V); + REQUIRE(msg != nullptr); + CHECK(*msg == "hello"); + + const auto* innerObj = std::get_if(&object->at("inner").V); + REQUIRE(innerObj != nullptr); + REQUIRE(innerObj->contains("k")); + const auto* innerValue = std::get_if(&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("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()); +} diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index e5f50e2..764584c 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include typedef boost::asio::detail::socket_option::integer rcv_timeout_option; @@ -507,23 +508,31 @@ std::shared_ptr TNetwork::Authentication(TConnection&& RawConnection) { bool BypassLimit = false; for (const auto& Result : Futures) { - if (!Result->Error && Result->Result.is()) { - auto Res = Result->Result.as(); + auto Snapshot = Result->GetDetachedSnapshot(); + if (!Snapshot.Error) { + const int* MaybeInt = std::get_if(&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& Result) -> bool { - if (!Result->Error && Result->Result.is()) { - Reason = Result->Result.as(); - return true; + auto Snapshot = Result->GetDetachedSnapshot(); + if (!Snapshot.Error) { + const std::string* MaybeStr = std::get_if(&Snapshot.Result.V); + if (MaybeStr != nullptr) { + Reason = *MaybeStr; + return true; + } } return false; }); diff --git a/src/TPluginMonitor.cpp b/src/TPluginMonitor.cpp index 049fd20..d766a70 100644 --- a/src/TPluginMonitor.cpp +++ b/src/TPluginMonitor.cpp @@ -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)); diff --git a/src/TServer.cpp b/src/TServer.cpp index d9d98ef..2c47542 100644 --- a/src/TServer.cpp +++ b/src/TServer.cpp @@ -265,9 +265,15 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::vectorGetName(), LockedClient->GetID(), PacketAsString.substr(PacketAsString.find(':', 3) + 1)); bool Rejected = std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Elem) { - return !Elem->Error - && Elem->Result.is() - && bool(Elem->Result.as()); + auto Snapshot = Elem->GetDetachedSnapshot(); + if (Snapshot.Error) { + return false; + } + const int* MaybeInt = std::get_if(&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& Result) { - return !Result->Error && Result->Result.is() && Result->Result.as() != 0; + auto Snapshot = Result->GetDetachedSnapshot(); + if (Snapshot.Error) { + return false; + } + const int* MaybeInt = std::get_if(&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& Result) { - return !Result->Error && Result->Result.is() && Result->Result.as() != 0; + auto Snapshot = Result->GetDetachedSnapshot(); + if (Snapshot.Error) { + return false; + } + const int* MaybeInt = std::get_if(&Snapshot.Result.V); + if (MaybeInt == nullptr) { + return false; + } + return *MaybeInt != 0; }); auto FoundPos = Packet.find('{');