diff --git a/cocos/scripting/js-bindings/event/EventDispatcher.cpp b/cocos/scripting/js-bindings/event/EventDispatcher.cpp index a56f3a78c2c..667b05272e4 100644 --- a/cocos/scripting/js-bindings/event/EventDispatcher.cpp +++ b/cocos/scripting/js-bindings/event/EventDispatcher.cpp @@ -289,6 +289,7 @@ void EventDispatcher::dispatchTickEvent(float dt) long long microSeconds = std::chrono::duration_cast(prevTime - se::ScriptEngine::getInstance()->getStartTime()).count(); args.push_back(se::Value((double)(microSeconds * 0.001))); _tickVal.toObject()->call(args, nullptr); + se::ScriptEngine::getInstance()->handlePromiseExceptions(); } void EventDispatcher::dispatchResizeEvent(int width, int height) diff --git a/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp b/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp index 052a8fd1a9f..232d6eaffed 100644 --- a/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp +++ b/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.cpp @@ -372,37 +372,146 @@ namespace se { } } + /** + * Bug in v8 stacktrace: + * "handlerAddedAfterPromiseRejected" event is triggered if a resolve handler is added. + * But if no reject handler is added, then "unhandledRejectedPromise" exception will be called again, but the stacktrace this time become empty + * LastStackTrace is used to store it. + */ + void ScriptEngine::pushPromiseExeception(const v8::Local &promise, const char *event, const char *stackTrace) { + using element_type = decltype(_promiseArray)::value_type; + element_type *current; + + auto itr = std::find_if(_promiseArray.begin(), _promiseArray.end(), [&](const element_type &e) -> bool { + return std::get<0>(e)->Get(_isolate) == promise; + }); + + if (itr == _promiseArray.end()) { // Not found, create one + auto newPromise = new v8::Persistent(); + newPromise->Reset(_isolate, promise); + _promiseArray.emplace_back(std::unique_ptr>(newPromise), std::vector{}); + current = &_promiseArray.back(); + } else { + current = &(*itr); + } + + auto &exceptions = std::get<1>(*current); + if (strcmp(event, "handlerAddedAfterPromiseRejected") == 0) { + for (int i = 0; i < exceptions.size(); i++) { + if (exceptions[i].event == "unhandledRejectedPromise") { + _lastStackTrace = exceptions[i].stackTrace; + exceptions.erase(exceptions.begin() + i); + return; + } + } + } + exceptions.push_back(PromiseExceptionMsg{event, stackTrace}); + } + + void ScriptEngine::handlePromiseExceptions() { + if (_promiseArray.empty()) { + return; + } + for (auto &exceptionsPair : _promiseArray) { + auto &exceptionVector = std::get<1>(exceptionsPair); + for (const auto &exceptions : exceptionVector) { + getInstance()->callExceptionCallback("", exceptions.event.c_str(), exceptions.stackTrace.c_str()); + } + std::get<0>(exceptionsPair).get()->Reset(); + } + _promiseArray.clear(); + _lastStackTrace.clear(); + } + void ScriptEngine::onPromiseRejectCallback(v8::PromiseRejectMessage msg) { + /* Reject message contains different types, yet not every type will lead to the exception in the end. + * A detection is needed: if the reject handler is added after the promise is triggered, it's actually valid.*/ v8::Isolate *isolate = getInstance()->_isolate; v8::HandleScope scope(isolate); + v8::TryCatch tryCatch(isolate); std::stringstream ss; auto event = msg.GetEvent(); - auto value = msg.GetValue(); - const char *eventName = "[invalidatePromiseEvent]"; - - if(event == v8::kPromiseRejectWithNoHandler) { - eventName = "unhandledRejectedPromise"; - }else if(event == v8::kPromiseHandlerAddedAfterReject) { - eventName = "handlerAddedAfterPromiseRejected"; - }else if(event == v8::kPromiseRejectAfterResolved) { - eventName = "rejectAfterPromiseResolved"; - }else if( event == v8::kPromiseResolveAfterResolved) { - eventName = "resolveAfterPromiseResolved"; - } - - if(!value.IsEmpty()) { + v8::Local value = msg.GetValue(); + auto promiseName = msg.GetPromise()->GetConstructorName(); + + if (!value.IsEmpty()) { // prepend error object to stack message - v8::Local str = value->ToString(isolate->GetCurrentContext()).ToLocalChecked(); - v8::String::Utf8Value valueUtf8(isolate, str); - ss << *valueUtf8 << std::endl; + // v8::MaybeLocal maybeStr = value->ToString(isolate->GetCurrentContext()); + if (value->IsString()) { + v8::Local str = value->ToString(isolate->GetCurrentContext()).ToLocalChecked(); + + v8::String::Utf8Value valueUtf8(isolate, str); + auto *strp = *valueUtf8; + if (strp == nullptr) { + ss << "value: null" << std::endl; + auto tn = value->TypeOf(isolate); + v8::String::Utf8Value tnUtf8(isolate, tn); + strp = *tnUtf8; + ss << " type: " << strp << std::endl; + } + + } else if (value->IsObject()) { + v8::MaybeLocal json = v8::JSON::Stringify(isolate->GetCurrentContext(), value); + if (!json.IsEmpty()) { + v8::String::Utf8Value jsonStr(isolate, json.ToLocalChecked()); + auto *strp = *jsonStr; + if (strp) { + ss << " obj: " << strp << std::endl; + } else { + ss << " obj: null" << std::endl; + } + } else { + v8::Local obj = value->ToObject(isolate->GetCurrentContext()).ToLocalChecked(); + v8::Local attrNames = obj->GetOwnPropertyNames(isolate->GetCurrentContext()).ToLocalChecked(); + + if (!attrNames.IsEmpty()) { + uint32_t size = attrNames->Length(); + for (uint32_t i = 0; i < size; i++) { + se::Value e; + + + v8::Local attrName = attrNames->Get(isolate->GetCurrentContext(), i) + .ToLocalChecked() + ->ToString(isolate->GetCurrentContext()) + .ToLocalChecked(); + v8::String::Utf8Value attrUtf8(isolate, attrName); + auto *strp = *attrUtf8; + ss << " obj.property " << strp << std::endl; + } + ss << " obj: JSON.parse failed!" << std::endl; + } + } + } + v8::String::Utf8Value valuePromiseConstructor(isolate, promiseName); + auto *strp = *valuePromiseConstructor; + if (strp) { + ss << "PromiseConstructor " << strp; + } } - auto stackStr = getInstance()->getCurrentStackTrace(); ss << "stacktrace: " << std::endl; - ss << stackStr << std::endl; - getInstance()->callExceptionCallback("", eventName, ss.str().c_str()); - + if (stackStr.empty()) { + ss << getInstance()->_lastStackTrace << std::endl; + } else { + ss << stackStr << std::endl; + } + // Check event immediately, for certain case throw exception. + switch (event) { + case v8::kPromiseRejectWithNoHandler: + getInstance()->pushPromiseExeception(msg.GetPromise(), "unhandledRejectedPromise", ss.str().c_str()); + break; + case v8::kPromiseHandlerAddedAfterReject: + getInstance()->pushPromiseExeception(msg.GetPromise(), "handlerAddedAfterPromiseRejected", ss.str().c_str()); + break; + // ignore v8::kPromiseRejectAfterResolved and v8::kPromiseResolveAfterResolved, because use promise.all and promise.race can trigger this problem. the issue please visit https://forum.cocos.org/t/topic/158288 + // case v8::kPromiseRejectAfterResolved: + // getInstance()->callExceptionCallback("", "rejectAfterPromiseResolved", stackStr.c_str()); + // break; + // case v8::kPromiseResolveAfterResolved: + // getInstance()->callExceptionCallback("", "resolveAfterPromiseResolved", stackStr.c_str()); + // break; + } } void ScriptEngine::privateDataFinalize(void* nativeObj) @@ -586,6 +695,8 @@ namespace se { hook(); } _beforeCleanupHookArray.clear(); + _promiseArray.clear(); + _lastStackTrace.clear(); SAFE_DEC_REF(_globalObj); Object::cleanup(); diff --git a/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.hpp b/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.hpp index 75e07b93657..265fe4ba916 100644 --- a/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.hpp +++ b/cocos/scripting/js-bindings/jswrapper/v8/ScriptEngine.hpp @@ -287,6 +287,11 @@ namespace se { */ void mainLoopUpdate(); + /** + * @brief Handle all exceptions throwed by promise + */ + void handlePromiseExceptions(); + /** * @brief Gets script virtual machine instance ID. Default value is 1, increase by 1 if `init` is invoked. */ @@ -310,7 +315,17 @@ namespace se { static void onPromiseRejectCallback(v8::PromiseRejectMessage msg); void callExceptionCallback(const char*, const char*, const char*); + // Push promise and exception msg to _promiseArray + void pushPromiseExeception(const v8::Local &promise, const char *event, const char *stackTrace); + + // Struct to save exception info + struct PromiseExceptionMsg { + std::string event; + std::string stackTrace; + }; + std::string _lastStackTrace; + std::vector>, std::vector>> _promiseArray; std::chrono::steady_clock::time_point _startTime; std::vector _registerCallbackArray; std::vector> _beforeInitHookArray;