Compare commits

..

1 commit

Author SHA1 Message Date
github-actions[bot]
5f15180feb Update Excel Schema
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
2025-12-06 12:48:33 +00:00
9 changed files with 88 additions and 157 deletions

View file

@ -1,10 +1,9 @@
name: Build Dalamud name: Build Dalamud
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
# Globally blocking because of git pushes in deploy step
concurrency: concurrency:
group: build_dalamud_${{ github.repository_owner }} group: build_dalamud_${{ github.ref_name }}
cancel-in-progress: false cancel-in-progress: true
jobs: jobs:
build: build:

View file

@ -6,8 +6,6 @@
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
struct exception_info struct exception_info
{ {
LPEXCEPTION_POINTERS pExceptionPointers; LPEXCEPTION_POINTERS pExceptionPointers;

View file

@ -331,51 +331,6 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually"); logging::I("VEH was disabled manually");
} }
// ============================== CLR Reporting =================================== //
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
// Ideally all of this will go away once they get to it.
static std::shared_ptr<hooks::global_import_hook<decltype(ReportEventW)>> s_report_event_hook;
s_report_event_hook = std::make_shared<hooks::global_import_hook<decltype(ReportEventW)>>(
"advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
HANDLE hEventLog,
WORD wType,
WORD wCategory,
DWORD dwEventID,
PSID lpUserSid,
WORD wNumStrings,
DWORD dwDataSize,
LPCWSTR* lpStrings,
LPVOID lpRawData)-> BOOL {
// Check for CLR Error Event IDs
// https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
{
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
if (wNumStrings == 0 || lpStrings == nullptr) {
logging::W("ReportEventW called with no strings.");
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
// In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
const std::wstring error_details(lpStrings[0]);
veh::raise_external_event(error_details);
return original_ret;
});
logging::I("ReportEventW hook installed.");
// ============================== Dalamud ==================================== // // ============================== Dalamud ==================================== //
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint)) if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))

View file

@ -31,8 +31,6 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr; HANDLE g_crashhandler_pipe_write = nullptr;
wchar_t g_external_event_info[16384] = L"";
std::recursive_mutex g_exception_handler_mutex; std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point<std::chrono::system_clock> g_time_start; std::chrono::time_point<std::chrono::system_clock> g_time_start;
@ -192,11 +190,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace; std::wstring stackTrace;
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT) if (!g_clr)
{
stackTrace = std::wstring(g_external_event_info);
}
else if (!g_clr)
{ {
stackTrace = L"(no CLR stack trace available)"; stackTrace = L"(no CLR stack trace available)";
} }
@ -257,12 +251,6 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{ {
// special case for CLR exceptions, always trigger crash handler
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
return exception_handler(ex);
}
if (ex->ExceptionRecord->ExceptionCode == 0x12345678) if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{ {
// pass // pass
@ -446,10 +434,3 @@ bool veh::remove_handler()
} }
return false; return false;
} }
void veh::raise_external_event(const std::wstring& info)
{
const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
}

View file

@ -4,5 +4,4 @@ namespace veh
{ {
bool add_handler(bool doFullDump, const std::string& workingDirectory); bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler(); bool remove_handler();
void raise_external_event(const std::wstring& info);
} }

View file

@ -9,6 +9,7 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events; namespace Dalamud.Game.Addon.Events;
@ -31,21 +32,25 @@ internal unsafe class AddonEventManager : IInternalDisposableService
private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor; private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers; private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AtkCursor.CursorType? cursorOverride; private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonEventManager() private AddonEventManager(TargetSigScanner sigScanner)
{ {
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>(); this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null; this.cursorOverride = null;
this.onUpdateCursor = Hook<AtkUnitManager.Delegates.UpdateCursor>.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour); this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener); this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@ -53,6 +58,8 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable(); this.onUpdateCursor.Enable();
} }
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
@ -110,7 +117,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// Force the game cursor to be the specified cursor. /// Force the game cursor to be the specified cursor.
/// </summary> /// </summary>
/// <param name="cursor">Which cursor to use.</param> /// <param name="cursor">Which cursor to use.</param>
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor; internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
/// <summary> /// <summary>
/// Un-forces the game cursor. /// Un-forces the game cursor.
@ -161,7 +168,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
} }
} }
private void UpdateCursorDetour(AtkUnitManager* thisPtr) private nint UpdateCursorDetour(RaptureAtkModule* module)
{ {
try try
{ {
@ -169,14 +176,13 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null) if (this.cursorOverride is not null && atkStage is not null)
{ {
ref var atkCursor = ref atkStage->AtkCursor; var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
if (cursor != this.cursorOverride)
if (atkCursor.Type != this.cursorOverride)
{ {
atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
} }
return; return nint.Zero;
} }
} }
catch (Exception e) catch (Exception e)
@ -184,7 +190,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour."); Log.Error(e, "Exception in UpdateCursorDetour.");
} }
this.onUpdateCursor!.Original(thisPtr); return this.onUpdateCursor!.Original(module);
} }
} }

View file

@ -0,0 +1,21 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// AddonEventManager memory address resolver.
/// </summary>
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the AtkModule UpdateCursor method.
/// </summary>
public nint UpdateCursor { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner)
{
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
}
}

View file

@ -292,43 +292,35 @@ std::wstring to_address_string(const DWORD64 address, const bool try_ptrderef =
void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) { void print_exception_info(HANDLE hThread, const EXCEPTION_POINTERS& ex, const CONTEXT& ctx, std::wostringstream& log) {
std::vector<EXCEPTION_RECORD> exRecs; std::vector<EXCEPTION_RECORD> exRecs;
if (ex.ExceptionRecord) if (ex.ExceptionRecord) {
{
size_t rec_index = 0; size_t rec_index = 0;
size_t read; size_t read;
for (auto pRemoteExRec = ex.ExceptionRecord;
pRemoteExRec && rec_index < 64;
rec_index++)
{
exRecs.emplace_back(); exRecs.emplace_back();
for (auto pRemoteExRec = ex.ExceptionRecord;
if (!ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read) pRemoteExRec
|| read < offsetof(EXCEPTION_RECORD, ExceptionInformation) && rec_index < 64
|| read < static_cast<size_t>(reinterpret_cast<const char*>(&exRecs.back().ExceptionInformation[exRecs. && ReadProcessMemory(g_hProcess, pRemoteExRec, &exRecs.back(), sizeof exRecs.back(), &read)
back().NumberParameters]) - reinterpret_cast<const char*>(&exRecs.back()))) && read >= offsetof(EXCEPTION_RECORD, ExceptionInformation)
{ && read >= static_cast<size_t>(reinterpret_cast<const char*>(&exRecs.back().ExceptionInformation[exRecs.back().NumberParameters]) - reinterpret_cast<const char*>(&exRecs.back()));
exRecs.pop_back(); rec_index++) {
break;
}
log << std::format(L"\nException Info #{}\n", rec_index); log << std::format(L"\nException Info #{}\n", rec_index);
log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode); log << std::format(L"Address: {:X}\n", exRecs.back().ExceptionCode);
log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags); log << std::format(L"Flags: {:X}\n", exRecs.back().ExceptionFlags);
log << std::format(L"Address: {:X}\n", reinterpret_cast<size_t>(exRecs.back().ExceptionAddress)); log << std::format(L"Address: {:X}\n", reinterpret_cast<size_t>(exRecs.back().ExceptionAddress));
if (exRecs.back().NumberParameters) if (!exRecs.back().NumberParameters)
{ continue;
log << L"Parameters: "; log << L"Parameters: ";
for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) for (DWORD i = 0; i < exRecs.back().NumberParameters; ++i) {
{
if (i != 0) if (i != 0)
log << L", "; log << L", ";
log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]); log << std::format(L"{:X}", exRecs.back().ExceptionInformation[i]);
} }
}
pRemoteExRec = exRecs.back().ExceptionRecord; pRemoteExRec = exRecs.back().ExceptionRecord;
exRecs.emplace_back();
} }
exRecs.pop_back();
} }
log << L"\nCall Stack\n{"; log << L"\nCall Stack\n{";
@ -938,19 +930,9 @@ int main() {
} while (false); } while (false);
} }
const bool is_external_event = exinfo.ExceptionRecord.ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT;
std::wostringstream log; std::wostringstream log;
if (!is_external_event)
{
log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl; log << std::format(L"Unhandled native exception occurred at {}", to_address_string(exinfo.ContextRecord.Rip, false)) << std::endl;
log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl; log << std::format(L"Code: {:X}", exinfo.ExceptionRecord.ExceptionCode) << std::endl;
}
else
{
log << L"CLR error occurred" << std::endl;
}
if (shutup) if (shutup)
log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl; log << L"======= Crash handler was globally muted(shutdown?) =======" << std::endl;
@ -967,19 +949,9 @@ int main() {
if (pProgressDialog) if (pProgressDialog)
pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL); pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL);
std::wstring window_log_str;
// Cut the log here for external events, the rest is unreadable and doesn't matter since we can't get
// symbols for mixed-mode stacks yet.
if (is_external_event)
window_log_str = log.str();
SymRefreshModuleList(GetCurrentProcess()); SymRefreshModuleList(GetCurrentProcess());
print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log); print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log);
const auto window_log_str = log.str();
if (!is_external_event)
window_log_str = log.str();
print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log); print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log);
std::wofstream(logPath) << log.str(); std::wofstream(logPath) << log.str();

@ -1 +1 @@
Subproject commit 6f339d8f725fa6922449f7e5c584ca6b8fa2fb19 Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f