mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-26 10:52:04 -04:00
Merge pull request #1679 from rmcrackan/rmcrackan/1677-linux-crash
Rmcrackan/1677 linux crash
This commit is contained in:
@@ -282,63 +282,64 @@ public static class LibationScaffolding
|
||||
}
|
||||
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
{
|
||||
#if DEBUG
|
||||
var mode = "Debug";
|
||||
var mode = "Debug";
|
||||
#else
|
||||
var mode = "Release";
|
||||
#endif
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
mode += " (Debugger attached)";
|
||||
if (Debugger.IsAttached)
|
||||
mode += " (Debugger attached)";
|
||||
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
|
||||
static int fileCount(FileManager.LongPath? longPath)
|
||||
{
|
||||
if (longPath is null)
|
||||
return -1;
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
catch { return -1; }
|
||||
}
|
||||
static int fileCount(FileManager.LongPath? longPath)
|
||||
{
|
||||
if (longPath is null)
|
||||
return -1;
|
||||
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||
{
|
||||
AppName = EntryAssembly?.GetName().Name,
|
||||
Version = BuildVersion?.ToString(),
|
||||
ReleaseIdentifier,
|
||||
Configuration.OS,
|
||||
Environment.OSVersion,
|
||||
InteropFactory.InteropFunctionsType,
|
||||
Mode = mode,
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||
{
|
||||
AppName = EntryAssembly?.GetName().Name,
|
||||
Version = BuildVersion?.ToString(),
|
||||
ReleaseIdentifier,
|
||||
Configuration.OS,
|
||||
Environment.OSVersion,
|
||||
InteropFactory.InteropFunctionsType,
|
||||
Mode = mode,
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||
|
||||
config.AutoScan,
|
||||
config.BetaOptIn,
|
||||
config.UseCoverAsFolderIcon,
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
config.AutoScan,
|
||||
config.BetaOptIn,
|
||||
config.UseCoverAsFolderIcon,
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
|
||||
config.InProgress,
|
||||
config.InProgress,
|
||||
|
||||
AudibleFileStorage.DownloadsInProgressDirectory,
|
||||
DownloadsInProgressFiles = fileCount(AudibleFileStorage.DownloadsInProgressDirectory),
|
||||
AudibleFileStorage.DownloadsInProgressDirectory,
|
||||
DownloadsInProgressFiles = fileCount(AudibleFileStorage.DownloadsInProgressDirectory),
|
||||
|
||||
AudibleFileStorage.DecryptInProgressDirectory,
|
||||
DecryptInProgressFiles = fileCount(AudibleFileStorage.DecryptInProgressDirectory),
|
||||
AudibleFileStorage.DecryptInProgressDirectory,
|
||||
DecryptInProgressFiles = fileCount(AudibleFileStorage.DecryptInProgressDirectory),
|
||||
|
||||
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
|
||||
});
|
||||
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
|
||||
});
|
||||
|
||||
if (InteropFactory.InteropFunctionsType is null)
|
||||
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
|
||||
}
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
if (InteropFactory.InteropFunctionsType is null)
|
||||
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
{
|
||||
LibraryCommands.LibrarySizeChanged += (object? _, List<DataLayer.LibraryBook> libraryBooks)
|
||||
=> SearchEngineCommands.FullReIndex(libraryBooks);
|
||||
@@ -389,8 +390,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);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace ApplicationServices;
|
||||
|
||||
public static class DbContexts
|
||||
{
|
||||
private static bool _sqliteDbValidated;
|
||||
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 //
|
||||
|
||||
120
Source/LibationFileManager/EssentialFileValidator.cs
Normal file
120
Source/LibationFileManager/EssentialFileValidator.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
|
||||
Log.Logger.Debug("Essential file validated: {DisplayName} at \"{Path}\"", displayName, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,19 +66,30 @@ public class LibationFiles
|
||||
|
||||
/// <summary>
|
||||
/// Set the location of the Libation Files directory, updating appsettings.json.
|
||||
/// Never persists a relative path; always writes an absolute path so resolution is consistent on all platforms.
|
||||
/// </summary>
|
||||
public void SetLibationFiles(LongPath libationFilesDirectory)
|
||||
{
|
||||
var pathToPersist = libationFilesDirectory.Path;
|
||||
if (!string.IsNullOrWhiteSpace(pathToPersist) && !Path.IsPathRooted(pathToPersist))
|
||||
{
|
||||
var basePath = AppsettingsJsonFile is not null ? Path.GetDirectoryName(AppsettingsJsonFile) : null;
|
||||
pathToPersist = Path.GetFullPath(pathToPersist, !string.IsNullOrEmpty(basePath) ? basePath : Configuration.ProcessDirectory);
|
||||
}
|
||||
|
||||
if (AppsettingsJsonFile is null)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(LIBATION_FILES_DIR, libationFilesDirectory);
|
||||
Environment.SetEnvironmentVariable(LIBATION_FILES_DIR, pathToPersist);
|
||||
Location = pathToPersist;
|
||||
return;
|
||||
}
|
||||
|
||||
Location = pathToPersist;
|
||||
|
||||
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = (string)(Location = libationFilesDirectory);
|
||||
jObj[LIBATION_FILES_KEY] = pathToPersist;
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents == endingContents)
|
||||
@@ -88,11 +99,11 @@ public class LibationFiles
|
||||
{
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
||||
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
|
||||
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, pathToPersist });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, libationFilesDirectory });
|
||||
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, pathToPersist });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +133,7 @@ public class LibationFiles
|
||||
try
|
||||
{
|
||||
File.WriteAllText(settingsFile, "{}");
|
||||
EssentialFileValidator.ValidateCreatedAndReport(settingsFile);
|
||||
}
|
||||
catch (Exception createEx)
|
||||
{
|
||||
@@ -210,6 +222,7 @@ public class LibationFiles
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(appsettingsFile, endingContents);
|
||||
EssentialFileValidator.ValidateCreatedAndReport(appsettingsFile);
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -248,6 +261,17 @@ public class LibationFiles
|
||||
libationFiles = runShellCommand("echo " + libationFiles) ?? libationFiles;
|
||||
}
|
||||
|
||||
// Resolve relative paths to absolute using the appsettings.json directory as base,
|
||||
// so that Location is consistent everywhere (e.g. avoids different resolution when
|
||||
// loading Serilog config vs. checking SettingsAreValid). Fixes Linux crash when
|
||||
// appsettings in process dir contains "./LibationFiles" (e.g. issue #1677).
|
||||
if (!string.IsNullOrWhiteSpace(libationFiles) && !Path.IsPathRooted(libationFiles))
|
||||
{
|
||||
var appSettingsDir = Path.GetDirectoryName(appsettingsPath.Path);
|
||||
var basePath = !string.IsNullOrEmpty(appSettingsDir) ? appSettingsDir : Configuration.ProcessDirectory;
|
||||
libationFiles = Path.GetFullPath(libationFiles, basePath);
|
||||
}
|
||||
|
||||
return libationFiles;
|
||||
|
||||
static string? runShellCommand(string command)
|
||||
|
||||
@@ -237,5 +237,7 @@ public class LibationSetup
|
||||
};
|
||||
var contents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
File.WriteAllText(settingsFilePath, contents);
|
||||
|
||||
EssentialFileValidator.ValidateCreatedAndReport(settingsFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 //
|
||||
|
||||
Reference in New Issue
Block a user