|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ErrorReporting
{
/// <summary>
/// Thrown when async code must cancel the current execution but does not have access to the <see cref="CancellationTokenSource"/> of the <see cref="CancellationToken"/> passed to the code.
/// Should be used in very rare cases where the <see cref="CancellationTokenSource"/> is out of our control (e.g. owned but not exposed by JSON RPC in certain call-back scenarios).
/// </summary>
internal sealed class OperationCanceledIgnoringCallerTokenException : OperationCanceledException
{
public OperationCanceledIgnoringCallerTokenException(Exception innerException)
: base(innerException.Message, innerException)
{
}
}
internal static class FatalError
{
public delegate void ErrorReporterHandler(Exception exception, ErrorSeverity severity, bool forceDump);
private static ErrorReporterHandler? s_handler;
private static ErrorReporterHandler? s_nonFatalHandler;
#pragma warning disable IDE0052 // Remove unread private members - We want to hold onto last exception to make investigation easier
private static Exception? s_reportedException;
private static string? s_reportedExceptionMessage;
#pragma warning restore IDE0052
/// <summary>
/// Set by the host to handle an error report; this may crash the process or report telemetry.
/// </summary>
/// <param name="nonFatalHandler">A handler that will not crash the process when called. Used when calling <see
/// cref="ReportNonFatalError(Exception, ErrorSeverity, bool)"/></param>
public static void SetHandlers(ErrorReporterHandler handler, ErrorReporterHandler? nonFatalHandler)
{
if (s_handler != handler)
{
Debug.Assert(s_handler == null, "Handler already set");
s_handler = handler;
s_nonFatalHandler = nonFatalHandler;
}
}
/// <summary>
/// Same as setting the Handler property except that it avoids the assert. This is useful in
/// test code which needs to verify the handler is called in specific cases and will continually
/// overwrite this value.
/// </summary>
public static void OverwriteHandler(ErrorReporterHandler? value)
{
s_handler = value;
}
/// <summary>
/// Copies the handler in this instance to the linked copy of this type in this other assembly.
/// </summary>
/// <remarks>
/// This file is in linked into multiple layers, but we want to ensure that all layers have the same copy.
/// This lets us copy the handler in this instance into the same in another instance.
/// </remarks>
public static void CopyHandlersTo(Assembly assembly)
{
copyHandlerTo(assembly, s_handler, nameof(s_handler));
copyHandlerTo(assembly, s_nonFatalHandler, nameof(s_nonFatalHandler));
static void copyHandlerTo(Assembly assembly, ErrorReporterHandler? handler, string handlerName)
{
var targetType = assembly.GetType(typeof(FatalError).FullName!, throwOnError: true)!;
var targetHandlerProperty = targetType.GetField(handlerName, BindingFlags.Static | BindingFlags.NonPublic)!;
if (handler is not null)
{
// We need to convert the delegate type to the type in the linked copy since they won't have identity.
var convertedDelegate = Delegate.CreateDelegate(targetHandlerProperty.FieldType, handler.Target, handler.Method);
targetHandlerProperty.SetValue(obj: null, value: convertedDelegate);
}
else
{
targetHandlerProperty.SetValue(obj: null, value: null);
}
}
}
/// <summary>
/// Use in an exception filter to report an error without catching the exception.
/// The error is reported by calling <see cref="s_handler"/>.
/// </summary>
/// <returns><see langword="false"/> to avoid catching the exception.</returns>
[DebuggerHidden]
public static bool ReportAndPropagate(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
Report(exception, severity);
return false;
}
/// <summary>
/// Use in an exception filter to report an error (by calling <see cref="s_handler"/>), unless the
/// operation has been cancelled. The exception is never caught.
/// </summary>
/// <returns><see langword="false"/> to avoid catching the exception.</returns>
[DebuggerHidden]
public static bool ReportAndPropagateUnlessCanceled(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
if (exception is OperationCanceledException)
{
return false;
}
return ReportAndPropagate(exception, severity);
}
/// <summary>
/// <para>Use in an exception filter to report an error (by calling <see cref="s_handler"/>), unless the
/// operation has been cancelled at the request of <paramref name="contextCancellationToken"/>. The exception is
/// never caught.</para>
///
/// <para>Cancellable operations are only expected to throw <see cref="OperationCanceledException"/> if the
/// applicable <paramref name="contextCancellationToken"/> indicates cancellation is requested by setting
/// <see cref="CancellationToken.IsCancellationRequested"/>. Unexpected cancellation, i.e. an
/// <see cref="OperationCanceledException"/> which occurs without <paramref name="contextCancellationToken"/>
/// requesting cancellation, is treated as an error by this method.</para>
///
/// <para>This method does not require <see cref="OperationCanceledException.CancellationToken"/> to match
/// <paramref name="contextCancellationToken"/>, provided cancellation is expected per the previous
/// paragraph.</para>
/// </summary>
/// <param name="contextCancellationToken">A <see cref="CancellationToken"/> which will have
/// <see cref="CancellationToken.IsCancellationRequested"/> set if cancellation is expected.</param>
/// <returns><see langword="false"/> to avoid catching the exception.</returns>
[DebuggerHidden]
public static bool ReportAndPropagateUnlessCanceled(Exception exception, CancellationToken contextCancellationToken, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
if (ExceptionUtilities.IsCurrentOperationBeingCancelled(exception, contextCancellationToken) || exception is OperationCanceledIgnoringCallerTokenException)
{
return false;
}
return ReportAndPropagate(exception, severity);
}
/// <summary>
/// Report an error.
/// Calls <see cref="s_handler"/> and doesn't pass the exception through (the method returns true).
/// This is generally expected to be used within an exception filter as that allows us to
/// capture data at the point the exception is thrown rather than when it is handled.
/// However, it can also be used outside of an exception filter. If the exception has not
/// already been thrown the method will throw and catch it itself to ensure we get a useful
/// stack trace.
/// </summary>
/// <returns>True to catch the exception.</returns>
[DebuggerHidden]
public static bool ReportAndCatch(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
Report(exception, severity);
return true;
}
// Since the command line compiler has no way to catch exceptions, report them, and march on, we
// simply don't offer such a mechanism here to avoid accidental swallowing of exceptions.
#if !COMPILERCORE
[DebuggerHidden]
public static bool ReportWithDumpAndCatch(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
Report(exception, severity, forceDump: true);
return true;
}
/// <summary>
/// Use in an exception filter to report an error (by calling <see cref="s_handler"/>) and catch
/// the exception, unless the operation was cancelled.
/// </summary>
/// <returns><see langword="true"/> to catch the exception if the error was reported; otherwise,
/// <see langword="false"/> to propagate the exception if the operation was cancelled.</returns>
[DebuggerHidden]
public static bool ReportAndCatchUnlessCanceled(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
if (exception is OperationCanceledException)
{
return false;
}
return ReportAndCatch(exception, severity);
}
/// <summary>
/// <para>Use in an exception filter to report an error (by calling <see cref="s_handler"/>) and
/// catch the exception, unless the operation was cancelled at the request of
/// <paramref name="contextCancellationToken"/>.</para>
///
/// <para>Cancellable operations are only expected to throw <see cref="OperationCanceledException"/> if the
/// applicable <paramref name="contextCancellationToken"/> indicates cancellation is requested by setting
/// <see cref="CancellationToken.IsCancellationRequested"/>. Unexpected cancellation, i.e. an
/// <see cref="OperationCanceledException"/> which occurs without <paramref name="contextCancellationToken"/>
/// requesting cancellation, is treated as an error by this method.</para>
///
/// <para>This method does not require <see cref="OperationCanceledException.CancellationToken"/> to match
/// <paramref name="contextCancellationToken"/>, provided cancellation is expected per the previous
/// paragraph.</para>
/// </summary>
/// <param name="contextCancellationToken">A <see cref="CancellationToken"/> which will have
/// <see cref="CancellationToken.IsCancellationRequested"/> set if cancellation is expected.</param>
/// <returns><see langword="true"/> to catch the exception if the error was reported; otherwise,
/// <see langword="false"/> to propagate the exception if the operation was cancelled.</returns>
[DebuggerHidden]
public static bool ReportAndCatchUnlessCanceled(Exception exception, CancellationToken contextCancellationToken, ErrorSeverity severity = ErrorSeverity.Uncategorized)
{
if (ExceptionUtilities.IsCurrentOperationBeingCancelled(exception, contextCancellationToken) || exception is OperationCanceledIgnoringCallerTokenException)
{
return false;
}
return ReportAndCatch(exception, severity);
}
#endif
// We use a Guid for the marker because it is used as a key in an exceptions Data dictionary, so we must make sure
// it's serializable if the exception crosses an RPC boundary. In particular System.Text.Json doesn't like plain
// object dictionary keys.
private static readonly object s_reportedMarker = Guid.NewGuid();
// Do not allow this method to be inlined. That way when we have a dump we can see this frame in the stack and
// can examine things like s_reportedExceptionMessage. Without this, it's a lot tricker as FatalError is linked
// into many assemblies and finding the right type can be much harder.
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Report(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized, bool forceDump = false)
{
ReportException(exception, severity, forceDump, s_handler);
}
/// <summary>
/// Used to report a non-fatal-watson (when possible) to report an exception. The exception is not caught. Does
/// nothing if no non-fatal error handler is registered. See the second argument to <see
/// cref="SetHandlers(ErrorReporterHandler, ErrorReporterHandler?)"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
public static void ReportNonFatalError(Exception exception, ErrorSeverity severity = ErrorSeverity.Uncategorized, bool forceDump = false)
{
ReportException(exception, severity, forceDump, s_nonFatalHandler);
}
private static void ReportException(Exception exception, ErrorSeverity severity, bool forceDump, ErrorReporterHandler? handler)
{
// hold onto last exception to make investigation easier
s_reportedException = exception;
s_reportedExceptionMessage = exception.ToString();
if (handler == null)
{
return;
}
// only report exception once
if (exception.Data[s_reportedMarker] != null)
{
return;
}
if (exception is AggregateException aggregate && aggregate.InnerExceptions.Count == 1 && aggregate.InnerExceptions[0].Data[s_reportedMarker] != null)
{
return;
}
if (!exception.Data.IsReadOnly)
{
exception.Data[s_reportedMarker] = s_reportedMarker;
}
handler(exception, severity, forceDump);
}
}
/// <summary>
/// The severity of the error, see the enum members for a description of when to use each. This is metadata that's included
/// in a non-fatal fault report, which we can take advantage of on the backend to automatically triage bugs. For example,
/// a critical severity issue we can open with a lower bug count compared to a low priority one.
/// </summary>
internal enum ErrorSeverity
{
/// <summary>
/// The severity hasn't been categorized. Don't use this in new code.
/// </summary>
Uncategorized,
/// <summary>
/// Something failed, but the user is unlikely to notice. Especially useful for background things that we can silently recover
/// from, like bugs in caching systems.
/// </summary>
Diagnostic,
/// <summary>
/// Something failed, and the user might notice, but they're still likely able to carry on. For example, if the user
/// asked for some information from the IDE (find references, completion, etc.) and we were able to give partial results.
/// </summary>
General,
/// <summary>
/// Something failed, and the user likely noticed. For example, the user pressed a button to do an action, and
/// we threw an exception so we completely failed to do that in an unrecoverable way. This may also be used
/// for back-end systems where a failure is going to result in a highly broken experience, for example if parsing a file
/// catastrophically failed.
/// </summary>
Critical
}
}
|