Просмотр исходного кода

HostPromiseRejection tracker:
1. Internal CC machinary for unhandled promise rejection.
2. Simplisitic implimentation in ch
3. Test case

rhuanjl 8 лет назад
Родитель
Сommit
d36457c881

+ 2 - 0
bin/ChakraCore/ChakraCore.def

@@ -62,3 +62,5 @@ JsLessThan
 JsLessThanOrEqual
 
 JsCreateEnhancedFunction
+
+JsSetHostPromiseRejectionTracker

+ 1 - 0
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");

+ 3 - 0
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, void *callbackState);
     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 @@ public:
     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, void *callbackState) { return HOOK_JS_API(SetHostPromiseRejectionTracker(callback, callbackState)); }
     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)); }

+ 1 - 0
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

+ 29 - 0
bin/ch/WScriptJsrt.cpp

@@ -1726,3 +1726,32 @@ 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, void *callbackState)
+{
+    Assert(promise != JS_INVALID_REFERENCE);
+    Assert(reason != JS_INVALID_REFERENCE);
+    JsValueRef strValue;
+    JsErrorCode error = ChakraRTInterface::JsConvertValueToString(reason, &strValue);
+
+    if (!handled)
+    {
+        wprintf(_u("Uncaught promise rejection\n"));
+    }
+    else
+    {
+        wprintf(_u("Promise rejection handled\n"));
+    }
+
+    if (error == JsNoError)
+    {
+        AutoString str(strValue);
+        if (str.GetError() == JsNoError)
+        {
+            wprintf(_u("%ls\n"), str.GetWideString());
+        }
+    }
+
+    fflush(stdout);
+}
+

+ 1 - 0
bin/ch/WScriptJsrt.h

@@ -58,6 +58,7 @@ public:
     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, void *callbackState);
 
     static LPCWSTR ConvertErrorCodeToMessage(JsErrorCode errorCode)
     {

+ 5 - 0
bin/ch/ch.cpp

@@ -757,6 +757,11 @@ HRESULT ExecuteTest(const char* fileName)
             IfFailGo(E_FAIL);
         }
 
+        if (HostConfigFlags::flags.TrackRejectedPromises)
+        {
+            ChakraRTInterface::JsSetHostPromiseRejectionTracker(WScriptJsrt::PromiseRejectionTrackerCallback, nullptr);
+        }
+        
         len = strlen(fullPath);
         if (HostConfigFlags::flags.GenerateLibraryByteCodeHeaderIsEnabled)
         {

+ 43 - 0
lib/Jsrt/ChakraCore.h

@@ -129,6 +129,27 @@ typedef struct JsNativeFunctionInfo
 /// <returns>The result of the call, if any.</returns>
 typedef _Ret_maybenull_ JsValueRef(CHAKRA_CALLBACK * JsEnhancedNativeFunction)(_In_ JsValueRef callee, _In_ JsValueRef *arguments, _In_ unsigned short argumentCount, _In_ JsNativeFunctionInfo *info, _In_opt_ void *callbackState);
 
+/// <summary>
+///     A Promise Rejection Tracker callback.
+/// </summary>
+/// <remarks>
+///     The host can specify a promise rejection tracker callback in <c>JsSetHostPromiseRejectionTracker</c>.
+///     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.
+///     Note - per draft ECMASpec 2018 25.4.1.9 this function should not set or return an exception
+///     Note also the promise and reason parameters may be garbage collected after this function is called
+///     if you wish to make further use of them you will need to use JsAddRef to preserve them
+///     However if you use JsAddRef you must also call JsRelease and not hold unto them after 
+///     a handled notification (both per spec and to avoid memory leaks)
+/// </remarks>
+/// <param name="promise">The promise object, represented as a JsValueRef.</param>
+/// <param name="reason">The value/cause of the rejection, represented as a JsValueRef.</param>
+/// <param name="handled">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.</param>
+/// <param name="callbackState">The state passed to <c>JsSetHostPromiseRejectionTracker</c>.</param>
+typedef void (CHAKRA_CALLBACK *JsHostPromiseRejectionTrackerCallback)(_In_ JsValueRef promise, _In_ JsValueRef reason, _In_ bool handled, _In_opt_ void *callbackState);
+
 /// <summary>
 ///     Creates a new enhanced JavaScript function.
 /// </summary>
@@ -992,5 +1013,27 @@ CHAKRA_API
         _In_ JsValueRef object,
         _In_ JsValueRef key,
         _Out_ bool *hasOwnProperty);
+
+/// <summary>
+///     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.
+/// </summary>
+/// <remarks>
+///     Requires an active script context.
+/// </remarks>
+/// <param name="promiseRejectionTrackerCallback">The callback function being set.</param>
+/// <param name="callbackState">
+///     User provided state that will be passed back to the callback.
+/// </param>
+/// <returns>
+///     The code <c>JsNoError</c> if the operation succeeded, a failure code otherwise.
+/// </returns>
+CHAKRA_API
+    JsSetHostPromiseRejectionTracker(
+        _In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback, 
+        _In_opt_ void *callbackState);
 #endif // _CHAKRACOREBUILD
 #endif // _CHAKRACORE_H_

+ 9 - 0
lib/Jsrt/Jsrt.cpp

@@ -5331,4 +5331,13 @@ CHAKRA_API JsGetDataViewInfo(
     END_JSRT_NO_EXCEPTION
 }
 
+CHAKRA_API JsSetHostPromiseRejectionTracker(_In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback, _In_opt_ void *callbackState)
+{
+  return ContextAPINoScriptWrapper_NoRecord([&](Js::ScriptContext *scriptContext) -> JsErrorCode {
+    scriptContext->GetLibrary()->SetNativeHostPromiseRejectionTrackerCallback((Js::JavascriptLibrary::HostPromiseRejectionTrackerCallback) promiseRejectionTrackerCallback, callbackState);
+    return JsNoError;
+  },
+    /*allowInObjectBeforeCollectCallback*/true);
+}
+
 #endif // _CHAKRACOREBUILD

+ 26 - 0
lib/Runtime/Library/JavascriptLibrary.cpp

@@ -5300,6 +5300,32 @@ namespace Js
         this->nativeHostPromiseContinuationFunctionState = state;
     }
 
+    void JavascriptLibrary::SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function, void *state)
+    {
+        this->nativeHostPromiseRejectionTracker = function;
+        this->nativeHostPromiseContinuationFunctionState = state;
+    }
+
+    void JavascriptLibrary::CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled)
+    {
+        if (this->nativeHostPromiseRejectionTracker != nullptr)
+        {
+            BEGIN_LEAVE_SCRIPT(scriptContext);
+            try
+            {
+               this->nativeHostPromiseRejectionTracker(promise, reason, handled, this->nativeHostPromiseContinuationFunctionState);
+            }
+            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

+ 6 - 0
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, void *callbackState);
 
         Var GetUndeclBlockVar() const { return undeclBlockVarSentinel; }
         bool IsUndeclBlockVar(Var var) const { return var == undeclBlockVarSentinel; }
@@ -491,6 +492,9 @@ namespace Js
         FieldNoBarrier(PromiseContinuationCallback) nativeHostPromiseContinuationFunction;
         Field(void *) nativeHostPromiseContinuationFunctionState;
 
+        FieldNoBarrier(HostPromiseRejectionTrackerCallback) nativeHostPromiseRejectionTracker = nullptr;
+        Field(void *) nativeHostPromiseRejectionTrackerState;
+
         typedef SList<Js::FunctionProxy*, Recycler> FunctionReferenceList;
         typedef JsUtil::WeakReferenceDictionary<uintptr_t, DynamicType, DictionarySizePolicy<PowerOf2Policy, 1>> JsrtExternalTypesCache;
 
@@ -948,6 +952,8 @@ namespace Js
         JavascriptFunction* GetThrowerFunction() const { return throwerFunction; }
 
         void SetNativeHostPromiseContinuationFunction(PromiseContinuationCallback function, void *state);
+        void SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function, void *state);
+        void CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled);
 
         void SetJsrtContext(FinalizableObject* jsrtContext);
         FinalizableObject* GetJsrtContext();

+ 11 - 0
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();
     }
 

+ 3 - 0
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; }
 
@@ -469,6 +471,7 @@ namespace Js
 
     protected:
         Field(PromiseStatus) status;
+        Field(bool) isHandled;
         Field(Var) result;
         Field(JavascriptPromiseReactionList*) resolveReactions;
         Field(JavascriptPromiseReactionList*) rejectReactions;

+ 43 - 0
test/es7/PromiseRejectionTracking.baseline

@@ -0,0 +1,43 @@
+Executing test #1 - Reject promise with no reactions.
+Uncaught promise rejection
+Rejection from test 1
+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
+Rejection from test 4
+Promise rejection handled
+Rejection from test 4
+Executing test #5 - Reject promise then add two catches afterwards.
+Uncaught promise rejection
+Rejection from test 5
+Promise rejection handled
+Rejection from test 5
+Executing test #6 - Async function that throws.
+Uncaught promise rejection
+Rejection from test 6
+Executing test #7 - Async function that throws but is caught.
+Uncaught promise rejection
+Rejection from test 7
+Promise rejection handled
+Rejection from test 7
+Executing test #8 - Async function that awaits a function that throws.
+Uncaught promise rejection
+Rejection from test 8
+Promise rejection handled
+Rejection from test 8
+Executing test #9 - Reject a handled promise then handle one of the handles but not the other.
+Executing test #10 - Reject a handled promise and don't handle either path.
+Begin async results:
+Uncaught promise rejection
+Rejection from test 8
+Uncaught promise rejection
+Rejection from test 9
+Uncaught promise rejection
+Rejection from test 10
+Uncaught promise rejection
+Rejection from test 10
+Promise rejection handled
+Rejection from test 9
+Uncaught promise rejection
+Rejection from test 9

+ 145 - 0
test/es7/PromiseRejectionTracking.js

@@ -0,0 +1,145 @@
+//-------------------------------------------------------------------------------------------------------
+// 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("Rejection from test " +  index);//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("Rejection from test " +  index);//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("Rejection from test " +  index);//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("Rejection from test " +  index);//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("Rejection from test " +  index);//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 ("Rejection from test " +  index);
+            }
+            aFunction();//Should notify rejected
+        }
+    },
+    {
+        name: "Async function that throws but is caught.",
+        body: function(index)
+        {
+            async function aFunction()
+            {
+                throw ("Rejection from test " +  index);
+            }
+            aFunction().catch(()=>{});//Should notify rejected AND then handled
+        }
+    },
+    {
+        name: "Async function that awaits a function that throws.",
+        body: function(index)
+        {
+            async function aFunction()
+            {
+                throw ("Rejection from test " +  index);//Should notify rejected
+            }
+            async function bFunction()
+            {
+                await aFunction();//Should notify handled
+            }
+            bFunction();//Should notify rejected in the async section
+        },
+    },
+    {
+        name: "Reject a handled promise then handle one of the handles but not the other.",
+        body: function(index)
+        {
+            let controller;
+            let promise = new Promise((resolve, reject) => {  controller = {resolve, reject};});
+            let a = promise.then(() => {});//a is not handled
+            let b = promise.then(() => {});//b is not handled
+            controller.reject("Rejection from test " +  index);//no notification as handled
+
+            let c = a.then(() => {}); //handle a
+
+            c.catch(() => {b.then(()=>{})}); // handle c
+            //b is still not handled -> notify once in async section
+            //b has an async handler -> will notify handled in async section
+            //the .then() on b is not handled so will notify in async section
+        },
+    },
+    {
+        name: "Reject a handled promise and don't handle either path.",
+        body: function(index)
+        {
+            let controller;
+            let promise = new Promise((resolve, reject) => {  controller = {resolve, reject};});
+            let a = promise.then(() => {});//a is not handled
+            let b = promise.then(() => {});//b is not handled
+            controller.reject("Rejection from test " +  index);//no notification as handled
+
+            let c = a.then(() => {}); //handle a
+
+            //b is not handled -> will notify in async section
+            //c is not handled -> will notify in async section
+        }
+    }
+];
+
+for(let i = 0; i < tests.length; ++i)
+{
+    WScript.Echo('Executing test #' + (i + 1) + ' - ' + tests[i].name);
+    tests[i].body(i+1);
+}
+WScript.Echo("Begin async results:");

+ 8 - 0
test/es7/rlexe.xml

@@ -91,4 +91,12 @@
       <compile-flags>-ESSharedArrayBuffer -args summary -endargs</compile-flags>
     </default>
   </test>
+    <test>
+    <default>
+      <files>PromiseRejectionTracking.js</files>
+      <compile-flags>-TrackRejectedPromises -args summary -endargs -nodeferparse</compile-flags>
+      <baseline>PromiseRejectionTracking.baseline</baseline>
+      <tags>exclude_jshost</tags>
+    </default>
+  </test>
 </regress-exe>