From daa9f72218491e27fd79f693c005482c17433e97 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 21 May 2023 22:43:28 +0200 Subject: [PATCH] IOC: scoped/on-demand services (#1120) --- Dalamud.CorePlugin/PluginImpl.cs | 4 +- .../PluginInstaller/PluginInstallerWindow.cs | 7 +- Dalamud/IoC/Internal/ServiceContainer.cs | 64 +++++++++-- Dalamud/IoC/Internal/ServiceScope.cs | 101 ++++++++++++++++++ Dalamud/Logging/PluginLog.cs | 69 +++++++++++- Dalamud/Plugin/DalamudPluginInterface.cs | 74 +++++++------ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 29 ++++- Dalamud/ServiceManager.cs | 23 +++- 8 files changed, 309 insertions(+), 62 deletions(-) create mode 100644 Dalamud/IoC/Internal/ServiceScope.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 5ed6d02ac..d352ad2c8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -54,7 +54,7 @@ namespace Dalamud.CorePlugin /// Initializes a new instance of the class. /// /// Dalamud plugin interface. - public PluginImpl(DalamudPluginInterface pluginInterface) + public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log) { try { @@ -68,7 +68,7 @@ namespace Dalamud.CorePlugin Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); - PluginLog.Information("CorePlugin ctor!"); + log.Information("CorePlugin ctor!"); } catch (Exception ex) { diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e98810132..c548b33fb 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2252,7 +2252,8 @@ internal class PluginInstallerWindow : Window, IDisposable disabled = disabled || (plugin.IsOrphaned && !plugin.IsLoaded); // Disable everything if the plugin failed to load - disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed; + // Now handled by the first case below + // disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed; // Disable everything if we're working disabled = disabled || plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading; @@ -2263,7 +2264,7 @@ internal class PluginInstallerWindow : Window, IDisposable StyleModelV1.DalamudStandard.Push(); - if (plugin.State == PluginState.UnloadError && !plugin.IsDev) + if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev) { ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown); @@ -3064,7 +3065,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_UpdateSingle(string version) => Loc.Localize("InstallerUpdateSingle", "Update to {0}").Format(version); - public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerUnloadFailedTooltip", "Plugin unload failed, please restart your game and try again."); + public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again."); #endregion diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 041049643..18d294a3e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -6,6 +6,7 @@ using System.Runtime.Serialization; using System.Threading.Tasks; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; namespace Dalamud.IoC.Internal; @@ -45,9 +46,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// /// The type of object to create. /// Scoped objects to be included in the constructor. + /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, params object[] scopedObjects) + public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) { + var scopeImpl = scope as ServiceScopeImpl; + var ctor = this.FindApplicableCtor(objectType, scopedObjects); if (ctor == null) { @@ -76,11 +80,22 @@ internal class ServiceContainer : IServiceProvider, IServiceType parameters .Select(async p => { + if (p.parameterType.GetCustomAttribute() != null) + { + if (scopeImpl == null) + { + Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); + return null; + } + + return await scopeImpl.CreatePrivateScopedObject(p.parameterType, scopedObjects); + } + var service = await this.GetService(p.parameterType, scopedObjects); if (service == null) { - Log.Error("Requested service type {TypeName} was not available (null)", p.parameterType.FullName!); + Log.Error("Requested ctor service type {TypeName} was not available (null)", p.parameterType.FullName!); } return service; @@ -95,7 +110,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var instance = FormatterServices.GetUninitializedObject(objectType); - if (!await this.InjectProperties(instance, scopedObjects)) + if (!await this.InjectProperties(instance, scopedObjects, scope)) { Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!); return null; @@ -112,10 +127,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// The properties can be marked with the to lock down versions. /// /// The object instance. - /// Scoped objects. + /// Scoped objects to be injected. + /// The scope to be used to create scoped services. /// Whether or not the injection was successful. - public async Task InjectProperties(object instance, params object[] scopedObjects) + public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) { + var scopeImpl = scope as ServiceScopeImpl; var objectType = instance.GetType(); var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | @@ -136,7 +153,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var prop in props) { - var service = await this.GetService(prop.propertyInfo.PropertyType, scopedObjects); + object service = null; + + if (prop.propertyInfo.PropertyType.GetCustomAttribute() != null) + { + if (scopeImpl == null) + { + Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); + } + else + { + service = await scopeImpl.CreatePrivateScopedObject(prop.propertyInfo.PropertyType, publicScopes); + } + } + + service ??= await this.GetService(prop.propertyInfo.PropertyType, publicScopes); if (service == null) { @@ -150,6 +181,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType return true; } + /// + /// Get a service scope, enabling the creation of objects with scoped services. + /// + /// An implementation of a service scope. + public IServiceScope GetScope() => new ServiceScopeImpl(this); + /// object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType); @@ -185,7 +222,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType } // resolve dependency from scoped objects - var scoped = scopedObjects.FirstOrDefault(o => o.GetType() == serviceType); + var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)); if (scoped == default) { return null; @@ -211,7 +248,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType .Union(this.instances.Keys) .ToArray(); - var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + // Allow resolving non-public ctors for Dalamud types + var ctorFlags = BindingFlags.Public | BindingFlags.Instance; + if (type.Assembly == Assembly.GetExecutingAssembly()) + ctorFlags |= BindingFlags.NonPublic; + + var ctors = type.GetConstructors(ctorFlags); foreach (var ctor in ctors) { if (this.ValidateCtor(ctor, types)) @@ -228,8 +270,10 @@ internal class ServiceContainer : IServiceProvider, IServiceType var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { - var contains = types.Contains(parameter.ParameterType); - if (!contains) + var contains = types.Any(x => x.IsAssignableTo(parameter.ParameterType)); + + // Scoped services are created on-demand + if (!contains && parameter.ParameterType.GetCustomAttribute() == null) { Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); return false; diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs new file mode 100644 index 000000000..01c18a8b2 --- /dev/null +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Dalamud.IoC.Internal; + +/// +/// Container enabling the creation of scoped services. +/// +internal interface IServiceScope : IDisposable +{ + /// + /// Register objects that may be injected to scoped services, + /// but not directly to created objects. + /// + /// The scopes to add. + public void RegisterPrivateScopes(params object[] scopes); + + /// + /// Create an object. + /// + /// The type of object to create. + /// Scoped objects to be included in the constructor. + /// The created object. + public Task CreateAsync(Type objectType, params object[] scopedObjects); + + /// + /// Inject interfaces into public or static properties on the provided object. + /// The properties have to be marked with the . + /// The properties can be marked with the to lock down versions. + /// + /// The object instance. + /// Scoped objects to be injected. + /// Whether or not the injection was successful. + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects); +} + +/// +/// Implementation of a service scope. +/// +internal class ServiceScopeImpl : IServiceScope +{ + private readonly ServiceContainer container; + + private readonly List privateScopedObjects = new(); + private readonly List scopeCreatedObjects = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The container this scope will use to create services. + public ServiceScopeImpl(ServiceContainer container) + { + this.container = container; + } + + /// + public void RegisterPrivateScopes(params object[] scopes) + { + this.privateScopedObjects.AddRange(scopes); + } + + /// + public Task CreateAsync(Type objectType, params object[] scopedObjects) + { + return this.container.CreateAsync(objectType, scopedObjects, this); + } + + /// + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) + { + return this.container.InjectProperties(instance, scopedObjects, this); + } + + /// + /// Create a service scoped to this scope, with private scoped objects. + /// + /// The type of object to create. + /// Additional scoped objects. + /// The created object, or null. + public async Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) + { + var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType); + if (instance != null) + return instance; + + instance = + await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray()); + if (instance != null) + this.scopeCreatedObjects.Add(instance); + + return instance; + } + + /// + public void Dispose() + { + foreach (var createdObject in this.scopeCreatedObjects.OfType()) createdObject.Dispose(); + } +} diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index b2f2a5065..83cc90bc4 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,6 +1,9 @@ using System; using System.Reflection; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; using Serilog; using Serilog.Events; @@ -9,9 +12,29 @@ namespace Dalamud.Logging; /// /// Class offering various static methods to allow for logging in plugins. /// -public static class PluginLog +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +public class PluginLog : IServiceType, IDisposable { - #region "Log" prefixed Serilog style methods + private readonly LocalPlugin plugin; + + /// + /// Initializes a new instance of the class. + /// Do not use this ctor, inject PluginLog instead. + /// + /// The plugin this service is scoped for. + internal PluginLog(LocalPlugin plugin) + { + this.plugin = plugin; + } + + /// + /// Gets or sets a prefix appended to log messages. + /// + public string? LogPrefix { get; set; } = null; + + #region Legacy static "Log" prefixed Serilog style methods /// /// Log a templated message to the in-game debug log. @@ -134,7 +157,7 @@ public static class PluginLog #endregion - #region Serilog style methods + #region Legacy static Serilog style methods /// /// Log a templated verbose message to the in-game debug log. @@ -254,6 +277,25 @@ public static class PluginLog public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) => WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values); + #region New instanced methods + + /// + /// Log some information. + /// + /// The message. + internal void Information(string message) + { + Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}"); + } + + #endregion + + /// + void IDisposable.Dispose() + { + // ignored + } + private static ILogger GetPluginLogger(string? pluginName) { return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); @@ -272,3 +314,24 @@ public static class PluginLog values); } } + +/// +/// Class offering logging services, for a specific type. +/// +/// The type to log for. +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +public class PluginLog : PluginLog +{ + /// + /// Initializes a new instance of the class. + /// Do not use this ctor, inject PluginLog instead. + /// + /// The plugin this service is scoped for. + internal PluginLog(LocalPlugin plugin) + : base(plugin) + { + this.LogPrefix = typeof(T).Name; + } +} diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 2912eaf97..747e0a087 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -33,33 +33,30 @@ namespace Dalamud.Plugin; /// public sealed class DalamudPluginInterface : IDisposable { - private readonly string pluginName; + private readonly LocalPlugin plugin; private readonly PluginConfigurations configs; /// /// Initializes a new instance of the class. /// Set up the interface and populate all fields needed. /// - /// The internal name of the plugin. - /// Location of the assembly. + /// The plugin this interface belongs to. /// The reason the plugin was loaded. - /// A value indicating whether this is a dev plugin. - /// The local manifest for this plugin. - internal DalamudPluginInterface(string pluginName, FileInfo assemblyLocation, PluginLoadReason reason, bool isDev, LocalPluginManifest manifest) + internal DalamudPluginInterface( + LocalPlugin plugin, + PluginLoadReason reason) { + this.plugin = plugin; var configuration = Service.Get(); var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(pluginName); + this.UiBuilder = new UiBuilder(plugin.Name); - this.pluginName = pluginName; - this.AssemblyLocation = assemblyLocation; this.configs = Service.Get().PluginConfigs; this.Reason = reason; - this.IsDev = isDev; - this.SourceRepository = isDev ? LocalPluginManifest.FlagDevPlugin : manifest.InstalledFromUrl; - this.IsTesting = manifest.Testing; + this.SourceRepository = this.IsDev ? LocalPluginManifest.FlagDevPlugin : plugin.Manifest.InstalledFromUrl; + this.IsTesting = plugin.IsTesting; this.LoadTime = DateTime.Now; this.LoadTimeUTC = DateTime.UtcNow; @@ -128,12 +125,12 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the current internal plugin name. /// - public string InternalName => this.pluginName; + public string InternalName => this.plugin.InternalName; /// /// Gets a value indicating whether this is a dev plugin. /// - public bool IsDev { get; } + public bool IsDev => this.plugin.IsDev; /// /// Gets a value indicating whether this is a testing release of a plugin. @@ -166,7 +163,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the location of your plugin assembly. /// - public FileInfo AssemblyLocation { get; } + public FileInfo AssemblyLocation => this.plugin.DllFile; /// /// Gets the directory your plugin configurations are stored in. @@ -176,7 +173,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the config file of your plugin. /// - public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName); + public FileInfo ConfigFile => this.configs.GetConfigFile(this.plugin.InternalName); /// /// Gets the instance which allows you to draw UI into the game via ImGui draw calls. @@ -238,7 +235,7 @@ public sealed class DalamudPluginInterface : IDisposable } dalamudInterface.OpenPluginInstallerPluginInstalled(); - dalamudInterface.SetPluginInstallerSearchText(this.pluginName); + dalamudInterface.SetPluginInstallerSearchText(this.plugin.InternalName); return true; } @@ -357,7 +354,7 @@ public sealed class DalamudPluginInterface : IDisposable if (currentConfig == null) return; - this.configs.Save(currentConfig, this.pluginName); + this.configs.Save(currentConfig, this.plugin.InternalName); } /// @@ -379,30 +376,32 @@ public sealed class DalamudPluginInterface : IDisposable { var mi = this.configs.GetType().GetMethod("LoadForType"); var fn = mi.MakeGenericMethod(type); - return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.pluginName }); + return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.plugin.InternalName }); } } // this shouldn't be a thing, I think, but just in case - return this.configs.Load(this.pluginName); + return this.configs.Load(this.plugin.InternalName); } /// /// Get the config directory. /// /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName. - public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.pluginName); + public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.plugin.InternalName); /// /// Get the loc directory. /// /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName/loc. - public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.pluginName, "loc")); + public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.plugin.InternalName, "loc")); #endregion #region Chat Links + // TODO API9: Move to chatgui, don't allow passing own commandId + /// /// Register a chat link handler. /// @@ -411,7 +410,7 @@ public sealed class DalamudPluginInterface : IDisposable /// Returns an SeString payload for the link. public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action commandAction) { - return Service.Get().AddChatLinkHandler(this.pluginName, commandId, commandAction); + return Service.Get().AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction); } /// @@ -420,7 +419,7 @@ public sealed class DalamudPluginInterface : IDisposable /// The ID of the command. public void RemoveChatLinkHandler(uint commandId) { - Service.Get().RemoveChatLinkHandler(this.pluginName, commandId); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName, commandId); } /// @@ -428,7 +427,7 @@ public sealed class DalamudPluginInterface : IDisposable /// public void RemoveChatLinkHandler() { - Service.Get().RemoveChatLinkHandler(this.pluginName); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); } #endregion @@ -444,11 +443,9 @@ public sealed class DalamudPluginInterface : IDisposable { var svcContainer = Service.Get(); - var realScopedObjects = new object[scopedObjects.Length + 1]; - realScopedObjects[0] = this; - Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length); - - return (T)svcContainer.CreateAsync(typeof(T), realScopedObjects).GetAwaiter().GetResult(); + return (T)this.plugin.ServiceScope!.CreateAsync( + typeof(T), + this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); } /// @@ -459,13 +456,9 @@ public sealed class DalamudPluginInterface : IDisposable /// Whether or not the injection succeeded. public bool Inject(object instance, params object[] scopedObjects) { - var svcContainer = Service.Get(); - - var realScopedObjects = new object[scopedObjects.Length + 1]; - realScopedObjects[0] = this; - Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length); - - return svcContainer.InjectProperties(instance, realScopedObjects).GetAwaiter().GetResult(); + return this.plugin.ServiceScope!.InjectPropertiesAsync( + instance, + this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); } #endregion @@ -476,7 +469,7 @@ public sealed class DalamudPluginInterface : IDisposable void IDisposable.Dispose() { this.UiBuilder.ExplicitDispose(); - Service.Get().RemoveChatLinkHandler(this.pluginName); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); Service.Get().LocalizationChanged -= this.OnLocalizationChanged; Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } @@ -510,4 +503,9 @@ public sealed class DalamudPluginInterface : IDisposable { this.GeneralChatType = dalamudConfiguration.GeneralChatType; } + + private object[] GetPublicIocScopes(IEnumerable scopedObjects) + { + return scopedObjects.Append(this).ToArray(); + } } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 2c77ff528..886bc2d7a 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; +using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; @@ -180,10 +181,15 @@ internal class LocalPlugin : IDisposable public AssemblyName? AssemblyName { get; private set; } /// - /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest. + /// Gets the plugin name from the manifest. /// public string Name => this.Manifest.Name; + /// + /// Gets the plugin internal name from the manifest. + /// + public string InternalName => this.Manifest.Name; + /// /// Gets an optional reason, if the plugin is banned. /// @@ -238,6 +244,11 @@ internal class LocalPlugin : IDisposable /// public bool IsDev => this is LocalDevPlugin; + /// + /// Gets the service scope for this plugin. + /// + public IServiceScope? ServiceScope { get; private set; } + /// public void Dispose() { @@ -259,6 +270,9 @@ internal class LocalPlugin : IDisposable this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface = null; + this.ServiceScope?.Dispose(); + this.ServiceScope = null; + this.pluginType = null; this.pluginAssembly = null; @@ -410,17 +424,20 @@ internal class LocalPlugin : IDisposable PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); this.DalamudInterface = - new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev, this.Manifest); + new DalamudPluginInterface(this, reason); + + this.ServiceScope = ioc.GetScope(); + this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1) { this.instance = await framework.RunOnFrameworkThread( - () => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; + () => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; } else { this.instance = - await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; + await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; } if (this.instance == null) @@ -466,6 +483,7 @@ internal class LocalPlugin : IDisposable { var configuration = Service.Get(); var framework = Service.GetNullable(); + var ioc = await Service.GetAsync(); await this.pluginLoadStateLock.WaitAsync(); try @@ -504,6 +522,9 @@ internal class LocalPlugin : IDisposable this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface = null; + this.ServiceScope?.Dispose(); + this.ServiceScope = null; + this.pluginType = null; this.pluginAssembly = null; diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index f237d8e57..2b96ae5ea 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -65,6 +65,11 @@ internal static class ServiceManager /// BlockingEarlyLoadedService = 1 << 2, + /// + /// Service that is only instantiable via scopes. + /// + ScopedService = 1 << 3, + /// /// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// @@ -133,10 +138,12 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { var serviceKind = serviceType.GetServiceKind(); - if (serviceKind == ServiceKind.None) + if (serviceKind is ServiceKind.None or ServiceKind.ScopedService) continue; - Debug.Assert(!serviceKind.HasFlag(ServiceKind.ManualService), "Regular services should never end up here"); + Debug.Assert( + !serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService), + "Regular and scoped services should never be loaded early"); var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); @@ -389,6 +396,9 @@ internal static class ServiceManager if (attr.IsAssignableTo(typeof(EarlyLoadedService))) return ServiceKind.EarlyLoadedService; + + if (attr.IsAssignableTo(typeof(ScopedService))) + return ServiceKind.ScopedService; return ServiceKind.ManualService; } @@ -435,6 +445,15 @@ internal static class ServiceManager { } + /// + /// Indicates that the class is a service that will be created specifically for a + /// service scope, and that it cannot be created outside of a scope. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ScopedService : Service + { + } + /// /// Indicates that the method should be called when the services given in the constructor are ready. ///