mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-13 14:17:15 -04:00
PreLoggingCrashLog returned its LongPath straight out, so on Windows the crash dialog named \\?\C:\Users\...\Log202608.log. The prefix is for the Win32 API, not for a person being asked to find the file and attach it to a bug report. Return PathWithoutPrefix instead, and use it for the LibationFiles line inside the record for the same reason. A test now pins that the reported path carries no prefix and is usable as-is; on Linux the two forms are identical, which is why local runs could not see this and the Windows CI job could. While in there: ReplaceInstallFile moved the old file aside and then wrote the new one, so a copy that failed after the move left nothing at all where a file used to be. Put the old one back before reporting the failure. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
171 lines
5.2 KiB
C#
171 lines
5.2 KiB
C#
using FileManager;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace LibationFileManager;
|
|
|
|
/// <summary>
|
|
/// Writes a crash record without Serilog, for the startup window where Serilog is not configured yet or
|
|
/// cannot be loaded at all.
|
|
/// <para/>
|
|
/// Shared by both UIs. Chardonnay had a private copy of this and Classic had nothing, so a Classic user
|
|
/// whose startup failed before logging was told, correctly, that the error could not be written anywhere.
|
|
/// See issue #2001.
|
|
/// </summary>
|
|
public static class PreLoggingCrashLog
|
|
{
|
|
public const string CrashFileName = "LibationCrash.log";
|
|
|
|
/// <summary>
|
|
/// Appends a crash record to the newest Libation log file, falling back to <see cref="CrashFileName"/>
|
|
/// in the Libation files folder and then in the user profile.
|
|
/// </summary>
|
|
/// <param name="exception">The failure to record.</param>
|
|
/// <param name="extraFields">
|
|
/// Anything the caller knows that this assembly cannot see, such as the release identifier.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The file written, to be named in the crash dialog, or null when nothing could be written. Callers
|
|
/// used to tell users to attach <c>LibationCrash.log</c> unconditionally, which is the wrong file
|
|
/// whenever a <c>Log*.log</c> already exists, and no file at all when this returns null.
|
|
/// <para/>
|
|
/// Reported without the Windows extended-length prefix. This path is for a person to read and to paste
|
|
/// into Explorer, so a <c>\\?\</c> in front of it would be noise at best.
|
|
/// </returns>
|
|
public static string? TryWrite(Exception? exception, IEnumerable<(string Name, string Value)>? extraFields = null)
|
|
{
|
|
string record;
|
|
try
|
|
{
|
|
record = BuildRecord(exception, extraFields);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Losing the whole record because one field could not be read is what happened before.
|
|
record = $"{DateTime.Now} - Libation Crash{Environment.NewLine} (crash record could not be built: {ex}){Environment.NewLine} === EXCEPTION ==={Environment.NewLine} {exception}";
|
|
}
|
|
|
|
foreach (var candidate in ResolveCandidateFiles())
|
|
{
|
|
if (TryAppend(candidate, record))
|
|
return candidate.PathWithoutPrefix;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Every value is read through <see cref="Describe"/>, so a property that throws contributes its error
|
|
/// text instead of taking the record down with it. <c>Books</c> genuinely does throw this early, and
|
|
/// <c>InteropFactory.InteropFunctionsType</c> ran a static constructor that itself needed Serilog.
|
|
/// </summary>
|
|
private static string BuildRecord(Exception? exception, IEnumerable<(string Name, string Value)>? extraFields)
|
|
{
|
|
var fields = new List<(string Name, string Value)>
|
|
{
|
|
("OS", Describe(() => Configuration.OS.ToString())),
|
|
("Version", Describe(() => Configuration.LibationVersion?.ToString() ?? "[null]")),
|
|
("InteropFunctionsType", Describe(() => InteropFactory.InteropFunctionsType?.ToString() ?? "[null]")),
|
|
("LibationFiles", Describe(() => Configuration.Instance.LibationFiles.Location.PathWithoutPrefix)),
|
|
("Books Folder", Describe(() => Configuration.Instance.Books ?? "[null]")),
|
|
};
|
|
|
|
if (extraFields is not null)
|
|
fields.AddRange(extraFields);
|
|
|
|
var width = fields.Max(f => f.Name.Length) + 2;
|
|
var lines = fields.Select(f => $" {f.Name.PadRight(width)}{f.Value}");
|
|
|
|
return $"""
|
|
{DateTime.Now} - Libation Crash
|
|
{string.Join(Environment.NewLine, lines)}
|
|
=== EXCEPTION ===
|
|
{exception}
|
|
""";
|
|
}
|
|
|
|
private static string Describe(Func<string> read)
|
|
{
|
|
try
|
|
{
|
|
return read();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return ex.ToString();
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<LongPath> ResolveCandidateFiles()
|
|
{
|
|
var logDirectory = Describe(() => Configuration.Instance.LibationFiles.Location.Path);
|
|
|
|
if (Directory.Exists(logDirectory))
|
|
{
|
|
var newestLog = NewestLogFile(logDirectory);
|
|
if (newestLog is not null)
|
|
yield return newestLog;
|
|
|
|
yield return Path.Combine(logDirectory, CrashFileName);
|
|
}
|
|
|
|
var userProfile = Describe(() => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
|
|
if (Directory.Exists(userProfile))
|
|
yield return Path.Combine(userProfile, CrashFileName);
|
|
}
|
|
|
|
private static string? NewestLogFile(string logDirectory)
|
|
{
|
|
try
|
|
{
|
|
return Directory.GetFiles(logDirectory, "Log*.log")
|
|
.Select(f => new FileInfo(f))
|
|
.OrderByDescending(f => f.CreationTimeUtc)
|
|
.FirstOrDefault()
|
|
?.FullName;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static bool TryAppend(LongPath path, string record)
|
|
{
|
|
try
|
|
{
|
|
// Without this the record ran straight onto the end of the last log line.
|
|
var separator = NeedsLeadingNewLine(path) ? Environment.NewLine : string.Empty;
|
|
|
|
using var writer = new StreamWriter(path, append: true);
|
|
writer.WriteLine(separator + record);
|
|
return true;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool NeedsLeadingNewLine(LongPath path)
|
|
{
|
|
try
|
|
{
|
|
var info = new FileInfo(path);
|
|
if (!info.Exists || info.Length == 0)
|
|
return false;
|
|
|
|
using var stream = File.OpenRead(path);
|
|
stream.Seek(-1, SeekOrigin.End);
|
|
var last = stream.ReadByte();
|
|
return last is not ('\n' or '\r');
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|