mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add crash handler stuff.
This commit is contained in:
parent
9ba6e4d0af
commit
e08e9c4d13
35 changed files with 1472 additions and 237 deletions
119
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal file
119
Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs
Normal 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})",
|
||||
};
|
||||
}
|
||||
85
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal file
85
Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs
Normal 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)
|
||||
{ }
|
||||
}
|
||||
215
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal file
215
Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs
Normal 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);
|
||||
}
|
||||
99
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal file
99
Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs
Normal 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)
|
||||
{ }
|
||||
}
|
||||
62
Penumbra.CrashHandler/CrashData.cs
Normal file
62
Penumbra.CrashHandler/CrashData.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
/// <summary> A base entry for crash data. </summary>
|
||||
public interface ICrashDataEntry
|
||||
{
|
||||
/// <summary> The timestamp of the event. </summary>
|
||||
DateTimeOffset Timestamp { get; }
|
||||
|
||||
/// <summary> The thread invoking the event. </summary>
|
||||
int ThreadId { get; }
|
||||
|
||||
/// <summary> The age of the event compared to the crash. (Redundantly with the timestamp) </summary>
|
||||
double Age { get; }
|
||||
}
|
||||
|
||||
/// <summary> A full set of crash data. </summary>
|
||||
public class CrashData
|
||||
{
|
||||
/// <summary> The mode this data was obtained - manually or from a crash. </summary>
|
||||
public string Mode { get; set; } = "Unknown";
|
||||
|
||||
/// <summary> The time this crash data was generated. </summary>
|
||||
public DateTimeOffset CrashTime { get; set; } = DateTimeOffset.UnixEpoch;
|
||||
|
||||
/// <summary> The FFXIV process ID when this data was generated. </summary>
|
||||
public int ProcessId { get; set; } = 0;
|
||||
|
||||
/// <summary> The FFXIV Exit Code (if any) when this data was generated. </summary>
|
||||
public int ExitCode { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of characters loaded during this session. </summary>
|
||||
public int TotalCharactersLoaded { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of modded files loaded during this session. </summary>
|
||||
public int TotalModdedFilesLoaded { get; set; } = 0;
|
||||
|
||||
/// <summary> The total amount of vfx functions invoked during this session. </summary>
|
||||
public int TotalVFXFuncsInvoked { get; set; } = 0;
|
||||
|
||||
/// <summary> The last character loaded before this crash data was generated. </summary>
|
||||
public CharacterLoadedEntry? LastCharacterLoaded
|
||||
=> LastCharactersLoaded.Count == 0 ? default : LastCharactersLoaded[0];
|
||||
|
||||
/// <summary> The last modded file loaded before this crash data was generated. </summary>
|
||||
public ModdedFileLoadedEntry? LastModdedFileLoaded
|
||||
=> LastModdedFilesLoaded.Count == 0 ? default : LastModdedFilesLoaded[0];
|
||||
|
||||
/// <summary> The last vfx function invoked before this crash data was generated. </summary>
|
||||
public VfxFuncInvokedEntry? LastVfxFuncInvoked
|
||||
=> LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0];
|
||||
|
||||
/// <summary> A collection of the last few characters loaded before this crash data was generated. </summary>
|
||||
public List<CharacterLoadedEntry> LastCharactersLoaded { get; } = [];
|
||||
|
||||
/// <summary> A collection of the last few modded files loaded before this crash data was generated. </summary>
|
||||
public List<ModdedFileLoadedEntry> LastModdedFilesLoaded { get; } = [];
|
||||
|
||||
/// <summary> A collection of the last few vfx functions invoked before this crash data was generated. </summary>
|
||||
public List<VfxFuncInvokedEntry> LastVfxFuncsInvoked { get; } = [];
|
||||
}
|
||||
53
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
53
Penumbra.CrashHandler/GameEventLogReader.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public interface IBufferReader
|
||||
{
|
||||
public uint TotalCount { get; }
|
||||
public IEnumerable<JsonObject> GetLines(DateTimeOffset crashTime);
|
||||
}
|
||||
|
||||
public sealed class GameEventLogReader : IDisposable
|
||||
{
|
||||
public readonly (IBufferReader Reader, string TypeSingular, string TypePlural)[] Readers =
|
||||
[
|
||||
(CharacterBaseBuffer.CreateReader(), "CharacterLoaded", "CharactersLoaded"),
|
||||
(ModdedFileBuffer.CreateReader(), "ModdedFileLoaded", "ModdedFilesLoaded"),
|
||||
(AnimationInvocationBuffer.CreateReader(), "VFXFuncInvoked", "VFXFuncsInvoked"),
|
||||
];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var (reader, _, _) in Readers)
|
||||
(reader as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public JsonObject Dump(string mode, int processId, int exitCode)
|
||||
{
|
||||
var crashTime = DateTimeOffset.UtcNow;
|
||||
var obj = new JsonObject
|
||||
{
|
||||
[nameof(CrashData.Mode)] = mode,
|
||||
[nameof(CrashData.CrashTime)] = DateTimeOffset.UtcNow,
|
||||
[nameof(CrashData.ProcessId)] = processId,
|
||||
[nameof(CrashData.ExitCode)] = exitCode,
|
||||
};
|
||||
|
||||
foreach (var (reader, singular, _) in Readers)
|
||||
obj["Last" + singular] = reader.GetLines(crashTime).FirstOrDefault();
|
||||
|
||||
foreach (var (reader, _, plural) in Readers)
|
||||
{
|
||||
obj["Total" + plural] = reader.TotalCount;
|
||||
var array = new JsonArray();
|
||||
foreach (var file in reader.GetLines(crashTime))
|
||||
array.Add(file);
|
||||
obj["Last" + plural] = array;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
17
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
17
Penumbra.CrashHandler/GameEventLogWriter.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Penumbra.CrashHandler.Buffers;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public sealed class GameEventLogWriter : IDisposable
|
||||
{
|
||||
public readonly ICharacterBaseBufferWriter CharacterBase = CharacterBaseBuffer.CreateWriter();
|
||||
public readonly IModdedFileBufferWriter FileLoaded = ModdedFileBuffer.CreateWriter();
|
||||
public readonly IAnimationInvocationBufferWriter AnimationFuncInvoked = AnimationInvocationBuffer.CreateWriter();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(CharacterBase as IDisposable)?.Dispose();
|
||||
(FileLoaded as IDisposable)?.Dispose();
|
||||
(AnimationFuncInvoked as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
28
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
28
Penumbra.CrashHandler/Penumbra.CrashHandler.csproj
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Windows'))">$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$([MSBuild]::IsOSPlatform('Linux'))">$(HOME)/.xlcore/dalamud/Hooks/dev/</DalamudLibPath>
|
||||
<DalamudLibPath Condition="$(DALAMUD_HOME) != ''">$(DALAMUD_HOME)/</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
30
Penumbra.CrashHandler/Program.cs
Normal file
30
Penumbra.CrashHandler/Program.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Penumbra.CrashHandler;
|
||||
|
||||
public class CrashHandler
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length < 2 || !int.TryParse(args[1], out var pid))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new GameEventLogReader();
|
||||
var parent = Process.GetProcessById(pid);
|
||||
|
||||
parent.WaitForExit();
|
||||
var exitCode = parent.ExitCode;
|
||||
var obj = reader.Dump("Crash", pid, exitCode);
|
||||
using var fs = File.Open(args[0], FileMode.Create);
|
||||
using var w = new Utf8JsonWriter(fs, new JsonWriterOptions { Indented = true });
|
||||
obj.WriteTo(w, new JsonSerializerOptions() { WriteIndented = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
File.WriteAllText(args[0], $"{DateTime.UtcNow} {pid} {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue