using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Utility.Timing; using JetBrains.Annotations; namespace Dalamud { /// /// Class to initialize Service<T>s. /// internal static class ServiceManager { /// /// Static log facility for Service{T}, to avoid duplicate instances for different types. /// public static readonly ModuleLog Log = new("SVC"); private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static readonly List LoadedServices = new(); /// /// 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 . public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration) { // Initialize the process information. var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); if (!cacheDir.Exists) cacheDir.Create(); lock (LoadedServices) { Service.Provide(dalamud); LoadedServices.Add(typeof(Dalamud)); Service.Provide(startInfo); LoadedServices.Add(typeof(DalamudStartInfo)); Service.Provide(configuration); LoadedServices.Add(typeof(DalamudConfiguration)); Service.Provide(new ServiceContainer()); LoadedServices.Add(typeof(ServiceContainer)); Service.Provide( new SigScanner( true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); LoadedServices.Add(typeof(SigScanner)); } using (Timings.Start("CS Resolver Init")) { FFXIVClientStructs.Resolver.InitializeParallel( new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); } } /// /// 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 dependencyServicesMap = new Dictionary>(); var getAsyncTaskMap = new Dictionary(); foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { var attr = serviceType.GetCustomAttribute(true)?.GetType(); if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true) continue; var getTask = (Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( "GetAsync", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) { getAsyncTaskMap[serviceType] = getTask; blockingEarlyLoadingServices.Add(serviceType); } else { earlyLoadingServices.Add(serviceType); } dependencyServicesMap[serviceType] = (List)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( "GetDependencyServices", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); } _ = Task.Run(async () => { try { await Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); BlockingServicesLoadedTaskCompletionSource.SetResult(); Timings.Event("BlockingServices Initialized"); } catch (Exception e) { BlockingServicesLoadedTaskCompletionSource.SetException(e); } }).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) { if (dependencyServicesMap[serviceType].Any( x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false)) continue; tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( "StartLoader", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null)); servicesToLoad.Remove(serviceType); tasks.Add(tasks.Last().ContinueWith(task => { if (task.IsFaulted) return; lock (LoadedServices) { LoadedServices.Add(serviceType); } })); } if (!tasks.Any()) throw new InvalidOperationException("Unresolvable dependency cycle detected"); 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; } lock (LoadedServices) { while (LoadedServices.Any()) { var serviceType = LoadedServices.Last(); LoadedServices.RemoveAt(LoadedServices.Count - 1); typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( "Unset", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null); } } } /// /// 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 class Service : Attribute { } /// /// Indicates that the class is a service, and will be instantiated automatically on startup. /// [AttributeUsage(AttributeTargets.Class)] public class EarlyLoadedService : Service { } /// /// 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 BlockingEarlyLoadedService : EarlyLoadedService { } /// /// Indicates that the method should be called when the services given in the constructor are ready. /// [AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public class CallWhenServicesReady : Attribute { } } }