diff --git a/bin/ChakraCore/ChakraCore.def b/bin/ChakraCore/ChakraCore.def index ca2a01dd616..a996d102fb4 100644 --- a/bin/ChakraCore/ChakraCore.def +++ b/bin/ChakraCore/ChakraCore.def @@ -60,3 +60,5 @@ JsLessThan JsLessThanOrEqual JsCreateEnhancedFunction + +JsSetHostPromiseRejectionTracker \ No newline at end of file diff --git a/bin/ch/ChakraRtInterface.cpp b/bin/ch/ChakraRtInterface.cpp index 191fa1c499f..503d6b8c025 100644 --- a/bin/ch/ChakraRtInterface.cpp +++ b/bin/ch/ChakraRtInterface.cpp @@ -120,6 +120,7 @@ bool ChakraRTInterface::LoadChakraDll(ArgInfo* argInfo, HINSTANCE *outLibrary) m_jsApiHooks.pfJsrtGetValueType = (JsAPIHooks::JsrtGetValueType)GetChakraCoreSymbol(library, "JsGetValueType"); m_jsApiHooks.pfJsrtSetIndexedProperty = (JsAPIHooks::JsrtSetIndexedPropertyPtr)GetChakraCoreSymbol(library, "JsSetIndexedProperty"); m_jsApiHooks.pfJsrtSetPromiseContinuationCallback = (JsAPIHooks::JsrtSetPromiseContinuationCallbackPtr)GetChakraCoreSymbol(library, "JsSetPromiseContinuationCallback"); + m_jsApiHooks.pfJsrtSetHostPromiseRejectionTracker = (JsAPIHooks::JsrtSetHostPromiseRejectionTrackerPtr)GetChakraCoreSymbol(library, "JsSetHostPromiseRejectionTracker"); m_jsApiHooks.pfJsrtGetContextOfObject = (JsAPIHooks::JsrtGetContextOfObject)GetChakraCoreSymbol(library, "JsGetContextOfObject"); m_jsApiHooks.pfJsrtInitializeModuleRecord = (JsAPIHooks::JsInitializeModuleRecordPtr)GetChakraCoreSymbol(library, "JsInitializeModuleRecord"); m_jsApiHooks.pfJsrtParseModuleSource = (JsAPIHooks::JsParseModuleSourcePtr)GetChakraCoreSymbol(library, "JsParseModuleSource"); diff --git a/bin/ch/ChakraRtInterface.h b/bin/ch/ChakraRtInterface.h index 6dbc3f77bd2..c9693f288b5 100644 --- a/bin/ch/ChakraRtInterface.h +++ b/bin/ch/ChakraRtInterface.h @@ -54,6 +54,7 @@ struct JsAPIHooks typedef JsErrorCode (WINAPI *JsrtGetValueType)(JsValueRef value, JsValueType *type); typedef JsErrorCode (WINAPI *JsrtSetIndexedPropertyPtr)(JsValueRef object, JsValueRef index, JsValueRef value); typedef JsErrorCode (WINAPI *JsrtSetPromiseContinuationCallbackPtr)(JsPromiseContinuationCallback callback, void *callbackState); + typedef JsErrorCode (WINAPI *JsrtSetHostPromiseRejectionTrackerPtr)(JsHostPromiseRejectionTrackerCallback callback); typedef JsErrorCode (WINAPI *JsrtGetContextOfObject)(JsValueRef object, JsContextRef *callbackState); typedef JsErrorCode(WINAPI *JsrtDiagStartDebugging)(JsRuntimeHandle runtimeHandle, JsDiagDebugEventCallback debugEventCallback, void* callbackState); @@ -152,6 +153,7 @@ struct JsAPIHooks JsrtGetValueType pfJsrtGetValueType; JsrtSetIndexedPropertyPtr pfJsrtSetIndexedProperty; JsrtSetPromiseContinuationCallbackPtr pfJsrtSetPromiseContinuationCallback; + JsrtSetHostPromiseRejectionTrackerPtr pfJsrtSetHostPromiseRejectionTracker; JsrtGetContextOfObject pfJsrtGetContextOfObject; JsrtDiagStartDebugging pfJsrtDiagStartDebugging; JsrtDiagStopDebugging pfJsrtDiagStopDebugging; @@ -356,6 +358,7 @@ class ChakraRTInterface static JsErrorCode WINAPI JsGetValueType(JsValueRef value, JsValueType *type) { return HOOK_JS_API(GetValueType(value, type)); } static JsErrorCode WINAPI JsSetIndexedProperty(JsValueRef object, JsValueRef index, JsValueRef value) { return HOOK_JS_API(SetIndexedProperty(object, index, value)); } static JsErrorCode WINAPI JsSetPromiseContinuationCallback(JsPromiseContinuationCallback callback, void *callbackState) { return HOOK_JS_API(SetPromiseContinuationCallback(callback, callbackState)); } + static JsErrorCode WINAPI JsSetHostPromiseRejectionTracker(JsHostPromiseRejectionTrackerCallback callback) { return HOOK_JS_API(SetHostPromiseRejectionTracker(callback)); } static JsErrorCode WINAPI JsGetContextOfObject(JsValueRef object, JsContextRef* context) { return HOOK_JS_API(GetContextOfObject(object, context)); } static JsErrorCode WINAPI JsDiagStartDebugging(JsRuntimeHandle runtimeHandle, JsDiagDebugEventCallback debugEventCallback, void* callbackState) { return HOOK_JS_API(DiagStartDebugging(runtimeHandle, debugEventCallback, callbackState)); } static JsErrorCode WINAPI JsDiagStopDebugging(JsRuntimeHandle runtimeHandle, void** callbackState) { return HOOK_JS_API(DiagStopDebugging(runtimeHandle, callbackState)); } diff --git a/bin/ch/HostConfigFlagsList.h b/bin/ch/HostConfigFlagsList.h index a2fa5bd75b1..0749efa34ec 100644 --- a/bin/ch/HostConfigFlagsList.h +++ b/bin/ch/HostConfigFlagsList.h @@ -15,5 +15,6 @@ FLAG(bool, IgnoreScriptErrorCode, "Don't return error code on script e FLAG(bool, MuteHostErrorMsg, "Mute host error output, e.g. module load failures", false) FLAG(bool, TraceHostCallback, "Output traces for host callbacks", false) FLAG(bool, Test262, "load Test262 harness", false) +FLAG(bool, TrackRejectedPromises, "Enable tracking of unhandled promise rejections", false) #undef FLAG #endif diff --git a/bin/ch/WScriptJsrt.cpp b/bin/ch/WScriptJsrt.cpp index 485ed60d73c..f9b412d1734 100644 --- a/bin/ch/WScriptJsrt.cpp +++ b/bin/ch/WScriptJsrt.cpp @@ -1871,3 +1871,19 @@ void WScriptJsrt::PromiseContinuationCallback(JsValueRef task, void *callbackSta WScriptJsrt::CallbackMessage *msg = new WScriptJsrt::CallbackMessage(0, task); messageQueue->InsertSorted(msg); } + +void WScriptJsrt::PromiseRejectionTrackerCallback(JsValueRef promise, JsValueRef reason, bool handled) +{ + Assert(promise != JS_INVALID_REFERENCE); + Assert(reason != JS_INVALID_REFERENCE); + if(!handled) + { + wprintf(_u("Uncaught promise rejection\n")); + } + else + { + wprintf(_u("Promise rejection handled\n")); + } + fflush(stdout); +} + diff --git a/bin/ch/WScriptJsrt.h b/bin/ch/WScriptJsrt.h index 3ac3be5452f..4d408d9d11f 100644 --- a/bin/ch/WScriptJsrt.h +++ b/bin/ch/WScriptJsrt.h @@ -58,6 +58,7 @@ class WScriptJsrt static JsErrorCode NotifyModuleReadyCallback(_In_opt_ JsModuleRecord referencingModule, _In_opt_ JsValueRef exceptionVar); static JsErrorCode InitializeModuleCallbacks(); static void CALLBACK PromiseContinuationCallback(JsValueRef task, void *callbackState); + static void CALLBACK PromiseRejectionTrackerCallback(JsValueRef promise, JsValueRef reason, bool handled); static LPCWSTR ConvertErrorCodeToMessage(JsErrorCode errorCode) { diff --git a/bin/ch/ch.cpp b/bin/ch/ch.cpp index fbd39ecfe47..8f0aea23d30 100644 --- a/bin/ch/ch.cpp +++ b/bin/ch/ch.cpp @@ -757,6 +757,10 @@ HRESULT ExecuteTest(const char* fileName) IfFailGo(E_FAIL); } + if(HostConfigFlags::flags.TrackRejectedPromises) + { + ChakraRTInterface::JsSetHostPromiseRejectionTracker(WScriptJsrt::PromiseRejectionTrackerCallback); + } len = strlen(fullPath); if (HostConfigFlags::flags.GenerateLibraryByteCodeHeaderIsEnabled) { diff --git a/lib/Jsrt/ChakraCore.h b/lib/Jsrt/ChakraCore.h index c0c7dd2a1cb..b263bf81f8b 100644 --- a/lib/Jsrt/ChakraCore.h +++ b/lib/Jsrt/ChakraCore.h @@ -130,6 +130,21 @@ typedef struct JsNativeFunctionInfo /// The result of the call, if any. typedef _Ret_maybenull_ JsValueRef(CHAKRA_CALLBACK * JsEnhancedNativeFunction)(_In_ JsValueRef callee, _In_ JsValueRef *arguments, _In_ unsigned short argumentCount, _In_ JsNativeFunctionInfo *info, _In_opt_ void *callbackState); +/// +/// A Promise Rejection Tracker callback. +/// +/// +/// The host can specify a promise rejection tracker callback in JsSetHostPromiseRejetionTracker. +/// If a promise is rejected with no reactions or a reaction is added to a promise that was rejected +/// before it had reactions by default nothing is done. +/// A Promise Rejection Tracker callback may be set - which will then be called when this occurs. +/// +/// The promise object, represented as a JsValueRef. +/// The value/cause of the rejection, represented as a JsValueRef. +/// Boolean - false for promiseRejected: i.e. if the promise has just been rejected with no handler, +/// true for promiseHandled: i.e. if it was rejected before without a handler and is now being handled. +typedef void (CHAKRA_CALLBACK *JsHostPromiseRejectionTrackerCallback)(_In_ JsValueRef promise, _In_ JsValueRef reason, _In_ bool handled); + /// /// Creates a new enhanced JavaScript function. /// @@ -993,5 +1008,26 @@ CHAKRA_API _In_ JsValueRef object, _In_ JsValueRef key, _Out_ bool *hasOwnProperty); + + +/// +/// Sets whether any action should be taken when a promise is rejected with no reactions +/// or a reaction is added to a promise that was rejected before it had reactions. +/// By default in either of these cases nothing occurs. +/// This function allows you to specify if something should occur and provide a callback +/// to implement whatever should occur. +/// +/// +/// +/// Requires an active script context. +/// +/// +/// The callback function being set. +/// +/// The code JsNoError if the operation succeeded, a failure code otherwise. +/// +CHAKRA_API + JsSetHostPromiseRejectionTracker( + _In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback); #endif // _CHAKRACOREBUILD #endif // _CHAKRACORE_H_ diff --git a/lib/Jsrt/Jsrt.cpp b/lib/Jsrt/Jsrt.cpp index 6aed238865c..9f8a813d871 100644 --- a/lib/Jsrt/Jsrt.cpp +++ b/lib/Jsrt/Jsrt.cpp @@ -3457,6 +3457,17 @@ CHAKRA_API JsSetPromiseContinuationCallback(_In_opt_ JsPromiseContinuationCallba /*allowInObjectBeforeCollectCallback*/true); } +CHAKRA_API JsSetHostPromiseRejectionTracker(_In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback) +{ + return ContextAPINoScriptWrapper_NoRecord([&](Js::ScriptContext *scriptContext) -> JsErrorCode { + scriptContext->GetLibrary()->SetNativeHostPromiseRejectionTrackerCallback((Js::JavascriptLibrary::HostPromiseRejectionTrackerCallback) promiseRejectionTrackerCallback); + return JsNoError; + }, + /*allowInObjectBeforeCollectCallback*/true); +} + + + JsErrorCode RunScriptCore(JsValueRef scriptSource, const byte *script, size_t cb, LoadScriptFlag loadScriptFlag, JsSourceContext sourceContext, const WCHAR *sourceUrl, bool parseOnly, JsParseScriptAttributes parseAttributes, diff --git a/lib/Runtime/Library/JavascriptLibrary.cpp b/lib/Runtime/Library/JavascriptLibrary.cpp index e7b91ff2aff..9e1728dace1 100644 --- a/lib/Runtime/Library/JavascriptLibrary.cpp +++ b/lib/Runtime/Library/JavascriptLibrary.cpp @@ -5295,6 +5295,32 @@ namespace Js this->nativeHostPromiseContinuationFunctionState = state; } + void JavascriptLibrary::SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function) + { + this->nativeHostPromiseRejectionTracker = function; + this->hostShouldTrackPromiseRejections = true; + } + + void JavascriptLibrary::CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled) + { + if(hostShouldTrackPromiseRejections) + { + BEGIN_LEAVE_SCRIPT(scriptContext); + try + { + nativeHostPromiseRejectionTracker(promise, reason, handled); + } + catch (...) + { + // Hosts are required not to pass exceptions back across the callback boundary. If + // this happens, it is a bug in the host, not something that we are expected to + // handle gracefully. + Js::Throw::FatalInternalError(); + } + END_LEAVE_SCRIPT(scriptContext); + } + } + void JavascriptLibrary::SetJsrtContext(FinalizableObject* jsrtContext) { // With JsrtContext supporting cross context, ensure that it doesn't get GCed diff --git a/lib/Runtime/Library/JavascriptLibrary.h b/lib/Runtime/Library/JavascriptLibrary.h index 6d94aedaee6..53dc0806cb3 100644 --- a/lib/Runtime/Library/JavascriptLibrary.h +++ b/lib/Runtime/Library/JavascriptLibrary.h @@ -242,6 +242,7 @@ namespace Js static DWORD GetRandSeed1Offset() { return offsetof(JavascriptLibrary, randSeed1); } static DWORD GetTypeDisplayStringsOffset() { return offsetof(JavascriptLibrary, typeDisplayStrings); } typedef bool (CALLBACK *PromiseContinuationCallback)(Var task, void *callbackState); + typedef void (CALLBACK *HostPromiseRejectionTrackerCallback)(Var promise, Var reason, bool handled); Var GetUndeclBlockVar() const { return undeclBlockVarSentinel; } bool IsUndeclBlockVar(Var var) const { return var == undeclBlockVarSentinel; } @@ -492,6 +493,9 @@ namespace Js FieldNoBarrier(PromiseContinuationCallback) nativeHostPromiseContinuationFunction; Field(void *) nativeHostPromiseContinuationFunctionState; + FieldNoBarrier(HostPromiseRejectionTrackerCallback) nativeHostPromiseRejectionTracker; + FieldNoBarrier(bool) hostShouldTrackPromiseRejections = false; + typedef SList FunctionReferenceList; typedef JsUtil::WeakReferenceDictionary> JsrtExternalTypesCache; @@ -949,6 +953,9 @@ namespace Js JavascriptFunction* GetThrowerFunction() const { return throwerFunction; } void SetNativeHostPromiseContinuationFunction(PromiseContinuationCallback function, void *state); + void SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function); + void CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled); + void SetJsrtContext(FinalizableObject* jsrtContext); FinalizableObject* GetJsrtContext(); diff --git a/lib/Runtime/Library/JavascriptPromise.cpp b/lib/Runtime/Library/JavascriptPromise.cpp index 27a33e62bcf..07a1424d2c8 100644 --- a/lib/Runtime/Library/JavascriptPromise.cpp +++ b/lib/Runtime/Library/JavascriptPromise.cpp @@ -12,6 +12,7 @@ namespace Js Assert(type->GetTypeId() == TypeIds_Promise); this->status = PromiseStatusCode_Undefined; + this->isHandled = false; this->result = nullptr; this->resolveReactions = nullptr; this->rejectReactions = nullptr; @@ -660,6 +661,10 @@ namespace Js { reactions = this->GetRejectReactions(); newStatus = PromiseStatusCode_HasRejection; + if(!GetIsHandled()) + { + scriptContext->GetLibrary()->CallNativeHostPromiseRejectionTracker(this, resolution, false); + } } else { @@ -838,6 +843,10 @@ namespace Js EnqueuePromiseReactionTask(resolveReaction, sourcePromise->result, scriptContext); break; case PromiseStatusCode_HasRejection: + if(!sourcePromise->GetIsHandled()) + { + scriptContext->GetLibrary()->CallNativeHostPromiseRejectionTracker(sourcePromise, sourcePromise->result, true); + } EnqueuePromiseReactionTask(rejectReaction, sourcePromise->result, scriptContext); break; default: @@ -845,6 +854,8 @@ namespace Js break; } + sourcePromise->SetIsHandled(); + return promiseCapability->GetPromise(); } diff --git a/lib/Runtime/Library/JavascriptPromise.h b/lib/Runtime/Library/JavascriptPromise.h index ae3b6791eb8..6d78aa81889 100644 --- a/lib/Runtime/Library/JavascriptPromise.h +++ b/lib/Runtime/Library/JavascriptPromise.h @@ -461,6 +461,8 @@ namespace Js PromiseStatusCode_HasRejection }; + bool GetIsHandled() {return isHandled;} + void SetIsHandled() {isHandled = true;} PromiseStatus GetStatus() const { return status; } Var GetResult() const { return result; } @@ -470,6 +472,7 @@ namespace Js protected: Field(PromiseStatus) status; Field(Var) result; + Field(bool) isHandled; Field(JavascriptPromiseReactionList*) resolveReactions; Field(JavascriptPromiseReactionList*) rejectReactions; diff --git a/test/es7/PromiseRejectionTracking.baseline b/test/es7/PromiseRejectionTracking.baseline new file mode 100644 index 00000000000..059778f2510 --- /dev/null +++ b/test/es7/PromiseRejectionTracking.baseline @@ -0,0 +1,19 @@ +Executing test #1 - Reject promise with no reactions. +Uncaught promise rejection +Executing test #2 - Reject promise with a catch reaction only. +Executing test #3 - Reject promise with catch and then reactions. +Executing test #4 - Reject promise then add a catch afterwards. +Uncaught promise rejection +Promise rejection handled +Executing test #5 - Reject promise then add two catches afterwards. +Uncaught promise rejection +Promise rejection handled +Executing test #6 - Async function that throws. +Uncaught promise rejection +Executing test #7 - Async function that throws but is caught. +Uncaught promise rejection +Promise rejection handled +Executing test #8 - Async function that awaits a function that throws. +Uncaught promise rejection +Promise rejection handled +Uncaught promise rejection diff --git a/test/es7/PromiseRejectionTracking.js b/test/es7/PromiseRejectionTracking.js new file mode 100644 index 00000000000..886c0c283d7 --- /dev/null +++ b/test/es7/PromiseRejectionTracking.js @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +// Test HostPromiseRejectionTracker - see ecma262 section 25.4.1.9 + +let tests = [ + { + name: "Reject promise with no reactions.", + body: function(index) + { + let controller; + let promise = new Promise((resolve, reject)=>{ + controller = {resolve, reject}; + }); + controller.reject();//Should notify rejected + } + }, + { + name: "Reject promise with a catch reaction only.", + body: function(index) + { + let controller; + let promise = new Promise((resolve, reject)=>{ + controller = {resolve, reject}; + }).catch(()=>{}); + controller.reject();//Should NOT notify + } + }, + { + name: "Reject promise with catch and then reactions.", + body: function(index) + { + let controller; + let promise = new Promise((resolve, reject)=>{ + controller = {resolve, reject}; + }).then(()=>{}).catch(()=>{}); + controller.reject();//Should NOT notify + } + }, + { + name: "Reject promise then add a catch afterwards.", + body: function(index) + { + let controller; + let promise = new Promise((resolve, reject)=>{ + controller = {resolve, reject}; + }); + controller.reject();//Should notify rejected + promise.catch(()=>{});//Should notify handled + } + }, + { + name: "Reject promise then add two catches afterwards.", + body: function(index) + { + let controller; + let promise = new Promise((resolve, reject)=>{ + controller = {resolve, reject}; + }); + controller.reject();//Should notify rejected + promise.catch(()=>{});//Should notify handled + promise.catch(()=>{});//Should NOT notify + } + }, + { + name: "Async function that throws.", + body: function(index) + { + async function aFunction() + { + throw "throwing"; + } + aFunction();//Should notify rejected + } + }, + { + name: "Async function that throws but is caught.", + body: function(index) + { + async function aFunction() + { + throw "throwing"; + } + aFunction().catch(()=>{});//Should notify rejected AND then handled + } + }, + { + name: "Async function that awaits a function that throws.", + body: function(index) + { + async function aFunction() + { + throw "throwing";//Should notify rejected + } + async function bFunction() + { + await aFunction();//Should notify handled + } + bFunction();//Should notify rejected + } + } +]; + +for(let i = 0; i < tests.length; ++i) +{ + WScript.Echo('Executing test #' + (i + 1) + ' - ' + tests[i].name); + tests[i].body(i+1); +} diff --git a/test/es7/rlexe.xml b/test/es7/rlexe.xml index 9a23c30fd1c..8c34409e4c2 100644 --- a/test/es7/rlexe.xml +++ b/test/es7/rlexe.xml @@ -92,4 +92,11 @@ exclude_xplat + + + PromiseRejectionTracking.js + -TrackRejectedPromises -args summary -endargs -nodeferparse + PromiseRejectionTracking.baseline + +