|
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.VisualStudio.LanguageServices.Telemetry;
using Microsoft.VisualStudio.Telemetry;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ErrorReporting
{
internal static class FaultReporter
{
private static readonly object _guard = new();
private static ImmutableArray<TelemetrySession> s_telemetrySessions = ImmutableArray<TelemetrySession>.Empty;
private static ImmutableArray<TraceSource> s_loggers = ImmutableArray<TraceSource>.Empty;
private static int s_dumpsSubmitted;
public static void InitializeFatalErrorHandlers()
{
FatalError.Handler = static (exception, severity, forceDump) => ReportFault(exception, ConvertSeverity(severity), forceDump);
FatalError.CopyHandlerTo(typeof(Compilation).Assembly);
}
private static FaultSeverity ConvertSeverity(ErrorSeverity severity)
{
return severity switch
{
ErrorSeverity.Uncategorized => FaultSeverity.Uncategorized,
ErrorSeverity.Diagnostic => FaultSeverity.Diagnostic,
ErrorSeverity.General => FaultSeverity.General,
ErrorSeverity.Critical => FaultSeverity.Critical,
_ => FaultSeverity.Uncategorized
};
}
public static void RegisterTelemetrySesssion(TelemetrySession session)
{
lock (_guard)
{
s_telemetrySessions = s_telemetrySessions.Add(session);
}
}
public static void UnregisterTelemetrySesssion(TelemetrySession session)
{
lock (_guard)
{
s_telemetrySessions = s_telemetrySessions.Remove(session);
}
}
public static void RegisterLogger(TraceSource logger)
{
lock (_guard)
{
s_loggers = s_loggers.Add(logger);
}
}
public static void UnregisterLogger(TraceSource logger)
{
lock (_guard)
{
s_loggers = s_loggers.Remove(logger);
}
}
/// <summary>
/// Report Non-Fatal Watson for a given unhandled exception.
/// </summary>
/// <param name="exception">Exception that triggered this non-fatal error</param>
/// <param name="forceDump">Force a dump to be created, even if the telemetry system is not
/// requesting one; we will still do a client-side limit to avoid sending too much at once.</param>
public static void ReportFault(Exception exception, FaultSeverity severity, bool forceDump)
{
try
{
if (exception is OperationCanceledException { InnerException: { } oceInnerException })
{
ReportFault(oceInnerException, severity, forceDump);
return;
}
if (exception is AggregateException aggregateException)
{
// We (potentially) have multiple exceptions; let's just report each of them
foreach (var innerException in aggregateException.Flatten().InnerExceptions)
ReportFault(innerException, severity, forceDump);
return;
}
var currentProcess = Process.GetCurrentProcess();
// write the exception to a log file:
var logMessage = $"[{currentProcess.ProcessName}:{currentProcess.Id}] Unexpected exception: {exception}";
foreach (var logger in s_loggers)
{
logger.TraceEvent(TraceEventType.Error, 1, logMessage);
}
var faultEvent = new FaultEvent(
eventName: TelemetryLogger.GetEventName(FunctionId.NonFatalWatson),
description: GetDescription(exception),
severity,
exceptionObject: exception,
gatherEventDetails: faultUtility =>
{
if (forceDump)
{
// Let's just send a maximum of three; number chosen arbitrarily
if (Interlocked.Increment(ref s_dumpsSubmitted) <= 3)
faultUtility.AddProcessDump(currentProcess.Id);
}
if (faultUtility is FaultEvent { IsIncludedInWatsonSample: true })
{
// add ServiceHub log files:
foreach (var path in CollectServiceHubLogFilePaths())
{
faultUtility.AddFile(path);
}
foreach (var loghubPath in CollectLogHubFilePaths())
{
faultUtility.AddFile(loghubPath);
}
}
// Returning "0" signals that, if sampled, we should send data to Watson.
// Any other value will cancel the Watson report. We never want to trigger a process dump manually,
// we'll let TargetedNotifications determine if a dump should be collected.
// See https://aka.ms/roslynnfwdocs for more details
return 0;
});
foreach (var session in s_telemetrySessions)
{
session.PostEvent(faultEvent);
}
}
catch (OutOfMemoryException)
{
FailFast.OnFatalException(exception);
}
catch (Exception e)
{
FailFast.OnFatalException(e);
}
}
private static string GetDescription(Exception exception)
{
const string CodeAnalysisNamespace = nameof(Microsoft) + "." + nameof(CodeAnalysis);
// Be resilient to failing here. If we can't get a suitable name, just fall back to the standard name we
// used to report.
try
{
// walk up the stack looking for the first call from a type that isn't in the ErrorReporting namespace.
var frames = new StackTrace(exception).GetFrames();
// On the .NET Framework, GetFrames() can return null even though it's not documented as such.
// At least one case here is if the exception's stack trace itself is null.
if (frames != null)
{
foreach (var frame in frames)
{
var method = frame?.GetMethod();
var methodName = method?.Name;
if (methodName == null)
continue;
var declaringTypeName = method?.DeclaringType?.FullName;
if (declaringTypeName == null)
continue;
if (!declaringTypeName.StartsWith(CodeAnalysisNamespace))
continue;
return declaringTypeName + "." + methodName;
}
}
}
catch
{
}
// If we couldn't get a stack, do this
return exception.Message;
}
private static IList<string> CollectLogHubFilePaths()
{
try
{
var logPath = Path.Combine(Path.GetTempPath(), "VSLogs");
var logs = CollectFilePaths(logPath, "*.svclog", shouldExcludeLogFile: (name) => !name.Contains("Roslyn") && !name.Contains("LSPClient"));
return logs;
}
catch (Exception)
{
// ignore failures
}
return SpecializedCollections.EmptyList<string>();
}
private static IList<string> CollectServiceHubLogFilePaths()
{
try
{
var logPath = Path.Combine(Path.GetTempPath(), "servicehub", "logs");
// TODO: https://github.com/dotnet/roslyn/issues/42582
// name our services more consistently to simplify filtering
var logs = CollectFilePaths(logPath, "*.log", shouldExcludeLogFile: (name) => !name.Contains("-" + ServiceDescriptor.ServiceNameTopLevelPrefix) &&
!name.Contains("-CodeLens") &&
!name.Contains("-ManagedLanguage.IDE.RemoteHostClient") &&
!name.Contains("-hub"));
return logs;
}
catch (Exception)
{
// ignore failures
}
return SpecializedCollections.EmptyList<string>();
}
private static List<string> CollectFilePaths(string logDirectoryPath, string logFileExtension, Func<string, bool> shouldExcludeLogFile)
{
var paths = new List<string>();
if (!Directory.Exists(logDirectoryPath))
{
return paths;
}
// attach all log files that are modified less than 1 day before.
var now = DateTime.UtcNow;
var oneDay = TimeSpan.FromDays(1);
foreach (var path in Directory.EnumerateFiles(logDirectoryPath, logFileExtension))
{
try
{
var name = Path.GetFileNameWithoutExtension(path);
// filter logs that are not relevant to Roslyn investigation
if (shouldExcludeLogFile(name))
{
continue;
}
var lastWrite = File.GetLastWriteTimeUtc(path);
if (now - lastWrite > oneDay)
{
continue;
}
paths.Add(path);
}
catch
{
// ignore file that can't be accessed
}
}
return paths;
}
}
}
|