diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 16bed5696..f2c65723c 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -492,7 +492,7 @@ public sealed class Framework : IDisposable, IServiceType Log.Information("Framework::Destroy!"); Service.Get().Unload(); this.RunPendingTickTasks(); - ServiceManager.UnloadAllServices(); + ServiceManager.WaitForServiceUnload(); Log.Information("Framework::Destroy OK!"); return this.destroyHook.OriginalDisposeSafe(framework); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 68b239c9d..164aa083f 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -32,14 +32,14 @@ namespace Dalamud.Plugin.Internal; /// /// Class responsible for loading and unloading plugins. +/// NOTE: ALL plugin exposed services are marked as dependencies for PluginManager in Service{T}. /// [ServiceManager.EarlyLoadedService] #pragma warning disable SA1015 -// DalamudTextureWrap registers textures to dispose with IM -[InherentDependency] -// DalamudPluginInterface asks to remove chat link handlers -[InherentDependency] +// DalamudTextureWrap registers textures to dispose with IM +[InherentDependency] + #pragma warning restore SA1015 internal partial class PluginManager : IDisposable, IServiceType { diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index d49fe5aa6..eb4c2eb51 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; @@ -19,6 +21,13 @@ namespace Dalamud; /// internal static class ServiceManager { + /** + * TODO: + * - Unify dependency walking code(load/unload + * - Visualize/output .dot or imgui thing + */ + + /// /// Static log facility for Service{T}, to avoid duplicate instances for different types. /// @@ -28,6 +37,40 @@ internal static class ServiceManager private static readonly List LoadedServices = new(); + private static ManualResetEvent unloadResetEvent = new(false); + + /// + /// Kinds of services. + /// + [Flags] + public enum ServiceKind + { + /// + /// Not a service. + /// + None = 0, + + /// + /// Regular service. + /// + ManualService = 1 << 0, + + /// + /// Service that is loaded asynchronously while the game starts. + /// + EarlyLoadedService = 1 << 1, + + /// + /// Service that is loaded before the game starts. + /// + BlockingEarlyLoadedService = 1 << 2, + + /// + /// Service that is loaded automatically when the game starts, synchronously or asynchronously. + /// + AutoLoadService = EarlyLoadedService | BlockingEarlyLoadedService, + } + /// /// Gets task that gets completed when all blocking early loading services are done loading. /// @@ -89,9 +132,11 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - var attr = serviceType.GetCustomAttribute(true)?.GetType(); - if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true) + var serviceKind = serviceType.GetServiceKind(); + if (serviceKind == ServiceKind.None) continue; + + Debug.Assert(!serviceKind.HasFlag(ServiceKind.ManualService), "Regular services should never end up here"); var getTask = (Task)typeof(Service<>) .MakeGenericType(serviceType) @@ -102,9 +147,9 @@ internal static class ServiceManager null, null); - if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) + if (serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService)) { - getAsyncTaskMap[serviceType] = getTask; + getAsyncTaskMap[typeof(Service<>).MakeGenericType(serviceType)] = getTask; blockingEarlyLoadingServices.Add(serviceType); } else @@ -148,8 +193,24 @@ internal static class ServiceManager { foreach (var serviceType in servicesToLoad) { - if (dependencyServicesMap[serviceType].Any( - x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false)) + var hasDeps = true; + 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; tasks.Add((Task)typeof(Service<>) @@ -227,23 +288,108 @@ internal static class ServiceManager return; } - lock (LoadedServices) - { - while (LoadedServices.Any()) - { - var serviceType = LoadedServices.Last(); - LoadedServices.RemoveAt(LoadedServices.Count - 1); + unloadResetEvent.Reset(); - typeof(Service<>) - .MakeGenericType(serviceType) + var dependencyServicesMap = new Dictionary>(); + var allToUnload = new HashSet(); + var unloadOrder = new List(); + + Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); + + foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) + { + if (!serviceType.IsAssignableTo(typeof(IServiceType))) + continue; + + dependencyServicesMap[serviceType] = + ((List)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( "Unset", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null); - } } + + lock (LoadedServices) + { + LoadedServices.Clear(); + } + + unloadResetEvent.Set(); + } + + /// + /// Wait until all services have been unloaded. + /// + public static void WaitForServiceUnload() + { + unloadResetEvent.WaitOne(); + } + + /// + /// Get the service type of this type. + /// + /// The type to check. + /// The type of service this type is. + public static ServiceKind GetServiceKind(this Type type) + { + var attr = type.GetCustomAttribute(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; } /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 8f805b5af..0e7d75369 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -121,18 +122,43 @@ internal static class Service where T : IServiceType public static List GetDependencyServices() { var res = new List(); - res.AddRange(GetServiceConstructor() - .GetParameters() - .Select(x => x.ParameterType)); + + var ctor = GetServiceConstructor(); + if (ctor != null) + { + res.AddRange(ctor + .GetParameters() + .Select(x => x.ParameterType)); + } + res.AddRange(typeof(T) - .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Select(x => x.FieldType) - .Where(x => x.GetCustomAttribute(true) != null)); + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Select(x => x.FieldType) + .Where(x => x.GetCustomAttribute(true) != null)); + res.AddRange(typeof(T) .GetCustomAttributes() .OfType() .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(true); + if (attr == null) + continue; + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); + res.Add(serviceType); + } + } + return res .Distinct() .Select(x => typeof(Service<>).MakeGenericType(x)) @@ -228,19 +254,22 @@ internal static class Service where T : IServiceType .GetValue(task); } - private static ConstructorInfo GetServiceConstructor() + private static ConstructorInfo? GetServiceConstructor() { const BindingFlags ctorBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding; return typeof(T) .GetConstructors(ctorBindingFlags) - .Single(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); + .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); } private static async Task ConstructObject() { var ctor = GetServiceConstructor(); + if (ctor == null) + throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); + var args = await Task.WhenAll( ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); using (Timings.Start($"{typeof(T).Name} Construct"))