From d26d9b9b2b35475fe8f84676b7f77344cc39bfae Mon Sep 17 00:00:00 2001 From: phosit Date: Wed, 15 Jan 2025 21:11:53 +0100 Subject: [PATCH] Rethrow exceptions from the top level of a module --- .../module/top_level_throw.js | 1 + source/scriptinterface/ModuleLoader.cpp | 43 ++++++++++++++++--- source/scriptinterface/ModuleLoader.h | 13 +++++- source/scriptinterface/tests/test_Module.h | 15 +++++++ 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 binaries/data/mods/_test.scriptinterface/module/top_level_throw.js diff --git a/binaries/data/mods/_test.scriptinterface/module/top_level_throw.js b/binaries/data/mods/_test.scriptinterface/module/top_level_throw.js new file mode 100644 index 0000000000..9751ff8ce1 --- /dev/null +++ b/binaries/data/mods/_test.scriptinterface/module/top_level_throw.js @@ -0,0 +1 @@ +throw new Error("Test reason"); diff --git a/source/scriptinterface/ModuleLoader.cpp b/source/scriptinterface/ModuleLoader.cpp index b42320760f..f5f41fe568 100644 --- a/source/scriptinterface/ModuleLoader.cpp +++ b/source/scriptinterface/ModuleLoader.cpp @@ -21,6 +21,7 @@ #include "ps/CStr.h" #include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/Promises.h" #include "scriptinterface/ScriptConversions.h" @@ -82,6 +83,7 @@ namespace return &val.toObject(); } +template bool Call(JSContext* cx, const unsigned argc, JS::Value* vp) { JS::CallArgs args{JS::CallArgsFromVp(argc, vp)}; @@ -92,14 +94,30 @@ bool Call(JSContext* cx, const unsigned argc, JS::Value* vp) if (!statusPtr) return true; - (*statusPtr) = ModuleLoader::Future::Fulfilled{}; + auto& status = *statusPtr; + + if (reject) + { + JS::HandleValue error{args.get(0)}; + std::string asString; + ScriptFunction::Call(rq, error, "toString", asString); + std::string stack; + Script::GetProperty(rq, error, "stack", stack); + status = ModuleLoader::Future::Rejected{std::make_exception_ptr(std::runtime_error{ + asString + '\n' + stack})}; + return true; + } + + status = ModuleLoader::Future::Fulfilled{}; return true; } +template constexpr JSClassOps callbackClassOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - /*call =*/Call, nullptr, nullptr}; + /*call =*/Call, nullptr, nullptr}; -constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps}; +template +constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps}; } // anonymous namespace ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsPath& filePath): @@ -126,14 +144,16 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP } ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath): - m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass)}}} + m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass)}, + {rq.cx, JS_NewObject(rq.cx, &callbackClass)}}} { JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)}; JS::RootedObject promise{rq.cx, Evaluate(rq, mod)}; SetReservedSlot(JS::PrivateValue(static_cast(&m_Status))); - if (!JS::AddPromiseReactions(rq.cx, promise, std::get(m_Status).fulfill, nullptr)) + Evaluating& evaluatingStatus{std::get(m_Status)}; + if (!JS::AddPromiseReactions(rq.cx, promise, evaluatingStatus.fulfill, evaluatingStatus.reject)) throw std::runtime_error{"Failed adding promise reaction."}; } @@ -159,7 +179,16 @@ ModuleLoader::Future::~Future() [[nodiscard]] bool ModuleLoader::Future::IsDone() const noexcept { - return std::holds_alternative(m_Status); + return std::holds_alternative(m_Status) || std::holds_alternative(m_Status); +} + +void ModuleLoader::Future::Get() +{ + if (std::holds_alternative(m_Status)) + return; + std::exception_ptr error{std::move(std::get(m_Status).error)}; + m_Status = Invalid{}; + std::rethrow_exception(std::move(error)); } void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept @@ -169,6 +198,8 @@ void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept return; if (evaluatingStatus->fulfill) JS::SetReservedSlot(evaluatingStatus->fulfill, 0, privateValue); + if (evaluatingStatus->reject) + JS::SetReservedSlot(evaluatingStatus->reject, 0, privateValue); } [[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq, diff --git a/source/scriptinterface/ModuleLoader.h b/source/scriptinterface/ModuleLoader.h index f114f13018..9b8c0fe2a5 100644 --- a/source/scriptinterface/ModuleLoader.h +++ b/source/scriptinterface/ModuleLoader.h @@ -21,6 +21,7 @@ #include "lib/file/vfs/vfs_path.h" #include "scriptinterface/ScriptTypes.h" +#include #include #include @@ -49,10 +50,15 @@ public: struct Evaluating { JS::PersistentRootedObject fulfill; + JS::PersistentRootedObject reject; }; struct Fulfilled {}; + struct Rejected + { + std::exception_ptr error; + }; struct Invalid {}; - using Status = std::variant; + using Status = std::variant; explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath); Future() = default; @@ -64,6 +70,11 @@ public: [[nodiscard]] bool IsDone() const noexcept; + /** + * Throws if the evaluation of the module failed. + */ + void Get(); + private: // It's save to not require a `JS::HandleValue` here. void SetReservedSlot(JS::Value privateValue) noexcept; diff --git a/source/scriptinterface/tests/test_Module.h b/source/scriptinterface/tests/test_Module.h index 88e015b3e6..d451bc8fb2 100644 --- a/source/scriptinterface/tests/test_Module.h +++ b/source/scriptinterface/tests/test_Module.h @@ -195,4 +195,19 @@ public: TS_ASSERT(future.IsDone()); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah"); } + + void test_TopLevelThrow() + { + ScriptInterface script{"Test", "Test", g_ScriptContext}; + const ScriptRequest rq{script}; + + // To silence the error. + const TestLogger _; + auto future = script.GetModuleLoader().LoadModule(rq, "top_level_throw.js"); + + g_ScriptContext->RunJobs(); + TS_ASSERT(future.IsDone()); + TS_ASSERT_THROWS_EQUALS(future.Get(), const std::runtime_error& e, e.what(), + "Error: Test reason\n@top_level_throw.js:1:7\n"); + } };