diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs
index 59d9c1431..57d23e90b 100644
--- a/src/UniGetUI.Avalonia/App.axaml.cs
+++ b/src/UniGetUI.Avalonia/App.axaml.cs
@@ -7,6 +7,7 @@
using Avalonia.Styling;
using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.Views;
+using UniGetUI.Avalonia.Views.DialogPages;
using UniGetUI.PackageEngine;
using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings;
@@ -47,7 +48,7 @@ public override void OnFrameworkInitializationCompleted()
ApplyTheme(CoreSettings.GetValue(CoreSettings.K.PreferredTheme));
var mainWindow = new MainWindow();
desktop.MainWindow = mainWindow;
- _ = AvaloniaBootstrapper.InitializeAsync();
+ _ = StartupAsync(mainWindow);
}
base.OnFrameworkInitializationCompleted();
@@ -80,6 +81,27 @@ private static void ExpandMacOSPath()
catch { /* keep the existing PATH if the shell can't be launched */ }
}
+ private static async Task StartupAsync(MainWindow mainWindow)
+ {
+ // Show crash report from the previous session and wait for the user
+ // to dismiss it before continuing with normal startup.
+ if (File.Exists(CrashHandler.PendingCrashFile))
+ {
+ try
+ {
+ string report = File.ReadAllText(CrashHandler.PendingCrashFile);
+ File.Delete(CrashHandler.PendingCrashFile);
+ // Yield once so the main window has time to open before
+ // ShowDialog tries to attach to it as owner.
+ await Task.Yield();
+ await new CrashReportWindow(report).ShowDialog(mainWindow);
+ }
+ catch { /* must not prevent normal startup */ }
+ }
+
+ await AvaloniaBootstrapper.InitializeAsync();
+ }
+
public static void ApplyTheme(string value)
{
Current!.RequestedThemeVariant = value switch
diff --git a/src/UniGetUI.Avalonia/CrashHandler.cs b/src/UniGetUI.Avalonia/CrashHandler.cs
new file mode 100644
index 000000000..b2cfb29eb
--- /dev/null
+++ b/src/UniGetUI.Avalonia/CrashHandler.cs
@@ -0,0 +1,116 @@
+using System.Diagnostics;
+using System.Text;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.Tools;
+
+namespace UniGetUI.Avalonia;
+
+public static class CrashHandler
+{
+ public static readonly string PendingCrashFile =
+ Path.Combine(Path.GetTempPath(), "UniGetUI_pending_crash.txt");
+
+ public static void ReportFatalException(Exception e)
+ {
+ Debugger.Break();
+
+ string langName = "Unknown";
+ try
+ {
+ langName = CoreTools.GetCurrentLocale();
+ }
+ catch { }
+
+ static string GetExceptionData(Exception ex)
+ {
+ try
+ {
+ var b = new StringBuilder();
+ foreach (var key in ex.Data.Keys)
+ b.AppendLine($"{key}: {ex.Data[key]}");
+ string r = b.ToString();
+ return r.Any() ? r : "No extra data was provided";
+ }
+ catch (Exception inner)
+ {
+ return $"Failed to get exception Data with exception {inner.Message}";
+ }
+ }
+
+ string iReport;
+ try
+ {
+ var integrityReport = IntegrityTester.CheckIntegrity(false);
+ iReport = IntegrityTester.GetReadableReport(integrityReport);
+ }
+ catch (Exception ex)
+ {
+ iReport = "Failed to compute integrity report: " + ex.GetType() + ": " + ex.Message;
+ }
+
+ string errorString = $$"""
+ Environment details:
+ OS version: {{Environment.OSVersion.VersionString}}
+ Language: {{langName}}
+ APP Version: {{CoreData.VersionName}}
+ APP Build number: {{CoreData.BuildNumber}}
+ Executable: {{Environment.ProcessPath}}
+ Command-line arguments: {{Environment.CommandLine}}
+
+ Integrity report:
+ {{iReport.Replace("\n", "\n ")}}
+
+ Exception type: {{e.GetType()?.Name}} ({{e.GetType()}})
+ Crash HResult: 0x{{(uint)e.HResult:X}} ({{(uint)e.HResult}}, {{e.HResult}})
+ Crash Message: {{e.Message}}
+
+ Crash Data:
+ {{GetExceptionData(e).Replace("\n", "\n ")}}
+
+ Crash Trace:
+ {{e.StackTrace?.Replace("\n", "\n ")}}
+ """;
+
+ try
+ {
+ int depth = 0;
+ while (e.InnerException is not null)
+ {
+ depth++;
+ e = e.InnerException;
+ errorString +=
+ "\n\n\n\n"
+ + $$"""
+ ———————————————————————————————————————————————————————————
+ Inner exception details (depth level: {{depth}})
+ Crash HResult: 0x{{(uint)e.HResult:X}} ({{(uint)e.HResult}}, {{e.HResult}})
+ Crash Message: {{e.Message}}
+
+ Crash Data:
+ {{GetExceptionData(e).Replace("\n", "\n ")}}
+
+ Crash Traceback:
+ {{e.StackTrace?.Replace("\n", "\n ")}}
+ """;
+ }
+
+ if (depth == 0)
+ errorString += "\n\n\nNo inner exceptions found";
+ }
+ catch { }
+
+ Console.WriteLine(errorString);
+
+ // Persist crash data so the next normal app launch can show the report.
+ try
+ {
+ File.WriteAllText(PendingCrashFile, errorString, Encoding.UTF8);
+ }
+ catch
+ {
+ // If we can't write the file, nothing more we can do — just exit.
+ }
+
+ Environment.Exit(1);
+ }
+}
diff --git a/src/UniGetUI.Avalonia/Program.cs b/src/UniGetUI.Avalonia/Program.cs
index 271d958a0..ea2f59c1b 100644
--- a/src/UniGetUI.Avalonia/Program.cs
+++ b/src/UniGetUI.Avalonia/Program.cs
@@ -9,8 +9,13 @@ sealed class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
- public static void Main(string[] args) => BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
+ public static void Main(string[] args)
+ {
+ AppDomain.CurrentDomain.UnhandledException += (_, e) =>
+ CrashHandler.ReportFatalException((Exception)e.ExceptionObject);
+
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ }
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
index 3b3ee8888..3c3542999 100644
--- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
+++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
@@ -167,6 +167,9 @@
ManagersHomepage.axaml
Code
+
+ CrashReportWindow.axaml
+
OperationOutputWindow.axaml
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml
new file mode 100644
index 000000000..6d5a5106c
--- /dev/null
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs
new file mode 100644
index 000000000..53c1d0633
--- /dev/null
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs
@@ -0,0 +1,65 @@
+using System.Net.Http;
+using System.Text;
+using System.Text.Json.Nodes;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.Tools;
+
+namespace UniGetUI.Avalonia.Views.DialogPages;
+
+internal sealed partial class CrashReportWindow : Window
+{
+ private readonly string _crashReport;
+
+ public CrashReportWindow(string crashReport)
+ {
+ _crashReport = crashReport;
+ InitializeComponent();
+ CrashReportText.Text = crashReport;
+ DontSendButton.Content = CoreTools.Translate("Don't Send");
+ SendButton.Content = CoreTools.Translate("Send Report");
+ }
+
+ private async void SendReport_Click(object? sender, RoutedEventArgs e)
+ {
+ SendButton.IsEnabled = false;
+ DontSendButton.IsEnabled = false;
+ SendButton.Content = CoreTools.Translate("Sending…");
+
+ string email = EmailBox.Text?.Trim() ?? string.Empty;
+ string details = DetailsBox.Text?.Trim() ?? string.Empty;
+
+ await Task.Run(() => SendReport(_crashReport, email, details));
+
+ Close();
+ }
+
+ private void DontSend_Click(object? sender, RoutedEventArgs e) => Close();
+
+ private static void SendReport(string errorBody, string email, string message)
+ {
+ try
+ {
+ var node = new JsonObject
+ {
+ ["email"] = email,
+ ["message"] = message,
+ ["errorMessage"] = errorBody,
+ ["productInfo"] = $"UniGetUI {CoreData.VersionName} (Build {CoreData.BuildNumber})"
+ };
+
+ using var client = new HttpClient(CoreTools.GenericHttpClientParameters);
+ client.Timeout = TimeSpan.FromSeconds(10);
+ using var content = new StringContent(
+ node.ToJsonString(), Encoding.UTF8, "application/json");
+ client.PostAsync(
+ "https://cloud.devolutions.net/api/senderrormessage", content)
+ .GetAwaiter().GetResult();
+ }
+ catch
+ {
+ // Network failures must not prevent the window from closing.
+ }
+ }
+}
diff --git a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json
index d10937885..28f07bce1 100644
--- a/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json
+++ b/src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json
@@ -1129,5 +1129,16 @@
"{pm} could not be found": "{pm} could not be found",
"{pm} found: {state}": "{pm} found: {state}",
"{pm} package manager specific preferences": "{pm} package manager specific preferences",
- "{pm} preferences": "{pm} preferences"
+ "{pm} preferences": "{pm} preferences",
+ "Additional details (optional)": "Additional details (optional)",
+ "Crash report": "Crash report",
+ "Describe what you were doing when the crash occurred…": "Describe what you were doing when the crash occurred…",
+ "Don't Send": "Don't Send",
+ "Email (optional)": "Email (optional)",
+ "Help us fix this by sending a crash report to Devolutions. All fields below are optional.": "Help us fix this by sending a crash report to Devolutions. All fields below are optional.",
+ "Send Report": "Send Report",
+ "Sending…": "Sending…",
+ "UniGetUI – Crash Report": "UniGetUI – Crash Report",
+ "UniGetUI has crashed": "UniGetUI has crashed",
+ "your@email.com": "your@email.com"
}
diff --git a/src/UniGetUI/App.xaml.cs b/src/UniGetUI/App.xaml.cs
index 970f5826c..745c564df 100644
--- a/src/UniGetUI/App.xaml.cs
+++ b/src/UniGetUI/App.xaml.cs
@@ -332,6 +332,24 @@ private async Task LoadComponentsAsync()
// Create MainWindow
InitializeMainWindow();
+ MainWindow.Activate();
+
+ // Show crash report from the previous session on top of the loading
+ // screen and wait for the user to dismiss it before continuing.
+ if (File.Exists(CrashHandler.PendingCrashFile))
+ {
+ try
+ {
+ string report = File.ReadAllText(CrashHandler.PendingCrashFile);
+ File.Delete(CrashHandler.PendingCrashFile);
+ var tcs = new TaskCompletionSource();
+ var crashWindow = new CrashReportWindow(report);
+ crashWindow.Closed += (_, _) => tcs.TrySetResult();
+ crashWindow.Activate();
+ await tcs.Task;
+ }
+ catch { /* must not prevent normal startup */ }
+ }
IEnumerable iniTasks =
[
diff --git a/src/UniGetUI/AutoUpdater.cs b/src/UniGetUI/AutoUpdater.cs
index f5b58d843..af157652d 100644
--- a/src/UniGetUI/AutoUpdater.cs
+++ b/src/UniGetUI/AutoUpdater.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+using System.Text.Json;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppNotifications;
diff --git a/src/UniGetUI/CrashHandler.cs b/src/UniGetUI/CrashHandler.cs
index 2f810d7fa..2425173dd 100644
--- a/src/UniGetUI/CrashHandler.cs
+++ b/src/UniGetUI/CrashHandler.cs
@@ -1,7 +1,6 @@
using System.Diagnostics;
-using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices;
using System.Text;
-using Microsoft.UI.Xaml.Markup;
using UniGetUI.Core.Data;
using UniGetUI.Core.Tools;
@@ -9,6 +8,9 @@ namespace UniGetUI;
public static class CrashHandler
{
+ public static readonly string PendingCrashFile =
+ Path.Combine(Path.GetTempPath(), "UniGetUI_pending_crash.txt");
+
private const uint MB_ICONSTOP = 0x00000010;
private const uint MB_OKCANCEL = 0x00000001;
private const uint MB_YESNOCANCEL = 0x00000003;
@@ -16,12 +18,10 @@ public static class CrashHandler
private const int IDYES = 6;
private const int IDNO = 7;
- [System.Runtime.InteropServices.DllImport(
- "user32.dll",
- CharSet = System.Runtime.InteropServices.CharSet.Unicode
- )]
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
+ // ── Missing-files handler ─────────────────────────────────────────────────
private static void _reportMissingFiles(out bool showDetailedReport)
{
try
@@ -206,19 +206,16 @@ Inner exception details (depth level: {{i}})
Console.WriteLine(Error_String);
- string ErrorUrl =
- $"https://www.marticliment.com/error-report/"
- + $"?appName=UniGetUI"
- + $"&appVersion={Uri.EscapeDataString(CoreData.VersionName)}"
- + $"&buildNumber={Uri.EscapeDataString(CoreData.BuildNumber.ToString())}"
- + $"&errorBody={Uri.EscapeDataString(Error_String)}";
- Console.WriteLine(ErrorUrl);
-
- using Process p = new();
- p.StartInfo.FileName = ErrorUrl;
- p.StartInfo.CreateNoWindow = true;
- p.StartInfo.UseShellExecute = true;
- p.Start();
+ // Persist crash data so the next normal app launch can show the report.
+ try
+ {
+ File.WriteAllText(PendingCrashFile, Error_String, Encoding.UTF8);
+ }
+ catch
+ {
+ // If we can't write the file, nothing more we can do — just exit.
+ }
+
Environment.Exit(1);
}
}
diff --git a/src/UniGetUI/CrashReportWindow.xaml b/src/UniGetUI/CrashReportWindow.xaml
new file mode 100644
index 000000000..0ded6bb9e
--- /dev/null
+++ b/src/UniGetUI/CrashReportWindow.xaml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI/CrashReportWindow.xaml.cs b/src/UniGetUI/CrashReportWindow.xaml.cs
new file mode 100644
index 000000000..efc9f3ff5
--- /dev/null
+++ b/src/UniGetUI/CrashReportWindow.xaml.cs
@@ -0,0 +1,91 @@
+using System.Net.Http;
+using System.Text;
+using System.Text.Json.Nodes;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using UniGetUI.Core.Data;
+using UniGetUI.Core.Tools;
+using Windows.Graphics;
+
+namespace UniGetUI;
+
+internal sealed partial class CrashReportWindow : Window
+{
+ private readonly string _crashReport;
+
+ public CrashReportWindow(string crashReport)
+ {
+ _crashReport = crashReport;
+ InitializeComponent();
+ ExtendsContentIntoTitleBar = true;
+
+ var area = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Primary);
+ int w = 1200;
+ int h = 1175;
+ AppWindow.Resize(new SizeInt32(w, h));
+ if (area is not null)
+ {
+ AppWindow.Move(new PointInt32(
+ (area.WorkArea.Width - w) / 2,
+ (area.WorkArea.Height - h) / 2
+ ));
+ }
+
+ Title = CoreTools.Translate("UniGetUI – Crash Report");
+ TitleText.Text = CoreTools.Translate("UniGetUI has crashed");
+ DescriptionText.Text = CoreTools.Translate("Help us fix this by sending a crash report to Devolutions. All fields below are optional.");
+ EmailBox.Header = CoreTools.Translate("Email (optional)");
+ EmailBox.PlaceholderText = CoreTools.Translate("your@email.com");
+ DetailsBox.Header = CoreTools.Translate("Additional details (optional)");
+ DetailsBox.PlaceholderText = CoreTools.Translate("Describe what you were doing when the crash occurred…");
+ CrashReportText.Header = CoreTools.Translate("Crash report");
+ CrashReportText.Text = crashReport;
+ DontSendButton.Content = CoreTools.Translate("Don't Send");
+ SendButton.Content = CoreTools.Translate("Send Report");
+ }
+
+ private async void SendReport_Click(object sender, RoutedEventArgs e)
+ {
+ SendButton.IsEnabled = false;
+ DontSendButton.IsEnabled = false;
+ SendButton.Content = CoreTools.Translate("Sending…");
+
+ string email = EmailBox.Text.Trim();
+ string details = DetailsBox.Text.Trim();
+
+ await Task.Run(() => SendReport(_crashReport, email, details));
+
+ Close();
+ }
+
+ private void DontSend_Click(object sender, RoutedEventArgs e) => Close();
+
+ private static void SendReport(
+ string errorBody,
+ string email,
+ string message)
+ {
+ try
+ {
+ var node = new JsonObject
+ {
+ ["email"] = email,
+ ["message"] = message,
+ ["errorMessage"] = errorBody,
+ ["productInfo"] = $"UniGetUI {CoreData.VersionName} (Build {CoreData.BuildNumber})"
+ };
+
+ using var client = new HttpClient(CoreTools.GenericHttpClientParameters);
+ client.Timeout = TimeSpan.FromSeconds(10);
+ using var content = new StringContent(
+ node.ToJsonString(), Encoding.UTF8, "application/json");
+ client.PostAsync(
+ "https://cloud.devolutions.net/api/senderrormessage", content)
+ .GetAwaiter().GetResult();
+ }
+ catch
+ {
+ // Network failures must not prevent the window from closing.
+ }
+ }
+}