Files
Libation/Source/LibationWinForms/Program.cs

219 lines
7.9 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;
[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();
LibationUiBase.Forms.MessageBoxBase.ShowAsyncImpl = ShowMessageBox;
// 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)
{
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);
}
catch
{
MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", 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;
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.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
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);
}
}