mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
342 lines
10 KiB
C#
342 lines
10 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using OtterGui.Services;
|
|
using Penumbra.Collections;
|
|
using Penumbra.Communication;
|
|
using Penumbra.CrashHandler;
|
|
using Penumbra.CrashHandler.Buffers;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.Interop.ResourceLoading;
|
|
using Penumbra.Interop.Structs;
|
|
using Penumbra.String;
|
|
using Penumbra.String.Classes;
|
|
using FileMode = System.IO.FileMode;
|
|
|
|
namespace Penumbra.Services;
|
|
|
|
public sealed class CrashHandlerService : IDisposable, IService
|
|
{
|
|
private readonly FilenameService _files;
|
|
private readonly CommunicatorService _communicator;
|
|
private readonly ActorManager _actors;
|
|
private readonly ResourceLoader _resourceLoader;
|
|
private readonly Configuration _config;
|
|
private readonly ValidityChecker _validityChecker;
|
|
|
|
private string _tempExecutableDirectory = string.Empty;
|
|
|
|
public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader,
|
|
Configuration config, ValidityChecker validityChecker)
|
|
{
|
|
_files = files;
|
|
_communicator = communicator;
|
|
_actors = actors;
|
|
_resourceLoader = resourceLoader;
|
|
_config = config;
|
|
_validityChecker = validityChecker;
|
|
|
|
if (!(_config.UseCrashHandler ?? false))
|
|
return;
|
|
|
|
OpenEventWriter();
|
|
LaunchCrashHandler();
|
|
if (_eventWriter != null)
|
|
Subscribe();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
CloseEventWriter();
|
|
_eventWriter?.Dispose();
|
|
if (_child != null)
|
|
{
|
|
_child.Kill();
|
|
Penumbra.Log.Debug($"Killed crash handler child process {_child.Id}.");
|
|
}
|
|
|
|
Unsubscribe();
|
|
CleanExecutables();
|
|
}
|
|
|
|
private Process? _child;
|
|
private GameEventLogWriter? _eventWriter;
|
|
|
|
public string CopiedExe = string.Empty;
|
|
|
|
public string OriginalExe
|
|
=> _files.CrashHandlerExe;
|
|
|
|
public string LogPath
|
|
=> _files.LogFileName;
|
|
|
|
public int ChildProcessId
|
|
=> _child?.Id ?? -1;
|
|
|
|
public int ProcessId
|
|
=> Environment.ProcessId;
|
|
|
|
public bool IsRunning
|
|
=> _eventWriter != null && _child is { HasExited: false };
|
|
|
|
public int ChildExitCode
|
|
=> IsRunning ? 0 : _child?.ExitCode ?? 0;
|
|
|
|
public void Enable()
|
|
{
|
|
if (_config.UseCrashHandler ?? false)
|
|
return;
|
|
|
|
_config.UseCrashHandler = true;
|
|
_config.Save();
|
|
OpenEventWriter();
|
|
LaunchCrashHandler();
|
|
if (_eventWriter != null)
|
|
Subscribe();
|
|
}
|
|
|
|
public void Disable()
|
|
{
|
|
if (!(_config.UseCrashHandler ?? false))
|
|
return;
|
|
|
|
_config.UseCrashHandler = false;
|
|
_config.Save();
|
|
CloseEventWriter();
|
|
CloseCrashHandler();
|
|
Unsubscribe();
|
|
}
|
|
|
|
public JsonObject? Load(string fileName)
|
|
{
|
|
if (!File.Exists(fileName))
|
|
return null;
|
|
|
|
try
|
|
{
|
|
var data = File.ReadAllText(fileName);
|
|
return JsonNode.Parse(data) as JsonObject;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Error($"Could not parse crash dump at {fileName}:\n{ex}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void CloseCrashHandler()
|
|
{
|
|
if (_child == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
if (_child.HasExited)
|
|
return;
|
|
|
|
_child.Kill();
|
|
Penumbra.Log.Debug($"Closed Crash Handler at {CopiedExe}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_child = null;
|
|
Penumbra.Log.Debug($"Closed not close Crash Handler at {CopiedExe}:\n{ex}.");
|
|
}
|
|
}
|
|
|
|
public void LaunchCrashHandler()
|
|
{
|
|
try
|
|
{
|
|
CloseCrashHandler();
|
|
CopiedExe = CopyExecutables();
|
|
var info = new ProcessStartInfo()
|
|
{
|
|
CreateNoWindow = true,
|
|
FileName = CopiedExe,
|
|
};
|
|
info.ArgumentList.Add(_files.LogFileName);
|
|
info.ArgumentList.Add(Environment.ProcessId.ToString());
|
|
info.ArgumentList.Add($"{_validityChecker.Version} ({_validityChecker.CommitHash})");
|
|
info.ArgumentList.Add(_validityChecker.GameVersion);
|
|
_child = Process.Start(info);
|
|
if (_child == null)
|
|
throw new Exception("Child Process could not be created.");
|
|
|
|
Penumbra.Log.Information($"Opened Crash Handler at {CopiedExe}, PID {_child.Id}.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Error($"Could not launch crash handler process:\n{ex}");
|
|
CloseCrashHandler();
|
|
_child = null;
|
|
}
|
|
}
|
|
|
|
public JsonObject? Dump()
|
|
{
|
|
if (_eventWriter == null)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
using var reader = new GameEventLogReader(Environment.ProcessId);
|
|
JsonObject jObj;
|
|
lock (_eventWriter)
|
|
{
|
|
jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0, $"{_validityChecker.Version} ({_validityChecker.CommitHash})",
|
|
_validityChecker.GameVersion);
|
|
}
|
|
|
|
var logFile = _files.LogFileName;
|
|
using var s = File.Open(logFile, FileMode.Create);
|
|
using var jw = new Utf8JsonWriter(s, new JsonWriterOptions() { Indented = true });
|
|
jObj.WriteTo(jw);
|
|
Penumbra.Log.Information($"Dumped crash handler memory to {logFile}.");
|
|
return jObj;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Error($"Error dumping crash handler memory to file:\n{ex}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void CleanExecutables()
|
|
{
|
|
var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!;
|
|
foreach (var dir in Directory.EnumerateDirectories(parent, "temp_*"))
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(dir, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Verbose($"Could not delete {dir}. This is generally not an error:\n{ex}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private string CopyExecutables()
|
|
{
|
|
CleanExecutables();
|
|
var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!;
|
|
_tempExecutableDirectory = Path.Combine(parent, $"temp_{Environment.ProcessId}");
|
|
Directory.CreateDirectory(_tempExecutableDirectory);
|
|
foreach (var file in Directory.EnumerateFiles(parent, "Penumbra.CrashHandler.*"))
|
|
File.Copy(file, Path.Combine(_tempExecutableDirectory, Path.GetFileName(file)), true);
|
|
return Path.Combine(_tempExecutableDirectory, Path.GetFileName(_files.CrashHandlerExe));
|
|
}
|
|
|
|
public void LogAnimation(nint character, ModCollection collection, AnimationInvocationType type)
|
|
{
|
|
if (_eventWriter == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var name = GetActorName(character);
|
|
lock (_eventWriter)
|
|
{
|
|
_eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Warning($"Error logging animation function {type} to crash handler:\n{ex}");
|
|
}
|
|
}
|
|
|
|
private void OnCreatingCharacterBase(nint address, Guid collection, nint _1, nint _2, nint _3)
|
|
{
|
|
if (_eventWriter == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var name = GetActorName(address);
|
|
|
|
lock (_eventWriter)
|
|
{
|
|
_eventWriter?.CharacterBase.WriteLine(address, name.Span, collection);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Warning($"Error logging character creation to crash handler:\n{ex}");
|
|
}
|
|
}
|
|
|
|
private unsafe ByteString GetActorName(nint address)
|
|
{
|
|
var obj = (GameObject*)address;
|
|
if (obj == null)
|
|
return ByteString.FromSpanUnsafe("Unknown"u8, true, false, true);
|
|
|
|
var id = _actors.FromObject(obj, out _, false, true, false);
|
|
return id.IsValid ? ByteString.FromStringUnsafe(id.Incognito(null), false) :
|
|
obj->Name[0] != 0 ? new ByteString(obj->Name) : ByteString.FromStringUnsafe($"Actor #{obj->ObjectIndex}", false);
|
|
}
|
|
|
|
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
|
|
{
|
|
if (manipulatedPath == null || _eventWriter == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1;
|
|
if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1)))
|
|
return;
|
|
|
|
var name = GetActorName(resolveData.AssociatedGameObject);
|
|
lock (_eventWriter)
|
|
{
|
|
_eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id,
|
|
manipulatedPath.Value.InternalName.Span, originalPath.Path.Span);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Warning($"Error logging resource to crash handler:\n{ex}");
|
|
}
|
|
}
|
|
|
|
private void CloseEventWriter()
|
|
{
|
|
if (_eventWriter == null)
|
|
return;
|
|
|
|
_eventWriter.Dispose();
|
|
_eventWriter = null;
|
|
Penumbra.Log.Debug("Closed Event Writer for crash handler.");
|
|
}
|
|
|
|
private void OpenEventWriter()
|
|
{
|
|
try
|
|
{
|
|
CloseEventWriter();
|
|
_eventWriter = new GameEventLogWriter(Environment.ProcessId);
|
|
Penumbra.Log.Debug("Opened new Event Writer for crash handler.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Penumbra.Log.Error($"Could not open Event Writer:\n{ex}");
|
|
CloseEventWriter();
|
|
}
|
|
}
|
|
|
|
private unsafe void Subscribe()
|
|
{
|
|
_communicator.CreatingCharacterBase.Subscribe(OnCreatingCharacterBase, CreatingCharacterBase.Priority.CrashHandler);
|
|
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
|
}
|
|
|
|
private unsafe void Unsubscribe()
|
|
{
|
|
_communicator.CreatingCharacterBase.Unsubscribe(OnCreatingCharacterBase);
|
|
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
|
|
}
|
|
}
|