diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 570b63332..92f340a7b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -1,19 +1,44 @@ -using Dalamud.Interface.Utility; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; + +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Ipc.Internal; + using ImGuiNET; +using Newtonsoft.Json; + +using Formatting = Newtonsoft.Json.Formatting; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying plugin data share modules. /// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] internal class DataShareWidget : IDataWindowWidget { + private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20); + + private readonly List<(string Name, byte[]? Data)> dataView = new(); + private int nextTab = -1; + private IReadOnlyDictionary? gates; + private List? gatesSorted; + /// public string[]? CommandShortcuts { get; init; } = { "datashare" }; - + /// - public string DisplayName { get; init; } = "Data Share"; + public string DisplayName { get; init; } = "Data Share & Call Gate"; /// public bool Ready { get; set; } @@ -25,28 +50,290 @@ internal class DataShareWidget : IDataWindowWidget } /// - public void Draw() + public unsafe void Draw() { - if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) + using var tabbar = ImRaii.TabBar("##tabbar"); + if (!tabbar.Success) + return; + + var d = true; + using (var tabitem = ImRaii.TabItem( + "Data Share##tabbar-datashare", + ref d, + NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawDataShare(); + } + + using (var tabitem = ImRaii.TabItem( + "Call Gate##tabbar-callgate", + ref d, + NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawCallGate(); + } + + for (var i = 0; i < this.dataView.Count; i++) + { + using var idpush = ImRaii.PushId($"##tabbar-data-{i}"); + var (name, data) = this.dataView[i]; + d = true; + using var tabitem = ImRaii.TabItem( + name, + ref d, + this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0); + if (!d) + this.dataView.RemoveAt(i--); + if (!tabitem.Success) + continue; + + if (ImGui.Button("Refresh")) + data = null; + + if (data is null) + { + try + { + var dataShare = Service.Get(); + var data2 = dataShare.GetData(name); + try + { + data = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + data2, + Formatting.Indented, + new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All })); + } + finally + { + dataShare.RelinquishData(name); + } + } + catch (Exception e) + { + data = Encoding.UTF8.GetBytes(e.ToString()); + } + + this.dataView[i] = (name, data); + } + + ImGui.SameLine(); + if (ImGui.Button("Copy")) + { + fixed (byte* pData = data) + ImGuiNative.igSetClipboardText(pData); + } + + fixed (byte* pLabel = "text"u8) + fixed (byte* pData = data) + { + ImGuiNative.igInputTextMultiline( + pLabel, + pData, + (uint)data.Length, + ImGui.GetContentRegionAvail(), + ImGuiInputTextFlags.ReadOnly, + null, + null); + } + } + + this.nextTab = -1; + } + + private static string ReprMethod(MethodInfo? mi, bool withParams) + { + if (mi is null) + return "-"; + + var sb = new StringBuilder(); + sb.Append(ReprType(mi.DeclaringType)) + .Append("::") + .Append(mi.Name); + if (!withParams) + return sb.ToString(); + sb.Append('('); + var parfirst = true; + foreach (var par in mi.GetParameters()) + { + if (!parfirst) + sb.Append(", "); + else + parfirst = false; + sb.AppendLine() + .Append('\t') + .Append(ReprType(par.ParameterType)) + .Append(' ') + .Append(par.Name); + } + + if (!parfirst) + sb.AppendLine(); + sb.Append(')'); + if (mi.ReturnType != typeof(void)) + sb.Append(" -> ").Append(ReprType(mi.ReturnType)); + return sb.ToString(); + + static string WithoutGeneric(string s) + { + var i = s.IndexOf('`'); + return i != -1 ? s[..i] : s; + } + + static string ReprType(Type? t) => + t switch + { + null => "null", + _ when t == typeof(string) => "string", + _ when t == typeof(object) => "object", + _ when t == typeof(void) => "void", + _ when t == typeof(decimal) => "decimal", + _ when t == typeof(bool) => "bool", + _ when t == typeof(double) => "double", + _ when t == typeof(float) => "float", + _ when t == typeof(char) => "char", + _ when t == typeof(ulong) => "ulong", + _ when t == typeof(long) => "long", + _ when t == typeof(uint) => "uint", + _ when t == typeof(int) => "int", + _ when t == typeof(ushort) => "ushort", + _ when t == typeof(short) => "short", + _ when t == typeof(byte) => "byte", + _ when t == typeof(sbyte) => "sbyte", + _ when t == typeof(nint) => "nint", + _ when t == typeof(nuint) => "nuint", + _ when t.IsArray && t.HasElementType => ReprType(t.GetElementType()) + "[]", + _ when t.IsPointer && t.HasElementType => ReprType(t.GetElementType()) + "*", + _ when t.IsGenericTypeDefinition => + t.Assembly == typeof(object).Assembly + ? t.Name + "<>" + : (t.FullName ?? t.Name) + "<>", + _ when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) => + ReprType(t.GetGenericArguments()[0]) + "?", + _ when t.IsGenericType => + WithoutGeneric(ReprType(t.GetGenericTypeDefinition())) + + "<" + string.Join(", ", t.GetGenericArguments().Select(ReprType)) + ">", + _ => t.Assembly == typeof(object).Assembly ? t.Name : t.FullName ?? t.Name, + }; + } + + private void DrawTextCell(string s, Func? tooltip = null, bool framepad = false) + { + ImGui.TableNextColumn(); + var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); + if (framepad) + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(s); + if (ImGui.IsItemHovered()) + { + ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); + var vp = ImGui.GetWindowViewport(); + var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); + using (ImRaii.Tooltip()) + { + ImGui.PushTextWrapPos(wrx); + ImGui.TextWrapped((tooltip?.Invoke() ?? s).Replace("%", "%%")); + ImGui.PopTextWrapPos(); + } + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(tooltip?.Invoke() ?? s); + Service.Get().AddNotification( + $"Copied {ImGui.TableGetColumnName()} to clipboard.", + this.DisplayName, + NotificationType.Success); + } + } + + private void DrawCallGate() + { + var callGate = Service.Get(); + if (ImGui.Button("Purge empty call gates")) + callGate.PurgeEmptyGates(); + + using var table = ImRaii.Table("##callgate-table", 5); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort); + ImGui.TableSetupColumn("Action"); + ImGui.TableSetupColumn("Func"); + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Subscriber"); + ImGui.TableHeadersRow(); + + var gates2 = callGate.Gates; + if (!ReferenceEquals(gates2, this.gates) || this.gatesSorted is null) + { + this.gatesSorted = (this.gates = gates2).Values.ToList(); + this.gatesSorted.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + } + + foreach (var item in this.gatesSorted) + { + var subs = item.Subscriptions; + for (var i = 0; i < subs.Count || i == 0; i++) + { + ImGui.TableNextRow(); + this.DrawTextCell(item.Name); + this.DrawTextCell( + ReprMethod(item.Action?.Method, false), + () => ReprMethod(item.Action?.Method, true)); + this.DrawTextCell( + ReprMethod(item.Func?.Method, false), + () => ReprMethod(item.Func?.Method, true)); + if (subs.Count == 0) + { + this.DrawTextCell("0"); + continue; + } + + this.DrawTextCell($"{i + 1}/{subs.Count}"); + this.DrawTextCell($"{subs[i].Method.DeclaringType}::{subs[i].Method.Name}"); + } + } + } + + private void DrawDataShare() + { + if (!ImGui.BeginTable("###DataShareTable", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) return; try { ImGui.TableSetupColumn("Shared Tag"); + ImGui.TableSetupColumn("Show"); ImGui.TableSetupColumn("Creator Assembly"); ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"); ImGui.TableHeadersRow(); foreach (var share in Service.Get().GetAllShares()) { + ImGui.TableNextRow(); + this.DrawTextCell(share.Tag, null, true); + ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Tag); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.CreatorAssembly); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Users.Length.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(string.Join(", ", share.Users)); + if (ImGui.Button($"Show##datasharetable-show-{share.Tag}")) + { + var index = 0; + for (; index < this.dataView.Count; index++) + { + if (this.dataView[index].Name == share.Tag) + break; + } + + if (index == this.dataView.Count) + this.dataView.Add((share.Tag, null)); + else + this.dataView[index] = (share.Tag, null); + this.nextTab = 2 + index; + } + + this.DrawTextCell(share.CreatorAssembly, null, true); + this.DrawTextCell(share.Users.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.Users), null, true); } } finally diff --git a/Dalamud/Plugin/Ipc/Internal/CallGate.cs b/Dalamud/Plugin/Ipc/Internal/CallGate.cs index 7d0f90cb6..fef4b97d0 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGate.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGate.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; namespace Dalamud.Plugin.Ipc.Internal; @@ -10,11 +11,28 @@ internal class CallGate : IServiceType { private readonly Dictionary gates = new(); + private ImmutableDictionary? gatesCopy; + [ServiceManager.ServiceConstructor] private CallGate() { } + /// + /// Gets the thread-safe view of the registered gates. + /// + public IReadOnlyDictionary Gates + { + get + { + var copy = this.gatesCopy; + if (copy is not null) + return copy; + lock (this.gates) + return this.gatesCopy ??= this.gates.ToImmutableDictionary(x => x.Key, x => x.Value); + } + } + /// /// Gets the provider associated with the specified name. /// @@ -22,8 +40,34 @@ internal class CallGate : IServiceType /// A CallGate registered under the given name. public CallGateChannel GetOrCreateChannel(string name) { - if (!this.gates.TryGetValue(name, out var gate)) - gate = this.gates[name] = new CallGateChannel(name); - return gate; + lock (this.gates) + { + if (!this.gates.TryGetValue(name, out var gate)) + { + gate = this.gates[name] = new(name); + this.gatesCopy = null; + } + + return gate; + } + } + + /// + /// Remove empty gates from . + /// + public void PurgeEmptyGates() + { + lock (this.gates) + { + var changed = false; + foreach (var (k, v) in this.Gates) + { + if (v.IsEmpty) + changed |= this.gates.Remove(k); + } + + if (changed) + this.gatesCopy = null; + } } } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 2e2c7249e..54adf2163 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -14,6 +14,17 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGateChannel { + /// + /// The actual storage. + /// + private readonly HashSet subscriptions = new(); + + /// + /// A copy of the actual storage, that will be cleared and populated depending on changes made to + /// . + /// + private ImmutableList? subscriptionsCopy; + /// /// Initializes a new instance of the class. /// @@ -31,17 +42,52 @@ internal class CallGateChannel /// /// Gets a list of delegate subscriptions for when SendMessage is called. /// - public List Subscriptions { get; } = new(); + public IReadOnlyList Subscriptions + { + get + { + var copy = this.subscriptionsCopy; + if (copy is not null) + return copy; + lock (this.subscriptions) + return this.subscriptionsCopy ??= this.subscriptions.ToImmutableList(); + } + } /// /// Gets or sets an action for when InvokeAction is called. /// - public Delegate Action { get; set; } + public Delegate? Action { get; set; } /// /// Gets or sets a func for when InvokeFunc is called. /// - public Delegate Func { get; set; } + public Delegate? Func { get; set; } + + /// + /// Gets a value indicating whether this is not being used. + /// + public bool IsEmpty => this.Action is null && this.Func is null && this.Subscriptions.Count == 0; + + /// + internal void Subscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Add(action); + } + } + + /// + internal void Unsubscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Remove(action); + } + } /// /// Invoke all actions that have subscribed to this IPC. @@ -49,9 +95,6 @@ internal class CallGateChannel /// Message arguments. internal void SendMessage(object?[]? args) { - if (this.Subscriptions.Count == 0) - return; - foreach (var subscription in this.Subscriptions) { var methodInfo = subscription.GetMethodInfo(); @@ -105,7 +148,14 @@ internal class CallGateChannel var paramTypes = methodInfo.GetParameters() .Select(pi => pi.ParameterType).ToArray(); - if (args?.Length != paramTypes.Length) + if (args is null) + { + if (paramTypes.Length == 0) + return; + throw new IpcLengthMismatchError(this.Name, 0, paramTypes.Length); + } + + if (args.Length != paramTypes.Length) throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length); for (var i = 0; i < args.Length; i++) @@ -137,7 +187,7 @@ internal class CallGateChannel } } - private IEnumerable GenerateTypes(Type type) + private IEnumerable GenerateTypes(Type? type) { while (type != null && type != typeof(object)) { @@ -148,6 +198,9 @@ internal class CallGateChannel private object? ConvertObject(object? obj, Type type) { + if (obj is null) + return null; + var json = JsonConvert.SerializeObject(obj); try diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs index 39d5b9f4d..cc54a563b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs @@ -1,5 +1,3 @@ -using System; - #pragma warning disable SA1402 // File may only contain a single type namespace Dalamud.Plugin.Ipc.Internal; @@ -37,7 +35,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider base.InvokeAction(); - /// + /// public TRet InvokeFunc() => this.InvokeFunc(); } @@ -75,7 +73,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider< public void InvokeAction(T1 arg1) => base.InvokeAction(arg1); - /// + /// public TRet InvokeFunc(T1 arg1) => this.InvokeFunc(arg1); } @@ -113,7 +111,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi public void InvokeAction(T1 arg1, T2 arg2) => base.InvokeAction(arg1, arg2); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2) => this.InvokeFunc(arg1, arg2); } @@ -151,7 +149,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP public void InvokeAction(T1 arg1, T2 arg2, T3 arg3) => base.InvokeAction(arg1, arg2, arg3); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3) => this.InvokeFunc(arg1, arg2, arg3); } @@ -189,7 +187,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => base.InvokeAction(arg1, arg2, arg3, arg4); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => this.InvokeFunc(arg1, arg2, arg3, arg4); } @@ -227,7 +225,7 @@ internal class CallGatePubSub : CallGatePubSubBase, IC public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5); } @@ -265,7 +263,7 @@ internal class CallGatePubSub : CallGatePubSubBase public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6); } @@ -303,7 +301,7 @@ internal class CallGatePubSub : CallGatePubSub public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7); } @@ -341,7 +339,7 @@ internal class CallGatePubSub : CallGatePu public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 40c0c4a59..b6a4e8a61 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Internal; @@ -13,7 +11,7 @@ internal abstract class CallGatePubSubBase /// Initializes a new instance of the class. /// /// The name of the IPC registration. - public CallGatePubSubBase(string name) + protected CallGatePubSubBase(string name) { this.Channel = Service.Get().GetOrCreateChannel(name); } @@ -54,14 +52,14 @@ internal abstract class CallGatePubSubBase /// /// Action to subscribe. private protected void Subscribe(Delegate action) - => this.Channel.Subscriptions.Add(action); + => this.Channel.Subscribe(action); /// /// Unsubscribe an expression from this registration. /// /// Action to unsubscribe. private protected void Unsubscribe(Delegate action) - => this.Channel.Subscriptions.Remove(action); + => this.Channel.Unsubscribe(action); /// /// Invoke an action registered for inter-plugin communication. diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index c357f77c2..38cea4866 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -1,5 +1,10 @@ -using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; + +using Dalamud.Plugin.Ipc.Exceptions; + +using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -8,10 +13,14 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal readonly struct DataCache { + /// Name of the data. + internal readonly string Tag; + /// The assembly name of the initial creator. internal readonly string CreatorAssemblyName; /// A not-necessarily distinct list of current users. + /// Also used as a reference count tracker. internal readonly List UserAssemblyNames; /// The type the data was registered as. @@ -23,14 +32,83 @@ internal readonly struct DataCache /// /// Initializes a new instance of the struct. /// + /// Name of the data. /// The assembly name of the initial creator. /// A reference to data. /// The type of the data. - public DataCache(string creatorAssemblyName, object? data, Type type) + public DataCache(string tag, string creatorAssemblyName, object? data, Type type) { + this.Tag = tag; this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = new List { creatorAssemblyName }; + this.UserAssemblyNames = new(); this.Data = data; this.Type = type; } + + /// + /// Creates a new instance of the struct, using the given data generator function. + /// + /// The name for the data cache. + /// The assembly name of the initial creator. + /// The function that generates the data if it does not already exist. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. + /// The new instance of . + public static DataCache From(string tag, string creatorAssemblyName, Func dataGenerator) + where T : class + { + try + { + var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); + Log.Verbose( + "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", + nameof(DataShare), + tag, + creatorAssemblyName); + return result; + } + catch (Exception e) + { + throw ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); + } + } + + /// + /// Attempts to fetch the data. + /// + /// The name of the caller assembly. + /// The value, if succeeded. + /// The exception, if failed. + /// Desired type of the data. + /// true on success. + public bool TryGetData( + string callerName, + [NotNullWhen(true)] out T? value, + [NotNullWhen(false)] out Exception? ex) + where T : class + { + switch (this.Data) + { + case null: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace(new DataCacheValueNullError(this.Tag, this.Type)); + return false; + + case T data: + value = data; + ex = null; + + // Register the access history + lock (this.UserAssemblyNames) + this.UserAssemblyNames.Add(callerName); + + return true; + + default: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type)); + return false; + } + } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index a3e314b80..b122f481d 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; using Dalamud.Plugin.Ipc.Exceptions; using Serilog; @@ -16,7 +14,11 @@ namespace Dalamud.Plugin.Ipc.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { - private readonly Dictionary caches = new(); + /// + /// Dictionary of cached values. Note that is being used, as it does its own locking, + /// effectively preventing calling the data generator multiple times concurrently. + /// + private readonly Dictionary> caches = new(); [ServiceManager.ServiceConstructor] private DataShare() @@ -39,38 +41,15 @@ internal class DataShare : IServiceType where T : class { var callerName = GetCallerName(); + + Lazy cacheLazy; lock (this.caches) { - if (this.caches.TryGetValue(tag, out var cache)) - { - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type); - } - - cache.UserAssemblyNames.Add(callerName); - return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type); - } - - try - { - var obj = dataGenerator.Invoke(); - if (obj == null) - { - throw new Exception("Returned data was null."); - } - - cache = new DataCache(callerName, obj, typeof(T)); - this.caches[tag] = cache; - - Log.Verbose("[DataShare] Created new data for [{Tag:l}] for creator {Creator:l}.", tag, callerName); - return obj; - } - catch (Exception e) - { - throw new DataCacheCreationError(tag, callerName, typeof(T), e); - } + if (!this.caches.TryGetValue(tag, out cacheLazy)) + this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator)); } + + return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; } /// @@ -80,34 +59,36 @@ internal class DataShare : IServiceType /// The name for the data cache. public void RelinquishData(string tag) { + DataCache cache; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out var cacheLazy)) return; - } var callerName = GetCallerName(); - lock (this.caches) - { - if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) - { - return; - } - if (this.caches.Remove(tag)) - { - if (cache.Data is IDisposable disposable) - { - disposable.Dispose(); - Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); - } - else - { - Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); - } - } + cache = cacheLazy.Value; + if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + return; + if (!this.caches.Remove(tag)) + return; + } + + if (cache.Data is IDisposable disposable) + { + try + { + disposable.Dispose(); + Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); } + catch (Exception e) + { + Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); + } + } + else + { + Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); } } @@ -123,23 +104,14 @@ internal class DataShare : IServiceType where T : class { data = null; + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T))) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) return false; - } - - var callerName = GetCallerName(); - data = cache.Data as T; - if (data == null) - { - return false; - } - - cache.UserAssemblyNames.Add(callerName); - return true; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); } /// @@ -155,27 +127,14 @@ internal class DataShare : IServiceType public T GetData(string tag) where T : class { + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); - } - - var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, callerName, typeof(T), cache.Type); - } - - if (cache.Data is not T data) - { - throw new DataCacheValueNullError(tag, typeof(T)); - } - - cache.UserAssemblyNames.Add(callerName); - return data; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out var value, out var ex) ? value : throw ex; } /// @@ -186,7 +145,8 @@ internal class DataShare : IServiceType { lock (this.caches) { - return this.caches.Select(kvp => (kvp.Key, kvp.Value.CreatorAssemblyName, kvp.Value.UserAssemblyNames.ToArray())); + return this.caches.Select( + kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); } }