mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -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>
305 lines
10 KiB
C#
305 lines
10 KiB
C#
using FileManager;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
[assembly: InternalsVisibleTo("AppScaffolding")]
|
|
[assembly: InternalsVisibleTo("LibationUiBase.Tests")]
|
|
[assembly: InternalsVisibleTo("LibationFileManager.Tests")]
|
|
|
|
namespace LibationFileManager;
|
|
|
|
/// <summary>
|
|
/// Provides access to Libation's configuration and settings file locations, including methods for validating and
|
|
/// updating the Libation files directory and Settings.json file. An instance is bount to a single appsettings.json file.
|
|
/// </summary>
|
|
public class LibationFiles
|
|
{
|
|
internal static string? s_DefaultLibationFilesDirectory;
|
|
public static string DefaultLibationFilesDirectory => s_DefaultLibationFilesDirectory ??= Configuration.IsWindows ? Configuration.UserProfile : Configuration.LocalAppData;
|
|
|
|
public const string LIBATION_FILES_KEY = "LibationFiles";
|
|
public const string SETTINGS_JSON = "Settings.json";
|
|
public const string LIBATION_FILES_DIR = "LIBATION_FILES_DIR";
|
|
|
|
/// <summary>
|
|
/// Directory pointed to by appsettings.json
|
|
/// </summary>
|
|
public LongPath Location { get; private set; }
|
|
/// <summary>
|
|
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
|
|
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
|
|
/// </summary>
|
|
public bool SettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
|
/// <summary>
|
|
/// Found Location of appsettings.json. This file must exist or be able to be created for Libation to start.
|
|
/// </summary>
|
|
public string? AppsettingsJsonFile { get; }
|
|
/// <summary>
|
|
/// File path to Settings.json inside <see cref="Location"/>
|
|
/// </summary>
|
|
public string SettingsFilePath => Path.Combine(Location, SETTINGS_JSON);
|
|
|
|
internal LibationFiles()
|
|
{
|
|
var libationFilesDir = Environment.GetEnvironmentVariable(LIBATION_FILES_DIR);
|
|
if (Directory.Exists(libationFilesDir))
|
|
{
|
|
Location = libationFilesDir;
|
|
}
|
|
else
|
|
{
|
|
AppsettingsJsonFile = GetOrCreateAppsettingsFile();
|
|
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
|
|
}
|
|
}
|
|
|
|
internal LibationFiles(string appSettingsFile)
|
|
{
|
|
AppsettingsJsonFile = appSettingsFile;
|
|
Location = GetLibationFilesFromAppsettings(AppsettingsJsonFile);
|
|
}
|
|
|
|
/// <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, pathToPersist);
|
|
Location = pathToPersist;
|
|
return;
|
|
}
|
|
|
|
Location = pathToPersist;
|
|
|
|
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
|
var jObj = JObject.Parse(startingContents);
|
|
|
|
jObj[LIBATION_FILES_KEY] = pathToPersist;
|
|
|
|
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
|
if (startingContents == endingContents)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// now it's set in the file again but no settings have moved yet
|
|
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
|
StartupLog.Information($"Libation files changed. {AppsettingsJsonFile} {LIBATION_FILES_KEY}={pathToPersist}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StartupLog.Error(ex, $"Failed to change Libation files location. {AppsettingsJsonFile} {LIBATION_FILES_KEY}={pathToPersist}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Configuration.Books"/> property has a non-null, non-empty value.
|
|
/// Does not verify the existence of the <see cref="Configuration.Books"/> directory.
|
|
/// </summary>
|
|
/// <param name="settingsFile">File path to the Settings.json file</param>
|
|
public static bool SettingsFileIsValid(string settingsFile)
|
|
{
|
|
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
|
|
return !string.IsNullOrWhiteSpace(settingsJson[nameof(Configuration.Books)]?.Value<string>());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StartupLog.Error(ex, $"Failed to load settings file: {settingsFile}");
|
|
try
|
|
{
|
|
StartupLog.Information($"Deleting invalid settings file: {settingsFile}");
|
|
FileUtility.SaferDelete(settingsFile);
|
|
StartupLog.Information($"Creating a new, empty setting file: {settingsFile}");
|
|
try
|
|
{
|
|
File.WriteAllText(settingsFile, "{}");
|
|
EssentialFileValidator.ValidateCreatedAndReport(settingsFile);
|
|
}
|
|
catch (Exception createEx)
|
|
{
|
|
StartupLog.Error(createEx, $"Failed to create new settings file: {settingsFile}");
|
|
}
|
|
}
|
|
catch (Exception deleteEx)
|
|
{
|
|
StartupLog.Error(deleteEx, $"Failed to delete the invalid settings file: {settingsFile}");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to find appsettings.json in the following locations:
|
|
/// <list type="number">
|
|
/// <item>
|
|
/// <description>[App Directory]</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <description>%LocalAppData%\Libation</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <description>%AppData%\Libation</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <description>%Temp%\Libation</description>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// If not found, try to create it in each of the same locations in-order until successful.
|
|
///
|
|
/// <para>This method must complete successfully for Libation to continue.</para>
|
|
/// </summary>
|
|
/// <returns>appsettings.json file path</returns>
|
|
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
|
|
private static string GetOrCreateAppsettingsFile()
|
|
{
|
|
const string appsettings_filename = "appsettings.json";
|
|
|
|
//Possible appsettings.json locations, in order of preference.
|
|
string[] possibleAppsettingsDirectories = new[]
|
|
{
|
|
Configuration.ProcessDirectory,
|
|
Configuration.LocalAppData,
|
|
Configuration.UserProfile,
|
|
Configuration.WinTemp,
|
|
};
|
|
|
|
//Try to find and validate appsettings.json in each folder
|
|
foreach (var dir in possibleAppsettingsDirectories)
|
|
{
|
|
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
|
|
|
if (File.Exists(appsettingsFile))
|
|
{
|
|
try
|
|
{
|
|
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
|
|
|
|
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
|
|
&& appSettings[LIBATION_FILES_KEY] is JValue jval
|
|
&& jval.Value is string settingsPath
|
|
&& !string.IsNullOrWhiteSpace(settingsPath))
|
|
return appsettingsFile;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
//Valid appsettings.json not found. Try to create it in each folder.
|
|
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
|
|
|
|
foreach (var dir in possibleAppsettingsDirectories)
|
|
{
|
|
//Don't try to create appsettings.json in the program files directory on *.nix systems.
|
|
//However, still _look_ for one there for backwards compatibility with previous installations
|
|
if (!Configuration.IsWindows && dir == Configuration.ProcessDirectory)
|
|
continue;
|
|
|
|
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
File.WriteAllText(appsettingsFile, endingContents);
|
|
EssentialFileValidator.ValidateCreatedAndReport(appsettingsFile);
|
|
return appsettingsFile;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StartupLog.Error(ex, $"Failed to create {appsettingsFile}");
|
|
}
|
|
}
|
|
|
|
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the LibationFiles directory from appsettings.json, expanding environment variables as needed.
|
|
/// </summary>
|
|
/// <param name="appsettingsPath"></param>
|
|
/// <returns></returns>
|
|
/// <exception cref="InvalidDataException">The appsettings.json file does not contain a "LibationFiles" key</exception>
|
|
private static string GetLibationFilesFromAppsettings(LongPath appsettingsPath)
|
|
{
|
|
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
|
// verify from live file. no try/catch. want failures to be visible
|
|
var jObjFinal = JObject.Parse(File.ReadAllText(appsettingsPath));
|
|
|
|
if (jObjFinal[LIBATION_FILES_KEY]?.Value<string>() is not string libationFiles)
|
|
throw new InvalidDataException($"{LIBATION_FILES_KEY} not found in {appsettingsPath}");
|
|
|
|
if (Configuration.IsWindows)
|
|
{
|
|
libationFiles = Environment.ExpandEnvironmentVariables(libationFiles);
|
|
}
|
|
else
|
|
{
|
|
//If the shell command fails and returns null, proceed with the verbatim
|
|
//LIBATION_FILES_KEY path and hope for the best. If Libation can't find
|
|
//anything at this path it will set LIBATION_FILES_KEY to UserProfile
|
|
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)
|
|
{
|
|
var psi = new ProcessStartInfo
|
|
{
|
|
FileName = "/bin/sh",
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
ArgumentList =
|
|
{
|
|
"-c",
|
|
command
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
var proc = Process.Start(psi);
|
|
proc?.WaitForExit();
|
|
return proc?.StandardOutput?.ReadToEnd()?.Trim();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
StartupLog.Error(e, $"Failed to run shell command: {string.Join(' ', psi.ArgumentList)}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|