From c9e841bcca1993bf715b86fb3758271b85410e66 Mon Sep 17 00:00:00 2001 From: Artur Kharin Date: Tue, 4 Feb 2025 19:50:56 +0200 Subject: [PATCH] Added crash error dialog. --- .../Configuration/ReportingSettings.cs | 7 + .../Exceptions/UnhandledExceptionHandler.cs | 126 ++++++++++++++++++ .../Hosting/WinUIHostingService.cs | 10 +- src/PPM.Application/PPM.Application.csproj | 12 +- src/PPM.Application/Program.cs | 7 + src/PPM.Application/app.manifest | 12 ++ src/PPM.Application/appsettings.json | 5 + src/PPM.Installer/AppComponents.wxs | 1 + 8 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/PPM.Application/Configuration/ReportingSettings.cs create mode 100644 src/PPM.Application/Exceptions/UnhandledExceptionHandler.cs create mode 100644 src/PPM.Application/appsettings.json diff --git a/src/PPM.Application/Configuration/ReportingSettings.cs b/src/PPM.Application/Configuration/ReportingSettings.cs new file mode 100644 index 0000000..1e22dfb --- /dev/null +++ b/src/PPM.Application/Configuration/ReportingSettings.cs @@ -0,0 +1,7 @@ +namespace Affinity_manager.Configuration +{ + public class ReportingSettings + { + public string? IssueReportingUrl { get; init; } + } +} diff --git a/src/PPM.Application/Exceptions/UnhandledExceptionHandler.cs b/src/PPM.Application/Exceptions/UnhandledExceptionHandler.cs new file mode 100644 index 0000000..7911006 --- /dev/null +++ b/src/PPM.Application/Exceptions/UnhandledExceptionHandler.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Affinity_manager.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.UI.Xaml; +using Vanara.PInvoke; +using Windows.ApplicationModel.DataTransfer; + +namespace Affinity_manager.Exceptions +{ + public class UnhandledExceptionHandler : IHostedService + { + public UnhandledExceptionHandler(IOptions reportingSettings) + { + Settings = reportingSettings.Value; + } + + public ReportingSettings Settings { get; } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + internal void AttachHandler(Application app) + { + app.UnhandledException += (sender, args) => + { + Exception exception = args.Exception; + string message = $"Unhandled exception: {exception.Message}"; + nint mainWindowHandle = nint.Zero; + + try + { + DisplayErrorDialog(mainWindowHandle, exception); + } + catch + { + // We don't want to go into the infinite loop of exceptions. + } + Environment.FailFast(message, exception); + }; + } + + private void DisplayErrorDialog(nint parentHandle, Exception exception) + { + // We are not using localized string here to avoid potential point of failure. + ComCtl32.TASKDIALOGCONFIG config = new() + { + dwCommonButtons = ComCtl32.TASKDIALOG_COMMON_BUTTON_FLAGS.TDCBF_CLOSE_BUTTON, + + MainInstruction = "The Process Priority Manager has encountered an unexpected error and will be closed.", + Content = "Please report this message to the developer by clicking the 'Report a problem' button. It will copy details to the clipboard and opens the issues page on the GitHub.", + ExpandedControlText = "Details", + ExpandedInformation = exception.ToString(), + hwndParent = parentHandle, + }; + + ComCtl32.TASKDIALOG_BUTTON[] buttons = + [ + new ComCtl32.TASKDIALOG_BUTTON() { nButtonID = 666, pszButtonText = Marshal.StringToHGlobalAuto("Report a problem") } + ]; + + config.cButtons = (uint)buttons.Length; + config.pButtons = Marshal.UnsafeAddrOfPinnedArrayElement(buttons, 0); + config.nDefaultButton = 8; // Close button + config.mainIcon = (nint)ComCtl32.TaskDialogIcon.TD_ERROR_ICON; + + ComCtl32.TaskDialogIndirect(config, out int pnButton, out _, out _); + if (pnButton == buttons[0].nButtonID && !string.IsNullOrWhiteSpace(Settings.IssueReportingUrl)) + { + ReportIssue(exception, Settings.IssueReportingUrl); + } + } + + private void ReportIssue(Exception exception, string issueReportingUrl) + { + try + { + // Just check that we are not launching any application or file. + Uri urlChecker = new(issueReportingUrl); + if (urlChecker.IsFile || urlChecker.IsUnc) + { + return; + } + } + catch + { + return; + } + + DataPackage dataPackage = new(); + dataPackage.SetText(GenerateExceptionReport(exception)); + + Clipboard.SetContentWithOptions(dataPackage, new ClipboardContentOptions { IsAllowedInHistory = true }); + Clipboard.Flush(); + + // An attempt to open the browser with the URL without elevated rights. + ProcessStartInfo psi = new() + { + FileName = "explorer.exe", + Arguments = issueReportingUrl, + UseShellExecute = false + }; + Process.Start(psi); + } + + private static string GenerateExceptionReport(Exception exception) + { + StringBuilder sb = new(); + sb.AppendLine("### Exception details"); + sb.Append(exception.ToString()); + return sb.ToString(); + } + } +} diff --git a/src/PPM.Application/Hosting/WinUIHostingService.cs b/src/PPM.Application/Hosting/WinUIHostingService.cs index a49ce00..05b33b1 100644 --- a/src/PPM.Application/Hosting/WinUIHostingService.cs +++ b/src/PPM.Application/Hosting/WinUIHostingService.cs @@ -1,6 +1,8 @@ using System; using System.Threading; using System.Threading.Tasks; +using Affinity_manager.Exceptions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; @@ -12,15 +14,18 @@ public class WinUIHostingService : IHostedService private readonly IServiceProvider _serviceProvider; private readonly IHostApplicationLifetime _applicationLifetime; - public WinUIHostingService(IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime) + public WinUIHostingService(IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, UnhandledExceptionHandler exceptionHandler) { _serviceProvider = serviceProvider; _applicationLifetime = applicationLifetime; + ExceptionHandler = exceptionHandler; } private DispatcherQueue? _dispatcherQueue; private Application? _app; + public UnhandledExceptionHandler ExceptionHandler { get; } + public Task StartAsync(CancellationToken cancellationToken) { Thread uiThread = new(() => @@ -32,7 +37,8 @@ public Task StartAsync(CancellationToken cancellationToken) DispatcherQueueSynchronizationContext context = new(_dispatcherQueue); SynchronizationContext.SetSynchronizationContext(context); - _app = (Application?)_serviceProvider.GetService(typeof(Application)); + _app = (Application)_serviceProvider.GetRequiredService(typeof(Application)); + ExceptionHandler.AttachHandler(_app); }); _dispatcherQueue = null; diff --git a/src/PPM.Application/PPM.Application.csproj b/src/PPM.Application/PPM.Application.csproj index ec782a2..6d2b8f3 100644 --- a/src/PPM.Application/PPM.Application.csproj +++ b/src/PPM.Application/PPM.Application.csproj @@ -70,6 +70,7 @@ + @@ -142,9 +143,6 @@ MSBuild:Compile - - - PreserveNewest @@ -364,4 +362,12 @@ ReswPlusAdvancedGenerator + + + + + + PreserveNewest + + \ No newline at end of file diff --git a/src/PPM.Application/Program.cs b/src/PPM.Application/Program.cs index 19b3835..9ac462d 100644 --- a/src/PPM.Application/Program.cs +++ b/src/PPM.Application/Program.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using Affinity_manager.Configuration; +using Affinity_manager.Exceptions; using Affinity_manager.Hosting; using Affinity_manager.Model; using Affinity_manager.Model.CRUD; using Affinity_manager.Model.DataGathering; using Affinity_manager.ViewModels; using Affinity_manager.ViewWrappers; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; @@ -23,13 +26,16 @@ static void Main(string[] args) XamlCheckProcessRequirements(); HostApplicationBuilder builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings { Args = args }); + builder.Configuration.AddJsonFile("appsettings.json", optional: false); AddDIServices(builder.Services); + builder.Services.Configure(builder.Configuration.GetSection("ReportingSettings")); builder.Build().Run(); } private static void AddDIServices(IServiceCollection services) { + services.AddSingleton(); services.AddSingleton(static provider => provider.GetService()!); services.AddTransient(); @@ -49,6 +55,7 @@ private static void AddDIServices(IServiceCollection services) view.AddProcessProvider(provider.GetRequiredService()); return view; }); + services.AddSingleton(); services.AddHostedService(); } } diff --git a/src/PPM.Application/app.manifest b/src/PPM.Application/app.manifest index b1e7368..699d648 100644 --- a/src/PPM.Application/app.manifest +++ b/src/PPM.Application/app.manifest @@ -18,6 +18,18 @@ + + + + + diff --git a/src/PPM.Application/appsettings.json b/src/PPM.Application/appsettings.json new file mode 100644 index 0000000..c2b11c1 --- /dev/null +++ b/src/PPM.Application/appsettings.json @@ -0,0 +1,5 @@ +{ + "ReportingSettings": { + "IssueReportingUrl": "https://github.com/Taron-art/Processes-Priority-Manager/issues" + } +} \ No newline at end of file diff --git a/src/PPM.Installer/AppComponents.wxs b/src/PPM.Installer/AppComponents.wxs index c57c12b..e3d62a4 100644 --- a/src/PPM.Installer/AppComponents.wxs +++ b/src/PPM.Installer/AppComponents.wxs @@ -20,6 +20,7 @@ +