JD2022-TU1/main/tools/extern/Bloomberg/Code/Clients/BloombergClientCSharp/Crash/CrashHandler.cs

559 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using Bloomberg.Client.Components;
using Bloomberg.Client.Report;
using Bloomberg.Message.Entities;
using JD.Bloomberg;
using Module=Bloomberg.Message.Entities.Module;
namespace Bloomberg.Client.Crash
{
/// <summary>
/// Exception Handler to catch exception and redirect to report manager
/// This class is the first to be called when a crash occur. With its own implementation
/// it computes all the crash data needed and calls the ReportManager (singleton) to
/// send the xml to the crash server or reporter (pc).
///
// You can Trigger manual crash by calling directly TriggerCrash and inputting your crash information.
/// </summary>
public class CrashHandler : IDisposable
{
#region Dll imports
[Flags]
public enum SetErrorModeMode : uint
{
SEM_FAILCRITICALERRORS = 0x0001,
SEM_NOGPFAULTERRORBOX = 0x0002,
SEM_NOALIGNMENTFAULTEXCEPT = 0x0004,
SEM_NOOPENFILEERRORBOX = 0x8000 }
[DllImport("kernel32.dll", EntryPoint = "SetErrorMode", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern SetErrorModeMode SetErrorMode(SetErrorModeMode mode);
[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();
#endregion
/// <summary>
/// This is a buffer to avoid problem when out of memory. It's really hard to avoid memory allocation in C#
/// Since we dont have a real limit, we prefer allowing fake data and forcing a GC collect
/// </summary>
// ReSharper disable NotAccessedField.Local
private byte[] _buffer = new byte[1024 * 256];
// ReSharper restore NotAccessedField.Local
private Semaphore _crashLock = new Semaphore(1, 1);
private bool _exceptionHandlerAttached;
private SetErrorModeMode _previousErrorMode = 0;
public bool IsEnabled { get; set; }
public CrashHandler()
{
IsEnabled = true;
}
#region Initialization
/// <summary>
/// Initialize the crash handler
/// </summary>
public void Initialize()
{
if (IsEnabled)
{
_previousErrorMode = SetErrorMode(SetErrorModeMode.SEM_NOGPFAULTERRORBOX | SetErrorModeMode.SEM_FAILCRITICALERRORS);
// Initialize CrashReport fixed data
ReportCrash.Instance.SetType(ReportType.Exception);
// Initialize the unhandled exception filter and other error callback
// We attach the exception handle if:
// 1 - No debugger is attached
// 2 - Debugger is attached, but is not enabled for Exception (so we assume we want to debug Exception handling)
if (!Debugger.IsAttached || !ReportCrash.Instance.HasEnableDebuggerDetectionFlag)
{
// If Bloomberg is initialized in the default application domain,
// It will capture any unhandled exception in any thread, from all application domains.
// This includes the UI thread.
//
// If Bloomberg is initialized in a non-default application domain,
// It will only capture unhandled exceptions from threads started in that domain.
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
// Winforms
// In Windows Forms, unhandled excpetions in the main application thread (UI thread) raise a ThreadException event; the UnhandledException event is not raised.
// By changing the UnhandledExceptionMode to ThrowException, we ensure the UnhandledException event is raised instead of ThreadException
//ChangeUnhandledExceptionMode(System.Windows.Forms.UnhandledExceptionMode.ThrowException); // <= This is original Bloomberg code changed for JD.
// BEGIN JUST DANCE MODIFICATIONS
ChangeUnhandledExceptionMode(System.Windows.Forms.UnhandledExceptionMode.CatchException);
System.Windows.Forms.Application.ThreadException += (sender, e) => AnalyzeException(e.Exception);
if (System.Windows.Application.Current != null) // Not in a WPF application
{
System.Windows.Application.Current.DispatcherUnhandledException += (sender, e) =>
{
e.Handled = true;
AnalyzeException(e.Exception);
};
}
// END
// MSDN DOC: Debug and trace share the same Listeners collection, so if you add a listener object to a Debug.Listeners collection in your application, it gets added to the Trace.Listeners collection as well.
// We overwrite the listeners to handle the assert. This one inherits from DefaultTraceListener (the one used by .NET by default).
for (int i = Trace.Listeners.Count - 1; i >= 0; i--)
if (Trace.Listeners[i] is DefaultTraceListener)
Trace.Listeners.RemoveAt(i);
Trace.Listeners.Insert(0, new BloombergTraceListener());
Tracer.LogFormat(TracerLogLevel.INFO, "CrashHandler.Initialize: Exception handler was attached.");
_exceptionHandlerAttached = true;
}
else
{
Tracer.LogFormat(TracerLogLevel.INFO,
"CrashHandler.Initialize: Bloomberg trace listener was not instanciated because debugger is attached ({0}) or debugger detection is activated ({1})",
Debugger.IsAttached, ReportCrash.Instance.HasEnableDebuggerDetectionFlag);
}
}
}
public void UnInitialize()
{
SetErrorMode(_previousErrorMode);
if (_exceptionHandlerAttached)
{
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
ChangeUnhandledExceptionMode(System.Windows.Forms.UnhandledExceptionMode.Automatic);
for (int i = Trace.Listeners.Count - 1; i >= 0; i--)
if (Trace.Listeners[i] is BloombergTraceListener)
Trace.Listeners.RemoveAt(i);
Trace.Listeners.Insert(0, new DefaultTraceListener());
_exceptionHandlerAttached = false;
Tracer.LogFormat(TracerLogLevel.INFO, "CrashHandler.UnInitialize: Exception handler was disabled.");
}
IsEnabled = false;
}
public void RegisterCrashHandler(bool registerCrashHandler)
{
if (registerCrashHandler)
Initialize();
else
UnInitialize();
}
public bool IsRegistered()
{
return _exceptionHandlerAttached;
}
#endregion
#region Exception handlers
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
AnalyzeException(e.ExceptionObject as Exception);
}
private void AnalyzeException(Exception e)
{
Tracer.LogFormat(TracerLogLevel.INFO, "Bloomberg has caught exception : {0}.", e.Message);
//while (dontWarnMe())
//{
// Thread.Sleep(100);
//}
ReportCrash.Instance.SetType(ReportType.Exception);
ReportCrash.Instance.SetSubType(Bloomberg.Message.Entities.TypeInfo.SUBTYPE_DEFAULT);
AnalyzeException(ReportCrash.Instance, null, e);
}
private void ChangeUnhandledExceptionMode(System.Windows.Forms.UnhandledExceptionMode mode)
{
try
{
System.Windows.Forms.Application.SetUnhandledExceptionMode(mode);
}
catch (InvalidOperationException ex)
{
//There is a constraint about changing the exception mode after a control was created. Since the application is going to quit after this it might not be necessary to do it anyway
//Exception actual message: Thread exception mode cannot be changed once any Controls are created on the thread
//ie: Will happen in a WPF application if a Winform control is hosted in a WPF window.
Tracer.LogFormat(TracerLogLevel.WARN, "System.Windows.Forms.Application.SetUnhandledExceptionMode(System.Windows.Forms.UnhandledExceptionMode.Automatic); failed. There is a constraint about changing the exception mode after a control was created. Since the application is going to quit after this it might not be necessary to do it anyway: " + ex.Message);
}
}
#endregion
#region Exception parsing
// always true
private bool dontWarnMe()
{
Random rnd = new Random();
return rnd.Next(1) < 2;
}
private ActionResult AnalyzeException(ReportCrash reportCrash, string errorMessage, Exception e)
{
_crashLock.WaitOne();
var result = ActionResult.None;
if (Reporter.Instance.CheckIfCrashAlreadyHandled())
{
// Crash already handled by native client
_crashLock.Release();
return result;
}
try
{
Exception innerException = GetInnerException(e);
// Free the temporary buffer
_buffer = null;
GC.Collect();
if (!AnalyzePrologue(reportCrash))
{
_crashLock.Release();
return ActionResult.Error;
}
// Find the reason
if (!String.IsNullOrEmpty(errorMessage))
{
reportCrash.Reason = innerException != null
? innerException.GetType() + ": " + errorMessage
: errorMessage;
}
else if (innerException != null)
{
reportCrash.Reason = innerException.GetType() + ": " + innerException.Message;
}
else
{
Tracer.LogFormat(TracerLogLevel.ERROR,
"CrashHandler.AnalyzeException: Cannot obtain exception message for current thread.");
_crashLock.Release();
return ActionResult.Error;
}
Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.AnalyzeException: Beginning analysis for {0}:{1}.",
reportCrash.TypeInfo.Description, reportCrash.Reason);
bool nativeClientIsEnabled = Reporter.Instance.CheckIfNativeClientEnabled();
if (nativeClientIsEnabled)
{
// CrashHandler.AnalyzeException: Native client is enabled.
// Since for now we don't have a method to check the native callstack from managed, we consider that we have a mixed callstack");
reportCrash.ThreadId = GetCurrentThreadId();
Reporter.Instance.CreateCrashAlreadyHandledFlag();
}
// Obtain the call stack & Module info
var callstackEntries = new List<CallstackEntry>();
var modules = new List<Module>();
if (reportCrash.HasEnableCallstackFlag)
{
GetCallstack(e, ref callstackEntries);
modules = GetModules();
}
reportCrash.CrashData = new CrashData
{
CallstackEntries = callstackEntries,
Modules = modules,
Details = innerException == null ? null : innerException.ToString(),
IsMixedCallstack = nativeClientIsEnabled
};
// [gharding] [BLOOM-1971]
// if there was more than one exception,
// Include all exceptions which were thrown in the report
var exceptions = new List<ExceptionEntry>();
GetExceptions(e, ref exceptions);
if (exceptions.Count > 1)
reportCrash.CrashData.Exceptions = exceptions;
if (ThreadsCallstack.Instance != null)
{
ThreadsCallstack.Instance.PrepareAdditionalCallstack();
}
//Send the CrashStatistic to Elasticsearch
using (var crashStatistic = new CrashStatistic())
{
crashStatistic.Callstack = e.StackTrace;
crashStatistic.Message = e.Message;
}
if (reportCrash.IsAsync)
{
result = Reporter.Instance.ReportManager.StartAsync(reportCrash);
if (result != ActionResult.Error)
{
ThreadPool.QueueUserWorkItem(state => FinishAnalysis(reportCrash, result));
}
else
{
_crashLock.Release();
}
}
else
{
// Process the report
result = Reporter.Instance.ReportManager.Process(reportCrash);
FinishAnalysis(reportCrash, result);
_crashLock.Release();
}
return result;
}
catch (Exception ex)
{
Tracer.LogFormat(TracerLogLevel.ERROR, "CrashHandler.AnalyzeException: Error while processing report. Error: {0}.", ex.ToString());
_crashLock.Release();
return ActionResult.Error;
}
}
private void FinishAnalysis(ReportCrash reportCrash, ActionResult result)
{
if (reportCrash.IsAsync)
{
Reporter.Instance.ReportManager.FinishAsync(reportCrash);
_crashLock.Release();
}
AnalyzeEpilogue(reportCrash);
reportCrash.IsAsync = false;
Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.AnalyzeException: Ending analysis for {0}:{1}.", reportCrash.TypeInfo.Description, reportCrash.Reason);
if ((result & ActionResult.Break) > 0 && reportCrash.TypeInfo.Type == ReportType.Exception)
{
Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.AnalyzeException: Breaking application (Action result: {0})", result);
Debugger.Launch();
}
// Shutdown appl if reboot is set to be true for the current report type
if ((result & ActionResult.Kill) > 0)
{
Tracer.LogFormat(TracerLogLevel.DEBUG, "CrashHandler.AnalyzeException: Killing application (Action result: {0})", result);
Process.GetCurrentProcess().Kill();
}
}
private static void GetExceptions(Exception e, ref List<ExceptionEntry> exceptions)
{
int depth = 0;
while(e != null)
{
exceptions.Insert(0, new ExceptionEntry
{
Depth = depth,
Message = e.Message,
Type = e.GetType().ToString()
});
++depth;
e = e.InnerException;
}
}
private static Exception GetInnerException(Exception e)
{
if (e != null)
{
return e.InnerException != null ? GetInnerException(e.InnerException) : e;
}
return null;
}
private static void GetCallstack(Exception e, ref List<CallstackEntry> callstackEntries, int depth = 0)
{
if (e != null)
{
if (e.InnerException != null)
GetCallstack(e.InnerException, ref callstackEntries, depth + 1);
callstackEntries.AddRange(InternalGetCallstack(e, depth));
return;
}
callstackEntries.AddRange(InternalGetCallstack(null, depth));
}
/// <summary>
/// This method return the list of callstack for an exception (or the current frame if the exception is null)
/// </summary>
/// <param name="e"></param>
/// <param name="depth"></param>
/// <returns></returns>
private static IEnumerable<CallstackEntry> InternalGetCallstack(Exception e, int depth)
{
var callstackEntries = new List<CallstackEntry>();
// e.StackTrace can be null when someone creates an exception manually instead of throw
StackTrace stackTrace = e == null || e.StackTrace == null ? new StackTrace(true) : new StackTrace(e, true);
StackFrame[] stackFrames = stackTrace.GetFrames(); // get method calls (frames)
if (stackFrames == null) return callstackEntries;
// write call stack method names
foreach (StackFrame stackFrame in stackFrames)
{
var methodInfo = stackFrame.GetMethod();
if (methodInfo == null) continue;
if (methodInfo.ReflectedType != null)
{
callstackEntries.Add(new CallstackEntry
{
Address = GetAddress(stackFrame, methodInfo),
FunctionName = methodInfo.ReflectedType.FullName + "." + methodInfo.Name,
FunctionOffset = (uint)stackFrame.GetILOffset(),
ModuleName = methodInfo.ReflectedType.Module.Name,
SourceFileLine = (ulong)stackFrame.GetFileLineNumber(),
SourceFileName = stackFrame.GetFileName(),
ExceptionDepth = depth
});
}
else
{
// This can happen when building lambda on the fly
callstackEntries.Add(new CallstackEntry
{
Address = GetAddress(stackFrame, methodInfo),
FunctionName = methodInfo.Name,
FunctionOffset = (uint)stackFrame.GetILOffset(),
ModuleName = methodInfo.Module.Assembly.FullName,
SourceFileLine = (ulong)stackFrame.GetFileLineNumber(),
SourceFileName = stackFrame.GetFileName(),
ExceptionDepth = depth
});
}
}
return callstackEntries;
}
/// <summary>
/// This will return the estimated address of the stack entry.
/// Sometime, the GetFunctionPointer returns an exception, the catch find another way to represent the address.
/// </summary>
/// <param name="stackFrame"></param>
/// <param name="methodInfo"></param>
/// <returns></returns>
private static ulong GetAddress(StackFrame stackFrame, MethodBase methodInfo)
{
try
{
return (ulong) (methodInfo.MethodHandle.GetFunctionPointer().ToInt64() + stackFrame.GetILOffset());
}
catch (Exception ex)
{
// TODO: THIS CAN CRASH methodInfo.MethodHandle.GetFunctionPointer() with
// new List<string>().Add(new string(Enumerable.Repeat('a', 100000000).ToArray()));
Tracer.LogFormat(TracerLogLevel.ERROR, "CrashHandler.GetAddress: Could not calculate the address for function {0} in module {1}. Error is: {2}", methodInfo.Name, methodInfo.ReflectedType.Assembly.FullName, ex);
return 0;
}
}
/// <summary>
/// This method return the list of assemblies (modules) loaded in the current appdomain.
/// </summary>
/// <returns></returns>
private static List<Module> GetModules()
{
var modules = new List<Module>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
System.Reflection.Module[] loadedModules = assembly.GetLoadedModules();
foreach(var loadedModule in loadedModules)
{
modules.Add(new Module
{
Name = loadedModule.Name,
Guid = loadedModule.ModuleVersionId.ToString(),
OriginalPdbPath = loadedModule.FullyQualifiedName,
Version = loadedModule.Assembly.GetName().Version.ToString()
});
}
}
return modules;
}
private bool AnalyzePrologue(ReportCrash reportCrash)
{
if (!IsEnabled)
{
Tracer.LogFormat(TracerLogLevel.INFO, "CrashHandler.AnalyzePrologue: Quitting analysis for {0} because of disabled crash reporting.", reportCrash.TypeInfo.Description);
return false;
}
if (reportCrash.HasEnableDebuggerDetectionFlag && Debugger.IsAttached)
{
Tracer.LogFormat(TracerLogLevel.INFO, "CrashHandler.AnalyzePrologue: Quitting analysis for {0} because of present debugger and because that report type is in the EnableDebuggerDetection mask.", reportCrash.TypeInfo.Description);
return false;
}
ReportCrash.Instance.SetType(reportCrash.TypeInfo.Type);
ReportCrash.Instance.SetSubType(reportCrash.TypeInfo.SubType);
return true;
}
private void AnalyzeEpilogue(ReportInterface reportCrash)
{
if (ReportCrash.Instance.HasDisableCrashReportingFlag)
{
Tracer.LogFormat(TracerLogLevel.INFO, "CrashHandler.AnalyzeEpilogue: Disabling crash reporting because {0} has been detected.", ReportCrash.Instance.TypeInfo.Description);
RegisterCrashHandler(false);
}
}
#endregion
public ActionResult TriggerCrash(ReportCrash reportCrash, string errorMessage, Exception e)
{
return AnalyzeException(reportCrash, errorMessage, e);
}
public ActionResult TriggerCrash(ReportCrash reportCrash, string errorMessage)
{
return TriggerCrash(reportCrash, errorMessage, null);
}
public void Dispose()
{
_crashLock?.Dispose();
}
}
}