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)
{ }
}

View 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; } = [];
}

View 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;
}
}

View 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();
}
}

View 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>

View 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}");
}
}
}