diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml index e8fdaed8..eebd8765 100644 --- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml +++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml @@ -18,7 +18,7 @@ Margin="10,10,10,0" ColumnDefinitions="Auto,*"> - + - diff --git a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs index ddcfecf6..7307263b 100644 --- a/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/MessageBoxAlertAdminDialog.axaml.cs @@ -37,33 +37,34 @@ namespace LibationAvalonia.Dialogs } catch { - await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error); + await MessageBox.Show(this, $"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private async void GoToLogs_Tapped(object sender, Avalonia.Input.TappedEventArgs e) { - LongPath dir = ""; try { - dir = LibationFileManager.Configuration.Instance.LibationFiles; - } - catch { } - - try - { - Go.To.Folder(dir.ShortPathName); + Go.To.File(LibationFileManager.LogFileFilter.LogFilePath); } catch { - await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error); - } + LongPath dir = ""; + try + { + dir = LibationFileManager.Configuration.Instance.LibationFiles; + Go.To.Folder(dir.ShortPathName); + } + catch + { + await MessageBox.Show(this, $"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } } public void OkButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) { SaveAndClose(); } - } } diff --git a/Source/LibationAvalonia/Program.cs b/Source/LibationAvalonia/Program.cs index da21f786..c437fb36 100644 --- a/Source/LibationAvalonia/Program.cs +++ b/Source/LibationAvalonia/Program.cs @@ -7,16 +7,21 @@ using AppScaffolding; using Avalonia; using ReactiveUI.Avalonia; using LibationFileManager; +using LibationAvalonia.Dialogs; +using Avalonia.Threading; +using FileManager; +using System.Linq; #nullable enable namespace LibationAvalonia { static class Program { + private static System.Threading.Lock SetupLock { get; } = new(); + private static bool LoggingEnabled { get; set; } [STAThread] static void Main(string[] args) { - if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover") { //Launch the Hangover app within the sandbox @@ -34,9 +39,8 @@ namespace LibationAvalonia $"\"{Configuration.ProcessDirectory}\""); return; } - AppDomain.CurrentDomain.UnhandledException += (o, e) => LogError(e.ExceptionObject); + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - bool loggingEnabled = false; //***********************************************// // // // do not use Configuration before this line // @@ -51,30 +55,86 @@ namespace LibationAvalonia // most migrations go in here LibationScaffolding.RunPostConfigMigrations(config); LibationScaffolding.RunPostMigrationScaffolding(Variety.Chardonnay, config); - loggingEnabled = true; + LoggingEnabled = true; //Start loading the library before loading the main form App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); } - - BuildAvaloniaApp().StartWithClassicDesktopLifetime([]); + BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([]); } catch (Exception ex) { - if (loggingEnabled) - Serilog.Log.Logger.Error(ex, "CRASH"); - else - LogError(ex); + LogAndShowCrashMessage(ex); } } - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace() - .UseReactiveUI(); + public static AppBuilder? BuildAvaloniaApp() + { + //Ensure that setup is only run once + SetupLock.Enter(); + if (Application.Current is not null) + { + SetupLock.Exit(); + return null; + } + else + { + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI() + .AfterSetup(_ => SetupLock.Exit()); + } + } - private static void LogError(object exceptionObject) + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + var ex = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject.ToString()); + LogAndShowCrashMessage(ex); + } + + private static void LogAndShowCrashMessage(Exception exception) + { + try + { + //Try to log the error message before displaying the crash dialog + if (LoggingEnabled) + Serilog.Log.Logger.Error(exception, "CRASH"); + else + LogErrorWithoutSerilog(exception); + } + catch { /* continue to show the crash dialog even if logging fails */ } + + //Run setup if needed so that we can show the crash dialog + BuildAvaloniaApp()?.SetupWithoutStarting(); + + try + { + Dispatcher.UIThread.Invoke(() => DisplayErrorMessage(exception)); + } + catch (Exception ex) + { + Environment.FailFast("Fatal error displaying crash message", new AggregateException(ex, exception)); + } + } + + private static void DisplayErrorMessage(Exception exception) + { + var dispatcher = new DispatcherFrame(); + + var mbAlert = new MessageBoxAlertAdminDialog(""" + Libation encountered a fatal error and must close. + + Please consider reporting this issue on GitHub, including the contents of the LibationCrash.log file created in your user folder. + """, + "Libation Crash", + exception); + mbAlert.Closed += (_, _) => dispatcher.Continue = false; + mbAlert.Show(); + Dispatcher.UIThread.PushFrame(dispatcher); + } + + private static void LogErrorWithoutSerilog(object exceptionObject) { var logError = $""" {DateTime.Now} - Libation Crash @@ -88,9 +148,25 @@ namespace LibationAvalonia {exceptionObject} """; - var crashLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log"); + LongPath logFile; + try + { + //Try to add crash message to the newest existing Libation log file + //then to LibationFiles/LibationCrash.log + //then to %UserProfile%/LibationCrash.log + string logDir = Configuration.Instance.LibationFiles; + var existingLogFiles = Directory.GetFiles(logDir, "Log*.log"); - using var sw = new StreamWriter(crashLog, true); + logFile = existingLogFiles.Length == 0 ? getFallbackLogFile() + : existingLogFiles.Select(f => new FileInfo(f)).OrderByDescending(f => f.CreationTimeUtc).First().FullName; + } + catch + { + logFile = getFallbackLogFile(); + } + + + using var sw = new StreamWriter(logFile, true); sw.WriteLine(logError); static string getConfigValue(Func selector) @@ -104,6 +180,23 @@ namespace LibationAvalonia return ex.ToString(); } } + + static string getFallbackLogFile() + { + try + { + + string logDir = Configuration.Instance.LibationFiles; + if (!Directory.Exists(logDir)) + logDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + return Path.Combine(logDir, "LibationCrash.log"); + } + catch + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "LibationCrash.log"); + } + } } } } diff --git a/Source/LibationFileManager/LogFileFilter.cs b/Source/LibationFileManager/LogFileFilter.cs index 62c89097..955b15a9 100644 --- a/Source/LibationFileManager/LogFileFilter.cs +++ b/Source/LibationFileManager/LogFileFilter.cs @@ -38,7 +38,7 @@ public class LogFileFilter : IDestructuringPolicy private static readonly object lockObj = new(); public static string? ZipFilePath { get; private set; } public static string? LogFilePath { get; private set; } - public static void SetLogFilePath(string? logFilePath) + internal static void SetLogFilePath(string? logFilePath) { lock(lockObj) {