IoC: Allow private scoped objects to resolve singleton services

This commit is contained in:
goaaats 2025-05-01 14:45:25 +02:00
parent c82bb8191d
commit 69d8968dca
9 changed files with 134 additions and 78 deletions

View file

@ -26,9 +26,9 @@ internal class ServicesWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "services" }; public string[]? CommandShortcuts { get; init; } = { "services" };
/// <inheritdoc/> /// <inheritdoc/>
public string DisplayName { get; init; } = "Service Container"; public string DisplayName { get; init; } = "Service Container";
/// <inheritdoc/> /// <inheritdoc/>
public bool Ready { get; set; } public bool Ready { get; set; }
@ -48,7 +48,7 @@ internal class ServicesWidget : IDataWindowWidget
{ {
if (ImGui.Button("Clear selection")) if (ImGui.Button("Clear selection"))
this.selectedNodes.Clear(); this.selectedNodes.Clear();
ImGui.SameLine(); ImGui.SameLine();
switch (this.includeUnloadDependencies) switch (this.includeUnloadDependencies)
{ {
@ -90,12 +90,12 @@ internal class ServicesWidget : IDataWindowWidget
var dl = ImGui.GetWindowDrawList(); var dl = ImGui.GetWindowDrawList();
var mouse = ImGui.GetMousePos(); var mouse = ImGui.GetMousePos();
var maxRowWidth = 0f; var maxRowWidth = 0f;
// 1. Layout // 1. Layout
for (var level = 0; level < this.dependencyNodes.Count; level++) for (var level = 0; level < this.dependencyNodes.Count; level++)
{ {
var levelNodes = this.dependencyNodes[level]; var levelNodes = this.dependencyNodes[level];
var rowWidth = 0f; var rowWidth = 0f;
foreach (var node in levelNodes) foreach (var node in levelNodes)
rowWidth += node.DisplayedNameSize.X + cellPad.X + margin.X; rowWidth += node.DisplayedNameSize.X + cellPad.X + margin.X;
@ -139,7 +139,7 @@ internal class ServicesWidget : IDataWindowWidget
{ {
var rect = this.nodeRects[node]; var rect = this.nodeRects[node];
var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y);
foreach (var parent in node.InvalidParents) foreach (var parent in node.InvalidParents)
{ {
rect = this.nodeRects[parent]; rect = this.nodeRects[parent];
@ -149,7 +149,7 @@ internal class ServicesWidget : IDataWindowWidget
dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale); dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale);
} }
foreach (var parent in node.Parents) foreach (var parent in node.Parents)
{ {
rect = this.nodeRects[parent]; rect = this.nodeRects[parent];
@ -170,7 +170,7 @@ internal class ServicesWidget : IDataWindowWidget
} }
} }
} }
// 3. Draw boxes // 3. Draw boxes
foreach (var levelNodes in this.dependencyNodes) foreach (var levelNodes in this.dependencyNodes)
{ {
@ -231,36 +231,49 @@ internal class ServicesWidget : IDataWindowWidget
} }
} }
} }
ImGui.SetCursorPos(default); ImGui.SetCursorPos(default);
ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight));
ImGui.EndChild(); ImGui.EndChild();
} }
} }
if (ImGui.CollapsingHeader("Plugin-facing Services")) if (ImGui.CollapsingHeader("Singleton Services"))
{ {
foreach (var instance in container.Instances) foreach (var instance in container.Instances)
{ {
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
var isPublic = instance.Key.IsPublic; var isPublic = instance.Key.IsPublic;
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
if (isPublic) if (isPublic)
{ {
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text("\t => PUBLIC!!!"); ImGui.Text("\t => PUBLIC!!!");
} }
switch (instance.Value.Visibility)
{
case ObjectInstanceVisibility.Internal:
ImGui.Text("\t => Internally resolved");
break;
case ObjectInstanceVisibility.ExposedToPlugins:
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text("\t => Exposed to plugins!");
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);
} }
} }
@ -301,7 +314,7 @@ internal class ServicesWidget : IDataWindowWidget
public string DisplayedName { get; } public string DisplayedName { get; }
public string TypeSuffix { get; } public string TypeSuffix { get; }
public uint TypeSuffixColor { get; } public uint TypeSuffixColor { get; }
public Vector2 DisplayedNameSize => public Vector2 DisplayedNameSize =>
@ -319,7 +332,7 @@ internal class ServicesWidget : IDataWindowWidget
public IEnumerable<ServiceDependencyNode> Relatives => public IEnumerable<ServiceDependencyNode> Relatives =>
this.parents.Concat(this.children).Concat(this.invalidParents); this.parents.Concat(this.children).Concat(this.invalidParents);
public int Level { get; private set; } public int Level { get; private set; }
public static List<ServiceDependencyNode> CreateTree(bool includeUnloadDependencies) public static List<ServiceDependencyNode> CreateTree(bool includeUnloadDependencies)

View file

@ -13,9 +13,11 @@ internal class ObjectInstance
/// </summary> /// </summary>
/// <param name="instanceTask">Weak reference to the underlying instance.</param> /// <param name="instanceTask">Weak reference to the underlying instance.</param>
/// <param name="type">Type of the underlying instance.</param> /// <param name="type">Type of the underlying instance.</param>
public ObjectInstance(Task<WeakReference> instanceTask, Type type) /// <param name="visibility">The visibility of this instance.</param>
public ObjectInstance(Task<WeakReference> instanceTask, Type type, ObjectInstanceVisibility visibility)
{ {
this.InstanceTask = instanceTask; this.InstanceTask = instanceTask;
this.Visibility = visibility;
} }
/// <summary> /// <summary>
@ -23,4 +25,9 @@ internal class ObjectInstance
/// </summary> /// </summary>
/// <returns>The underlying instance.</returns> /// <returns>The underlying instance.</returns>
public Task<WeakReference> InstanceTask { get; } public Task<WeakReference> InstanceTask { get; }
/// <summary>
/// Gets or sets the visibility of the object instance.
/// </summary>
public ObjectInstanceVisibility Visibility { get; set; }
} }

View file

@ -0,0 +1,17 @@
namespace Dalamud.IoC.Internal;
/// <summary>
/// Enum that declares the visibility of an object instance in the service container.
/// </summary>
internal enum ObjectInstanceVisibility
{
/// <summary>
/// The object instance is only visible to other internal services.
/// </summary>
Internal,
/// <summary>
/// The object instance is visible to all services and plugins.
/// </summary>
ExposedToPlugins,
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -12,7 +13,7 @@ namespace Dalamud.IoC.Internal;
/// <summary> /// <summary>
/// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution.
/// ///
/// This is only used to resolve dependencies for plugins. /// This is only used to resolve dependencies for plugins.
/// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment.
/// </summary> /// </summary>
@ -29,13 +30,18 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// </summary> /// </summary>
public ServiceContainer() public ServiceContainer()
{ {
// Register the service container itself as a singleton.
// For all other services, this is done through the static constructor of Service{T}.
this.instances.Add(
typeof(IServiceContainer),
new(new Task<WeakReference>(() => new WeakReference(this), TaskCreationOptions.RunContinuationsAsynchronously), typeof(ServiceContainer), ObjectInstanceVisibility.Internal));
} }
/// <summary> /// <summary>
/// Gets a dictionary of all registered instances. /// Gets a dictionary of all registered instances.
/// </summary> /// </summary>
public IReadOnlyDictionary<Type, ObjectInstance> Instances => this.instances; public IReadOnlyDictionary<Type, ObjectInstance> Instances => this.instances;
/// <summary> /// <summary>
/// Gets a dictionary mapping interfaces to their implementations. /// Gets a dictionary mapping interfaces to their implementations.
/// </summary> /// </summary>
@ -45,15 +51,13 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// Register a singleton object of any type into the current IOC container. /// Register a singleton object of any type into the current IOC container.
/// </summary> /// </summary>
/// <param name="instance">The existing instance to register in the container.</param> /// <param name="instance">The existing instance to register in the container.</param>
/// <param name="visibility">The visibility of this singleton.</param>
/// <typeparam name="T">The type to register.</typeparam> /// <typeparam name="T">The type to register.</typeparam>
public void RegisterSingleton<T>(Task<T> instance) public void RegisterSingleton<T>(Task<T> instance, ObjectInstanceVisibility visibility)
{ {
if (instance == null) ArgumentNullException.ThrowIfNull(instance);
{
throw new ArgumentNullException(nameof(instance));
}
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T), visibility);
} }
/// <summary> /// <summary>
@ -69,7 +73,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
foreach (var resolvableType in resolveViaTypes) foreach (var resolvableType in resolveViaTypes)
{ {
Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???"); Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???");
Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed"); Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed");
Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
@ -81,10 +85,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType
/// Create an object. /// Create an object.
/// </summary> /// </summary>
/// <param name="objectType">The type of object to create.</param> /// <param name="objectType">The type of object to create.</param>
/// <param name="allowedVisibility">Defines which services are allowed to be directly resolved into this type.</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>
/// <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, ObjectInstanceVisibility allowedVisibility, object[] scopedObjects, IServiceScope? scope = null)
{ {
var errorStep = "constructor lookup"; var errorStep = "constructor lookup";
@ -174,7 +179,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
{ {
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
serviceType = implementingType; serviceType = implementingType;
if (serviceType.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null) if (serviceType.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null)
{ {
if (scope == null) if (scope == null)
@ -211,7 +216,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects) private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects)
{ {
// get a list of all the available types: scoped and singleton // get a list of all the available types: scoped and singleton
var types = scopedObjects var allValidServiceTypes = scopedObjects
.Select(o => o.GetType()) .Select(o => o.GetType())
.Union(this.instances.Keys) .Union(this.instances.Keys)
.ToArray(); .ToArray();
@ -224,7 +229,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
var ctors = type.GetConstructors(ctorFlags); var ctors = type.GetConstructors(ctorFlags);
foreach (var ctor in ctors) foreach (var ctor in ctors)
{ {
if (this.ValidateCtor(ctor, types)) if (this.ValidateCtor(ctor, allValidServiceTypes))
{ {
return ctor; return ctor;
} }
@ -233,28 +238,30 @@ internal class ServiceContainer : IServiceProvider, IServiceType
return null; return null;
} }
private bool ValidateCtor(ConstructorInfo ctor, Type[] types) private bool ValidateCtor(ConstructorInfo ctor, Type[] validTypes)
{ {
bool IsTypeValid(Type type) bool IsTypeValid(Type type)
{ {
var contains = types.Any(x => x.IsAssignableTo(type)); var contains = validTypes.Any(x => x.IsAssignableTo(type));
// Scoped services are created on-demand // Scoped services are created on-demand
return contains || type.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null; return contains || type.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null;
} }
var parameters = ctor.GetParameters(); var parameters = ctor.GetParameters();
foreach (var parameter in parameters) foreach (var parameter in parameters)
{ {
var valid = IsTypeValid(parameter.ParameterType); var valid = IsTypeValid(parameter.ParameterType);
// If this service is provided by an interface // If this service is provided by an interface
if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType)) if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType))
valid = IsTypeValid(implementationType); valid = IsTypeValid(implementationType);
if (!valid) if (!valid)
{ {
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); Log.Error("Ctor from {DeclaringType}: Failed to validate {TypeName}, unable to find any services that satisfy the type",
ctor.DeclaringType?.FullName ?? ctor.DeclaringType?.Name ?? "null",
parameter.ParameterType.FullName!);
return false; return false;
} }
} }

View file

@ -25,9 +25,10 @@ internal interface IServiceScope : IAsyncDisposable
/// Create an object. /// Create an object.
/// </summary> /// </summary>
/// <param name="objectType">The type of object to create.</param> /// <param name="objectType">The type of object to create.</param>
/// <param name="allowedVisibility">Defines which services are allowed to be directly resolved into this type.</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>
Task<object> CreateAsync(Type objectType, params object[] scopedObjects); Task<object> CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, 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.
@ -72,13 +73,13 @@ internal class ServiceScopeImpl : IServiceScope
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) public Task<object> CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects)
{ {
this.disposeLock.EnterReadLock(); this.disposeLock.EnterReadLock();
try try
{ {
ObjectDisposedException.ThrowIf(this.disposed, this); ObjectDisposedException.ThrowIf(this.disposed, this);
return this.container.CreateAsync(objectType, scopedObjects, this); return this.container.CreateAsync(objectType, allowedVisibility, scopedObjects, this);
} }
finally finally
{ {
@ -117,7 +118,9 @@ internal class ServiceScopeImpl : IServiceScope
objectType, objectType,
static (objectType, p) => p.Scope.container.CreateAsync( static (objectType, p) => p.Scope.container.CreateAsync(
objectType, objectType,
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), ObjectInstanceVisibility.Internal, // We are allowed to resolve internal services here since this is a private scoped object.
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray(),
p.Scope),
(Scope: this, Objects: scopedObjects)); (Scope: this, Objects: scopedObjects));
} }
finally finally

View file

@ -19,6 +19,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.Settings;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
@ -482,7 +483,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// <inheritdoc/> /// <inheritdoc/>
public async Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class => public async Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class =>
(T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), ObjectInstanceVisibility.ExposedToPlugins, this.GetPublicIocScopes(scopedObjects));
/// <inheritdoc/> /// <inheritdoc/>
public bool Inject(object instance, params object[] scopedObjects) public bool Inject(object instance, params object[] scopedObjects)

View file

@ -577,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
return await newInstanceTask.ConfigureAwait(false); return await newInstanceTask.ConfigureAwait(false);
async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface); async Task<IDalamudPlugin> Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface);
} }
private static void SetupLoaderConfig(LoaderConfig config) private static void SetupLoaderConfig(LoaderConfig config)

View file

@ -72,7 +72,7 @@ internal static class ServiceManager
/// <param name="justification">The justification for using this feature.</param> /// <param name="justification">The justification for using this feature.</param>
[InjectableType] [InjectableType]
public delegate void RegisterUnloadAfterDelegate(IEnumerable<Type> unloadAfter, string justification); public delegate void RegisterUnloadAfterDelegate(IEnumerable<Type> unloadAfter, string justification);
/// <summary> /// <summary>
/// Kinds of services. /// Kinds of services.
/// </summary> /// </summary>
@ -83,27 +83,27 @@ internal static class ServiceManager
/// Not a service. /// Not a service.
/// </summary> /// </summary>
None = 0, None = 0,
/// <summary> /// <summary>
/// Service that is loaded manually. /// Service that is loaded manually.
/// </summary> /// </summary>
ProvidedService = 1 << 0, ProvidedService = 1 << 0,
/// <summary> /// <summary>
/// Service that is loaded asynchronously while the game starts. /// Service that is loaded asynchronously while the game starts.
/// </summary> /// </summary>
EarlyLoadedService = 1 << 1, EarlyLoadedService = 1 << 1,
/// <summary> /// <summary>
/// Service that is loaded before the game starts. /// Service that is loaded before the game starts.
/// </summary> /// </summary>
BlockingEarlyLoadedService = 1 << 2, BlockingEarlyLoadedService = 1 << 2,
/// <summary> /// <summary>
/// Service that is only instantiable via scopes. /// Service that is only instantiable via scopes.
/// </summary> /// </summary>
ScopedService = 1 << 3, ScopedService = 1 << 3,
/// <summary> /// <summary>
/// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// Service that is loaded automatically when the game starts, synchronously or asynchronously.
/// </summary> /// </summary>
@ -114,7 +114,7 @@ internal static class ServiceManager
/// Gets task that gets completed when all blocking early loading services are done loading. /// Gets task that gets completed when all blocking early loading services are done loading.
/// </summary> /// </summary>
public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task;
/// <summary> /// <summary>
/// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state /// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state
/// during initialization or during regular operation. /// during initialization or during regular operation.
@ -139,10 +139,13 @@ internal static class ServiceManager
#if DEBUG #if DEBUG
lock (LoadedServices) lock (LoadedServices)
{ {
// ServiceContainer MUST be first. The static ctor of Service<T> will call Service<ServiceContainer>.Get()
// which causes a deadlock otherwise.
ProvideService(new ServiceContainer());
ProvideService(dalamud); ProvideService(dalamud);
ProvideService(fs); ProvideService(fs);
ProvideService(configuration); ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner); ProvideService(scanner);
ProvideService(localization); ProvideService(localization);
} }
@ -193,7 +196,7 @@ internal static class ServiceManager
var getAsyncTaskMap = new Dictionary<Type, Task>(); var getAsyncTaskMap = new Dictionary<Type, Task>();
var serviceContainer = Service<ServiceContainer>.Get(); var serviceContainer = Service<ServiceContainer>.Get();
foreach (var serviceType in GetConcreteServiceTypes()) foreach (var serviceType in GetConcreteServiceTypes())
{ {
var serviceKind = serviceType.GetServiceKind(); var serviceKind = serviceType.GetServiceKind();
@ -202,13 +205,13 @@ internal static class ServiceManager
// Let IoC know about the interfaces this service implements // Let IoC know about the interfaces this service implements
serviceContainer.RegisterInterfaces(serviceType); serviceContainer.RegisterInterfaces(serviceType);
// Scoped service do not go through Service<T> and are never early loaded // Scoped service do not go through Service<T> and are never early loaded
if (serviceKind.HasFlag(ServiceKind.ScopedService)) if (serviceKind.HasFlag(ServiceKind.ScopedService))
continue; continue;
var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType);
var getTask = (Task)genericWrappedServiceType var getTask = (Task)genericWrappedServiceType
.InvokeMember( .InvokeMember(
nameof(Service<IServiceType>.GetAsync), nameof(Service<IServiceType>.GetAsync),
@ -290,7 +293,7 @@ internal static class ServiceManager
var tasks = tasksEnumerable.AsReadOnlyCollection(); var tasks = tasksEnumerable.AsReadOnlyCollection();
if (tasks.Count == 0) if (tasks.Count == 0)
return; return;
// Time we wait until showing the loading dialog // Time we wait until showing the loading dialog
const int loadingDialogTimeout = 10000; const int loadingDialogTimeout = 10000;
@ -330,7 +333,7 @@ internal static class ServiceManager
hasDeps = false; hasDeps = false;
} }
} }
if (!hasDeps) if (!hasDeps)
continue; continue;
@ -437,7 +440,7 @@ internal static class ServiceManager
public static void UnloadAllServices() public static void UnloadAllServices()
{ {
UnloadCancellationTokenSource.Cancel(); UnloadCancellationTokenSource.Cancel();
var framework = Service<Framework>.GetNullable(Service<Framework>.ExceptionPropagationMode.None); var framework = Service<Framework>.GetNullable(Service<Framework>.ExceptionPropagationMode.None);
if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false })
{ {
@ -450,14 +453,14 @@ internal static class ServiceManager
var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>(); var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>();
var allToUnload = new HashSet<Type>(); var allToUnload = new HashSet<Type>();
var unloadOrder = new List<Type>(); var unloadOrder = new List<Type>();
Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
foreach (var serviceType in GetConcreteServiceTypes()) foreach (var serviceType in GetConcreteServiceTypes())
{ {
if (!serviceType.IsAssignableTo(typeof(IServiceType))) if (!serviceType.IsAssignableTo(typeof(IServiceType)))
continue; continue;
// Scoped services shall never be unloaded here. // Scoped services shall never be unloaded here.
// Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault. // Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault.
if (serviceType.GetServiceKind() == ServiceKind.ScopedService) if (serviceType.GetServiceKind() == ServiceKind.ScopedService)
@ -485,12 +488,12 @@ internal static class ServiceManager
unloadOrder.Add(serviceType); unloadOrder.Add(serviceType);
Log.Information("Queue for unload {Type}", serviceType.FullName!); Log.Information("Queue for unload {Type}", serviceType.FullName!);
} }
foreach (var serviceType in allToUnload) foreach (var serviceType in allToUnload)
{ {
UnloadService(serviceType); UnloadService(serviceType);
} }
Log.Information("==== UNLOADING ALL SERVICES ===="); Log.Information("==== UNLOADING ALL SERVICES ====");
unloadOrder.Reverse(); unloadOrder.Reverse();
@ -507,7 +510,7 @@ internal static class ServiceManager
null, null,
null); null);
} }
#if DEBUG #if DEBUG
lock (LoadedServices) lock (LoadedServices)
{ {
@ -536,17 +539,17 @@ internal static class ServiceManager
var attr = type.GetCustomAttribute<ServiceAttribute>(true)?.GetType(); var attr = type.GetCustomAttribute<ServiceAttribute>(true)?.GetType();
if (attr == null) if (attr == null)
return ServiceKind.None; return ServiceKind.None;
Debug.Assert( Debug.Assert(
type.IsAssignableTo(typeof(IServiceType)), type.IsAssignableTo(typeof(IServiceType)),
"Service did not inherit from IServiceType"); "Service did not inherit from IServiceType");
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute)))
return ServiceKind.BlockingEarlyLoadedService; return ServiceKind.BlockingEarlyLoadedService;
if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute)))
return ServiceKind.EarlyLoadedService; return ServiceKind.EarlyLoadedService;
if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) if (attr.IsAssignableTo(typeof(ScopedServiceAttribute)))
return ServiceKind.ScopedService; return ServiceKind.ScopedService;
@ -572,7 +575,7 @@ internal static class ServiceManager
var isAnyDisposable = var isAnyDisposable =
isServiceDisposable isServiceDisposable
|| serviceType.IsAssignableTo(typeof(IDisposable)) || serviceType.IsAssignableTo(typeof(IDisposable))
|| serviceType.IsAssignableTo(typeof(IAsyncDisposable)); || serviceType.IsAssignableTo(typeof(IAsyncDisposable));
if (isAnyDisposable && !isServiceDisposable) if (isAnyDisposable && !isServiceDisposable)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(

View file

@ -42,8 +42,13 @@ internal static class Service<T> where T : IServiceType
else else
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name);
if (exposeToPlugins) // We can't use the service container to register itself. It does so in its constructor.
Service<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task); if (typeof(T) != typeof(ServiceContainer))
{
Service<ServiceContainer>.Get().RegisterSingleton(
instanceTcs.Task,
exposeToPlugins ? ObjectInstanceVisibility.ExposedToPlugins : ObjectInstanceVisibility.Internal);
}
} }
/// <summary> /// <summary>
@ -163,7 +168,7 @@ internal static class Service<T> where T : IServiceType
return dependencyServices; return dependencyServices;
var res = new List<Type>(); var res = new List<Type>();
ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name);
var ctor = GetServiceConstructor(); var ctor = GetServiceConstructor();
@ -174,12 +179,12 @@ internal static class Service<T> where T : IServiceType
.Select(x => x.ParameterType) .Select(x => x.ParameterType)
.Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None));
} }
res.AddRange(typeof(T) res.AddRange(typeof(T)
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null) .Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null)
.Select(x => x.FieldType)); .Select(x => x.FieldType));
res.AddRange(typeof(T) res.AddRange(typeof(T)
.GetCustomAttributes() .GetCustomAttributes()
.OfType<InherentDependencyAttribute>() .OfType<InherentDependencyAttribute>()
@ -351,7 +356,7 @@ internal static class Service<T> where T : IServiceType
var ctor = GetServiceConstructor(); var ctor = GetServiceConstructor();
if (ctor == null) if (ctor == null)
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects)
.ConfigureAwait(false); .ConfigureAwait(false);
using (Timings.Start($"{typeof(T).Name} Construct")) using (Timings.Start($"{typeof(T).Name} Construct"))
@ -387,7 +392,7 @@ internal static class Service<T> where T : IServiceType
argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType));
continue; continue;
} }
argTask = (Task<object>)typeof(Service<>) argTask = (Task<object>)typeof(Service<>)
.MakeGenericType(argType) .MakeGenericType(argType)
.InvokeMember( .InvokeMember(