using ApplicationServices; using AppScaffolding; using Avalonia; using Avalonia.Controls; using Avalonia.Threading; using LibationAvalonia.Dialogs; using LibationFileManager; using LibationUiBase.Forms; using ReactiveUI.Avalonia; using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; namespace LibationAvalonia; static class Program { private static System.Threading.Lock SetupLock { get; } = new(); /// Held for the lifetime of the process to enforce a single instance per LibationFiles folder. private static SingleInstance? SingleInstanceLock { get; set; } [STAThread] static void Main(string[] args) { if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover") { //Launch the Hangover app within the sandbox //We can do this because we're already executing inside the sandbox. //Any process created in the sandbox executes in the same sandbox. //Unfortunately, all sandbox files are read/execute, so no writing! HangoverLauncher.Launch(); return; } if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "cli") { //Open a new Terminal in the sandbox Process.Start( "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", $"\"{Configuration.ProcessDirectory}\""); return; } AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; // When essential file validation fails and the error cannot be written to the log, show the user EssentialFileValidator.ShowUserWhenLogUnavailable = msg => Dispatcher.UIThread.Post(() => _ = MessageBoxBase.Show(null, msg, "Libation - Essential File Error", MessageBoxButtons.OK, MessageBoxIcon.Warning)); //***********************************************// // // // do not use Configuration before this line // // // //***********************************************// // Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration try { var config = LibationScaffolding.RunPreConfigMigrations(); // A rollback swaps install files out from under assemblies this process has already loaded, so // it must not go on to touch the database or open a window. App reports it and shuts down // instead. See issue #2001. App.StartupRecoveryNotice = StartupAssemblyBootstrap.RecoverFromIncompleteUpgradeIfNeeded(); if (App.StartupRecoveryNotice is null) { // Prevent a second instance from racing on the same database, search index, and log file. // Hold the lock for the whole process; skip all database access when we are not the first // instance so the running copy's state is never touched. See issue #1931. SingleInstanceLock = SingleInstance.TryAcquire(config.LibationFiles.Location); App.IsAnotherInstanceRunning = !SingleInstanceLock.IsFirstInstance; if (SingleInstanceLock.IsFirstInstance && config.LibationFiles.SettingsAreValid) { App.RunMigrations(config); StartupAssemblyBootstrap.PrepareForBackgroundDataAccess(); App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); } } BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([], ShutdownMode.OnExplicitShutdown); // After the lifetime ends, so the new process starts into a folder this one has finished with // and does not race it for the single-instance lock. if (App.RestartRequested) InstallRelauncher.TryRelaunch(); } catch (Exception ex) { if (new StackTrace(ex).GetFrames().Any(f => f.GetMethod()?.DeclaringType?.Assembly == typeof(NativeWebDialog).Assembly)) { //Many of the NativeWebDialog exceptions cannot be handled by user code, //so a webview failure is a fatal error. Disable webview usage and rely //on the external browser login method instead. Configuration.Instance.UseWebView = false; } LogAndShowCrashMessage(ex); } } 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 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) { string? crashLogFile = null; try { //Try to log the error message before displaying the crash dialog if (Configuration.Instance.SerilogInitialized) Serilog.Log.Logger.Error(exception, "CRASH"); else crashLogFile = PreLoggingCrashLog.TryWrite(exception, [("ReleaseIdentifier", LibationScaffolding.ReleaseIdentifier.ToString())]); } 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, crashLogFile)); } catch (Exception ex) { Environment.FailFast("Fatal error displaying crash message", new AggregateException(ex, exception)); } } private static void DisplayErrorMessage(Exception exception, string? crashLogFile) { var dispatcher = new DispatcherFrame(); var fatalMessage = StartupAssemblyBootstrap.GetFatalStartupMessage( exception, new FatalStartupMessage( "Libation Crash", $""" Libation encountered a fatal error and must close. {DescribeCrashLog(crashLogFile)} """)); var mbAlert = new MessageBoxAlertAdminDialog(fatalMessage.Body, fatalMessage.Title, exception); mbAlert.Closed += (_, _) => dispatcher.Continue = false; mbAlert.Show(); Dispatcher.UIThread.PushFrame(dispatcher); } /// /// Names the file the crash was actually written to. This used to name LibationCrash.log /// unconditionally, which is not where the record goes when a Log*.log already exists, so reporters /// went looking for a file that was not there and attached nothing. See issue #2001. /// private static string DescribeCrashLog(string? crashLogFile) => crashLogFile is null ? "Please consider reporting this issue on GitHub. Libation could not write this error to a log file, so please include the text below." : $""" Please consider reporting this issue on GitHub, including the contents of this file: {crashLogFile} """; }