diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs
index 72e5d3a1..c6ab21f7 100644
--- a/Source/AppScaffolding/LibationScaffolding.cs
+++ b/Source/AppScaffolding/LibationScaffolding.cs
@@ -294,6 +294,10 @@ public static class LibationScaffolding
// begin logging session with a form feed
Log.Logger.Information("\r\n\f");
+ // Validate log file was created and is writable (OS may delay availability)
+ var logFilePath = Path.Combine(config.LibationFiles.Location, "Log.log");
+ EssentialFileValidator.ValidateCreatedAndReport(logFilePath);
+
static int fileCount(FileManager.LongPath? longPath)
{
if (longPath is null)
@@ -389,8 +393,8 @@ public static class LibationScaffolding
}
catch (Exception ex)
{
- // different text to make it easier to identify in logs, vs the AggregateException case above
- Log.Logger.Error(ex, "Version check failed. General exception");
+ // different text to make it easier to identify in logs, vs the AggregateException case above
+ Log.Logger.Error(ex, "Version check failed. General exception");
}
return (null, null, null, false, false);
}
diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs
index 55198181..4ede0548 100644
--- a/Source/ApplicationServices/DbContexts.cs
+++ b/Source/ApplicationServices/DbContexts.cs
@@ -7,6 +7,8 @@ namespace ApplicationServices;
public static class DbContexts
{
+ private static bool _sqliteDbValidated;
+
/// Use for fully functional context, incl. SaveChanges(). For query-only, use the other method
public static LibationContext GetContext()
{
@@ -14,6 +16,14 @@ public static class DbContexts
? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString)
: LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString);
context.Database.Migrate();
+
+ // Validate SQLite DB file was created and is accessible (once per process; OS may delay availability)
+ if (!_sqliteDbValidated && string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString))
+ {
+ EssentialFileValidator.ValidateCreatedAndReport(SqliteStorage.DatabasePath);
+ _sqliteDbValidated = true;
+ }
+
return context;
}
diff --git a/Source/LibationAvalonia/Program.cs b/Source/LibationAvalonia/Program.cs
index 1862045e..9e3491d0 100644
--- a/Source/LibationAvalonia/Program.cs
+++ b/Source/LibationAvalonia/Program.cs
@@ -6,6 +6,7 @@ using Avalonia.Threading;
using FileManager;
using LibationAvalonia.Dialogs;
using LibationFileManager;
+using LibationUiBase.Forms;
using ReactiveUI.Avalonia;
using System;
using System.Diagnostics;
@@ -40,6 +41,9 @@ static class Program
}
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+ // When essential file validation fails and the error cannot be written to the log, show the user
+ EssentialFileValidator.ShowUserWhenLogUnavailable = msg => Dispatcher.UIThread.Post(() => _ = MessageBoxBase.Show(null, msg, "Libation - Essential File Error", MessageBoxButtons.OK, MessageBoxIcon.Warning));
+
//***********************************************//
// //
// do not use Configuration before this line //
diff --git a/Source/LibationFileManager/EssentialFileValidator.cs b/Source/LibationFileManager/EssentialFileValidator.cs
new file mode 100644
index 00000000..9f4f8d7b
--- /dev/null
+++ b/Source/LibationFileManager/EssentialFileValidator.cs
@@ -0,0 +1,119 @@
+using Serilog;
+using System;
+using System.IO;
+using System.Threading;
+
+namespace LibationFileManager;
+
+///
+/// Validates that essential files were created correctly after creation, with retries to allow for OS delay.
+/// Callers should use to log errors and show the user when the log cannot be written.
+///
+public static class EssentialFileValidator
+{
+ ///
+ /// 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.
+ ///
+ public static Action? ShowUserWhenLogUnavailable { get; set; }
+
+ ///
+ /// Default retry total duration (ms) when checking that a file is available after creation.
+ ///
+ public const int DefaultMaxRetriesMs = 1000;
+
+ ///
+ /// Default delay (ms) between retries.
+ ///
+ public const int DefaultDelayMs = 50;
+
+ ///
+ /// Validates that the file at 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 .
+ ///
+ /// Full path to the file.
+ /// Total time to retry (ms).
+ /// Delay between retries (ms).
+ /// (true, null) if valid; (false, errorMessage) if validation failed.
+ 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
+ }
+
+ 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);
+ }
+
+ ///
+ /// Validates that the file was created correctly and, if validation fails, reports the failure (log and optionally user).
+ /// Equivalent to calling then when the result is not valid.
+ ///
+ /// True if the file is valid; false if validation failed (and failure has been reported).
+ 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;
+ }
+
+ ///
+ /// Reports a validation failure: tries to log the error; if logging fails, invokes .
+ /// The message is prefixed with a strongly worded error notice for both log and user display.
+ ///
+ /// Message to log and optionally show to the user.
+ public static void ReportValidationFailure(string errorMessage)
+ {
+ var fullMessage = $"Critical error! Essential file validation failed: {errorMessage}";
+ try
+ {
+ // Maybe change log level to Fatal later to stand out more
+ Log.Logger.Error(fullMessage);
+ }
+ catch
+ {
+ ShowUserWhenLogUnavailable?.Invoke(fullMessage);
+ }
+ }
+}
diff --git a/Source/LibationFileManager/LibationFiles.cs b/Source/LibationFileManager/LibationFiles.cs
index 24e59788..e0a593c6 100644
--- a/Source/LibationFileManager/LibationFiles.cs
+++ b/Source/LibationFileManager/LibationFiles.cs
@@ -133,6 +133,7 @@ public class LibationFiles
try
{
File.WriteAllText(settingsFile, "{}");
+ EssentialFileValidator.ValidateCreatedAndReport(settingsFile);
}
catch (Exception createEx)
{
@@ -221,6 +222,7 @@ public class LibationFiles
{
Directory.CreateDirectory(dir);
File.WriteAllText(appsettingsFile, endingContents);
+ EssentialFileValidator.ValidateCreatedAndReport(appsettingsFile);
return appsettingsFile;
}
catch (Exception ex)
diff --git a/Source/LibationUiBase/LibationSetup.cs b/Source/LibationUiBase/LibationSetup.cs
index 6855566a..3d45ae2c 100644
--- a/Source/LibationUiBase/LibationSetup.cs
+++ b/Source/LibationUiBase/LibationSetup.cs
@@ -237,5 +237,7 @@ public class LibationSetup
};
var contents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
File.WriteAllText(settingsFilePath, contents);
+
+ EssentialFileValidator.ValidateCreatedAndReport(settingsFilePath);
}
}
diff --git a/Source/LibationWinForms/Program.cs b/Source/LibationWinForms/Program.cs
index fddd2bb3..28edd8ed 100644
--- a/Source/LibationWinForms/Program.cs
+++ b/Source/LibationWinForms/Program.cs
@@ -36,6 +36,9 @@ static class Program
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 //