/*
* Copyright (c) 2025 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN 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.
*
* ProtonVPN 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 ProtonVPN. If not, see .
*/
using System.Diagnostics;
using System.Runtime.CompilerServices;
using TaskExtensions = ProtonVPN.Common.Core.Extensions.TaskExtensions;
namespace ProtonVPN.Logging.Events;
public static class GlobalExceptionHandler
{
public static void Initialize()
{
EventLogger.Initialize();
AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
TaskExtensions.SetDefaultExceptionHandler(ex =>
TryWriteEventLog("Fire-and-forget task exception", ex));
}
private static void OnAppDomainUnhandledException(object? sender, UnhandledExceptionEventArgs eventArgs)
{
const string HANDLER = "AppDomain unhandled exception";
string terminatingText = eventArgs.IsTerminating ? "(Terminating)" : string.Empty;
Exception ex = eventArgs.ExceptionObject switch
{
RuntimeWrappedException wrappedException => new Exception(
$"Non-Exception object thrown: " +
$"{wrappedException.WrappedException?.GetType().FullName} — {wrappedException.WrappedException}",
wrappedException),
Exception exception => exception,
object thrownObject => new Exception(
$"Non-Exception object thrown: " +
$"{thrownObject.GetType().FullName} — {thrownObject}"),
null => new Exception("Non-Exception object thrown: ")
};
TryWriteEventLog($"{HANDLER} {terminatingText}", ex);
}
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs ex)
{
TryWriteEventLog("Unobserved task exception", ex.Exception);
ex.SetObserved();
}
public static void TryWriteEventLog(string handler, Exception? ex)
{
try
{
if (ex is null)
{
return;
}
Exception diagnosticEx = ex is AggregateException agg && agg.Flatten().InnerExceptions.Count == 1
? agg.Flatten().InnerExceptions[0]
: ex;
string flattenedDetails = ex is AggregateException aggregate
? FormatAggregateException(aggregate)
: string.Empty;
string message =
$"Proton VPN Windows error event log{Environment.NewLine}" +
$"Date: {DateTimeOffset.UtcNow:o}{Environment.NewLine}" +
$"Handler: {handler}{Environment.NewLine}" +
Environment.NewLine +
$"Exception HResult: 0x{diagnosticEx.HResult:X8}{Environment.NewLine}" +
$"Exception type: {diagnosticEx.GetType().FullName}{Environment.NewLine}" +
$"Exception message: {diagnosticEx.Message}{Environment.NewLine}" +
flattenedDetails +
Environment.NewLine +
$"Full exception: {ex}";
EventLogger.Log(EventLogEntryType.Error, message);
}
catch { }
}
private static string FormatAggregateException(AggregateException aggregate)
{
AggregateException flattened = aggregate.Flatten();
if (flattened.InnerExceptions.Count <= 1)
{
return string.Empty;
}
string details = string.Join(Environment.NewLine,
flattened.InnerExceptions.Select((e, i) =>
$" [{i + 1}] {e.GetType().FullName}: {e.Message}"));
return $"Inner exceptions ({flattened.InnerExceptions.Count}):{Environment.NewLine}" +
details + Environment.NewLine;
}
}