public int RecordInvocation(bool captureCallStack) { _totalInvocationCount++; if (_firstInvocationInfo == null) { _firstInvocationInfo = InvocationInfo.Capture(captureCallStack); } return(_totalInvocationCount); }
/// <summary> /// Returns an Action that determines whether SynchronizationContext.Send or Post was called after the underlying request finished. /// The instrumentation can be a performance hit, so this method should not be called if AppVerifier is not enabled. /// </summary> /// <param name="syncContext">The ISyncContext (HttpApplication, WebSocketPipeline, etc.) on which to perform the check.</param> /// <param name="errorHandler">The listener that can handle verification failures.</param> /// <returns>A callback which performs the verification.</returns> internal static Action GetSyncContextCheckDelegateImpl(ISyncContext syncContext, Action <AppVerifierException> errorHandler) { Uri requestUrl = null; object originalThreadContextId = null; // collect all of the diagnostic information upfront HttpContext originalHttpContext = (syncContext != null) ? syncContext.HttpContext : null; if (originalHttpContext != null) { if (!originalHttpContext.HideRequestResponse && originalHttpContext.Request != null) { requestUrl = originalHttpContext.Request.Unvalidated.Url; } // This will be used as a surrogate for the captured HttpContext so that we don't // have a long-lived reference to a heavy object graph. See comments on ThreadContextId // for more info. originalThreadContextId = originalHttpContext.ThreadContextId; originalHttpContext = null; } // If the condition passed to this method evaluates to false, we will raise an error to whoever is listening. AssertDelegate assert = (condition, errorCode) => { long mask = 1L << (int)errorCode; // assert only if it was not masked out by a bit set bool enableAssert = (AppVerifierErrorCodeEnableAssertMask & mask) == mask; if (!condition && enableAssert) { // capture the stack only if it was not masked out by a bit set bool captureStack = (AppVerifierErrorCodeCollectCallStackMask & mask) == mask; InvocationInfo assertInvocationInfo = InvocationInfo.Capture(captureStack); // header StringBuilder errorString = new StringBuilder(); errorString.AppendLine(FormatErrorString(SR.AppVerifier_Title)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_Subtitle)); errorString.AppendLine(); // basic info (about the assert) errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_URL, requestUrl)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ErrorCode, (int)errorCode)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_Description, GetLocalizedDescriptionStringForError(errorCode))); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ThreadInfo, assertInvocationInfo.ThreadId, assertInvocationInfo.Timestamp.ToLocalTime())); errorString.AppendLine(assertInvocationInfo.StackTrace.ToString()); AppVerifierException ex = new AppVerifierException(errorCode, errorString.ToString()); errorHandler(ex); throw ex; } }; return(() => { try { // Make sure that the ISyncContext is still associated with the same HttpContext that // we captured earlier. HttpContext currentHttpContext = (syncContext != null) ? syncContext.HttpContext : null; object currentThreadContextId = (currentHttpContext != null) ? currentHttpContext.ThreadContextId : null; assert(currentThreadContextId != null && ReferenceEquals(originalThreadContextId, currentThreadContextId), AppVerifierErrorCode.SyncContextSendOrPostCalledAfterRequestCompleted); } catch (AppVerifierException) { // We want to ---- any exceptions thrown by our verification logic, as the failure // has already been recorded by the appropriate listener. Propagate the original // exception upward. } }); }
/// <summary> /// Wraps the Begin* part of a Begin / End method pair to allow for signaling when assertions have been violated. /// The instrumentation can be a performance hit, so this method should not be called if AppVerifier is not enabled. /// </summary> /// <param name="httpApplication">The HttpApplication instance for this request, used to get HttpContext and related items.</param> /// <param name="beginMethod">The Begin* part of a Begin / End method pair, likely wrapped in a lambda so only the AsyncCallback and object parameters are exposed.</param> /// <param name="originalDelegate">The original user-provided delegate, e.g. the thing that 'beginMethod' wraps. Provided so that we can show correct methods when asserting.</param> /// <param name="errorHandler">The listener that can handle verification failures.</param> /// <returns>The instrumented Begin* method.</returns> internal static Func <AsyncCallback, object, IAsyncResult> WrapBeginMethodImpl(HttpApplication httpApplication, Func <AsyncCallback, object, IAsyncResult> beginMethod, Delegate originalDelegate, Action <AppVerifierException> errorHandler, CallStackCollectionBitMasks callStackMask) { return((callback, state) => { // basic diagnostic info goes at the top since it's used during generation of the error message AsyncCallbackInvocationHelper asyncCallbackInvocationHelper = new AsyncCallbackInvocationHelper(); CallStackCollectionBitMasks myBeginMask = callStackMask & CallStackCollectionBitMasks.AllBeginMask; bool captureBeginStack = (myBeginMask & (CallStackCollectionBitMasks)AppVerifierCollectCallStackMask) == myBeginMask; InvocationInfo beginHandlerInvocationInfo = InvocationInfo.Capture(captureBeginStack); Uri requestUrl = null; RequestNotification?currentNotification = null; bool isPostNotification = false; Type httpHandlerType = null; // need to collect all this up-front since it might go away during the async operation if (httpApplication != null) { HttpContext context = httpApplication.Context; if (context != null) { if (!context.HideRequestResponse && context.Request != null) { requestUrl = context.Request.Unvalidated.Url; } if (context.NotificationContext != null) { currentNotification = context.NotificationContext.CurrentNotification; isPostNotification = context.NotificationContext.IsPostNotification; } if (context.Handler != null) { httpHandlerType = context.Handler.GetType(); } } } // If the condition passed to this method evaluates to false, we will raise an error to whoever is listening. AssertDelegate assert = (condition, errorCode) => { long mask = 1L << (int)errorCode; // assert only if it was not masked out by a bit set bool enableAssert = (AppVerifierErrorCodeEnableAssertMask & mask) == mask; if (!condition && enableAssert) { // capture the stack only if it was not masked out by a bit set bool captureStack = (AppVerifierErrorCodeCollectCallStackMask & mask) == mask; InvocationInfo assertInvocationInfo = InvocationInfo.Capture(captureStack); // header StringBuilder errorString = new StringBuilder(); errorString.AppendLine(FormatErrorString(SR.AppVerifier_Title)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_Subtitle)); errorString.AppendLine(); // basic info (about the assert) errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_URL, requestUrl)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ErrorCode, (int)errorCode)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_Description, GetLocalizedDescriptionStringForError(errorCode))); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BasicInfo_ThreadInfo, assertInvocationInfo.ThreadId, assertInvocationInfo.Timestamp.ToLocalTime())); errorString.AppendLine(assertInvocationInfo.StackTrace.ToString()); // Begin* method info errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_EntryMethod, PrettyPrintDelegate(originalDelegate))); if (currentNotification != null) { errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_RequestNotification_Integrated, currentNotification, isPostNotification)); } else { errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_RequestNotification_NotIntegrated)); } errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_CurrentHandler, httpHandlerType)); errorString.AppendLine(FormatErrorString(SR.AppVerifier_BeginMethodInfo_ThreadInfo, beginHandlerInvocationInfo.ThreadId, beginHandlerInvocationInfo.Timestamp.ToLocalTime())); errorString.AppendLine(beginHandlerInvocationInfo.StackTrace.ToString()); // AsyncCallback info int totalAsyncInvocationCount; InvocationInfo firstAsyncInvocation = asyncCallbackInvocationHelper.GetFirstInvocationInfo(out totalAsyncInvocationCount); errorString.AppendLine(FormatErrorString(SR.AppVerifier_AsyncCallbackInfo_InvocationCount, totalAsyncInvocationCount)); if (firstAsyncInvocation != null) { errorString.AppendLine(FormatErrorString(SR.AppVerifier_AsyncCallbackInfo_FirstInvocation_ThreadInfo, firstAsyncInvocation.ThreadId, firstAsyncInvocation.Timestamp.ToLocalTime())); errorString.AppendLine(firstAsyncInvocation.StackTrace.ToString()); } AppVerifierException ex = new AppVerifierException(errorCode, errorString.ToString()); errorHandler(ex); throw ex; } }; assert(httpApplication != null, AppVerifierErrorCode.HttpApplicationInstanceWasNull); assert(originalDelegate != null, AppVerifierErrorCode.BeginHandlerDelegateWasNull); object lockObj = new object(); // used to synchronize access to certain locals which can be touched by multiple threads IAsyncResult asyncResultReturnedByBeginHandler = null; IAsyncResult asyncResultPassedToCallback = null; object beginHandlerReturnValueHolder = null; // used to hold the IAsyncResult returned by or Exception thrown by BeginHandler; see comments on Holder<T> for more info Thread threadWhichCalledBeginHandler = Thread.CurrentThread; // used to determine whether the callback was invoked synchronously bool callbackRanToCompletion = false; // don't need to lock when touching this local since it's only read in the synchronous case HttpContext assignedContextUponCallingBeginHandler = httpApplication.Context; // used to determine whether the underlying request disappeared try { asyncResultReturnedByBeginHandler = beginMethod( asyncResult => { try { CallStackCollectionBitMasks myCallbackMask = callStackMask & CallStackCollectionBitMasks.AllCallbackMask; bool captureEndCallStack = (myCallbackMask & AppVerifierCollectCallStackMask) == myCallbackMask; // The callback must never be called more than once. int newAsyncCallbackInvocationCount = asyncCallbackInvocationHelper.RecordInvocation(captureEndCallStack); assert(newAsyncCallbackInvocationCount == 1, AppVerifierErrorCode.AsyncCallbackInvokedMultipleTimes); // The 'asyncResult' parameter must never be null. assert(asyncResult != null, AppVerifierErrorCode.AsyncCallbackInvokedWithNullParameter); object tempBeginHandlerReturnValueHolder; Thread tempThreadWhichCalledBeginHandler; lock (lockObj) { asyncResultPassedToCallback = asyncResult; tempBeginHandlerReturnValueHolder = beginHandlerReturnValueHolder; tempThreadWhichCalledBeginHandler = threadWhichCalledBeginHandler; } // At this point, 'IsCompleted = true' is mandatory. assert(asyncResult.IsCompleted, AppVerifierErrorCode.AsyncCallbackGivenAsyncResultWhichWasNotCompleted); if (tempBeginHandlerReturnValueHolder == null) { // BeginHandler hasn't yet returned, so this call may be synchronous or asynchronous. // We can tell by comparing the current thread with the thread which called BeginHandler. // From a correctness perspective, it is valid to invoke the AsyncCallback delegate either // synchronously or asynchronously. From [....]: if 'CompletedSynchronously = true', then // AsyncCallback invocation can happen either on the same thread or on a different thread, // just as long as BeginHandler hasn't yet returned (which in true in this case). if (!asyncResult.CompletedSynchronously) { // If 'CompletedSynchronously = false', we must be on a different thread than the BeginHandler invocation. assert(tempThreadWhichCalledBeginHandler != Thread.CurrentThread, AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyButAsyncResultWasNotMarkedCompletedSynchronously); } } else { // BeginHandler already returned, so this invocation is definitely asynchronous. Holder <IAsyncResult> asyncResultHolder = tempBeginHandlerReturnValueHolder as Holder <IAsyncResult>; if (asyncResultHolder != null) { // We need to verify that the IAsyncResult we're given is the same that was returned by BeginHandler // and that the IAsyncResult is marked 'CompletedSynchronously = false'. assert(asyncResult == asyncResultHolder.Value, AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultInstance); assert(!asyncResult.CompletedSynchronously, AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyButAsyncResultWasMarkedCompletedSynchronously); } else { // If we reached this point, BeginHandler threw an exception. // The AsyncCallback should never be invoked if BeginHandler has already failed. assert(false, AppVerifierErrorCode.BeginHandlerThrewThenAsyncCallbackInvokedAsynchronously); } } // AsyncState must match the 'state' parameter passed to BeginHandler assert(asyncResult.AsyncState == state, AppVerifierErrorCode.AsyncCallbackInvokedWithUnexpectedAsyncResultAsyncState); // Make sure the underlying HttpApplication is still assigned to the captured HttpContext instance. // If not, this AsyncCallback invocation could end up completing *some other request's* operation, // resulting in data corruption. assert(assignedContextUponCallingBeginHandler == httpApplication.Context, AppVerifierErrorCode.AsyncCallbackCalledAfterHttpApplicationReassigned); } catch (AppVerifierException) { // We want to ---- any exceptions thrown by our verification logic, as the failure // has already been recorded by the appropriate listener. Just go straight to // invoking the callback. } // all checks complete - delegate control to the actual callback if (callback != null) { callback(asyncResult); } callbackRanToCompletion = true; }, state); // The return value must never be null. assert(asyncResultReturnedByBeginHandler != null, AppVerifierErrorCode.BeginHandlerReturnedNull); lock (lockObj) { beginHandlerReturnValueHolder = new Holder <IAsyncResult>(asyncResultReturnedByBeginHandler); } if (asyncResultReturnedByBeginHandler.CompletedSynchronously) { // If 'CompletedSynchronously = true', the IAsyncResult must be marked 'IsCompleted = true' // and the AsyncCallback must have been invoked synchronously (checked in the AsyncCallback verification logic). assert(asyncResultReturnedByBeginHandler.IsCompleted, AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButWhichWasNotCompleted); assert(asyncCallbackInvocationHelper.TotalInvocations != 0, AppVerifierErrorCode.BeginHandlerReturnedAsyncResultMarkedCompletedSynchronouslyButAsyncCallbackNeverCalled); } IAsyncResult tempAsyncResultPassedToCallback; lock (lockObj) { tempAsyncResultPassedToCallback = asyncResultPassedToCallback; } // The AsyncCallback may have been invoked (either synchronously or asynchronously). If it has been // invoked, we need to verify that it was given the same IAsyncResult returned by BeginHandler. // If the AsyncCallback hasn't yet been called, we skip this check, as the AsyncCallback verification // logic will eventually perform the check at the appropriate time. if (tempAsyncResultPassedToCallback != null) { assert(tempAsyncResultPassedToCallback == asyncResultReturnedByBeginHandler, AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultInstance); } // AsyncState must match the 'state' parameter passed to BeginHandler assert(asyncResultReturnedByBeginHandler.AsyncState == state, AppVerifierErrorCode.BeginHandlerReturnedUnexpectedAsyncResultAsyncState); // all checks complete return asyncResultReturnedByBeginHandler; } catch (AppVerifierException) { // We want to ---- any exceptions thrown by our verification logic, as the failure // has already been recorded by the appropriate listener. Just return the original // IAsyncResult so that the application continues to run. return asyncResultReturnedByBeginHandler; } catch (Exception ex) { if (asyncResultReturnedByBeginHandler == null) { // If we reached this point, an exception was thrown by BeginHandler, so we need to // record it and rethrow it. IAsyncResult tempAsyncResultPassedToCallback; lock (lockObj) { beginHandlerReturnValueHolder = new Holder <Exception>(ex); tempAsyncResultPassedToCallback = asyncResultPassedToCallback; } try { // The AsyncCallback should only be invoked if BeginHandler ran to completion. if (tempAsyncResultPassedToCallback != null) { // If AsyncCallback was invoked asynchronously, then by definition it was // scheduled prematurely since BeginHandler hadn't yet run to completion // (since whatever additional work it did after invoking the callback failed). // Therefore it is always wrong for BeginHandler to both throw and // asynchronously invoke AsyncCallback. assert(tempAsyncResultPassedToCallback.CompletedSynchronously, AppVerifierErrorCode.AsyncCallbackInvokedAsynchronouslyThenBeginHandlerThrew); // If AsyncCallback was invoked synchronously, then it must have been invoked // before BeginHandler surfaced the exception (since otherwise BeginHandler // wouldn't have reached the line of code that invoked AsyncCallback). But // AsyncCallback itself could have thrown, bubbling the exception up through // BeginHandler and back to us. If AsyncCallback ran to completion, then this // means BeginHandler did extra work (which failed) after invoking AsyncCallback, // so BeginHandler by definition hadn't yet run to completion. assert(!callbackRanToCompletion, AppVerifierErrorCode.AsyncCallbackInvokedSynchronouslyThenBeginHandlerThrew); } } catch (AppVerifierException) { // We want to ---- any exceptions thrown by our verification logic, as the failure // has already been recorded by the appropriate listener. Propagate the original // exception upward. } throw; } else { // We want to ---- any exceptions thrown by our verification logic, as the failure // has already been recorded by the appropriate listener. Just return the original // IAsyncResult so that the application continues to run. return asyncResultReturnedByBeginHandler; } } finally { // Since our local variables are GC-rooted in an anonymous object, we should // clear references to objects we no longer need so that the GC can reclaim // them if appropriate. lock (lockObj) { threadWhichCalledBeginHandler = null; } } }); }