Files
Libation/Source/LibationFileManager/InteropFactory.cs
T
Cursor Agentandrmcrackan 1729f84dbb Log startup recovery without Serilog so a broken install cannot silence it
Libation's releases are ReadyToRun, so a Serilog reference resolves lazily
when the line holding it runs. An install folder whose Serilog.dll is missing
or older than the build therefore threw inside the very catch blocks that were
meant to report the problem, which destroyed the original exception and aborted
the in-app upgrade recovery before it could roll anything back.

Add StartupLog: a Serilog-free, never-throwing buffer for the window before
ConfigureLogging runs, replayed into Serilog once logging exists. Move the
pre-logging call sites onto it, including the crash-message and crash-dialog
paths that run precisely when the install is broken.

Those messages were silent before this change: Serilog.Log.Logger is still
Serilog's silent logger until App.RunMigrations. They now reach the log file.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-26 12:44:36 +00:00

132 lines
4.1 KiB
C#

using Dinah.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace LibationFileManager;
public static class InteropFactory
{
public static Type? InteropFunctionsType { get; }
public static IInteropFunctions Create() => _create();
//// examples of the pattern which could be useful later
//public static IInteropFunctions Create(string str, int i) => _create(str, i);
//public static IInteropFunctions Create(params object[] values) => _create(values);
private static IInteropFunctions? instance { get; set; }
private static IInteropFunctions _create(params object[] values)
{
instance ??=
InteropFunctionsType is null
? new NullInteropFunctions()
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
if (instance is null)
throw new TypeLoadException();
return instance;
}
#region load types
private const string CONFIG_APP_ENDING = "ConfigApp.dll";
public static Func<string, bool> MatchesOS { get; }
= Configuration.IsWindows ? a => Path.GetFileName(a).StartsWithInsensitive("win")
: Configuration.IsLinux ? a => Path.GetFileName(a).StartsWithInsensitive("linux")
: Configuration.IsMacOs ? a => Path.GetFileName(a).StartsWithInsensitive("mac") || Path.GetFileName(a).StartsWithInsensitive("osx")
: _ => false;
private static readonly EnumerationOptions enumerationOptions = new()
{
MatchType = MatchType.Simple,
MatchCasing = MatchCasing.CaseInsensitive,
IgnoreInaccessible = true,
RecurseSubdirectories = false,
ReturnSpecialDirectories = false
};
static InteropFactory()
{
// searches file names for potential matches; doesn't run anything
var configApp = getOSConfigApp();
// nothing to load (normal for LibationCli / Docker: OS interop is optional)
if (configApp is null)
{
StartupLog.Warning($"Unable to locate *{CONFIG_APP_ENDING}; continuing without OS interop helpers");
return;
}
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
try
{
var configAppAssembly = Assembly.LoadFrom(configApp);
var type = typeof(IInteropFunctions);
InteropFunctionsType = configAppAssembly
.GetTypes()
.FirstOrDefault(type.IsAssignableFrom);
}
catch (Exception e)
{
//None of the interop functions are strictly necessary for Libation to run.
StartupLog.Warning(e, $"Unable to load types from assembly {configApp}; continuing without OS interop helpers");
}
}
private static string? getOSConfigApp()
{
// find '*ConfigApp.dll' files
var appName =
Directory.EnumerateFiles(Configuration.ProcessDirectory, $"*{CONFIG_APP_ENDING}", enumerationOptions)
.FirstOrDefault(exe => MatchesOS(exe));
return appName;
}
private static Dictionary<string, Assembly?> lowEffortCache { get; } = new();
private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{
var asmName = new AssemblyName(args.Name);
var here = Configuration.ProcessDirectory;
var key = $"{asmName}|{here}";
if (lowEffortCache.TryGetValue(key, out var value))
return value;
var assembly = CurrentDomain_AssemblyResolve_internal(asmName, here: here);
lowEffortCache[key] = assembly;
// Let the runtime handle any dll not found exceptions
if (assembly is null)
StartupLog.Warning($"Unable to load module {args.Name}");
return assembly;
}
private static Assembly? CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
{
/*
* Find the requested assembly in the program files directory.
* Assumes that all assemblies are in this application's directory.
* If they're not (e.g. the app is not self-contained), you will need
* to located them. The original way of doing this was to execute the
* config app, wait for the runtime to load all dependencies, and
* then seach the Process.Modules for the assembly name. Code for
* this approach is still in the _Demos projects.
*/
var modulePath =
Directory.EnumerateFiles(here, $"{asmName.Name}.dll", enumerationOptions)
.SingleOrDefault();
return modulePath is null ? null : Assembly.LoadFrom(modulePath);
}
#endregion
}