Merge pull request #1966 from Soreepeong/fix/servicescope

ServiceScope fixes
This commit is contained in:
goat 2024-07-25 21:30:14 +02:00 committed by GitHub
commit 8c593bc31d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 165 additions and 166 deletions

View file

@ -84,72 +84,57 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param> /// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <param name="scope">The scope to be used to create scoped services.</param> /// <param name="scope">The scope to be used to create scoped services.</param>
/// <returns>The created object.</returns> /// <returns>The created object.</returns>
public async Task<object?> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) public async Task<object> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null)
{ {
var scopeImpl = scope as ServiceScopeImpl; var errorStep = "constructor lookup";
var ctor = this.FindApplicableCtor(objectType, scopedObjects); try
if (ctor == null)
{ {
Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName!); var scopeImpl = scope as ServiceScopeImpl;
return null;
}
// validate dependency versions (if they exist) var ctor = this.FindApplicableCtor(objectType, scopedObjects)
var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList(); ?? throw new InvalidOperationException("An eligible ctor with satisfiable services could not be found");
var resolvedParams = errorStep = "requested service resolution";
await Task.WhenAll( var resolvedParams =
parameterTypes await Task.WhenAll(
.Select(async type => 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) tcs.SetResult();
{ });
Log.Error("Requested ctor service type {TypeName} was not available (null)", type.FullName!);
}
return service; thr.Start();
})); await tcs.Task.ConfigureAwait(false);
thr.Join();
var hasNull = resolvedParams.Any(p => p == null); return instance;
if (hasNull)
{
Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName!);
return null;
} }
catch (Exception e)
var instance = RuntimeHelpers.GetUninitializedObject(objectType);
if (!await this.InjectProperties(instance, scopedObjects, scope))
{ {
Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!); throw new AggregateException($"Failed to create {objectType.FullName ?? objectType.Name} ({errorStep})", e);
return null;
} }
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;
} }
/// <summary> /// <summary>
@ -159,28 +144,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// <param name="instance">The object instance.</param> /// <param name="instance">The object instance.</param>
/// <param name="publicScopes">Scoped objects to be injected.</param> /// <param name="publicScopes">Scoped objects to be injected.</param>
/// <param name="scope">The scope to be used to create scoped services.</param> /// <param name="scope">The scope to be used to create scoped services.</param>
/// <returns>Whether or not the injection was successful.</returns> /// <returns>A <see cref="ValueTask"/> representing the operation.</returns>
public async Task<bool> InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null)
{ {
var scopeImpl = scope as ServiceScopeImpl; var scopeImpl = scope as ServiceScopeImpl;
var objectType = instance.GetType(); var objectType = instance.GetType();
var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | var props =
BindingFlags.NonPublic).Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()).ToArray(); objectType
.GetProperties(
BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any())
.ToArray();
foreach (var prop in props) foreach (var prop in props)
{ prop.SetValue(instance, await this.GetService(prop.PropertyType, scopeImpl, publicScopes));
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;
} }
/// <summary> /// <summary>
@ -192,7 +170,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// <inheritdoc/> /// <inheritdoc/>
object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType); object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType);
private async Task<object?> GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) private async Task<object> GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects)
{ {
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
serviceType = implementingType; serviceType = implementingType;
@ -201,8 +179,8 @@ internal class ServiceContainer : IServiceProvider, IServiceType
{ {
if (scope == null) if (scope == null)
{ {
Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!); throw new InvalidOperationException(
return null; $"Failed to create {serviceType.FullName ?? serviceType.Name}, is scoped but no scope provided");
} }
return await scope.CreatePrivateScopedObject(serviceType, scopedObjects); return await scope.CreatePrivateScopedObject(serviceType, scopedObjects);
@ -210,18 +188,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
var singletonService = await this.GetSingletonService(serviceType, false); var singletonService = await this.GetSingletonService(serviceType, false);
if (singletonService != null) if (singletonService != null)
{
return singletonService; return singletonService;
}
// resolve dependency from scoped objects // resolve dependency from scoped objects
var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)); return scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType))
if (scoped == default) ?? throw new InvalidOperationException(
{ $"Requested type {serviceType.FullName ?? serviceType.Name} could not be found from {nameof(scopedObjects)}");
return null;
}
return scoped;
} }
private async Task<object?> GetSingletonService(Type serviceType, bool tryGetInterface = true) private async Task<object?> GetSingletonService(Type serviceType, bool tryGetInterface = true)

View file

@ -1,7 +1,10 @@
using System.Collections.Generic; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Serilog;
namespace Dalamud.IoC.Internal; namespace Dalamud.IoC.Internal;
/// <summary> /// <summary>
@ -14,7 +17,7 @@ internal interface IServiceScope : IDisposable
/// but not directly to created objects. /// but not directly to created objects.
/// </summary> /// </summary>
/// <param name="scopes">The scopes to add.</param> /// <param name="scopes">The scopes to add.</param>
public void RegisterPrivateScopes(params object[] scopes); void RegisterPrivateScopes(params object[] scopes);
/// <summary> /// <summary>
/// Create an object. /// Create an object.
@ -22,7 +25,7 @@ internal interface IServiceScope : IDisposable
/// <param name="objectType">The type of object to create.</param> /// <param name="objectType">The type of object to create.</param>
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param> /// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <returns>The created object.</returns> /// <returns>The created object.</returns>
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects); Task<object> CreateAsync(Type objectType, params object[] scopedObjects);
/// <summary> /// <summary>
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object. /// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
@ -30,8 +33,8 @@ internal interface IServiceScope : IDisposable
/// </summary> /// </summary>
/// <param name="instance">The object instance.</param> /// <param name="instance">The object instance.</param>
/// <param name="scopedObjects">Scoped objects to be injected.</param> /// <param name="scopedObjects">Scoped objects to be injected.</param>
/// <returns>Whether or not the injection was successful.</returns> /// <returns>A <see cref="ValueTask"/> representing the status of the operation.</returns>
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects); Task InjectPropertiesAsync(object instance, params object[] scopedObjects);
} }
/// <summary> /// <summary>
@ -41,35 +44,24 @@ internal class ServiceScopeImpl : IServiceScope
{ {
private readonly ServiceContainer container; private readonly ServiceContainer container;
private readonly List<object> privateScopedObjects = new(); private readonly List<object> privateScopedObjects = [];
private readonly List<object> scopeCreatedObjects = new(); private readonly ConcurrentDictionary<Type, Task<object>> scopeCreatedObjects = new();
/// <summary> /// <summary>Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.</summary>
/// Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.
/// </summary>
/// <param name="container">The container this scope will use to create services.</param> /// <param name="container">The container this scope will use to create services.</param>
public ServiceScopeImpl(ServiceContainer container) public ServiceScopeImpl(ServiceContainer container) => this.container = container;
{
this.container = container;
}
/// <inheritdoc/> /// <inheritdoc/>
public void RegisterPrivateScopes(params object[] scopes) public void RegisterPrivateScopes(params object[] scopes) =>
{
this.privateScopedObjects.AddRange(scopes); this.privateScopedObjects.AddRange(scopes);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects) public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) =>
{ this.container.CreateAsync(objectType, scopedObjects, this);
return this.container.CreateAsync(objectType, scopedObjects, this);
}
/// <inheritdoc /> /// <inheritdoc />
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects) public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) =>
{ this.container.InjectProperties(instance, scopedObjects, this);
return this.container.InjectProperties(instance, scopedObjects, this);
}
/// <summary> /// <summary>
/// Create a service scoped to this scope, with private scoped objects. /// Create a service scoped to this scope, with private scoped objects.
@ -77,34 +69,39 @@ internal class ServiceScopeImpl : IServiceScope
/// <param name="objectType">The type of object to create.</param> /// <param name="objectType">The type of object to create.</param>
/// <param name="scopedObjects">Additional scoped objects.</param> /// <param name="scopedObjects">Additional scoped objects.</param>
/// <returns>The created object, or null.</returns> /// <returns>The created object, or null.</returns>
public async Task<object?> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) =>
{ this.scopeCreatedObjects.GetOrAdd(
var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType); objectType,
if (instance != null) static (objectType, p) => p.Scope.container.CreateAsync(
return instance; objectType,
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
instance = (Scope: this, Objects: scopedObjects));
await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray());
if (instance != null)
this.scopeCreatedObjects.Add(instance);
return instance;
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
foreach (var createdObject in this.scopeCreatedObjects) foreach (var objectTask in this.scopeCreatedObjects)
{ {
switch (createdObject) objectTask.Value.ContinueWith(
{ static r =>
case IInternalDisposableService d: {
d.DisposeService(); if (!r.IsCompletedSuccessfully)
break; {
case IDisposable d: if (r.Exception is { } e)
d.Dispose(); Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl));
break; return;
} }
switch (r.Result)
{
case IInternalDisposableService d:
d.DisposeService();
break;
case IDisposable d:
d.Dispose();
break;
}
});
} }
} }
} }

View file

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
@ -26,6 +27,8 @@ using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Ipc.Internal; using Dalamud.Plugin.Ipc.Internal;
using Serilog;
namespace Dalamud.Plugin; namespace Dalamud.Plugin;
/// <summary> /// <summary>
@ -458,34 +461,52 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
#region Dependency Injection #region Dependency Injection
/// <summary> /// <inheritdoc/>
/// Create a new object of the provided type using its default constructor, then inject objects and properties.
/// </summary>
/// <param name="scopedObjects">Objects to inject additionally.</param>
/// <typeparam name="T">The type to create.</typeparam>
/// <returns>The created and initialized type.</returns>
public T? Create<T>(params object[] scopedObjects) where T : class public T? Create<T>(params object[] scopedObjects) where T : class
{ {
var svcContainer = Service<IoC.Internal.ServiceContainer>.Get(); var t = this.CreateAsync<T>(scopedObjects);
t.Wait();
return (T)this.plugin.ServiceScope!.CreateAsync( if (t.Exception is { } e)
typeof(T), {
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); 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;
} }
/// <summary> /// <inheritdoc/>
/// Inject services into properties on the provided object instance. public async Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class =>
/// </summary> (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects));
/// <param name="instance">The instance to inject services into.</param>
/// <param name="scopedObjects">Objects to inject additionally.</param> /// <inheritdoc/>
/// <returns>Whether or not the injection succeeded.</returns>
public bool Inject(object instance, params object[] scopedObjects) public bool Inject(object instance, params object[] scopedObjects)
{ {
return this.plugin.ServiceScope!.InjectPropertiesAsync( var t = this.InjectAsync(instance, scopedObjects);
instance, t.Wait();
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult();
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;
} }
/// <inheritdoc/>
public Task InjectAsync(object instance, params object[] scopedObjects) =>
this.plugin.ServiceScope!.InjectPropertiesAsync(instance, this.GetPublicIocScopes(scopedObjects));
#endregion #endregion
/// <summary>Unregister the plugin and dispose all references.</summary> /// <summary>Unregister the plugin and dispose all references.</summary>

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Threading.Tasks;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.Text; using Dalamud.Game.Text;
@ -304,14 +305,30 @@ public interface IDalamudPluginInterface
/// </summary> /// </summary>
/// <param name="scopedObjects">Objects to inject additionally.</param> /// <param name="scopedObjects">Objects to inject additionally.</param>
/// <typeparam name="T">The type to create.</typeparam> /// <typeparam name="T">The type to create.</typeparam>
/// <returns>The created and initialized type.</returns> /// <returns>The created and initialized type, or <c>null</c> on failure.</returns>
T? Create<T>(params object[] scopedObjects) where T : class; T? Create<T>(params object[] scopedObjects) where T : class;
/// <summary>
/// Create a new object of the provided type using its default constructor, then inject objects and properties.
/// </summary>
/// <param name="scopedObjects">Objects to inject additionally.</param>
/// <typeparam name="T">The type to create.</typeparam>
/// <returns>A task representing the created and initialized type.</returns>
Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class;
/// <summary> /// <summary>
/// Inject services into properties on the provided object instance. /// Inject services into properties on the provided object instance.
/// </summary> /// </summary>
/// <param name="instance">The instance to inject services into.</param> /// <param name="instance">The instance to inject services into.</param>
/// <param name="scopedObjects">Objects to inject additionally.</param> /// <param name="scopedObjects">Objects to inject additionally.</param>
/// <returns>Whether or not the injection succeeded.</returns> /// <returns>Whether the injection succeeded.</returns>
bool Inject(object instance, params object[] scopedObjects); bool Inject(object instance, params object[] scopedObjects);
/// <summary>
/// Inject services into properties on the provided object instance.
/// </summary>
/// <param name="instance">The instance to inject services into.</param>
/// <param name="scopedObjects">Objects to inject additionally.</param>
/// <returns>A <see cref="ValueTask"/> representing the status of the operation.</returns>
Task InjectAsync(object instance, params object[] scopedObjects);
} }

View file

@ -417,24 +417,16 @@ internal class LocalPlugin : IDisposable
try try
{ {
if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1;
{ var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
var newInstance = await framework.RunOnFrameworkThread( this.instance = await newInstanceTask.ConfigureAwait(false);
() => this.ServiceScope.CreateAsync(
this.pluginType!, async Task<IDalamudPlugin> Create() =>
this.DalamudInterface!)).ConfigureAwait(false); (IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!);
this.instance = newInstance as IDalamudPlugin;
}
else
{
this.instance =
await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception in plugin constructor"); Log.Error(ex, "Exception during plugin initialization");
this.instance = null; this.instance = null;
} }