Merge pull request #2377 from KazWolfe/assert-blame
Some checks failed
Tag Build / Tag Build (push) Successful in 4s
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled

feat: Identify the plugin causing an assertion failure
This commit is contained in:
goat 2025-11-04 21:44:38 +01:00 committed by GitHub
commit 832edaf005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,9 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
@ -55,7 +57,8 @@ internal class AssertHandler : IDisposable
/// </summary> /// </summary>
public unsafe void Setup() public unsafe void Setup()
{ {
CustomNativeFunctions.igCustom_SetAssertCallback(Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer()); CustomNativeFunctions.igCustom_SetAssertCallback(
Marshal.GetFunctionPointerForDelegate(this.callback).ToPointer());
} }
/// <summary> /// <summary>
@ -72,16 +75,53 @@ internal class AssertHandler : IDisposable
this.Shutdown(); this.Shutdown();
} }
private static string? ExtractImguiFunction(StackTrace stackTrace)
{
var frame = stackTrace.GetFrames()
.FirstOrDefault(f => f.GetMethod()?.DeclaringType?.Namespace == "Dalamud.Bindings.ImGui");
if (frame == null)
return null;
var method = frame.GetMethod();
if (method == null)
return null;
return $"{method.Name}({string.Join(", ", method.GetParameters().Select(p => p.Name))})";
}
private static StackTrace GenerateStackTrace()
{
var trace = DiagnosticUtil.GetUsefulTrace(new StackTrace(true));
var frames = trace.GetFrames().ToList();
// Remove everything that happens in the assert context.
var lastAssertIdx = frames.FindLastIndex(f => f.GetMethod()?.DeclaringType == typeof(AssertHandler));
if (lastAssertIdx >= 0)
{
frames.RemoveRange(0, lastAssertIdx + 1);
}
var firstInterfaceManagerIdx = frames.FindIndex(f => f.GetMethod()?.DeclaringType == typeof(InterfaceManager));
if (firstInterfaceManagerIdx >= 0)
{
frames.RemoveRange(firstInterfaceManagerIdx, frames.Count - firstInterfaceManagerIdx);
}
return new StackTrace(frames);
}
private unsafe void OnImGuiAssert(void* pExpr, void* pFile, int line) private unsafe void OnImGuiAssert(void* pExpr, void* pFile, int line)
{ {
var expr = Marshal.PtrToStringAnsi(new IntPtr(pExpr)); var expr = Marshal.PtrToStringAnsi(new IntPtr(pExpr));
var file = Marshal.PtrToStringAnsi(new IntPtr(pFile)); var file = Marshal.PtrToStringAnsi(new IntPtr(pFile));
if (expr == null || file == null) if (expr == null || file == null)
{ {
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)", Log.Warning(
expr, "ImGui assertion failed: {Expr} at {File}:{Line} (failed to parse)",
file, expr,
line); file,
line);
return; return;
} }
@ -93,7 +133,7 @@ internal class AssertHandler : IDisposable
if (!this.ShowAsserts && !this.everShownAssertThisSession) if (!this.ShowAsserts && !this.everShownAssertThisSession)
return; return;
Lazy<string> stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()).ToString()); Lazy<StackTrace> stackTrace = new(GenerateStackTrace);
if (!this.EnableVerboseLogging) if (!this.EnableVerboseLogging)
{ {
@ -103,11 +143,12 @@ internal class AssertHandler : IDisposable
if (count <= HideThreshold || count % HidePrintEvery == 0) if (count <= HideThreshold || count % HidePrintEvery == 0)
{ {
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)", Log.Warning(
expr, "ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
file, expr,
line, file,
count); line,
count);
} }
} }
else else
@ -117,11 +158,12 @@ internal class AssertHandler : IDisposable
} }
else else
{ {
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}", Log.Warning(
expr, "ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
file, expr,
line, file,
stackTrace.Value); line,
stackTrace.Value.ToString());
} }
if (!this.ShowAsserts) if (!this.ShowAsserts)
@ -145,7 +187,8 @@ internal class AssertHandler : IDisposable
} }
// grab the stack trace now that we've decided to show UI. // grab the stack trace now that we've decided to show UI.
_ = stackTrace.Value; var responsiblePlugin = Service<PluginManager>.GetNullable()?.FindCallingPlugin(stackTrace.Value);
var responsibleMethodCall = ExtractImguiFunction(stackTrace.Value);
var gitHubUrl = GetRepoUrl(); var gitHubUrl = GetRepoUrl();
var showOnGitHubButton = new TaskDialogButton var showOnGitHubButton = new TaskDialogButton
@ -175,12 +218,37 @@ internal class AssertHandler : IDisposable
var ignoreButton = TaskDialogButton.Ignore; var ignoreButton = TaskDialogButton.Ignore;
TaskDialogButton? result = null; TaskDialogButton? result = null;
void DialogThreadStart() void DialogThreadStart()
{ {
// TODO(goat): This is probably not gonna work if we showed the loading dialog // TODO(goat): This is probably not gonna work if we showed the loading dialog
// this session since it already loaded visual styles... // this session since it already loaded visual styles...
Application.EnableVisualStyles(); Application.EnableVisualStyles();
string text;
if (responsiblePlugin != null)
{
text = $"The plugin \"{responsiblePlugin.Name}\" appears to have caused an ImGui assertion failure. " +
$"Please report this problem to the plugin's developer.\n\n";
}
else
{
text = "Some code in a plugin or Dalamud itself has caused an ImGui assertion failure. " +
"Please report this problem in the Dalamud discord.\n\n";
}
text += $"You may attempt to continue running the game, but Dalamud UI elements may not work " +
$"correctly, or the game may crash after resuming.\n\n";
if (responsibleMethodCall != null)
{
text += $"Assertion failed: {expr} when performing {responsibleMethodCall}\n{file}:{line}";
}
else
{
text += $"Assertion failed: {expr}\nAt: {file}:{line}";
}
var page = new TaskDialogPage var page = new TaskDialogPage
{ {
Heading = "ImGui assertion failed", Heading = "ImGui assertion failed",
@ -189,9 +257,9 @@ internal class AssertHandler : IDisposable
{ {
CollapsedButtonText = "Show stack trace", CollapsedButtonText = "Show stack trace",
ExpandedButtonText = "Hide stack trace", ExpandedButtonText = "Hide stack trace",
Text = stackTrace.Value, Text = stackTrace.Value.ToString(),
}, },
Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}", Text = text,
Icon = TaskDialogIcon.Warning, Icon = TaskDialogIcon.Warning,
Buttons = Buttons =
[ [