mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-14 14:47:30 -04:00
Chardonnay's Serilog-free crash record was private to its entry point, so Classic wrote nothing at all when startup failed before logging. Move it to LibationFileManager as PreLoggingCrashLog, where both UIs can call it and where the existing test project can cover it. It needs nothing from AppScaffolding: Configuration.LibationVersion already holds the version, and ReleaseIdentifier is not populated this early, so callers pass what they know. Guard every field, since an unguarded InteropFactory.InteropFunctionsType, whose static constructor logged through Serilog, was enough for the bare catch to swallow the whole record and leave no trace of the crash. Return the file written so the dialog names it: both UIs pointed at LibationCrash.log, which is not where the record goes when a Log*.log already exists. RecoverFromIncompleteUpgradeIfNeeded now reports a completed rollback, and both entry points show it and quit. Continuing meant running the assemblies already loaded against the older files just restored underneath them. Consuming the alert at startup also retires the two late call sites that could never fire. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
270 lines
10 KiB
C#
270 lines
10 KiB
C#
using ApplicationServices;
|
|
using AppScaffolding;
|
|
using DataLayer;
|
|
using LibationFileManager;
|
|
using LibationUiBase;
|
|
using LibationWinForms.Dialogs;
|
|
using Serilog;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
|
|
namespace LibationWinForms;
|
|
|
|
static class Program
|
|
{
|
|
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
|
static extern bool AllocConsole();
|
|
|
|
private static Form1? form1;
|
|
|
|
/// <summary>Held for the lifetime of the process to enforce a single instance per LibationFiles folder.</summary>
|
|
private static SingleInstance? singleInstanceLock;
|
|
|
|
[STAThread]
|
|
static void Main()
|
|
{
|
|
Task<List<LibraryBook>> libraryLoadTask;
|
|
|
|
try
|
|
{
|
|
//// Uncomment to see Console. Must be called before anything writes to Console.
|
|
//// Only use while debugging. Acts erratically in the wild
|
|
//AllocConsole();
|
|
|
|
// run as early as possible. see notes in postLoggingGlobalExceptionHandling
|
|
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
|
|
|
|
ApplicationConfiguration.Initialize();
|
|
|
|
// When essential file validation fails and the error cannot be written to the log, show the user
|
|
EssentialFileValidator.ShowUserWhenLogUnavailable = msg => MessageBox.Show(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
|
|
var config = AppScaffolding.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. Nothing here has done either yet,
|
|
// which makes this the safe place to stop. See issue #2001.
|
|
if (StartupAssemblyBootstrap.RecoverFromIncompleteUpgradeIfNeeded() is { } recovery)
|
|
{
|
|
MessageBox.Show(recovery.Body, recovery.Title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
// Prevent a second instance from racing on the same database, search index, and log file.
|
|
// Bail out before any database access when we are not the first instance. See issue #1931.
|
|
singleInstanceLock = SingleInstance.TryAcquire(config.LibationFiles.Location);
|
|
if (!singleInstanceLock.IsFirstInstance)
|
|
{
|
|
MessageBox.Show(
|
|
"Libation is already running.\r\n\r\n"
|
|
+ "Please use the Libation window that is already open. Running more than one copy of "
|
|
+ "Libation against the same folder at the same time can corrupt your library.",
|
|
"Libation is already running",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox;
|
|
BadBookActionDialogBase.ShowAsyncImpl = ShowBadBookActionDialog;
|
|
|
|
// do this as soon as possible (post-config)
|
|
RunSetupIfNeededAsync(config);
|
|
|
|
// most migrations go in here
|
|
LibationScaffolding.RunPostConfigMigrations(config);
|
|
SetThemeColor(config);
|
|
|
|
// migrations which require Forms or are long-running
|
|
RunWindowsOnlyMigrations(config);
|
|
|
|
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
|
|
|
// 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)
|
|
{
|
|
string? crashLogFile = null;
|
|
try
|
|
{
|
|
if (Configuration.Instance.SerilogInitialized)
|
|
Log.Error(ex, "Fatal error during startup");
|
|
else
|
|
crashLogFile = PreLoggingCrashLog.TryWrite(ex, [("ReleaseIdentifier", LibationScaffolding.ReleaseIdentifier.ToString())]);
|
|
}
|
|
catch { /* continue to show the dialog even if logging fails */ }
|
|
|
|
var fatalMessage = StartupAssemblyBootstrap.GetFatalStartupMessage(
|
|
ex,
|
|
new FatalStartupMessage(
|
|
"Fatal error, pre-logging",
|
|
crashLogFile is null
|
|
? "An unrecoverable error occurred before logging could be initialized, and it could not be written to a log file. Please include the text below when reporting this."
|
|
: $"An unrecoverable error occurred before logging could be initialized.{Environment.NewLine}{Environment.NewLine}It was written to:{Environment.NewLine}{crashLogFile}"));
|
|
|
|
try
|
|
{
|
|
MessageBoxLib.ShowAdminAlert(null, fatalMessage.Body, fatalMessage.Title, ex);
|
|
}
|
|
catch
|
|
{
|
|
MessageBox.Show($"{fatalMessage.Body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", fatalMessage.Title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
|
|
postLoggingGlobalExceptionHandling();
|
|
|
|
form1 = new Form1();
|
|
form1.Load += async (_, _) => await LoadLibraryIntoFormAsync(form1, libraryLoadTask);
|
|
Application.Run(form1);
|
|
}
|
|
|
|
#region Message Box Handler for LibationUiBase
|
|
static Task<LibationUiBase.Forms.DialogResult> ShowMessageBox(
|
|
object? owner,
|
|
string message,
|
|
string caption,
|
|
LibationUiBase.Forms.MessageBoxButtons buttons,
|
|
LibationUiBase.Forms.MessageBoxIcon icon,
|
|
LibationUiBase.Forms.MessageBoxDefaultButton defaultButton,
|
|
bool _)
|
|
{
|
|
Func<DialogResult> showMessageBox = () => MessageBox.Show(
|
|
owner as IWin32Window ?? form1,
|
|
message,
|
|
caption,
|
|
(MessageBoxButtons)buttons,
|
|
(MessageBoxIcon)icon,
|
|
(MessageBoxDefaultButton)defaultButton);
|
|
|
|
|
|
var result = form1 is null ? showMessageBox() : form1.Invoke(showMessageBox);
|
|
return Task.FromResult((LibationUiBase.Forms.DialogResult)result);
|
|
}
|
|
#endregion;
|
|
|
|
#region Bad Book Action Dialog Handler for LibationUiBase
|
|
static Task<BadBookDialogResult> ShowBadBookActionDialog(object? owner, string message, string caption)
|
|
{
|
|
Func<BadBookDialogResult> showDialog = () =>
|
|
{
|
|
BadBookDialogResult result = new(LibationUiBase.Forms.DialogResult.Retry, false, false);
|
|
using var dialog = new BadBookActionDialog(message, caption);
|
|
dialog.ShowDialog(owner as IWin32Window ?? form1);
|
|
return dialog.Result;
|
|
};
|
|
|
|
var result = form1 is null ? showDialog() : form1.Invoke(showDialog);
|
|
return Task.FromResult(result);
|
|
}
|
|
#endregion;
|
|
|
|
private static void SetThemeColor(Configuration config)
|
|
{
|
|
var theme = config.ThemeVariant switch
|
|
{
|
|
Configuration.Theme.Light => SystemColorMode.Classic,
|
|
Configuration.Theme.Dark => SystemColorMode.Dark,
|
|
_ => SystemColorMode.System,
|
|
};
|
|
|
|
Application.SetColorMode(theme);
|
|
}
|
|
|
|
private static void RunSetupIfNeededAsync(Configuration config)
|
|
{
|
|
var setup = new LibationSetup(config.LibationFiles)
|
|
{
|
|
SetupPrompt = ShowSetup,
|
|
SelectFolderPrompt = SelectInstallLocation
|
|
};
|
|
|
|
if (!setup.RunSetupIfNeededAsync().GetAwaiter().GetResult())
|
|
{
|
|
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
Application.Exit();
|
|
Environment.Exit(-1);
|
|
}
|
|
|
|
static ILibationSetup ShowSetup()
|
|
{
|
|
var setupDialog = new SetupDialog();
|
|
setupDialog.ShowDialog();
|
|
return setupDialog;
|
|
}
|
|
|
|
static ILibationInstallLocation? SelectInstallLocation()
|
|
{
|
|
var libationFilesDialog = new LibationFilesDialog();
|
|
return libationFilesDialog.ShowDialog() is DialogResult.OK ? libationFilesDialog : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>migrations which require Forms or are long-running</summary>
|
|
private static void RunWindowsOnlyMigrations(Configuration config)
|
|
{
|
|
// examples:
|
|
// - only supported in winforms. don't move to app scaffolding
|
|
// - long running. won't get a chance to finish in cli. don't move to app scaffolding
|
|
|
|
const string hasMigratedKey = "hasMigratedToHighDPI";
|
|
if (!config.GetNonString(defaultValue: false, hasMigratedKey))
|
|
{
|
|
config.RemoveProperty(nameof(config.GridColumnsWidths));
|
|
|
|
foreach (var form in typeof(Program).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Form))))
|
|
config.RemoveProperty(form.Name);
|
|
|
|
config.SetNonString(true, hasMigratedKey);
|
|
}
|
|
}
|
|
|
|
private static async Task LoadLibraryIntoFormAsync(Form1 form, Task<List<LibraryBook>> libraryLoadTask)
|
|
{
|
|
try
|
|
{
|
|
await form.InitLibraryAsync(await libraryLoadTask);
|
|
}
|
|
catch (Exception ex) when (StartupAssemblyBootstrap.IsInstallFolderAssemblyLoadFailure(ex))
|
|
{
|
|
Log.Error(ex, "Failed to load library at startup");
|
|
FatalStartupMessage failure = StartupAssemblyBootstrap.GetStartupFailureMessage(ex)!;
|
|
MessageBoxLib.ShowAdminAlert(form, failure.Body, failure.Title, ex);
|
|
await form.InitLibraryAsync([]);
|
|
}
|
|
}
|
|
|
|
private static void postLoggingGlobalExceptionHandling()
|
|
{
|
|
// this line is all that's needed for strict handling
|
|
AppDomain.CurrentDomain.UnhandledException += (_, e) => MessageBoxLib.ShowAdminAlert(null, "Libation has crashed due to an unhandled error.", "Application crash!", (Exception)e.ExceptionObject);
|
|
|
|
// these 2 lines makes it graceful. sync (eg in main form's ctor) and thread exceptions will still crash us, but event (sync, void async, Task async) will not
|
|
Application.ThreadException += (_, e) => MessageBoxLib.ShowAdminAlert(null, "Libation has encountered an unexpected error.", "Unexpected error", e.Exception);
|
|
// move to beginning of execution. crashes app if this is called post-RunInstaller: System.InvalidOperationException: 'Thread exception mode cannot be changed once any Controls are created on the thread.'
|
|
//// I never found a case where including made a difference. I think this enum is default and including it will override app user config file
|
|
//Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
|
|
}
|
|
} |