Merge pull request #1151 from goaaats/unload_order

Walk dependencies to unload services
This commit is contained in:
goat 2023-03-09 12:11:03 +01:00 committed by GitHub
commit 01d7f7a09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 28 deletions

View file

@ -492,7 +492,7 @@ public sealed class Framework : IDisposable, IServiceType
Log.Information("Framework::Destroy!"); Log.Information("Framework::Destroy!");
Service<Dalamud>.Get().Unload(); Service<Dalamud>.Get().Unload();
this.RunPendingTickTasks(); this.RunPendingTickTasks();
ServiceManager.UnloadAllServices(); ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!"); Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework); return this.destroyHook.OriginalDisposeSafe(framework);

View file

@ -32,14 +32,14 @@ namespace Dalamud.Plugin.Internal;
/// <summary> /// <summary>
/// Class responsible for loading and unloading plugins. /// Class responsible for loading and unloading plugins.
/// NOTE: ALL plugin exposed services are marked as dependencies for PluginManager in Service{T}.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
#pragma warning disable SA1015 #pragma warning disable SA1015
// DalamudTextureWrap registers textures to dispose with IM
[InherentDependency<InterfaceManager.InterfaceManagerWithScene>]
// DalamudPluginInterface asks to remove chat link handlers // DalamudTextureWrap registers textures to dispose with IM
[InherentDependency<ChatGui>] [InherentDependency<InterfaceManager>]
#pragma warning restore SA1015 #pragma warning restore SA1015
internal partial class PluginManager : IDisposable, IServiceType internal partial class PluginManager : IDisposable, IServiceType
{ {

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
@ -19,6 +21,13 @@ namespace Dalamud;
/// </summary> /// </summary>
internal static class ServiceManager internal static class ServiceManager
{ {
/**
* TODO:
* - Unify dependency walking code(load/unload
* - Visualize/output .dot or imgui thing
*/
/// <summary> /// <summary>
/// Static log facility for Service{T}, to avoid duplicate instances for different types. /// Static log facility for Service{T}, to avoid duplicate instances for different types.
/// </summary> /// </summary>
@ -28,6 +37,40 @@ internal static class ServiceManager
private static readonly List<Type> LoadedServices = new(); private static readonly List<Type> LoadedServices = new();
private static ManualResetEvent unloadResetEvent = new(false);
/// <summary>
/// Kinds of services.
/// </summary>
[Flags]
public enum ServiceKind
{
/// <summary>
/// Not a service.
/// </summary>
None = 0,
/// <summary>
/// Regular service.
/// </summary>
ManualService = 1 << 0,
/// <summary>
/// Service that is loaded asynchronously while the game starts.
/// </summary>
EarlyLoadedService = 1 << 1,
/// <summary>
/// Service that is loaded before the game starts.
/// </summary>
BlockingEarlyLoadedService = 1 << 2,
/// <summary>
/// Service that is loaded automatically when the game starts, synchronously or asynchronously.
/// </summary>
AutoLoadService = EarlyLoadedService | BlockingEarlyLoadedService,
}
/// <summary> /// <summary>
/// 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>
@ -89,9 +132,11 @@ internal static class ServiceManager
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
{ {
var attr = serviceType.GetCustomAttribute<Service>(true)?.GetType(); var serviceKind = serviceType.GetServiceKind();
if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true) if (serviceKind == ServiceKind.None)
continue; continue;
Debug.Assert(!serviceKind.HasFlag(ServiceKind.ManualService), "Regular services should never end up here");
var getTask = (Task)typeof(Service<>) var getTask = (Task)typeof(Service<>)
.MakeGenericType(serviceType) .MakeGenericType(serviceType)
@ -102,9 +147,9 @@ internal static class ServiceManager
null, null,
null); null);
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) if (serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService))
{ {
getAsyncTaskMap[serviceType] = getTask; getAsyncTaskMap[typeof(Service<>).MakeGenericType(serviceType)] = getTask;
blockingEarlyLoadingServices.Add(serviceType); blockingEarlyLoadingServices.Add(serviceType);
} }
else else
@ -148,8 +193,24 @@ internal static class ServiceManager
{ {
foreach (var serviceType in servicesToLoad) foreach (var serviceType in servicesToLoad)
{ {
if (dependencyServicesMap[serviceType].Any( var hasDeps = true;
x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false)) foreach (var dependency in dependencyServicesMap[serviceType])
{
var depServiceKind = dependency.GetServiceKind();
var depResolveTask = getAsyncTaskMap.GetValueOrDefault(dependency);
if (depResolveTask == null && (depServiceKind.HasFlag(ServiceKind.EarlyLoadedService) || depServiceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService)))
{
Log.Error("{Type}: {Dependency} has no resolver task, is it early loaded or blocking early loaded?", serviceType.FullName!, dependency.FullName!);
Debug.Assert(false, $"No resolver for dependent service {dependency.FullName}");
}
else if (depResolveTask is { IsCompleted: false })
{
hasDeps = false;
}
}
if (!hasDeps)
continue; continue;
tasks.Add((Task)typeof(Service<>) tasks.Add((Task)typeof(Service<>)
@ -227,23 +288,108 @@ internal static class ServiceManager
return; return;
} }
lock (LoadedServices) unloadResetEvent.Reset();
{
while (LoadedServices.Any())
{
var serviceType = LoadedServices.Last();
LoadedServices.RemoveAt(LoadedServices.Count - 1);
typeof(Service<>) var dependencyServicesMap = new Dictionary<Type, List<Type>>();
.MakeGenericType(serviceType) var allToUnload = new HashSet<Type>();
var unloadOrder = new List<Type>();
Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
{
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
continue;
dependencyServicesMap[serviceType] =
((List<Type>)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null))!
.Select(x => x.GetGenericArguments()[0]).ToList();
allToUnload.Add(serviceType);
}
void UnloadService(Type serviceType)
{
if (unloadOrder.Contains(serviceType))
return;
var deps = dependencyServicesMap[serviceType];
foreach (var dep in deps)
{
UnloadService(dep);
}
unloadOrder.Add(serviceType);
Log.Information("Queue for unload {Type}", serviceType.FullName!);
}
foreach (var serviceType in allToUnload)
{
UnloadService(serviceType);
}
Log.Information("==== UNLOADING ALL SERVICES ====");
unloadOrder.Reverse();
foreach (var type in unloadOrder)
{
Log.Verbose("Unload {Type}", type.FullName!);
typeof(Service<>)
.MakeGenericType(type)
.InvokeMember( .InvokeMember(
"Unset", "Unset",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
null, null,
null, null,
null); null);
}
} }
lock (LoadedServices)
{
LoadedServices.Clear();
}
unloadResetEvent.Set();
}
/// <summary>
/// Wait until all services have been unloaded.
/// </summary>
public static void WaitForServiceUnload()
{
unloadResetEvent.WaitOne();
}
/// <summary>
/// Get the service type of this type.
/// </summary>
/// <param name="type">The type to check.</param>
/// <returns>The type of service this type is.</returns>
public static ServiceKind GetServiceKind(this Type type)
{
var attr = type.GetCustomAttribute<Service>(true)?.GetType();
if (attr == null)
return ServiceKind.None;
Debug.Assert(
type.IsAssignableTo(typeof(IServiceType)),
"Service did not inherit from IServiceType");
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService)))
return ServiceKind.BlockingEarlyLoadedService;
if (attr.IsAssignableTo(typeof(EarlyLoadedService)))
return ServiceKind.EarlyLoadedService;
return ServiceKind.ManualService;
} }
/// <summary> /// <summary>

View file

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Utility.Timing; using Dalamud.Utility.Timing;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -121,18 +122,43 @@ internal static class Service<T> where T : IServiceType
public static List<Type> GetDependencyServices() public static List<Type> GetDependencyServices()
{ {
var res = new List<Type>(); var res = new List<Type>();
res.AddRange(GetServiceConstructor()
.GetParameters() var ctor = GetServiceConstructor();
.Select(x => x.ParameterType)); if (ctor != null)
{
res.AddRange(ctor
.GetParameters()
.Select(x => x.ParameterType));
}
res.AddRange(typeof(T) res.AddRange(typeof(T)
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Select(x => x.FieldType) .Select(x => x.FieldType)
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null)); .Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null));
res.AddRange(typeof(T) res.AddRange(typeof(T)
.GetCustomAttributes() .GetCustomAttributes()
.OfType<InherentDependencyAttribute>() .OfType<InherentDependencyAttribute>()
.Select(x => x.GetType().GetGenericArguments().First())); .Select(x => x.GetType().GetGenericArguments().First()));
// HACK: PluginManager needs to depend on ALL plugin exposed services
if (typeof(T) == typeof(PluginManager))
{
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
{
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
continue;
var attr = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
if (attr == null)
continue;
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
res.Add(serviceType);
}
}
return res return res
.Distinct() .Distinct()
.Select(x => typeof(Service<>).MakeGenericType(x)) .Select(x => typeof(Service<>).MakeGenericType(x))
@ -228,19 +254,22 @@ internal static class Service<T> where T : IServiceType
.GetValue(task); .GetValue(task);
} }
private static ConstructorInfo GetServiceConstructor() private static ConstructorInfo? GetServiceConstructor()
{ {
const BindingFlags ctorBindingFlags = const BindingFlags ctorBindingFlags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding; BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding;
return typeof(T) return typeof(T)
.GetConstructors(ctorBindingFlags) .GetConstructors(ctorBindingFlags)
.Single(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
} }
private static async Task<T> ConstructObject() private static async Task<T> ConstructObject()
{ {
var ctor = GetServiceConstructor(); var ctor = GetServiceConstructor();
if (ctor == null)
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
var args = await Task.WhenAll( var args = await Task.WhenAll(
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
using (Timings.Start($"{typeof(T).Name} Construct")) using (Timings.Start($"{typeof(T).Name} Construct"))