mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 14:17:15 -04:00
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>
122 lines
4.3 KiB
C#
122 lines
4.3 KiB
C#
using Serilog;
|
|
using System;
|
|
using System.IO;
|
|
using System.Threading;
|
|
|
|
namespace LibationFileManager;
|
|
|
|
/// <summary>
|
|
/// Validates that essential files were created correctly after creation, with retries to allow for OS delay.
|
|
/// Callers should use <see cref="ReportValidationFailure"/> to log errors and show the user when the log cannot be written.
|
|
/// </summary>
|
|
public static class EssentialFileValidator
|
|
{
|
|
/// <summary>
|
|
/// Called when an essential file validation fails and the error could not be written to the log.
|
|
/// Set by the host (e.g. LibationAvalonia, LibationWinForms) to display the message to the user.
|
|
/// </summary>
|
|
public static Action<string>? ShowUserWhenLogUnavailable { get; set; }
|
|
|
|
/// <summary>
|
|
/// Default retry total duration (ms) when checking that a file is available after creation.
|
|
/// </summary>
|
|
public const int DefaultMaxRetriesMs = 1000;
|
|
|
|
/// <summary>
|
|
/// Default delay (ms) between retries.
|
|
/// </summary>
|
|
public const int DefaultDelayMs = 50;
|
|
|
|
/// <summary>
|
|
/// Validates that the file at <paramref name="path"/> exists and is readable and writable,
|
|
/// with retries to allow for OS delay between create and availability.
|
|
/// Error messages use the file name portion of <paramref name="path"/>.
|
|
/// </summary>
|
|
/// <param name="path">Full path to the file.</param>
|
|
/// <param name="maxRetriesMs">Total time to retry (ms).</param>
|
|
/// <param name="delayMs">Delay between retries (ms).</param>
|
|
/// <returns>(true, null) if valid; (false, errorMessage) if validation failed.</returns>
|
|
public static (bool success, string? errorMessage) ValidateCreated(
|
|
string path,
|
|
int maxRetriesMs = DefaultMaxRetriesMs,
|
|
int delayMs = DefaultDelayMs)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return (false, "(unknown file): path is null or empty.");
|
|
|
|
var displayName = Path.GetFileName(path);
|
|
if (string.IsNullOrWhiteSpace(displayName))
|
|
displayName = path;
|
|
|
|
var stopAt = DateTime.UtcNow.AddMilliseconds(maxRetriesMs);
|
|
Exception? lastEx = null;
|
|
|
|
while (DateTime.UtcNow < stopAt)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
lastEx = new FileNotFoundException($"File not found after creation: {path}");
|
|
Thread.Sleep(delayMs);
|
|
continue;
|
|
}
|
|
|
|
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
|
|
{
|
|
// ensure we can open for read and write
|
|
}
|
|
|
|
// A Serilog failure here would be caught below and retried as if the file itself were bad.
|
|
StartupLog.Debug($"Essential file validated: {displayName} at \"{path}\"");
|
|
return (true, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastEx = ex;
|
|
Thread.Sleep(delayMs);
|
|
}
|
|
}
|
|
|
|
var msg = lastEx is not null
|
|
? $"{displayName} could not be validated at \"{path}\": {lastEx.Message}"
|
|
: $"{displayName} could not be validated at \"{path}\" (file not found or not accessible).";
|
|
return (false, msg);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that the file was created correctly and, if validation fails, reports the failure (log and optionally user).
|
|
/// Equivalent to calling <see cref="ValidateCreated"/> then <see cref="ReportValidationFailure"/> when the result is not valid.
|
|
/// </summary>
|
|
/// <returns>True if the file is valid; false if validation failed (and failure has been reported).</returns>
|
|
public static bool ValidateCreatedAndReport(
|
|
string path,
|
|
int maxRetriesMs = DefaultMaxRetriesMs,
|
|
int delayMs = DefaultDelayMs)
|
|
{
|
|
var (success, errorMessage) = ValidateCreated(path, maxRetriesMs, delayMs);
|
|
if (!success && errorMessage is not null)
|
|
ReportValidationFailure(errorMessage);
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reports a validation failure: tries to log the error; if logging fails, invokes <see cref="ShowUserWhenLogUnavailable"/>.
|
|
/// The message is prefixed with a strongly worded error notice for both log and user display.
|
|
/// </summary>
|
|
/// <param name="errorMessage">Message to log and optionally show to the user.</param>
|
|
public static void ReportValidationFailure(string errorMessage)
|
|
{
|
|
var fullMessage = $"Critical error! Essential file validation failed: {errorMessage}";
|
|
try
|
|
{
|
|
Log.Logger.Error("Critical error! Essential file validation failed: {ErrorMessage}. Call stack: {StackTrace}",
|
|
errorMessage, Environment.StackTrace);
|
|
}
|
|
catch
|
|
{
|
|
ShowUserWhenLogUnavailable?.Invoke(fullMessage);
|
|
}
|
|
}
|
|
}
|