From e31699998b924ddfb80f1268626805f7a7864c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Ko=CC=88nig?= Date: Sun, 24 Mar 2024 09:31:06 +0100 Subject: [PATCH] Added RolandK.AvaloniaExtensions.ExceptionHandling --- RolandK.AvaloniaExtensions.sln | 6 + src/Directory.Build.props | 2 +- ...xtensions.DependencyInjection.Tests.csproj | 2 +- ...loniaExtensions.DependencyInjection.csproj | 2 +- .../Analyzers/AggregateExceptionAnalyzer.cs | 26 +++ .../Analyzers/ArgumentExceptionAnalyzer.cs | 21 +++ .../Analyzers/DefaultExceptionAnalyzer.cs | 39 +++++ .../Analyzers/SystemIOExceptionAnalyzer.cs | 34 ++++ .../Data/ExceptionInfo.cs | 112 ++++++++++++ .../Data/ExceptionInfoNode.cs | 57 +++++++ .../Data/ExceptionProperty.cs | 18 ++ .../Data/IExceptionAnalyzer.cs | 27 +++ .../ExceptionViewerApplication.cs | 49 ++++++ .../GlobalErrorReporting.cs | 161 ++++++++++++++++++ ...valoniaExtensions.ExceptionHandling.csproj | 21 +++ .../UnexpectedErrorDialog.axaml | 69 ++++++++ .../UnexpectedErrorDialog.axaml.cs | 20 +++ .../RolandK.AvaloniaExtensions.TestApp.csproj | 12 +- .../RolandK.AvaloniaExtensions.Tests.csproj | 4 +- .../RolandK.AvaloniaExtensions.csproj | 2 +- 20 files changed, 672 insertions(+), 12 deletions(-) create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/AggregateExceptionAnalyzer.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/ArgumentExceptionAnalyzer.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/DefaultExceptionAnalyzer.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/SystemIOExceptionAnalyzer.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfo.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfoNode.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionProperty.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/IExceptionAnalyzer.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/ExceptionViewerApplication.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/GlobalErrorReporting.cs create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/RolandK.AvaloniaExtensions.ExceptionHandling.csproj create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/UnexpectedErrorDialog.axaml create mode 100644 src/RolandK.AvaloniaExtensions.ExceptionHandling/UnexpectedErrorDialog.axaml.cs diff --git a/RolandK.AvaloniaExtensions.sln b/RolandK.AvaloniaExtensions.sln index 8c46065..a7c630c 100644 --- a/RolandK.AvaloniaExtensions.sln +++ b/RolandK.AvaloniaExtensions.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RolandK.AvaloniaExtensions. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RolandK.AvaloniaExtensions.DependencyInjection.Tests", "src\RolandK.AvaloniaExtensions.DependencyInjection.Tests\RolandK.AvaloniaExtensions.DependencyInjection.Tests.csproj", "{03D69FBA-0153-4CF3-AC81-07E8CC0BB526}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RolandK.AvaloniaExtensions.ExceptionHandling", "src\RolandK.AvaloniaExtensions.ExceptionHandling\RolandK.AvaloniaExtensions.ExceptionHandling.csproj", "{FC6CF99C-571A-454F-B385-52F0DEC21BFA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {03D69FBA-0153-4CF3-AC81-07E8CC0BB526}.Debug|Any CPU.Build.0 = Debug|Any CPU {03D69FBA-0153-4CF3-AC81-07E8CC0BB526}.Release|Any CPU.ActiveCfg = Release|Any CPU {03D69FBA-0153-4CF3-AC81-07E8CC0BB526}.Release|Any CPU.Build.0 = Release|Any CPU + {FC6CF99C-571A-454F-B385-52F0DEC21BFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC6CF99C-571A-454F-B385-52F0DEC21BFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC6CF99C-571A-454F-B385-52F0DEC21BFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC6CF99C-571A-454F-B385-52F0DEC21BFA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b12ba65..90ff9fd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - 2.0.4 + 2.1.0 diff --git a/src/RolandK.AvaloniaExtensions.DependencyInjection.Tests/RolandK.AvaloniaExtensions.DependencyInjection.Tests.csproj b/src/RolandK.AvaloniaExtensions.DependencyInjection.Tests/RolandK.AvaloniaExtensions.DependencyInjection.Tests.csproj index 0752a5f..43ab347 100644 --- a/src/RolandK.AvaloniaExtensions.DependencyInjection.Tests/RolandK.AvaloniaExtensions.DependencyInjection.Tests.csproj +++ b/src/RolandK.AvaloniaExtensions.DependencyInjection.Tests/RolandK.AvaloniaExtensions.DependencyInjection.Tests.csproj @@ -19,7 +19,7 @@ all - + diff --git a/src/RolandK.AvaloniaExtensions.DependencyInjection/RolandK.AvaloniaExtensions.DependencyInjection.csproj b/src/RolandK.AvaloniaExtensions.DependencyInjection/RolandK.AvaloniaExtensions.DependencyInjection.csproj index dcfd085..8dc8aa0 100644 --- a/src/RolandK.AvaloniaExtensions.DependencyInjection/RolandK.AvaloniaExtensions.DependencyInjection.csproj +++ b/src/RolandK.AvaloniaExtensions.DependencyInjection/RolandK.AvaloniaExtensions.DependencyInjection.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/AggregateExceptionAnalyzer.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/AggregateExceptionAnalyzer.cs new file mode 100644 index 0000000..e949f31 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/AggregateExceptionAnalyzer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data.Analyzers; + +public class AggregateExceptionAnalyzer : IExceptionAnalyzer +{ + /// + public IEnumerable? ReadExceptionInfo(Exception ex) + { + return null; + } + + /// + public IEnumerable? GetInnerExceptions(Exception ex) + { + if (ex is AggregateException aggEx) + { + foreach (var actInnerException in aggEx.InnerExceptions) + { + yield return actInnerException; + } + } + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/ArgumentExceptionAnalyzer.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/ArgumentExceptionAnalyzer.cs new file mode 100644 index 0000000..5e9e7e4 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/ArgumentExceptionAnalyzer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data.Analyzers; + +public class ArgumentExceptionAnalyzer : IExceptionAnalyzer +{ + /// + public IEnumerable? ReadExceptionInfo(Exception ex) + { + if (ex is not ArgumentException argumentException) { yield break; } + + yield return new ExceptionProperty("ParamName", argumentException.ParamName ?? string.Empty); + } + + /// + public IEnumerable? GetInnerExceptions(Exception ex) + { + return null; + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/DefaultExceptionAnalyzer.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/DefaultExceptionAnalyzer.cs new file mode 100644 index 0000000..4953d6f --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/DefaultExceptionAnalyzer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data.Analyzers; + +public class DefaultExceptionAnalyzer : IExceptionAnalyzer +{ + /// + public IEnumerable? ReadExceptionInfo(Exception ex) + { + yield return new ExceptionProperty("Type", ex.GetType().FullName ?? string.Empty); + yield return new ExceptionProperty("Message", ex.Message); + yield return new ExceptionProperty("HResult", ex.HResult.ToString()); + yield return new ExceptionProperty("HelpLink", ex.HelpLink ?? string.Empty); + yield return new ExceptionProperty("Source", ex.Source ?? string.Empty); + + if (ex.TargetSite != null) + { + var sourceMethod = ex.TargetSite; + yield return new ExceptionProperty("SourceMethod.Name", sourceMethod.Name); + yield return new ExceptionProperty("SourceMethod.IsStatic", sourceMethod.IsStatic.ToString()); + yield return new ExceptionProperty( + "SourceMethod.Type", + sourceMethod.DeclaringType?.FullName ?? string.Empty); + } + + yield return new ExceptionProperty("StackTrace", ex.StackTrace ?? string.Empty); + } + + /// + public IEnumerable? GetInnerExceptions(Exception ex) + { + if(ex.InnerException != null) + { + yield return ex.InnerException; + } + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/SystemIOExceptionAnalyzer.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/SystemIOExceptionAnalyzer.cs new file mode 100644 index 0000000..5a99928 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/Analyzers/SystemIOExceptionAnalyzer.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data.Analyzers; + +public class SystemIOExceptionAnalyzer : IExceptionAnalyzer +{ + /// + public IEnumerable? ReadExceptionInfo(Exception ex) + { + switch (ex) + { + case FileLoadException fileLoadEx: + yield return new ExceptionProperty("FileName", fileLoadEx.FileName ?? string.Empty); + yield return new ExceptionProperty("FusionLog", fileLoadEx.FusionLog ?? string.Empty); + break; + + case FileNotFoundException fileNotFoundEx: + yield return new ExceptionProperty("FileName", fileNotFoundEx.FileName ?? string.Empty); + yield return new ExceptionProperty("FusionLog", fileNotFoundEx.FusionLog ?? string.Empty); + break; + + case DirectoryNotFoundException: + break; + } + } + + /// + public IEnumerable? GetInnerExceptions(Exception ex) + { + return null; + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfo.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfo.cs new file mode 100644 index 0000000..e79ff0a --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfo.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RolandK.AvaloniaExtensions.ExceptionHandling.Data.Analyzers; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +public class ExceptionInfo +{ + /// + /// Gets a collection containing all child nodes. + /// + public List ChildNodes { get; set; } = new(); + + /// + /// Gets or sets the main message. + /// + public string MainMessage + { + get; + set; + } = string.Empty; + + public string Description + { + get; + set; + } = string.Empty; + + public ExceptionInfo() + { + + } + + /// + /// Initializes a new instance of the class. + /// + public ExceptionInfo(Exception ex, IEnumerable? exceptionAnalyzers = null) + { + exceptionAnalyzers ??= CreateDefaultAnalyzers(); + + this.MainMessage = "Unexpected Error"; + this.Description = ex.Message; + + // Analyze the given exception + ExceptionInfoNode newNode = new(ex); + this.ChildNodes.Add(newNode); + + AnalyzeException(ex, newNode, exceptionAnalyzers); + } + + public static IEnumerable CreateDefaultAnalyzers() + { + yield return new DefaultExceptionAnalyzer(); + yield return new SystemIOExceptionAnalyzer(); + yield return new AggregateExceptionAnalyzer(); + yield return new ArgumentExceptionAnalyzer(); + } + + /// + /// Analyzes the given exception. + /// + /// The exception to be analyzed. + /// The target node where to put all data to. + /// All loaded analyzer objects. + private static void AnalyzeException(Exception ex, ExceptionInfoNode targetNode, IEnumerable exceptionAnalyzers) + { + // Query over all exception data + var analyzedInnerExceptions = new HashSet(2); + foreach(IExceptionAnalyzer actAnalyzer in exceptionAnalyzers) + { + // Read all properties of the current exception + var exceptionInfos = actAnalyzer.ReadExceptionInfo(ex); + if (exceptionInfos != null) + { + foreach (ExceptionProperty actProperty in exceptionInfos) + { + if (string.IsNullOrEmpty(actProperty.Name)) { continue; } + + ExceptionInfoNode propertyNode = new(actProperty); + + targetNode.ChildNodes ??= new List(); + targetNode.ChildNodes.Add(propertyNode); + } + } + + // Read all inner exception information + var innerExceptions = actAnalyzer.GetInnerExceptions(ex); + if (innerExceptions == null) { continue; } + + foreach (Exception actInnerException in innerExceptions) + { + if(analyzedInnerExceptions.Contains(actInnerException)){ continue; } + analyzedInnerExceptions.Add(actInnerException); + + ExceptionInfoNode actInfoNode = new(actInnerException); + AnalyzeException(actInnerException, actInfoNode, exceptionAnalyzers); + + targetNode.ChildNodes ??= new List(); + targetNode.ChildNodes.Add(actInfoNode); + } + } + + // Sort all generated nodes + if (targetNode.ChildNodes?.Count > 0) + { + targetNode.ChildNodes.Sort(); + } + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfoNode.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfoNode.cs new file mode 100644 index 0000000..d225255 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionInfoNode.cs @@ -0,0 +1,57 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +public class ExceptionInfoNode : IComparable +{ + /// + /// Gets a collection containing all child nodes. + /// + public List? ChildNodes { get; set; } + + public bool IsExceptionNode { get; set; } + + public string PropertyName { get; set; } = string.Empty; + + public string PropertyValue { get; set; } = string.Empty; + + public ExceptionInfoNode() + { + + } + + /// + /// Initializes a new instance of the class. + /// + public ExceptionInfoNode(Exception ex) + { + this.IsExceptionNode = true; + this.PropertyName = ex.GetType().GetTypeInfo().Name; + this.PropertyValue = ex.Message; + } + + public ExceptionInfoNode(ExceptionProperty property) + { + this.PropertyName = property.Name; + this.PropertyValue = property.Value; + } + + public int CompareTo(ExceptionInfoNode? other) + { + if (other == null) { return -1; } + if(this.IsExceptionNode != other.IsExceptionNode) + { + if (this.IsExceptionNode) { return 1; } + else { return -1; } + } + + return 0; + } + + public override string ToString() + { + return $"{this.PropertyName}: {this.PropertyValue}"; + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionProperty.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionProperty.cs new file mode 100644 index 0000000..fa6fb9f --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/ExceptionProperty.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +public class ExceptionProperty(string name, string value) +{ + public string Name { get; } = name; + public string Value { get; } = value; + + public override string ToString() + { + return $"{this.Name}: {this.Value}"; + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/IExceptionAnalyzer.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/IExceptionAnalyzer.cs new file mode 100644 index 0000000..ff33f87 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/Data/IExceptionAnalyzer.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +/// +/// This interface is used by the error-reporting framework. +/// It queries for all information provided by an exception which will be presented to +/// the user / developer. +/// +public interface IExceptionAnalyzer +{ + /// + /// Reads all exception information from the given exception object. + /// + /// The exception to be analyzed. + IEnumerable? ReadExceptionInfo(Exception ex); + + /// + /// Gets all inner exceptions provided by the given exception object. + /// + /// The exception to be analyzed. + IEnumerable? GetInnerExceptions(Exception ex); +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/ExceptionViewerApplication.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/ExceptionViewerApplication.cs new file mode 100644 index 0000000..9e32ed6 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/ExceptionViewerApplication.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; +using RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling; + +public class ExceptionViewerApplication : Application +{ + public override void OnFrameworkInitializationCompleted() + { + if (this.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + ExceptionInfo? exceptionInfo = null; + try + { + var filePath = desktop!.Args![0]; + + using var inStream = File.OpenRead(filePath); + exceptionInfo = JsonSerializer.Deserialize(inStream); + } + catch (Exception) + { + // Nothing we can do here + } + + if (exceptionInfo == null) + { + // We need to wait some time. Otherwise, an exception is thrown after Shutdown() + Task.Delay(100).ContinueWith(_ => + { + Dispatcher.UIThread.Invoke(() => + { + desktop.Shutdown(); + }); + }); + base.OnFrameworkInitializationCompleted(); + return; + } + + var dialog = new UnexpectedErrorDialog(); + dialog.DataContext = exceptionInfo; + desktop.MainWindow = dialog; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/GlobalErrorReporting.cs b/src/RolandK.AvaloniaExtensions.ExceptionHandling/GlobalErrorReporting.cs new file mode 100644 index 0000000..a5e0778 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/GlobalErrorReporting.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Avalonia.Controls; +using RolandK.AvaloniaExtensions.ExceptionHandling.Data; + +namespace RolandK.AvaloniaExtensions.ExceptionHandling; + +public static class GlobalErrorReporting +{ + /// + /// Shows an error dialog in the current process. + /// + /// The exception to be shown to the user. + /// The parent window for the error dialog. + /// If null, a default collection of IExceptionAnalyzers ist used. + public static async Task ShowGlobalExceptionDialogAsync( + Exception exception, + Window parentWindow, + IEnumerable? exceptionAnalyzers = null) + { + var exceptionInfo = new ExceptionInfo(exception, exceptionAnalyzers); + + var dialog = new UnexpectedErrorDialog(); + dialog.DataContext = exceptionInfo; + await dialog.ShowDialog(parentWindow); + } + + /// + /// Tries to show an error dialog with some exception details. + /// If it is not possible for any reason, this method simply does nothing. + /// + /// The exception to be shown to the user. + /// This should be a technical name, the method uses it to create a temporary directory in the filesystem. + /// The project name of the executable showing the error dialog. + /// If null, a default collection of IExceptionAnalyzers ist used. + public static void TryShowGlobalExceptionDialogInAnotherProcess( + Exception exception, + string applicationTempDirectoryName, + string exceptionViewerExecutableProjectName, + IEnumerable? exceptionAnalyzers = null) + { + try + { + // Write exception details to a temporary file + var errorDirectoryPath = GetErrorFileDirectoryAndEnsureCreated(applicationTempDirectoryName); + var errorFilePath = GenerateErrorFilePath(errorDirectoryPath); + + WriteExceptionInfoToFile(exception, exceptionAnalyzers, errorFilePath); + try + { + if (!TryFindViewerExecutable(exceptionViewerExecutableProjectName, out var executablePath)) + { + return; + } + + ShowGlobalException(errorFilePath, executablePath); + } + finally + { + // Delete the temporary file + File.Delete(errorFilePath); + } + } + catch(Exception) + { + // Nothing to do here.. + } + } + + private static string GetErrorFileDirectoryAndEnsureCreated(string applicationTempDirectoryName) + { + var errorDirectoryPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + applicationTempDirectoryName); + if (!Directory.Exists(errorDirectoryPath)) + { + Directory.CreateDirectory(errorDirectoryPath); + } + return errorDirectoryPath; + } + + private static string GenerateErrorFilePath(string errorDirectoryPath) + { + string errorFilePath; + do + { + var errorGuid = Guid.NewGuid(); + errorFilePath = Path.Combine( + errorDirectoryPath, + $"Error-{errorGuid}.err"); + } while (File.Exists(errorFilePath)); + + return errorFilePath; + } + + private static void WriteExceptionInfoToFile( + Exception exception, + IEnumerable? exceptionAnalyzers, + string targetFileName) + { + using var outStream = File.Create(targetFileName); + + var exceptionInfo = new ExceptionInfo(exception, exceptionAnalyzers); + JsonSerializer.Serialize( + outStream, + exceptionInfo, + new JsonSerializerOptions(JsonSerializerDefaults.General) + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } + + private static bool TryFindViewerExecutable( + string exceptionViewerExecutableProjectName, + out string executablePath) + { + executablePath = string.Empty; + + var executingAssembly = Assembly.GetExecutingAssembly(); + var executingAssemblyDirectory = Path.GetDirectoryName(executingAssembly.Location); + if (string.IsNullOrEmpty(executingAssemblyDirectory) || + string.IsNullOrEmpty(exceptionViewerExecutableProjectName)) + { + return false; + } + + var executablePathCheck = Path.Combine(executingAssemblyDirectory, exceptionViewerExecutableProjectName); + if (!File.Exists(executablePathCheck)) + { + executablePathCheck += ".exe"; + if (!File.Exists(executablePathCheck)) + { + return false; + } + } + + executablePath = executablePathCheck; + return true; + } + + private static void ShowGlobalException(string exceptionDetailsFilePath, string executablePath) + { + var processStartInfo = new ProcessStartInfo( + executablePath, + $"\"{exceptionDetailsFilePath}\""); + processStartInfo.ErrorDialog = false; + processStartInfo.UseShellExecute = false; + + var childProcess = Process.Start(processStartInfo); + if (childProcess != null) + { + childProcess.WaitForExit(); + } + } +} \ No newline at end of file diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/RolandK.AvaloniaExtensions.ExceptionHandling.csproj b/src/RolandK.AvaloniaExtensions.ExceptionHandling/RolandK.AvaloniaExtensions.ExceptionHandling.csproj new file mode 100644 index 0000000..b5d8b57 --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/RolandK.AvaloniaExtensions.ExceptionHandling.csproj @@ -0,0 +1,21 @@ + + + + net6.0;net7.0;net8.0 + enable + enable + True + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/RolandK.AvaloniaExtensions.ExceptionHandling/UnexpectedErrorDialog.axaml b/src/RolandK.AvaloniaExtensions.ExceptionHandling/UnexpectedErrorDialog.axaml new file mode 100644 index 0000000..face98b --- /dev/null +++ b/src/RolandK.AvaloniaExtensions.ExceptionHandling/UnexpectedErrorDialog.axaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + +