From 9d9339de59fd9e7ba3487e446cbc4854035711c7 Mon Sep 17 00:00:00 2001 From: Aptivi CEO Date: Tue, 3 Dec 2024 23:06:57 +0300 Subject: [PATCH] add - doc - Added crash handler tool --- We've added an essential tool for this library: crash handling --- Type: add Breaking: False Doc Required: True Backport Required: False Part: 1/1 --- private/Aptivestigate.Demo/Program.cs | 4 +- public/Aptivestigate/Aptivestigate.csproj | 4 + .../Aptivestigate/CrashHandler/CrashTools.cs | 313 ++++++++++++++++++ 3 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 public/Aptivestigate/CrashHandler/CrashTools.cs diff --git a/private/Aptivestigate.Demo/Program.cs b/private/Aptivestigate.Demo/Program.cs index fef00bd..f76f622 100644 --- a/private/Aptivestigate.Demo/Program.cs +++ b/private/Aptivestigate.Demo/Program.cs @@ -17,6 +17,7 @@ // along with this program. If not, see . // +using Aptivestigate.CrashHandler; using Aptivestigate.Log4Net; using Aptivestigate.Logging; using Aptivestigate.NLog; @@ -35,6 +36,7 @@ static int Main(string[] args) bool isLogForNet = args.Contains("-log4net"); bool isNLog = args.Contains("-nlog"); bool isSerilog = args.Contains("-serilog"); + CrashTools.InstallCrashHandler(); // We need to choose the adapter according to the command line arguments BaseLogger logger; @@ -64,7 +66,7 @@ static int Main(string[] args) LogTools.Fatal(logger, "FATAL ERROR!"); LogTools.Fatal(logger, "FATAL ERROR: We can't do this! {0}", "Invalid operation."); LogTools.Fatal(logger, exc, "FATAL ERROR!"); - return 0; + throw exc; } } } diff --git a/public/Aptivestigate/Aptivestigate.csproj b/public/Aptivestigate/Aptivestigate.csproj index e5a44f2..531db02 100644 --- a/public/Aptivestigate/Aptivestigate.csproj +++ b/public/Aptivestigate/Aptivestigate.csproj @@ -31,4 +31,8 @@ + + + + diff --git a/public/Aptivestigate/CrashHandler/CrashTools.cs b/public/Aptivestigate/CrashHandler/CrashTools.cs new file mode 100644 index 0000000..3dcdc52 --- /dev/null +++ b/public/Aptivestigate/CrashHandler/CrashTools.cs @@ -0,0 +1,313 @@ +// +// Aptivestigate Copyright (C) 2024-2025 Aptivi +// +// This file is part of Aptivestigate +// +// Aptivestigate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Aptivestigate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +using SpecProbe.Software.Platform; +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace Aptivestigate.CrashHandler +{ + /// + /// Application crash handling tools + /// + public static class CrashTools + { + private static bool crashHandling = false; + private static Action? crashHandler = null; + + /// + /// Whether the crash handler is installed or not + /// + public static bool CrashHandling => + crashHandling; + + /// + /// Installs the application crash handler + /// + /// Crash handler to use + /// + public static void InstallCrashHandler(Action? crashHandler = null) + { + // Check to see if we already have a handler installed + if (CrashHandling) + throw new InvalidOperationException("The crash handler is already installed."); + + AppDomain.CurrentDomain.UnhandledException += HandleCrash; + CrashTools.crashHandler = crashHandler; + crashHandling = true; + } + + private static void HandleCrash(object sender, UnhandledExceptionEventArgs e) + { + bool isDefault = crashHandler is null; + var finalDelegate = crashHandler ?? new Action(DefaultCrashHandler); + try + { + // Run the crash handler (first chance) + finalDelegate.Invoke(e); + } + catch (Exception ex) + { + try + { + // Looks like that the crash handler has crashed, so check the type and re-run if necessary + // with the default crash handler + if (!isDefault) + { + finalDelegate = new Action(DefaultCrashHandler); + HandleCrash(sender, e); + } + else + throw ex; + } + catch (Exception ex2) + { + // Looks like that we've crashed again (second fault). As a last chance, try to make a crash + // file representing a fatal crash and dump only the necessary details. + try + { + FatalCrashHandler(ex2); + } + catch (Exception ex3) + { + // We're totally screwed! Bail out! + Console.WriteLine("---- FATAL CRASH DETECTED ----"); + Console.WriteLine(); + Console.WriteLine("We apologize for your inconvenience, but this program can't continue."); + Environment.FailFast( + $""" + + FATAL CRASH DETECTED (TRIPLE FAULT) + =================================== + + First chance + ------------ + {ex} + + Second chance + ------------- + {ex2} + """, ex3); + } + } + } + } + + private static void DefaultCrashHandler(UnhandledExceptionEventArgs eventArgs) + { + Console.WriteLine("---- CRASH DETECTED ----"); + Console.WriteLine(); + Console.WriteLine("A problem has been detected in the application that has to shut down. Writing crash dump..."); + + // Write the crash dump + using var crashFileWriter = CreateCrashFile(out Guid crashId); + bool isException = eventArgs.ExceptionObject is Exception; + crashFileWriter.WriteLine( + $""" + =============================== Crash report =============================== + + Below is the crash report about what happened in the application while it + was performing your requested operation. + + Crash ID: {crashId} + Time of incident: {DateTimeOffset.Now} + + --------------------------- Exception Information -------------------------- + + Terminating: {eventArgs.IsTerminating} + + {(isException ? eventArgs.ExceptionObject : "General fault. This may indicate a serious problem.")} + + ---------------------------- Exception Analysis ---------------------------- + + Below may help you find out why: + + {AnalyzeException(eventArgs.ExceptionObject as Exception)} + + ---------------------------- Runtime Information --------------------------- + + Operating system: {PlatformHelper.GetPlatform()} + OS description: {RuntimeInformation.OSDescription} + OS architecture: {RuntimeInformation.OSArchitecture} + Process architecture: {RuntimeInformation.ProcessArchitecture} + + .NET runtime: {RuntimeInformation.FrameworkDescription} + Running on dotnetfx: {PlatformHelper.IsDotNetFx()} + + Generic runtime ID: {PlatformHelper.GetCurrentGenericRid()} + General runtime ID: {PlatformHelper.GetCurrentGenericRid(false)} + + --------------------------- Operation Information -------------------------- + + Running from N-KS: {PlatformHelper.IsRunningFromNitrocid()} + Running from Mono: {PlatformHelper.IsRunningFromMono()} + Running from Screen: {PlatformHelper.IsRunningFromScreen()} + Running from TMUX: {PlatformHelper.IsRunningFromTmux()} + + Running on WSL: {PlatformHelper.IsOnUnixWsl()} + Running on MUSL: {PlatformHelper.IsOnUnixMusl()} + Running on Android: {PlatformHelper.IsOnAndroid()} + + Running on GUI: {PlatformHelper.IsOnGui()} + Running on X.Org: {PlatformHelper.IsOnX11()} + Running on Wayland: {PlatformHelper.IsOnWayland()} + + Terminal type: {PlatformHelper.GetTerminalType()} + Terminal emulator: {PlatformHelper.GetTerminalEmulator()} + + =============================== Crash report =============================== + """ + ); + crashFileWriter.Close(); + } + + private static void FatalCrashHandler(Exception exception) + { + Console.WriteLine("---- SECOND CRASH DETECTED ----"); + Console.WriteLine(); + Console.WriteLine("While writing crash dump, we've found a second crash."); + + // Write the crash dump + using var crashFileWriter = CreateCrashFile(out Guid crashId, true); + crashFileWriter.WriteLine( + $""" + ============================ Fatal crash report ============================ + + Below is the fatal crash report about what happened in the application while + it was handling the error. + + Crash ID: {crashId} + Time of incident: {DateTimeOffset.Now} + + --------------------------- Exception Information -------------------------- + + {exception} + + =============================== Crash report =============================== + """ + ); + crashFileWriter.Close(); + } + + private static string AnalyzeException(Exception? exception) + { + // Check the exception + if (exception is null) + return "There is no exception to analyze."; + + // Now, analyze the exception + var analysisBuilder = new StringBuilder(); + var stackTrace = new StackTrace(exception, true); + var traceFrames = stackTrace.GetFrames(); + analysisBuilder.AppendLine( + $""" + Frame count: {stackTrace.FrameCount} + + """ + ); + for (int i = 0; i < traceFrames.Length; i++) + { + StackFrame? traceFrame = traceFrames[i]; + analysisBuilder.AppendLine( + $""" + .............................................. + + Frame number: {i + 1}/{stackTrace.FrameCount} + + """ + ); + if (traceFrame.HasMethod()) + { + var method = traceFrame.GetMethod(); + var parameters = method.GetParameters(); + analysisBuilder.AppendLine( + $""" + Method: {method.Name} + Static method: {method.IsStatic} + Method type: {method.MemberType} + Declaring type: {method.DeclaringType} + Reflected type: {method.ReflectedType} + Attributes: {method.Attributes} + Implementation flags: {method.MethodImplementationFlags} + Method address: 0x{method.MethodHandle.Value.ToInt64():X2} + """ + ); + foreach (var parameter in parameters) + { + analysisBuilder.AppendLine( + $""" + Parameter type name: {parameter.ParameterType.Name} + Parameter full type: {parameter.ParameterType.FullName} + Parameter name: {parameter.Name} + Position (from 0): {parameter.Position} + Contains default: {parameter.HasDefaultValue} + Default value: {parameter.DefaultValue} + """ + ); + } + } + if (traceFrame.HasILOffset()) + { + analysisBuilder.AppendLine( + $""" + IL offset: 0x{traceFrame.GetILOffset():X2} + """ + ); + } + if (traceFrame.HasNativeImage()) + { + analysisBuilder.AppendLine( + $""" + Native image base: 0x{traceFrame.GetNativeImageBase().ToInt64():X2} + Native image IP: 0x{traceFrame.GetNativeIP().ToInt64():X2} + Native image offset: 0x{traceFrame.GetNativeOffset():X2} + """ + ); + } + if (traceFrame.HasSource()) + { + analysisBuilder.AppendLine( + $""" + File name: {traceFrame.GetFileName()} + File line number: {traceFrame.GetFileLineNumber()} + File column number: {traceFrame.GetFileColumnNumber()} + """ + ); + } + } + return analysisBuilder.ToString(); + } + + private static StreamWriter CreateCrashFile(out Guid crashId, bool fatal = false) + { + string dumpFilePath = + PlatformHelper.IsOnWindows() ? + $"{Environment.GetEnvironmentVariable("LOCALAPPDATA")}\\Aptivi\\Crashes" : + $"{Environment.GetEnvironmentVariable("HOME")}/.config/Aptivi/Crashes"; + string assembly = Assembly.GetEntryAssembly().GetName().Name; + crashId = Guid.NewGuid(); + Directory.CreateDirectory(dumpFilePath); + return File.CreateText(Path.Combine(dumpFilePath, $"{(fatal ? "f_" : "")}crash_{assembly}_{DateTimeOffset.Now:yyyyMMddhhmmssfffffff}_{crashId}.txt")); + } + } +}