Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/UniGetUI.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions src/UniGetUI.Avalonia/CrashHandler.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
9 changes: 7 additions & 2 deletions src/UniGetUI.Avalonia/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@
<DependentUpon>ManagersHomepage.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\DialogPages\CrashReportWindow.axaml.cs">
<DependentUpon>CrashReportWindow.axaml</DependentUpon>
</Compile>
<Compile Update="Views\DialogPages\OperationOutputWindow.axaml.cs">
<DependentUpon>OperationOutputWindow.axaml</DependentUpon>
</Compile>
Expand Down
69 changes: 69 additions & 0 deletions src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions"
x:Class="UniGetUI.Avalonia.Views.DialogPages.CrashReportWindow"
Title="{t:Translate UniGetUI – Crash Report}"
Width="800" MinWidth="500"
Height="580" MinHeight="380"
CanResize="True"
ShowInTaskbar="False"
Background="{DynamicResource AppDialogBackground}"
WindowStartupLocation="CenterOwner">

<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="24,20,24,24" Spacing="16">

<!-- Title + description -->
<StackPanel Spacing="4">
<TextBlock Text="{t:Translate UniGetUI has crashed}"
FontSize="22"
FontWeight="SemiBold"/>
<TextBlock Text="{t:Translate Text='Help us fix this by sending a crash report to Devolutions. All fields below are optional.'}"
Opacity="0.7"
TextWrapping="Wrap"/>
</StackPanel>

<!-- Email field -->
<StackPanel Spacing="4">
<TextBlock Text="{t:Translate Text='Email (optional)'}" FontSize="12" Opacity="0.7"/>
<TextBox x:Name="EmailBox"
PlaceholderText="{t:Translate Text='your@email.com'}"/>
</StackPanel>

<!-- Additional details field -->
<StackPanel Spacing="4">
<TextBlock Text="{t:Translate Text='Additional details (optional)'}" FontSize="12" Opacity="0.7"/>
<TextBox x:Name="DetailsBox"
AcceptsReturn="True"
Height="90"
PlaceholderText="{t:Translate Text='Describe what you were doing when the crash occurred…'}"
TextWrapping="Wrap"/>
</StackPanel>

<!-- Crash report (read-only) -->
<StackPanel Spacing="4">
<TextBlock Text="{t:Translate Text='Crash report'}" FontSize="12" Opacity="0.7"/>
<TextBox x:Name="CrashReportText"
AcceptsReturn="True"
FontFamily="Cascadia Code,Cascadia Mono,Consolas,Menlo,monospace"
FontSize="11"
Height="160"
IsReadOnly="True"
TextWrapping="NoWrap"/>
</StackPanel>

<!-- Action buttons (content set in code-behind for translation) -->
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="8">
<Button x:Name="DontSendButton"
Click="DontSend_Click"/>
<Button x:Name="SendButton"
Classes="accent"
Click="SendReport_Click"/>
</StackPanel>

</StackPanel>
</ScrollViewer>

</Window>
65 changes: 65 additions & 0 deletions src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs
Original file line number Diff line number Diff line change
@@ -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.
}
}
}
13 changes: 12 additions & 1 deletion src/UniGetUI.Core.LanguageEngine/Assets/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
18 changes: 18 additions & 0 deletions src/UniGetUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task> iniTasks =
[
Expand Down
1 change: 1 addition & 0 deletions src/UniGetUI/AutoUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading