using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Storage; using Dalamud.Utility; using Dalamud.Utility.Timing; using JetBrains.Annotations; namespace Dalamud; // TODO: // - Unify dependency walking code(load/unload) // - Visualize/output .dot or imgui thing /// /// Class to initialize . /// internal static class ServiceManager { /// /// Static log facility for Service{T}, to avoid duplicate instances for different types. /// public static readonly ModuleLog Log = new("SVC"); #if DEBUG /// /// Marks which service constructor the current thread's in. For use from only. /// internal static readonly ThreadLocal CurrentConstructorServiceType = new(); [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")] private static readonly List LoadedServices = new(); #endif private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); /// /// Delegate for registering startup blocker task.
/// Do not use this delegate outside the constructor. ///
/// The blocker task. /// The justification for using this feature. [InjectableType] public delegate void RegisterStartupBlockerDelegate(Task t, string justification); /// /// Delegate for registering services that should be unloaded before self.
/// Intended for use with . If you think you need to use this outside /// of that, consider having a discussion first.
/// Do not use this delegate outside the constructor. ///
/// Services that should be unloaded first. /// The justification for using this feature. [InjectableType] public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); /// /// Kinds of services. /// [Flags] public enum ServiceKind { /// /// Not a service. /// None = 0, /// /// Service that is loaded manually. /// ProvidedService = 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 only instantiable via scopes. /// ScopedService = 1 << 3, /// /// 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. /// public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; /// /// Initializes Provided Services and FFXIVClientStructs. /// /// Instance of . /// Instance of . /// Instance of . /// Instance of . public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) { #if DEBUG lock (LoadedServices) { ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); } return; void ProvideService(T service) where T : IServiceType { Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); Service.Provide(service); LoadedServices.Add(typeof(T)); } #else ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); return; void ProvideService(T service) where T : IServiceType => Service.Provide(service); #endif } /// /// Gets the concrete types of services, i.e. the non-abstract non-interface types. /// /// The enumerable of service types, that may be enumerated only once per call. public static IEnumerable GetConcreteServiceTypes() => Assembly.GetExecutingAssembly() .GetTypes() .Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract); /// /// Kicks off construction of services that can handle early loading. /// /// Task for initializing all services. public static async Task InitializeEarlyLoadableServices() { using var serviceInitializeTimings = Timings.Start("Services Init"); var earlyLoadingServices = new HashSet(); var blockingEarlyLoadingServices = new HashSet(); var providedServices = new HashSet(); var dependencyServicesMap = new Dictionary>(); var getAsyncTaskMap = new Dictionary(); var serviceContainer = Service.Get(); foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); CheckServiceTypeContracts(serviceType); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); // Scoped service do not go through Service and are never early loaded if (serviceKind.HasFlag(ServiceKind.ScopedService)) continue; var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); var getTask = (Task)genericWrappedServiceType .InvokeMember( nameof(Service.GetAsync), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); getAsyncTaskMap[serviceType] = getTask; // We don't actually need to load provided services, something else does if (serviceKind.HasFlag(ServiceKind.ProvidedService)) { providedServices.Add(serviceType); continue; } Debug.Assert( serviceKind.HasFlag(ServiceKind.EarlyLoadedService) || serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService), "At this point, service must be either early loaded or blocking early loaded"); if (serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService)) { blockingEarlyLoadingServices.Add(serviceType); } else { earlyLoadingServices.Add(serviceType); } var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false) .Select(x => typeof(Service<>).MakeGenericType(x)) .ToList(); } var blockerTasks = new List(); _ = Task.Run(async () => { try { // Wait for all blocking constructors to complete first. await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); // All the BlockingEarlyLoadedService constructors have been run, // and blockerTasks now will not change. Now wait for them. // Note that ServiceManager.CallWhenServicesReady does not get to register a blocker. await WaitWithTimeoutConsent(blockerTasks); BlockingServicesLoadedTaskCompletionSource.SetResult(); Timings.Event("BlockingServices Initialized"); } catch (Exception e) { BlockingServicesLoadedTaskCompletionSource.SetException(e); } return; async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable) { var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; var aggregatedTask = Task.WhenAll(tasks); while (await Task.WhenAny(aggregatedTask, Task.Delay(120000)) != aggregatedTask) { if (NativeFunctions.MessageBoxW( IntPtr.Zero, "Dalamud is taking a long time to load. Would you like to continue without Dalamud?\n" + "This can be caused by a faulty plugin, or a bug in Dalamud.", "Dalamud", NativeFunctions.MessageBoxType.IconWarning | NativeFunctions.MessageBoxType.YesNo) == 6) { throw new TimeoutException( "Failed to load services in the given time limit, " + "and the user chose to continue without Dalamud."); } } } }).ConfigureAwait(false); var tasks = new List(); try { var servicesToLoad = new HashSet(); servicesToLoad.UnionWith(earlyLoadingServices); servicesToLoad.UnionWith(blockingEarlyLoadingServices); while (servicesToLoad.Any()) { foreach (var serviceType in servicesToLoad) { var hasDeps = true; foreach (var dependency in dependencyServicesMap[serviceType]) { var depUnderlyingServiceType = dependency.GetGenericArguments().First(); var depResolveTask = getAsyncTaskMap.GetValueOrDefault(depUnderlyingServiceType); if (depResolveTask == null) { Log.Error("{Type}: {Dependency} has no resolver task", serviceType.FullName!, dependency.FullName!); Debug.Assert(false, $"No resolver for dependent service {depUnderlyingServiceType.FullName}"); } else if (depResolveTask is { IsCompleted: false }) { hasDeps = false; } } if (!hasDeps) continue; // This object will be used in a task. Each task must receive a new object. var startLoaderArgs = new List(); if (serviceType.GetCustomAttribute() is not null) { startLoaderArgs.Add( new RegisterStartupBlockerDelegate( (task, justification) => { #if DEBUG if (CurrentConstructorServiceType.Value != serviceType) throw new InvalidOperationException("Forbidden."); #endif blockerTasks.Add(task); // No need to store the justification; the fact that the reason is specified is good enough. _ = justification; })); } tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( nameof(Service.StartLoader), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, new object[] { startLoaderArgs })); servicesToLoad.Remove(serviceType); #if DEBUG tasks.Add(tasks.Last().ContinueWith(task => { if (task.IsFaulted) return; lock (LoadedServices) { LoadedServices.Add(serviceType); } })); #endif } if (!tasks.Any()) { // No more services we can start loading for now. // Either we're waiting for provided services, or there's a dependency cycle. providedServices.RemoveWhere(x => getAsyncTaskMap[x].IsCompleted); if (providedServices.Any()) await Task.WhenAny(providedServices.Select(x => getAsyncTaskMap[x])); else throw new InvalidOperationException("Unresolvable dependency cycle detected"); continue; } if (servicesToLoad.Any()) { await Task.WhenAny(tasks); var faultedTasks = tasks.Where(x => x.IsFaulted).Select(x => (Exception)x.Exception!).ToArray(); if (faultedTasks.Any()) throw new AggregateException(faultedTasks); } else { await Task.WhenAll(tasks); } tasks.RemoveAll(x => x.IsCompleted); } } catch (Exception e) { Log.Error(e, "Failed resolving services"); try { BlockingServicesLoadedTaskCompletionSource.SetException(e); } catch (Exception) { // don't care, as this means task result/exception has already been set } while (tasks.Any()) { await Task.WhenAny(tasks); tasks.RemoveAll(x => x.IsCompleted); } UnloadAllServices(); throw; } } /// /// Unloads all services, in the reverse order of load. /// public static void UnloadAllServices() { var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) { framework.RunOnFrameworkThread(UnloadAllServices).Wait(); return; } unloadResetEvent.Reset(); var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; // 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. if (serviceType.GetServiceKind() == ServiceKind.ScopedService) continue; Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!); var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true); 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); } #if DEBUG lock (LoadedServices) { LoadedServices.Clear(); } #endif 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(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; return ServiceKind.ProvidedService; } /// Validate service type contracts, and throws exceptions accordingly. /// An instance of that is supposed to be a service type. /// Does nothing on non-debug builds. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CheckServiceTypeContracts(Type serviceType) { #if DEBUG try { if (!serviceType.IsAssignableTo(typeof(IServiceType))) throw new InvalidOperationException($"Non-{nameof(IServiceType)} passed."); if (serviceType.GetServiceKind() == ServiceKind.None) throw new InvalidOperationException("Service type is not specified."); var isServiceDisposable = serviceType.IsAssignableTo(typeof(IInternalDisposableService)); var isAnyDisposable = isServiceDisposable || serviceType.IsAssignableTo(typeof(IDisposable)) || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); if (isAnyDisposable && !isServiceDisposable) { throw new InvalidOperationException( $"A service must be an {nameof(IInternalDisposableService)} without specifying " + $"{nameof(IDisposable)} nor {nameof(IAsyncDisposable)} if it is purely meant to be a service, " + $"or an {nameof(IPublicDisposableService)} if it also is allowed to be constructed not as a " + $"service to be used elsewhere and has to offer {nameof(IDisposable)} or " + $"{nameof(IAsyncDisposable)}. See {nameof(ReliableFileStorage)} for an example of " + $"{nameof(IPublicDisposableService)}."); } } catch (Exception e) { throw new InvalidOperationException($"{serviceType.Name}: {e.Message}"); } #endif } /// /// Indicates that this constructor will be called for early initialization. /// [AttributeUsage(AttributeTargets.Constructor)] [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public class ServiceConstructor : Attribute { } /// /// Indicates that the field is a service that should be loaded before constructing the class. /// [AttributeUsage(AttributeTargets.Field)] public class ServiceDependency : Attribute { } /// /// Indicates that the class is a service. /// [AttributeUsage(AttributeTargets.Class)] public abstract class ServiceAttribute : Attribute { /// /// Initializes a new instance of the class. /// /// The kind of the service. protected ServiceAttribute(ServiceKind kind) => this.Kind = kind; /// /// Gets the kind of the service. /// public ServiceKind Kind { get; } } /// /// Indicates that the class is a service, that is provided by some other source. /// [AttributeUsage(AttributeTargets.Class)] public class ProvidedServiceAttribute : ServiceAttribute { /// /// Initializes a new instance of the class. /// public ProvidedServiceAttribute() : base(ServiceKind.ProvidedService) { } } /// /// Indicates that the class is a service, and will be instantiated automatically on startup. /// [AttributeUsage(AttributeTargets.Class)] public class EarlyLoadedServiceAttribute : ServiceAttribute { /// /// Initializes a new instance of the class. /// public EarlyLoadedServiceAttribute() : this(ServiceKind.EarlyLoadedService) { } /// /// Initializes a new instance of the class. /// /// The service kind. protected EarlyLoadedServiceAttribute(ServiceKind kind) : base(kind) { } } /// /// Indicates that the class is a service, and will be instantiated automatically on startup, /// blocking game main thread until it completes. /// [AttributeUsage(AttributeTargets.Class)] public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute { /// /// Initializes a new instance of the class. /// public BlockingEarlyLoadedServiceAttribute() : base(ServiceKind.BlockingEarlyLoadedService) { } } /// /// Indicates that the class is a service that will be created specifically for a /// service scope, and that it cannot be created outside of a scope. /// [AttributeUsage(AttributeTargets.Class)] public class ScopedServiceAttribute : ServiceAttribute { /// /// Initializes a new instance of the class. /// public ScopedServiceAttribute() : base(ServiceKind.ScopedService) { } } /// /// Indicates that the method should be called when the services given in the marked method's parameters are ready. /// This will be executed immediately after the constructor has run, if all services specified as its parameters /// are already ready, or no parameter is given. /// [AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public class CallWhenServicesReady : Attribute { /// /// Initializes a new instance of the class. /// /// Specify the reason here. public CallWhenServicesReady(string justification) { // No need to store the justification; the fact that the reason is specified is good enough. _ = justification; } } /// /// Indicates that something is a candidate for being considered as an injected parameter for constructors. /// [AttributeUsage( AttributeTargets.Delegate | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface)] public class InjectableTypeAttribute : Attribute { } }