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