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
{
}
}
}