From 74b18c170a5ebade716b78daba70d581f1f64037 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Mon, 18 May 2026 08:54:40 -0400 Subject: [PATCH] #1822 - Fix the startup crash after auto-upgrade by loading the library only after InteropFactory assembly resolution is ready, checking that Microsoft.EntityFrameworkCore.Sqlite.dll is present in the install folder, and showing a clear reinstall message instead of a generic "Unexpected error" when that still fails. --- Source/LibationAvalonia/App.axaml.cs | 19 +++++- Source/LibationAvalonia/Program.cs | 3 +- .../StartupAssemblyBootstrap.cs | 62 +++++++++++++++++++ Source/LibationWinForms/Program.cs | 54 +++++++++++----- 4 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 Source/LibationFileManager/StartupAssemblyBootstrap.cs diff --git a/Source/LibationAvalonia/App.axaml.cs b/Source/LibationAvalonia/App.axaml.cs index f69744ab..260e8ed8 100644 --- a/Source/LibationAvalonia/App.axaml.cs +++ b/Source/LibationAvalonia/App.axaml.cs @@ -73,6 +73,7 @@ public class App : Application { // setup succeeded or wasn't needed and LibationFiles are valid RunMigrations(config); + StartupAssemblyBootstrap.PrepareForBackgroundDataAccess(); LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); ShowMainWindow(desktop); } @@ -142,8 +143,22 @@ public class App : Application { if (LibraryTask is not null && MainWindow is not null) { - List library = await LibraryTask; - await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); + try + { + List library = await LibraryTask; + await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library)); + } + catch (Exception ex) when (StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex)) + { + Serilog.Log.Logger.Error(ex, "Failed to load library at startup"); + await MessageBox.Show( + MainWindow, + StartupAssemblyBootstrap.GetLibraryLoadFailureMessage(), + "Library load failed", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync([])); + } } } } diff --git a/Source/LibationAvalonia/Program.cs b/Source/LibationAvalonia/Program.cs index a0762347..decad869 100644 --- a/Source/LibationAvalonia/Program.cs +++ b/Source/LibationAvalonia/Program.cs @@ -56,8 +56,7 @@ static class Program if (config.LibationFiles.SettingsAreValid) { App.RunMigrations(config); - // When running inside Snap, UseWebView getter returns false to avoid portal/sandbox crashes (e.g. github ticket #1664). - //Start loading the library before loading the main form + StartupAssemblyBootstrap.PrepareForBackgroundDataAccess(); App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); } BuildAvaloniaApp()?.StartWithClassicDesktopLifetime([], ShutdownMode.OnExplicitShutdown); diff --git a/Source/LibationFileManager/StartupAssemblyBootstrap.cs b/Source/LibationFileManager/StartupAssemblyBootstrap.cs new file mode 100644 index 00000000..5ef8468e --- /dev/null +++ b/Source/LibationFileManager/StartupAssemblyBootstrap.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; + +namespace LibationFileManager; + +/// +/// Ensures OS interop assembly resolution and required dependency files are ready before background library load. +/// +public static class StartupAssemblyBootstrap +{ + public const string EntityFrameworkCoreSqliteAssemblyFileName = "Microsoft.EntityFrameworkCore.Sqlite.dll"; + + /// + /// Registers assembly resolution and verifies required install-folder assemblies exist. + /// Call before Task.Run loads the library or opens the database on a thread-pool thread. + /// + public static void PrepareForBackgroundDataAccess() + { + _ = InteropFactory.InteropFunctionsType; + ValidateEntityFrameworkCoreSqlitePresent(); + } + + public static string GetLibraryLoadFailureMessage() => + $""" + Libation could not load its database components (Entity Framework Core for SQLite). + + This often happens after an incomplete in-app upgrade. Quit Libation completely, then install a fresh copy of the latest release to a new folder (do not overlay files on top of the old install). + + Install folder: + {Configuration.ProcessDirectory} + + Expected file: + {Path.Combine(Configuration.ProcessDirectory, EntityFrameworkCoreSqliteAssemblyFileName)} + """; + + public static bool IsMissingDependencyAssembly(Exception ex) + { + for (var current = ex; current is not null; current = current.InnerException) + { + if (current is not FileNotFoundException and not FileLoadException) + continue; + + var name = (current as FileNotFoundException)?.FileName ?? current.Message; + if (name.Contains("EntityFrameworkCore", StringComparison.OrdinalIgnoreCase) + || name.Contains("Microsoft.Data.Sqlite", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + private static void ValidateEntityFrameworkCoreSqlitePresent() + { + var path = Path.Combine(Configuration.ProcessDirectory, EntityFrameworkCoreSqliteAssemblyFileName); + if (File.Exists(path)) + return; + + throw new FileNotFoundException( + $"Required file '{EntityFrameworkCoreSqliteAssemblyFileName}' was not found in the Libation install folder.{Environment.NewLine}{Environment.NewLine}{GetLibraryLoadFailureMessage()}", + path); + } +} diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs index 28edd8ed..7ce5be0b 100644 --- a/Source/LibationWinForms/Program.cs +++ b/Source/LibationWinForms/Program.cs @@ -4,6 +4,7 @@ using DataLayer; using LibationFileManager; using LibationUiBase; using LibationWinForms.Dialogs; +using Serilog; using System; using System.Collections.Generic; using System.Linq; @@ -58,26 +59,31 @@ static class Program // migrations which require Forms or are long-running RunWindowsOnlyMigrations(config); - //*******************************************************************// - // // - // Start loading the library as soon as possible // - // // - // Before calling anything else, including subscribing to events, // - // to ensure database exists. If we wait and let it happen lazily, // - // race conditions and errors are likely during new installs // - // // - //*******************************************************************// - libraryLoadTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); - MessageBoxLib.VerboseLoggingWarning_ShowIfTrue(); - // logging is init'd here + // logging is init'd here (also initializes InteropFactory via logStartupState) LibationScaffolding.RunPostMigrationScaffolding(Variety.Classic, config); + + //*******************************************************************// + // // + // Start loading the library as soon as possible after logging // + // and InteropFactory assembly resolution are ready. // + // // + //*******************************************************************// + StartupAssemblyBootstrap.PrepareForBackgroundDataAccess(); + libraryLoadTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); } catch (Exception ex) { - var title = "Fatal error, pre-logging"; - var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."; + if (Configuration.Instance.SerilogInitialized) + Log.Error(ex, "Fatal error during startup"); + + var (title, body) = StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex) + ? ("Library load failed", StartupAssemblyBootstrap.GetLibraryLoadFailureMessage()) + : ( + "Fatal error, pre-logging", + "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file."); + try { MessageBoxLib.ShowAdminAlert(null, body, title, ex); @@ -93,7 +99,7 @@ static class Program postLoggingGlobalExceptionHandling(); form1 = new Form1(); - form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask); + form1.Load += async (_, _) => await LoadLibraryIntoFormAsync(form1, libraryLoadTask); Application.Run(form1); } @@ -181,6 +187,24 @@ static class Program } } + private static async Task LoadLibraryIntoFormAsync(Form1 form, Task> libraryLoadTask) + { + try + { + await form.InitLibraryAsync(await libraryLoadTask); + } + catch (Exception ex) when (StartupAssemblyBootstrap.IsMissingDependencyAssembly(ex)) + { + Log.Error(ex, "Failed to load library at startup"); + MessageBoxLib.ShowAdminAlert( + form, + StartupAssemblyBootstrap.GetLibraryLoadFailureMessage(), + "Library load failed", + ex); + await form.InitLibraryAsync([]); + } + } + private static void postLoggingGlobalExceptionHandling() { // this line is all that's needed for strict handling