Add crash handler stuff.

This commit is contained in:
Ottermandias 2024-03-16 16:20:34 +01:00
parent 9ba6e4d0af
commit e08e9c4d13
35 changed files with 1472 additions and 237 deletions

View file

@ -0,0 +1,119 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> The types of currently hooked and relevant animation loading functions. </summary>
public enum AnimationInvocationType : int
{
PapLoad,
ActionLoad,
ScheduleClipUpdate,
LoadTimelineResources,
LoadCharacterVfx,
LoadCharacterSound,
ApricotSoundPlay,
LoadAreaVfx,
CharacterBaseLoadAnimation,
}
/// <summary> The full crash entry for an invoked vfx function. </summary>
public record struct VfxFuncInvokedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string InvocationType,
string CharacterName,
string CharacterAddress,
string CollectionName) : ICrashDataEntry;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IAnimationInvocationBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="type"> The type of VFX func called. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type);
}
internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 64;
private const int _lineCapacity = 256;
private const string _name = "Penumbra.AnimationInvocation";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, AnimationInvocationType type)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, (int)type);
accessor.Write(16, characterAddress);
var span = GetSpan(accessor, 24, 104);
WriteSpan(characterName, span);
span = GetSpan(accessor, 128);
WriteString(collectionName, span);
}
}
public uint TotalCount
=> TotalWrittenLines;
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]);
var address = BitConverter.ToUInt64(line[16..]);
var characterName = ReadString(line[24..]);
var collectionName = ReadString(line[128..]);
yield return new JsonObject()
{
[nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(VfxFuncInvokedEntry.Timestamp)] = timestamp,
[nameof(VfxFuncInvokedEntry.ThreadId)] = thread,
[nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type),
[nameof(VfxFuncInvokedEntry.CharacterName)] = characterName,
[nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName,
};
}
}
public static IBufferReader CreateReader()
=> new AnimationInvocationBuffer(false);
public static IAnimationInvocationBufferWriter CreateWriter()
=> new AnimationInvocationBuffer();
private AnimationInvocationBuffer(bool writer)
: base(_name, _version)
{ }
private AnimationInvocationBuffer()
: base(_name, _version, _lineCount, _lineCapacity)
{ }
private static string ToName(AnimationInvocationType type)
=> type switch
{
AnimationInvocationType.PapLoad => "PAP Load",
AnimationInvocationType.ActionLoad => "Action Load",
AnimationInvocationType.ScheduleClipUpdate => "Schedule Clip Update",
AnimationInvocationType.LoadTimelineResources => "Load Timeline Resources",
AnimationInvocationType.LoadCharacterVfx => "Load Character VFX",
AnimationInvocationType.LoadCharacterSound => "Load Character Sound",
AnimationInvocationType.ApricotSoundPlay => "Apricot Sound Play",
AnimationInvocationType.LoadAreaVfx => "Load Area VFX",
AnimationInvocationType.CharacterBaseLoadAnimation => "Load Animation (CharacterBase)",
_ => $"Unknown ({(int)type})",
};
}

View file

@ -0,0 +1,85 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface ICharacterBaseBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName);
}
/// <summary> The full crash entry for a loaded character base. </summary>
public record struct CharacterLoadedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string CharacterName,
string CharacterAddress,
string CollectionName) : ICrashDataEntry;
internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 10;
private const int _lineCapacity = 256;
private const string _name = "Penumbra.CharacterBase";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 108);
WriteSpan(characterName, span);
span = GetSpan(accessor, 128);
WriteString(collectionName, span);
}
}
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var characterName = ReadString(line[20..]);
var collectionName = ReadString(line[128..]);
yield return new JsonObject
{
[nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(CharacterLoadedEntry.Timestamp)] = timestamp,
[nameof(CharacterLoadedEntry.ThreadId)] = thread,
[nameof(CharacterLoadedEntry.CharacterName)] = characterName,
[nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(CharacterLoadedEntry.CollectionName)] = collectionName,
};
}
}
public uint TotalCount
=> TotalWrittenLines;
public static IBufferReader CreateReader()
=> new CharacterBaseBuffer(false);
public static ICharacterBaseBufferWriter CreateWriter()
=> new CharacterBaseBuffer();
private CharacterBaseBuffer(bool writer)
: base(_name, _version)
{ }
private CharacterBaseBuffer()
: base(_name, _version, _lineCount, _lineCapacity)
{ }
}

View file

@ -0,0 +1,215 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.MemoryMappedFiles;
using System.Numerics;
using System.Text;
namespace Penumbra.CrashHandler.Buffers;
public class MemoryMappedBuffer : IDisposable
{
private const int MinHeaderLength = 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4;
private readonly MemoryMappedFile _file;
private readonly MemoryMappedViewAccessor _header;
private readonly MemoryMappedViewAccessor[] _lines;
public readonly int Version;
public readonly uint LineCount;
public readonly uint LineCapacity;
private readonly uint _lineMask;
private bool _disposed;
protected uint CurrentLineCount
{
get => _header.ReadUInt32(16);
set => _header.Write(16, value);
}
protected uint CurrentLinePosition
{
get => _header.ReadUInt32(20);
set => _header.Write(20, value);
}
public uint TotalWrittenLines
{
get => _header.ReadUInt32(24);
protected set => _header.Write(24, value);
}
public MemoryMappedBuffer(string mapName, int version, uint lineCount, uint lineCapacity)
{
Version = version;
LineCount = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCount, 2, int.MaxValue >> 3));
LineCapacity = BitOperations.RoundUpToPowerOf2(Math.Clamp(lineCapacity, 2, int.MaxValue >> 3));
_lineMask = LineCount - 1;
var fileName = Encoding.UTF8.GetBytes(mapName);
var headerLength = (uint)(4 + 4 + 4 + 4 + 4 + 4 + 4 + fileName.Length + 1);
headerLength = (headerLength & 0b111) > 0 ? (headerLength & ~0b111u) + 0b1000 : headerLength;
var capacity = LineCount * LineCapacity + headerLength;
_file = MemoryMappedFile.CreateNew(mapName, capacity, MemoryMappedFileAccess.ReadWrite, MemoryMappedFileOptions.None,
HandleInheritability.Inheritable);
_header = _file.CreateViewAccessor(0, headerLength);
_header.Write(0, headerLength);
_header.Write(4, Version);
_header.Write(8, LineCount);
_header.Write(12, LineCapacity);
_header.WriteArray(28, fileName, 0, fileName.Length);
_header.Write(fileName.Length + 28, (byte)0);
_lines = Enumerable.Range(0, (int)LineCount).Select(i
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
.ToArray();
}
public MemoryMappedBuffer(string mapName, int? expectedVersion = null, uint? expectedMinLineCount = null,
uint? expectedMinLineCapacity = null)
{
_file = MemoryMappedFile.OpenExisting(mapName, MemoryMappedFileRights.ReadWrite, HandleInheritability.Inheritable);
using var headerLine = _file.CreateViewAccessor(0, 4, MemoryMappedFileAccess.Read);
var headerLength = headerLine.ReadUInt32(0);
if (headerLength < MinHeaderLength)
Throw($"Map {mapName} did not contain a valid header.");
_header = _file.CreateViewAccessor(0, headerLength, MemoryMappedFileAccess.ReadWrite);
Version = _header.ReadInt32(4);
LineCount = _header.ReadUInt32(8);
LineCapacity = _header.ReadUInt32(12);
_lineMask = LineCount - 1;
if (expectedVersion.HasValue && expectedVersion.Value != Version)
Throw($"Map {mapName} has version {Version} instead of {expectedVersion.Value}.");
if (LineCount < expectedMinLineCount)
Throw($"Map {mapName} has line count {LineCount} but line count >= {expectedMinLineCount.Value} is required.");
if (LineCapacity < expectedMinLineCapacity)
Throw($"Map {mapName} has line capacity {LineCapacity} but line capacity >= {expectedMinLineCapacity.Value} is required.");
var name = ReadString(GetSpan(_header, 28));
if (name != mapName)
Throw($"Map {mapName} does not contain its map name at the expected location.");
_lines = Enumerable.Range(0, (int)LineCount).Select(i
=> _file.CreateViewAccessor(headerLength + i * LineCapacity, LineCapacity, MemoryMappedFileAccess.ReadWrite))
.ToArray();
[DoesNotReturn]
void Throw(string text)
{
_file.Dispose();
_disposed = true;
throw new Exception(text);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
_disposed = true;
}
protected static string ReadString(Span<byte> span)
{
if (span.IsEmpty)
throw new Exception("String from empty span requested.");
var termination = span.IndexOf((byte)0);
if (termination < 0)
throw new Exception("String in span is not terminated.");
return Encoding.UTF8.GetString(span[..termination]);
}
protected static int WriteString(string text, Span<byte> span)
{
var bytes = Encoding.UTF8.GetBytes(text);
var length = bytes.Length + 1;
if (length > span.Length)
throw new Exception($"String {text} is too long to write into span.");
bytes.CopyTo(span);
span[bytes.Length] = 0;
return length;
}
protected static int WriteSpan(ReadOnlySpan<byte> input, Span<byte> span)
{
var length = input.Length + 1;
if (length > span.Length)
throw new Exception("Byte array is too long to write into span.");
input.CopyTo(span);
span[input.Length] = 0;
return length;
}
protected Span<byte> GetLine(int i)
{
if (i < 0 || i > LineCount)
return null;
lock (_header)
{
var lineIdx = CurrentLinePosition + i & _lineMask;
if (lineIdx > CurrentLineCount)
return null;
return GetSpan(_lines[lineIdx]);
}
}
protected MemoryMappedViewAccessor GetCurrentLineLocking()
{
MemoryMappedViewAccessor view;
lock (_header)
{
var currentLineCount = CurrentLineCount;
if (currentLineCount == LineCount)
{
var currentLinePos = CurrentLinePosition;
view = _lines[currentLinePos]!;
CurrentLinePosition = currentLinePos + 1 & _lineMask;
}
else
{
view = _lines[currentLineCount];
++CurrentLineCount;
}
++TotalWrittenLines;
_header.Flush();
}
return view;
}
protected static Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset = 0)
=> GetSpan(accessor, offset, (int)accessor.Capacity - offset);
protected static unsafe Span<byte> GetSpan(MemoryMappedViewAccessor accessor, int offset, int size)
{
byte* ptr = null;
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
size = Math.Min(size, (int)accessor.Capacity - offset);
if (size < 0)
return [];
var span = new Span<byte>(ptr + offset + accessor.PointerOffset, size);
return span;
}
protected void Dispose(bool disposing)
{
if (_disposed)
return;
_header.Dispose();
foreach (var line in _lines)
line.Dispose();
_file.Dispose();
}
~MemoryMappedBuffer()
=> Dispose(false);
}

View file

@ -0,0 +1,99 @@
using System.Text.Json.Nodes;
namespace Penumbra.CrashHandler.Buffers;
/// <summary> Only expose the write interface for the buffer. </summary>
public interface IModdedFileBufferWriter
{
/// <summary> Write a line into the buffer with the given data. </summary>
/// <param name="characterAddress"> The address of the related character, if known. </param>
/// <param name="characterName"> The name of the related character, anonymized or relying on index if unavailable, if known. </param>
/// <param name="collectionName"> The name of the associated collection. Not anonymized. </param>
/// <param name="requestedFileName"> The file name as requested by the game. </param>
/// <param name="actualFileName"> The actual modded file name loaded. </param>
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName);
}
/// <summary> The full crash entry for a loaded modded file. </summary>
public record struct ModdedFileLoadedEntry(
double Age,
DateTimeOffset Timestamp,
int ThreadId,
string CharacterName,
string CharacterAddress,
string CollectionName,
string RequestedFileName,
string ActualFileName) : ICrashDataEntry;
internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader
{
private const int _version = 1;
private const int _lineCount = 128;
private const int _lineCapacity = 1024;
private const string _name = "Penumbra.ModdedFile";
public void WriteLine(nint characterAddress, ReadOnlySpan<byte> characterName, string collectionName, ReadOnlySpan<byte> requestedFileName,
ReadOnlySpan<byte> actualFileName)
{
var accessor = GetCurrentLineLocking();
lock (accessor)
{
accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
accessor.Write(8, Environment.CurrentManagedThreadId);
accessor.Write(12, characterAddress);
var span = GetSpan(accessor, 20, 80);
WriteSpan(characterName, span);
span = GetSpan(accessor, 92, 80);
WriteString(collectionName, span);
span = GetSpan(accessor, 172, 260);
WriteSpan(requestedFileName, span);
span = GetSpan(accessor, 432);
WriteSpan(actualFileName, span);
}
}
public uint TotalCount
=> TotalWrittenLines;
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime)
{
var lineCount = (int)CurrentLineCount;
for (var i = lineCount - 1; i >= 0; --i)
{
var line = GetLine(i);
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line));
var thread = BitConverter.ToInt32(line[8..]);
var address = BitConverter.ToUInt64(line[12..]);
var characterName = ReadString(line[20..]);
var collectionName = ReadString(line[92..]);
var requestedFileName = ReadString(line[172..]);
var actualFileName = ReadString(line[432..]);
yield return new JsonObject()
{
[nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds,
[nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp,
[nameof(ModdedFileLoadedEntry.ThreadId)] = thread,
[nameof(ModdedFileLoadedEntry.CharacterName)] = characterName,
[nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"),
[nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName,
[nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName,
[nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName,
};
}
}
public static IBufferReader CreateReader()
=> new ModdedFileBuffer(false);
public static IModdedFileBufferWriter CreateWriter()
=> new ModdedFileBuffer();
private ModdedFileBuffer(bool writer)
: base(_name, _version)
{ }
private ModdedFileBuffer()
: base(_name, _version, _lineCount, _lineCapacity)
{ }
}