feat: attempt to prevent some plugin disposal crashes by stopping Framework::Update event dispatching before unloading

This commit is contained in:
goat 2021-04-20 22:01:17 +02:00
parent 35d88ac554
commit de98b75336
No known key found for this signature in database
GPG key ID: F18F057873895461
2 changed files with 42 additions and 31 deletions

View file

@ -358,6 +358,10 @@ namespace Dalamud
{ {
try try
{ {
// this must be done before unloading plugins, to prevent crashes due to errors
// in plugin cleanup
this.Framework.DispatchUpdateEvents = false;
// this must be done before unloading plugins, or it can cause a race condition // this must be done before unloading plugins, or it can cause a race condition
// due to rendering happening on another thread, where a plugin might receive // due to rendering happening on another thread, where a plugin might receive
// a render call after it has been disposed, which can crash if it attempts to // a render call after it has been disposed, which can crash if it attempts to

View file

@ -17,6 +17,8 @@ namespace Dalamud.Game.Internal {
public sealed class Framework : IDisposable { public sealed class Framework : IDisposable {
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
internal bool DispatchUpdateEvents { get; set; } = true;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework); private delegate bool OnUpdateDetour(IntPtr framework);
@ -39,8 +41,8 @@ namespace Dalamud.Game.Internal {
/// A raw pointer to the instance of Client::Framework /// A raw pointer to the instance of Client::Framework
/// </summary> /// </summary>
public FrameworkAddressResolver Address { get; } public FrameworkAddressResolver Address { get; }
#region Stats #region Stats
public static bool StatsEnabled { get; set; } public static bool StatsEnabled { get; set; }
public static Dictionary<string, List<double>> StatsHistory = new Dictionary<string, List<double>>(); public static Dictionary<string, List<double>> StatsHistory = new Dictionary<string, List<double>>();
private static Stopwatch statsStopwatch = new Stopwatch(); private static Stopwatch statsStopwatch = new Stopwatch();
@ -125,39 +127,42 @@ namespace Dalamud.Game.Internal {
} catch (Exception ex) { } catch (Exception ex) {
Log.Error(ex, "Exception while handling Framework::Update hook."); Log.Error(ex, "Exception while handling Framework::Update hook.");
} }
try {
if (StatsEnabled && OnUpdateEvent != null) {
// Stat Tracking for Framework Updates
var invokeList = OnUpdateEvent.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList) {
statsStopwatch.Restart();
d.Method.Invoke(d.Target, new object[]{ this });
statsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key)) notUpdated.Remove(key);
if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List<double>());
StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds);
if (StatsHistory[key].Count > 1000) {
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
}
}
// Cleanup handlers that are no longer being called if (this.DispatchUpdateEvents)
foreach (var key in notUpdated) { {
if (StatsHistory[key].Count > 0) { try {
StatsHistory[key].RemoveAt(0); if (StatsEnabled && OnUpdateEvent != null) {
} else { // Stat Tracking for Framework Updates
StatsHistory.Remove(key); var invokeList = OnUpdateEvent.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList) {
statsStopwatch.Restart();
d.Method.Invoke(d.Target, new object[]{ this });
statsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key)) notUpdated.Remove(key);
if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List<double>());
StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds);
if (StatsHistory[key].Count > 1000) {
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
}
} }
// Cleanup handlers that are no longer being called
foreach (var key in notUpdated) {
if (StatsHistory[key].Count > 0) {
StatsHistory[key].RemoveAt(0);
} else {
StatsHistory.Remove(key);
}
}
} else {
OnUpdateEvent?.Invoke(this);
} }
} else { } catch (Exception ex) {
OnUpdateEvent?.Invoke(this); Log.Error(ex, "Exception while dispatching Framework::Update event.");
} }
} catch (Exception ex) {
Log.Error(ex, "Exception while dispatching Framework::Update event.");
} }
return this.updateHook.Original(framework); return this.updateHook.Original(framework);
@ -166,6 +171,8 @@ namespace Dalamud.Game.Internal {
private IntPtr HandleFrameworkDestroy() { private IntPtr HandleFrameworkDestroy() {
Log.Information("Framework::OnDestroy!"); Log.Information("Framework::OnDestroy!");
this.DispatchUpdateEvents = false;
// Store the pointer to the original trampoline location // Store the pointer to the original trampoline location
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.destroyHook.Original); var originalPtr = Marshal.GetFunctionPointerForDelegate(this.destroyHook.Original);