diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 7a0b4347d..a8eacb02d 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -84,72 +84,57 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// 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, object[] scopedObjects, IServiceScope? scope = null) + public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) { - var scopeImpl = scope as ServiceScopeImpl; + var errorStep = "constructor lookup"; - var ctor = this.FindApplicableCtor(objectType, scopedObjects); - if (ctor == null) + try { - Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName!); - return null; - } + var scopeImpl = scope as ServiceScopeImpl; - // validate dependency versions (if they exist) - var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList(); + var ctor = this.FindApplicableCtor(objectType, scopedObjects) + ?? throw new InvalidOperationException("An eligible ctor with satisfiable services could not be found"); - var resolvedParams = - await Task.WhenAll( - parameterTypes - .Select(async type => + errorStep = "requested service resolution"; + var resolvedParams = + await Task.WhenAll( + ctor.GetParameters() + .Select(p => p.ParameterType) + .Select(type => this.GetService(type, scopeImpl, scopedObjects))); + + var instance = RuntimeHelpers.GetUninitializedObject(objectType); + + errorStep = "property injection"; + await this.InjectProperties(instance, scopedObjects, scope); + + errorStep = "ctor invocation"; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var thr = new Thread( + () => + { + try { - var service = await this.GetService(type, scopeImpl, scopedObjects); + ctor.Invoke(instance, resolvedParams); + } + catch (Exception e) + { + tcs.SetException(e); + return; + } - if (service == null) - { - Log.Error("Requested ctor service type {TypeName} was not available (null)", type.FullName!); - } + tcs.SetResult(); + }); - return service; - })); + thr.Start(); + await tcs.Task.ConfigureAwait(false); + thr.Join(); - var hasNull = resolvedParams.Any(p => p == null); - if (hasNull) - { - Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName!); - return null; + return instance; } - - var instance = RuntimeHelpers.GetUninitializedObject(objectType); - - if (!await this.InjectProperties(instance, scopedObjects, scope)) + catch (Exception e) { - Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!); - return null; + throw new AggregateException($"Failed to create {objectType.FullName ?? objectType.Name} ({errorStep})", e); } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var thr = new Thread( - () => - { - try - { - ctor.Invoke(instance, resolvedParams); - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - - tcs.SetResult(); - }); - - thr.Start(); - await tcs.Task.ConfigureAwait(false); - thr.Join(); - - return instance; } /// @@ -159,28 +144,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// The object instance. /// 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, object[] publicScopes, IServiceScope? scope = null) + /// A representing the operation. + 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 | - BindingFlags.NonPublic).Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()).ToArray(); + var props = + objectType + .GetProperties( + BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()) + .ToArray(); foreach (var prop in props) - { - var service = await this.GetService(prop.PropertyType, scopeImpl, publicScopes); - if (service == null) - { - Log.Error("Requested service type {TypeName} was not available (null)", prop.PropertyType.FullName!); - return false; - } - - prop.SetValue(instance, service); - } - - return true; + prop.SetValue(instance, await this.GetService(prop.PropertyType, scopeImpl, publicScopes)); } /// @@ -192,7 +170,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType); - private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) + private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; @@ -201,8 +179,8 @@ internal class ServiceContainer : IServiceProvider, IServiceType { if (scope == null) { - Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!); - return null; + throw new InvalidOperationException( + $"Failed to create {serviceType.FullName ?? serviceType.Name}, is scoped but no scope provided"); } return await scope.CreatePrivateScopedObject(serviceType, scopedObjects); @@ -210,18 +188,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType var singletonService = await this.GetSingletonService(serviceType, false); if (singletonService != null) - { return singletonService; - } // resolve dependency from scoped objects - var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)); - if (scoped == default) - { - return null; - } - - return scoped; + return scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)) + ?? throw new InvalidOperationException( + $"Requested type {serviceType.FullName ?? serviceType.Name} could not be found from {nameof(scopedObjects)}"); } private async Task GetSingletonService(Type serviceType, bool tryGetInterface = true) diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index c21e73f34..4fc299f6e 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Serilog; + namespace Dalamud.IoC.Internal; /// @@ -14,7 +17,7 @@ internal interface IServiceScope : IDisposable /// but not directly to created objects. /// /// The scopes to add. - public void RegisterPrivateScopes(params object[] scopes); + void RegisterPrivateScopes(params object[] scopes); /// /// Create an object. @@ -22,7 +25,7 @@ internal interface IServiceScope : IDisposable /// 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); + Task CreateAsync(Type objectType, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -30,8 +33,8 @@ internal interface IServiceScope : IDisposable /// /// The object instance. /// Scoped objects to be injected. - /// Whether or not the injection was successful. - public Task InjectPropertiesAsync(object instance, params object[] scopedObjects); + /// A representing the status of the operation. + Task InjectPropertiesAsync(object instance, params object[] scopedObjects); } /// @@ -41,35 +44,24 @@ internal class ServiceScopeImpl : IServiceScope { private readonly ServiceContainer container; - private readonly List privateScopedObjects = new(); - private readonly List scopeCreatedObjects = new(); + private readonly List privateScopedObjects = []; + private readonly ConcurrentDictionary> scopeCreatedObjects = new(); - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The container this scope will use to create services. - public ServiceScopeImpl(ServiceContainer container) - { - this.container = container; - } + public ServiceScopeImpl(ServiceContainer container) => this.container = container; /// - public void RegisterPrivateScopes(params object[] scopes) - { + 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 CreateAsync(Type objectType, params object[] scopedObjects) => + this.container.CreateAsync(objectType, scopedObjects, this); /// - public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) - { - return this.container.InjectProperties(instance, scopedObjects, this); - } + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) => + this.container.InjectProperties(instance, scopedObjects, this); /// /// Create a service scoped to this scope, with private scoped objects. @@ -77,34 +69,39 @@ internal class ServiceScopeImpl : IServiceScope /// 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 Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) => + this.scopeCreatedObjects.GetOrAdd( + objectType, + static (objectType, p) => p.Scope.container.CreateAsync( + objectType, + p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), + (Scope: this, Objects: scopedObjects)); /// public void Dispose() { - foreach (var createdObject in this.scopeCreatedObjects) + foreach (var objectTask in this.scopeCreatedObjects) { - switch (createdObject) - { - case IInternalDisposableService d: - d.DisposeService(); - break; - case IDisposable d: - d.Dispose(); - break; - } + objectTask.Value.ContinueWith( + static r => + { + if (!r.IsCompletedSuccessfully) + { + if (r.Exception is { } e) + Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl)); + return; + } + + switch (r.Result) + { + case IInternalDisposableService d: + d.DisposeService(); + break; + case IDisposable d: + d.Dispose(); + break; + } + }); } } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index bb92b6b0c..ecd2e0799 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Configuration.Internal; @@ -26,6 +27,8 @@ using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; +using Serilog; + namespace Dalamud.Plugin; /// @@ -458,34 +461,52 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #region Dependency Injection - /// - /// Create a new object of the provided type using its default constructor, then inject objects and properties. - /// - /// Objects to inject additionally. - /// The type to create. - /// The created and initialized type. + /// public T? Create(params object[] scopedObjects) where T : class { - var svcContainer = Service.Get(); + var t = this.CreateAsync(scopedObjects); + t.Wait(); - return (T)this.plugin.ServiceScope!.CreateAsync( - typeof(T), - this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); + if (t.Exception is { } e) + { + Log.Error( + e, + "{who}: Exception during {where}: {what}", + this.plugin.Name, + nameof(this.Create), + typeof(T).FullName ?? typeof(T).Name); + } + + return t.IsCompletedSuccessfully ? t.Result : null; } - /// - /// Inject services into properties on the provided object instance. - /// - /// The instance to inject services into. - /// Objects to inject additionally. - /// Whether or not the injection succeeded. + /// + public async Task CreateAsync(params object[] scopedObjects) where T : class => + (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); + + /// public bool Inject(object instance, params object[] scopedObjects) { - return this.plugin.ServiceScope!.InjectPropertiesAsync( - instance, - this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); + var t = this.InjectAsync(instance, scopedObjects); + t.Wait(); + + if (t.Exception is { } e) + { + Log.Error( + e, + "{who}: Exception during {where}: {what}", + this.plugin.Name, + nameof(this.Inject), + instance.GetType().FullName ?? instance.GetType().Name); + } + + return t.IsCompletedSuccessfully; } + /// + public Task InjectAsync(object instance, params object[] scopedObjects) => + this.plugin.ServiceScope!.InjectPropertiesAsync(instance, this.GetPublicIocScopes(scopedObjects)); + #endregion /// Unregister the plugin and dispose all references. diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 6393dc5ab..100d4570e 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Game.Text; @@ -304,14 +305,30 @@ public interface IDalamudPluginInterface /// /// Objects to inject additionally. /// The type to create. - /// The created and initialized type. + /// The created and initialized type, or null on failure. T? Create(params object[] scopedObjects) where T : class; + /// + /// Create a new object of the provided type using its default constructor, then inject objects and properties. + /// + /// Objects to inject additionally. + /// The type to create. + /// A task representing the created and initialized type. + Task CreateAsync(params object[] scopedObjects) where T : class; + /// /// Inject services into properties on the provided object instance. /// /// The instance to inject services into. /// Objects to inject additionally. - /// Whether or not the injection succeeded. + /// Whether the injection succeeded. bool Inject(object instance, params object[] scopedObjects); + + /// + /// Inject services into properties on the provided object instance. + /// + /// The instance to inject services into. + /// Objects to inject additionally. + /// A representing the status of the operation. + Task InjectAsync(object instance, params object[] scopedObjects); } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7bb3495c..00fa9d243 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -417,24 +417,16 @@ internal class LocalPlugin : IDisposable try { - if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) - { - var newInstance = await framework.RunOnFrameworkThread( - () => this.ServiceScope.CreateAsync( - this.pluginType!, - this.DalamudInterface!)).ConfigureAwait(false); - - this.instance = newInstance as IDalamudPlugin; - } - else - { - this.instance = - await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; - } + var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1; + var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); + this.instance = await newInstanceTask.ConfigureAwait(false); + + async Task Create() => + (IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!); } catch (Exception ex) { - Log.Error(ex, "Exception in plugin constructor"); + Log.Error(ex, "Exception during plugin initialization"); this.instance = null; }