mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-21 15:27:43 +01:00
Add "loading dialog" for service init, unify blocking logic (#1779)
* wip * hacky fix for overlapping event text in profiler * move IsResumeGameAfterPluginLoad logic to PluginManager * fix some warnings * handle exceptions properly * remove ability to cancel, rename button to "hide" instead * undo Dalamud.Service refactor for now * warnings * add explainer, show which plugins are still loading * add some text if loading takes more than 3 minutes * undo wrong CS merge
This commit is contained in:
parent
93adea0ac9
commit
448b0d16ea
294 changed files with 560 additions and 506 deletions
25
Dalamud/Service/IServiceType.cs
Normal file
25
Dalamud/Service/IServiceType.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace Dalamud;
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for service types.
|
||||
/// </summary>
|
||||
public interface IServiceType
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary><see cref="IDisposable"/>, but for <see cref="IServiceType"/>.</summary>
|
||||
/// <remarks>Use this to prevent services from accidentally being disposed by plugins or <c>using</c> clauses.</remarks>
|
||||
internal interface IInternalDisposableService : IServiceType
|
||||
{
|
||||
/// <summary>Disposes the service.</summary>
|
||||
void DisposeService();
|
||||
}
|
||||
|
||||
/// <summary>An <see cref="IInternalDisposableService"/> which happens to be public and needs to expose
|
||||
/// <see cref="IDisposable.Dispose"/>.</summary>
|
||||
internal interface IPublicDisposableService : IInternalDisposableService, IDisposable
|
||||
{
|
||||
/// <summary>Marks that only <see cref="IInternalDisposableService.DisposeService"/> should respond,
|
||||
/// while suppressing <see cref="IDisposable.Dispose"/>.</summary>
|
||||
void MarkDisposeOnlyFromService();
|
||||
}
|
||||
252
Dalamud/Service/LoadingDialog.cs
Normal file
252
Dalamud/Service/LoadingDialog.cs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Utility;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Dalamud;
|
||||
|
||||
/// <summary>
|
||||
/// Class providing an early-loading dialog.
|
||||
/// </summary>
|
||||
internal class LoadingDialog
|
||||
{
|
||||
// TODO: We can't localize any of what's in here at the moment, because Localization is an EarlyLoadedService.
|
||||
|
||||
private static int wasGloballyHidden = 0;
|
||||
|
||||
private Thread? thread;
|
||||
private TaskDialogButton? inProgressHideButton;
|
||||
private TaskDialogPage? page;
|
||||
private bool canHide;
|
||||
private State currentState = State.LoadingDalamud;
|
||||
private DateTime firstShowTime;
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing the state of the dialog.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
/// <summary>
|
||||
/// Show a message stating that Dalamud is currently loading.
|
||||
/// </summary>
|
||||
LoadingDalamud,
|
||||
|
||||
/// <summary>
|
||||
/// Show a message stating that Dalamud is currently loading plugins.
|
||||
/// </summary>
|
||||
LoadingPlugins,
|
||||
|
||||
/// <summary>
|
||||
/// Show a message stating that Dalamud is currently updating plugins.
|
||||
/// </summary>
|
||||
AutoUpdatePlugins,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current state of the dialog.
|
||||
/// </summary>
|
||||
public State CurrentState
|
||||
{
|
||||
get => this.currentState;
|
||||
set
|
||||
{
|
||||
this.currentState = value;
|
||||
this.UpdatePage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the dialog can be hidden by the user.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if called before the dialog has been created.</exception>
|
||||
public bool CanHide
|
||||
{
|
||||
get => this.canHide;
|
||||
set
|
||||
{
|
||||
this.canHide = value;
|
||||
this.UpdatePage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the dialog.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
if (Volatile.Read(ref wasGloballyHidden) == 1)
|
||||
return;
|
||||
|
||||
if (this.thread?.IsAlive == true)
|
||||
return;
|
||||
|
||||
this.thread = new Thread(this.ThreadStart)
|
||||
{
|
||||
Name = "Dalamud Loading Dialog",
|
||||
};
|
||||
this.thread.SetApartmentState(ApartmentState.STA);
|
||||
this.thread.Start();
|
||||
|
||||
this.firstShowTime = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide the dialog.
|
||||
/// </summary>
|
||||
public void HideAndJoin()
|
||||
{
|
||||
if (this.thread == null || !this.thread.IsAlive)
|
||||
return;
|
||||
|
||||
this.inProgressHideButton?.PerformClick();
|
||||
this.thread!.Join();
|
||||
}
|
||||
|
||||
private void UpdatePage()
|
||||
{
|
||||
if (this.page == null)
|
||||
return;
|
||||
|
||||
this.page.Heading = this.currentState switch
|
||||
{
|
||||
State.LoadingDalamud => "Dalamud is loading...",
|
||||
State.LoadingPlugins => "Waiting for plugins to load...",
|
||||
State.AutoUpdatePlugins => "Updating plugins...",
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
|
||||
var context = string.Empty;
|
||||
if (this.currentState == State.LoadingPlugins)
|
||||
{
|
||||
context = "\nPreparing...";
|
||||
|
||||
var tracker = Service<PluginManager>.GetNullable()?.StartupLoadTracking;
|
||||
if (tracker != null)
|
||||
{
|
||||
var nameString = tracker.GetPendingInternalNames()
|
||||
.Select(x => tracker.GetPublicName(x))
|
||||
.Where(x => x != null)
|
||||
.Aggregate(string.Empty, (acc, x) => acc + x + ", ");
|
||||
|
||||
if (!nameString.IsNullOrEmpty())
|
||||
context = $"\nWaiting for: {nameString[..^2]}";
|
||||
}
|
||||
}
|
||||
|
||||
// Add some text if loading takes more than a few minutes
|
||||
if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3))
|
||||
context += "\nIt's been a while now. Please report this issue on our Discord server.";
|
||||
|
||||
this.page.Text = this.currentState switch
|
||||
{
|
||||
State.LoadingDalamud => "Please wait while Dalamud loads...",
|
||||
State.LoadingPlugins => "Please wait while Dalamud loads plugins...",
|
||||
State.AutoUpdatePlugins => "Please wait while Dalamud updates your plugins...",
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
#pragma warning disable SA1513
|
||||
} + context;
|
||||
#pragma warning restore SA1513
|
||||
|
||||
this.inProgressHideButton!.Enabled = this.canHide;
|
||||
}
|
||||
|
||||
private async Task DialogStatePeriodicUpdate(CancellationToken token)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
await timer.WaitForNextTickAsync(token);
|
||||
this.UpdatePage();
|
||||
}
|
||||
}
|
||||
|
||||
private void ThreadStart()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
|
||||
this.inProgressHideButton = new TaskDialogButton("Hide", this.canHide);
|
||||
|
||||
// We don't have access to the asset service here.
|
||||
var workingDirectory = Service<Dalamud>.Get().StartInfo.WorkingDirectory;
|
||||
TaskDialogIcon? dialogIcon = null;
|
||||
if (!workingDirectory.IsNullOrEmpty())
|
||||
{
|
||||
var extractedIcon = Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
|
||||
if (extractedIcon != null)
|
||||
{
|
||||
dialogIcon = new TaskDialogIcon(extractedIcon);
|
||||
}
|
||||
}
|
||||
|
||||
dialogIcon ??= TaskDialogIcon.Information;
|
||||
this.page = new TaskDialogPage
|
||||
{
|
||||
ProgressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.Marquee),
|
||||
Caption = "Dalamud",
|
||||
Icon = dialogIcon,
|
||||
Buttons = { this.inProgressHideButton },
|
||||
AllowMinimize = false,
|
||||
AllowCancel = false,
|
||||
Expander = new TaskDialogExpander
|
||||
{
|
||||
CollapsedButtonText = "What does this mean?",
|
||||
ExpandedButtonText = "What does this mean?",
|
||||
Text = "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
|
||||
"This is likely normal, please wait a little while longer.",
|
||||
},
|
||||
SizeToContent = true,
|
||||
};
|
||||
|
||||
this.UpdatePage();
|
||||
|
||||
// Call private TaskDialog ctor
|
||||
var ctor = typeof(TaskDialog).GetConstructor(
|
||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
null,
|
||||
Array.Empty<Type>(),
|
||||
null);
|
||||
|
||||
var taskDialog = (TaskDialog)ctor!.Invoke(Array.Empty<object>())!;
|
||||
|
||||
this.page.Created += (_, _) =>
|
||||
{
|
||||
var hwnd = new HWND(taskDialog.Handle);
|
||||
|
||||
// Bring to front
|
||||
Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
|
||||
Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0,
|
||||
SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOSIZE |
|
||||
SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
|
||||
Windows.Win32.PInvoke.SetForegroundWindow(hwnd);
|
||||
Windows.Win32.PInvoke.SetFocus(hwnd);
|
||||
Windows.Win32.PInvoke.SetActiveWindow(hwnd);
|
||||
};
|
||||
|
||||
// Call private "ShowDialogInternal"
|
||||
var showDialogInternal = typeof(TaskDialog).GetMethod(
|
||||
"ShowDialogInternal",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
null,
|
||||
[typeof(IntPtr), typeof(TaskDialogPage), typeof(TaskDialogStartupLocation)],
|
||||
null);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
_ = this.DialogStatePeriodicUpdate(cts.Token);
|
||||
|
||||
showDialogInternal!.Invoke(
|
||||
taskDialog,
|
||||
[IntPtr.Zero, this.page, TaskDialogStartupLocation.CenterScreen]);
|
||||
|
||||
Interlocked.Exchange(ref wasGloballyHidden, 1);
|
||||
cts.Cancel();
|
||||
}
|
||||
}
|
||||
724
Dalamud/Service/ServiceManager.cs
Normal file
724
Dalamud/Service/ServiceManager.cs
Normal file
|
|
@ -0,0 +1,724 @@
|
|||
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;
|
||||
|
||||
// API10 TODO: Move to Dalamud.Service namespace. Some plugins reflect this... including my own, oops. There's a todo
|
||||
// for more reflective APIs, so I'll just leave it for now.
|
||||
namespace Dalamud;
|
||||
|
||||
// TODO:
|
||||
// - Unify dependency walking code(load/unload)
|
||||
// - Visualize/output .dot or imgui thing
|
||||
|
||||
/// <summary>
|
||||
/// Class to initialize <see cref="Service{T}"/>.
|
||||
/// </summary>
|
||||
internal static class ServiceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Static log facility for Service{T}, to avoid duplicate instances for different types.
|
||||
/// </summary>
|
||||
public static readonly ModuleLog Log = new("SVC");
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Marks which service constructor the current thread's in. For use from <see cref="Service{T}"/> only.
|
||||
/// </summary>
|
||||
internal static readonly ThreadLocal<Type?> CurrentConstructorServiceType = new();
|
||||
|
||||
[SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")]
|
||||
private static readonly List<Type> LoadedServices = new();
|
||||
#endif
|
||||
|
||||
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
|
||||
private static readonly CancellationTokenSource UnloadCancellationTokenSource = new();
|
||||
|
||||
private static ManualResetEvent unloadResetEvent = new(false);
|
||||
|
||||
private static LoadingDialog loadingDialog = new();
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for registering startup blocker task.<br />
|
||||
/// Do not use this delegate outside the constructor.
|
||||
/// </summary>
|
||||
/// <param name="t">The blocker task.</param>
|
||||
/// <param name="justification">The justification for using this feature.</param>
|
||||
[InjectableType]
|
||||
public delegate void RegisterStartupBlockerDelegate(Task t, string justification);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for registering services that should be unloaded before self.<br />
|
||||
/// Intended for use with <see cref="Plugin.Internal.PluginManager"/>. If you think you need to use this outside
|
||||
/// of that, consider having a discussion first.<br />
|
||||
/// Do not use this delegate outside the constructor.
|
||||
/// </summary>
|
||||
/// <param name="unloadAfter">Services that should be unloaded first.</param>
|
||||
/// <param name="justification">The justification for using this feature.</param>
|
||||
[InjectableType]
|
||||
public delegate void RegisterUnloadAfterDelegate(IEnumerable<Type> unloadAfter, string justification);
|
||||
|
||||
/// <summary>
|
||||
/// Kinds of services.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ServiceKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Not a service.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is loaded manually.
|
||||
/// </summary>
|
||||
ProvidedService = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is loaded asynchronously while the game starts.
|
||||
/// </summary>
|
||||
EarlyLoadedService = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is loaded before the game starts.
|
||||
/// </summary>
|
||||
BlockingEarlyLoadedService = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is only instantiable via scopes.
|
||||
/// </summary>
|
||||
ScopedService = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is loaded automatically when the game starts, synchronously or asynchronously.
|
||||
/// </summary>
|
||||
AutoLoadService = EarlyLoadedService | BlockingEarlyLoadedService,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets task that gets completed when all blocking early loading services are done loading.
|
||||
/// </summary>
|
||||
public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state
|
||||
/// during initialization or during regular operation.
|
||||
/// </summary>
|
||||
public static CancellationToken UnloadCancellationToken => UnloadCancellationTokenSource.Token;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Provided Services and FFXIVClientStructs.
|
||||
/// </summary>
|
||||
/// <param name="dalamud">Instance of <see cref="Dalamud"/>.</param>
|
||||
/// <param name="fs">Instance of <see cref="ReliableFileStorage"/>.</param>
|
||||
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
|
||||
/// <param name="scanner">Instance of <see cref="TargetSigScanner"/>.</param>
|
||||
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>(T service) where T : IServiceType
|
||||
{
|
||||
Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute");
|
||||
Service<T>.Provide(service);
|
||||
LoadedServices.Add(typeof(T));
|
||||
}
|
||||
#else
|
||||
ProvideService(dalamud);
|
||||
ProvideService(fs);
|
||||
ProvideService(configuration);
|
||||
ProvideService(new ServiceContainer());
|
||||
ProvideService(scanner);
|
||||
return;
|
||||
|
||||
void ProvideService<T>(T service) where T : IServiceType => Service<T>.Provide(service);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the concrete types of services, i.e. the non-abstract non-interface types.
|
||||
/// </summary>
|
||||
/// <returns>The enumerable of service types, that may be enumerated only once per call.</returns>
|
||||
public static IEnumerable<Type> GetConcreteServiceTypes() =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract);
|
||||
|
||||
/// <summary>
|
||||
/// Kicks off construction of services that can handle early loading.
|
||||
/// </summary>
|
||||
/// <returns>Task for initializing all services.</returns>
|
||||
public static async Task InitializeEarlyLoadableServices()
|
||||
{
|
||||
using var serviceInitializeTimings = Timings.Start("Services Init");
|
||||
|
||||
var earlyLoadingServices = new HashSet<Type>();
|
||||
var blockingEarlyLoadingServices = new HashSet<Type>();
|
||||
var providedServices = new HashSet<Type>();
|
||||
|
||||
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
|
||||
var getAsyncTaskMap = new Dictionary<Type, Task>();
|
||||
|
||||
var serviceContainer = Service<ServiceContainer>.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<T> and are never early loaded
|
||||
if (serviceKind.HasFlag(ServiceKind.ScopedService))
|
||||
continue;
|
||||
|
||||
var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType);
|
||||
|
||||
var getTask = (Task)genericWrappedServiceType
|
||||
.InvokeMember(
|
||||
nameof(Service<IServiceType>.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>();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait for all blocking constructors to complete first.
|
||||
await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]),
|
||||
LoadingDialog.State.LoadingDalamud);
|
||||
|
||||
// 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,
|
||||
LoadingDialog.State.LoadingPlugins);
|
||||
|
||||
Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ===============");
|
||||
Timings.Event("BlockingServices Initialized");
|
||||
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
||||
loadingDialog.HideAndJoin();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// ignored, may have been set by the try/catch below
|
||||
}
|
||||
|
||||
Log.Error(e, "Failed resolving blocking services");
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
async Task WaitWithTimeoutConsent(IEnumerable<Task> tasksEnumerable, LoadingDialog.State state)
|
||||
{
|
||||
var tasks = tasksEnumerable.AsReadOnlyCollection();
|
||||
if (tasks.Count == 0)
|
||||
return;
|
||||
|
||||
// Time we wait until showing the loading dialog
|
||||
const int loadingDialogTimeout = 5000;
|
||||
|
||||
var aggregatedTask = Task.WhenAll(tasks);
|
||||
while (await Task.WhenAny(aggregatedTask, Task.Delay(loadingDialogTimeout)) != aggregatedTask)
|
||||
{
|
||||
loadingDialog.Show();
|
||||
loadingDialog.CanHide = true;
|
||||
loadingDialog.CurrentState = state;
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var tasks = new List<Task>();
|
||||
try
|
||||
{
|
||||
var servicesToLoad = new HashSet<Type>();
|
||||
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<object>();
|
||||
if (serviceType.GetCustomAttribute<BlockingEarlyLoadedServiceAttribute>() 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<IServiceType>.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)
|
||||
{
|
||||
UnloadCancellationTokenSource.Cancel();
|
||||
|
||||
Log.Error(e, "Failed resolving services");
|
||||
try
|
||||
{
|
||||
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
||||
loadingDialog.HideAndJoin();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads all services, in the reverse order of load.
|
||||
/// </summary>
|
||||
public static void UnloadAllServices()
|
||||
{
|
||||
UnloadCancellationTokenSource.Cancel();
|
||||
|
||||
var framework = Service<Framework>.GetNullable(Service<Framework>.ExceptionPropagationMode.None);
|
||||
if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false })
|
||||
{
|
||||
framework.RunOnFrameworkThread(UnloadAllServices).Wait();
|
||||
return;
|
||||
}
|
||||
|
||||
unloadResetEvent.Reset();
|
||||
|
||||
var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>();
|
||||
var allToUnload = new HashSet<Type>();
|
||||
var unloadOrder = new List<Type>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait until all services have been unloaded.
|
||||
/// </summary>
|
||||
public static void WaitForServiceUnload()
|
||||
{
|
||||
unloadResetEvent.WaitOne();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the service type of this type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check.</param>
|
||||
/// <returns>The type of service this type is.</returns>
|
||||
public static ServiceKind GetServiceKind(this Type type)
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ServiceAttribute>(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;
|
||||
}
|
||||
|
||||
/// <summary>Validate service type contracts, and throws exceptions accordingly.</summary>
|
||||
/// <param name="serviceType">An instance of <see cref="Type"/> that is supposed to be a service type.</param>
|
||||
/// <remarks>Does nothing on non-debug builds.</remarks>
|
||||
[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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that this constructor will be called for early initialization.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Constructor)]
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
public class ServiceConstructor : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the field is a service that should be loaded before constructing the class.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ServiceDependency : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the class is a service.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public abstract class ServiceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="kind">The kind of the service.</param>
|
||||
protected ServiceAttribute(ServiceKind kind) => this.Kind = kind;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of the service.
|
||||
/// </summary>
|
||||
public ServiceKind Kind { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the class is a service, that is provided by some other source.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ProvidedServiceAttribute : ServiceAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProvidedServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
public ProvidedServiceAttribute()
|
||||
: base(ServiceKind.ProvidedService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the class is a service, and will be instantiated automatically on startup.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class EarlyLoadedServiceAttribute : ServiceAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
public EarlyLoadedServiceAttribute()
|
||||
: this(ServiceKind.EarlyLoadedService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="kind">The service kind.</param>
|
||||
protected EarlyLoadedServiceAttribute(ServiceKind kind)
|
||||
: base(kind)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the class is a service, and will be instantiated automatically on startup,
|
||||
/// blocking game main thread until it completes.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockingEarlyLoadedServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="blockReason">Reason of blocking the game startup.</param>
|
||||
public BlockingEarlyLoadedServiceAttribute(string blockReason)
|
||||
: base(ServiceKind.BlockingEarlyLoadedService)
|
||||
{
|
||||
this.BlockReason = blockReason;
|
||||
}
|
||||
|
||||
/// <summary>Gets the reason of blocking the startup of the game.</summary>
|
||||
public string BlockReason { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ScopedServiceAttribute : ServiceAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScopedServiceAttribute"/> class.
|
||||
/// </summary>
|
||||
public ScopedServiceAttribute()
|
||||
: base(ServiceKind.ScopedService)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
[MeansImplicitUse]
|
||||
public class CallWhenServicesReady : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallWhenServicesReady"/> class.
|
||||
/// </summary>
|
||||
/// <param name="justification">Specify the reason here.</param>
|
||||
public CallWhenServicesReady(string justification)
|
||||
{
|
||||
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||
_ = justification;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that something is a candidate for being considered as an injected parameter for constructors.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Delegate
|
||||
| AttributeTargets.Class
|
||||
| AttributeTargets.Struct
|
||||
| AttributeTargets.Enum
|
||||
| AttributeTargets.Interface)]
|
||||
public class InjectableTypeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
473
Dalamud/Service/Service{T}.cs
Normal file
473
Dalamud/Service/Service{T}.cs
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Dalamud;
|
||||
|
||||
/// <summary>
|
||||
/// Basic service locator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The class you want to store in the service locator.</typeparam>
|
||||
[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")]
|
||||
internal static class Service<T> where T : IServiceType
|
||||
{
|
||||
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
||||
private static TaskCompletionSource<T> instanceTcs = new();
|
||||
private static List<Type>? dependencyServices;
|
||||
private static List<Type>? dependencyServicesForUnload;
|
||||
|
||||
static Service()
|
||||
{
|
||||
var type = typeof(T);
|
||||
ServiceAttribute =
|
||||
type.GetCustomAttribute<ServiceManager.ServiceAttribute>(true)
|
||||
?? throw new InvalidOperationException(
|
||||
$"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations.");
|
||||
|
||||
var exposeToPlugins = type.GetCustomAttribute<PluginInterfaceAttribute>() != null;
|
||||
if (exposeToPlugins)
|
||||
ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", type.Name);
|
||||
else
|
||||
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name);
|
||||
|
||||
if (exposeToPlugins)
|
||||
Service<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how to handle the cases of failed services when calling <see cref="Service{T}.GetNullable"/>.
|
||||
/// </summary>
|
||||
public enum ExceptionPropagationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Propagate all exceptions.
|
||||
/// </summary>
|
||||
PropagateAll,
|
||||
|
||||
/// <summary>
|
||||
/// Propagate all exceptions, except for <see cref="UnloadedException"/>.
|
||||
/// </summary>
|
||||
PropagateNonUnloaded,
|
||||
|
||||
/// <summary>
|
||||
/// Treat all exceptions as null.
|
||||
/// </summary>
|
||||
None,
|
||||
}
|
||||
|
||||
/// <summary>Does nothing.</summary>
|
||||
/// <remarks>Used to invoke the static ctor.</remarks>
|
||||
public static void Nop()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the type in the service locator to the given object.
|
||||
/// </summary>
|
||||
/// <param name="obj">Object to set.</param>
|
||||
public static void Provide(T obj)
|
||||
{
|
||||
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
|
||||
if (obj is IPublicDisposableService pds)
|
||||
pds.MarkDisposeOnlyFromService();
|
||||
instanceTcs.SetResult(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the service load state to failure.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception.</param>
|
||||
public static void ProvideException(Exception exception)
|
||||
{
|
||||
ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name);
|
||||
instanceTcs.SetException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pull the instance out of the service locator, waiting if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The object.</returns>
|
||||
public static T Get()
|
||||
{
|
||||
#if DEBUG
|
||||
if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService
|
||||
&& ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType)
|
||||
{
|
||||
var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false);
|
||||
if (!deps.Contains(typeof(T)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Calling {nameof(Service<IServiceType>)}<{typeof(T)}>.{nameof(Get)} which is not one of the" +
|
||||
$" dependency services is forbidden from the service constructor of {currentServiceType}." +
|
||||
$" This has a high chance of introducing hard-to-debug hangs.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!instanceTcs.Task.IsCompleted)
|
||||
instanceTcs.Task.Wait(ServiceManager.UnloadCancellationToken);
|
||||
return instanceTcs.Task.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pull the instance out of the service locator, waiting if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The object.</returns>
|
||||
public static Task<T> GetAsync() => instanceTcs.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to pull the instance out of the service locator.
|
||||
/// </summary>
|
||||
/// <param name="propagateException">Specifies which exceptions to propagate.</param>
|
||||
/// <returns>The object if registered, null otherwise.</returns>
|
||||
public static T? GetNullable(ExceptionPropagationMode propagateException = ExceptionPropagationMode.PropagateNonUnloaded)
|
||||
{
|
||||
if (instanceTcs.Task.IsCompletedSuccessfully)
|
||||
return instanceTcs.Task.Result;
|
||||
if (instanceTcs.Task.IsFaulted && propagateException != ExceptionPropagationMode.None)
|
||||
{
|
||||
if (propagateException == ExceptionPropagationMode.PropagateNonUnloaded
|
||||
&& instanceTcs.Task.Exception!.InnerExceptions.FirstOrDefault() is UnloadedException)
|
||||
return default;
|
||||
throw instanceTcs.Task.Exception!;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
|
||||
/// without blocking.
|
||||
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||
/// </summary>
|
||||
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||
/// <returns>List of dependency services.</returns>
|
||||
public static IReadOnlyCollection<Type> GetDependencyServices(bool includeUnloadDependencies)
|
||||
{
|
||||
if (includeUnloadDependencies && dependencyServicesForUnload is not null)
|
||||
return dependencyServicesForUnload;
|
||||
|
||||
if (dependencyServices is not null)
|
||||
return dependencyServices;
|
||||
|
||||
var res = new List<Type>();
|
||||
|
||||
ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name);
|
||||
|
||||
var ctor = GetServiceConstructor();
|
||||
if (ctor != null)
|
||||
{
|
||||
res.AddRange(ctor
|
||||
.GetParameters()
|
||||
.Select(x => x.ParameterType)
|
||||
.Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None));
|
||||
}
|
||||
|
||||
res.AddRange(typeof(T)
|
||||
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Where(x => x.GetCustomAttribute<ServiceManager.ServiceDependency>(true) != null)
|
||||
.Select(x => x.FieldType));
|
||||
|
||||
res.AddRange(typeof(T)
|
||||
.GetCustomAttributes()
|
||||
.OfType<InherentDependencyAttribute>()
|
||||
.Select(x => x.GetType().GetGenericArguments().First()));
|
||||
|
||||
foreach (var type in res)
|
||||
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
|
||||
|
||||
var deps = res
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (typeof(T).GetCustomAttribute<ServiceManager.BlockingEarlyLoadedServiceAttribute>() is not null)
|
||||
{
|
||||
var offenders = deps.Where(
|
||||
x => x.GetCustomAttribute<ServiceManager.ServiceAttribute>(true)!.Kind
|
||||
is not ServiceManager.ServiceKind.BlockingEarlyLoadedService
|
||||
and not ServiceManager.ServiceKind.ProvidedService)
|
||||
.ToArray();
|
||||
if (offenders.Any())
|
||||
{
|
||||
const string bels = nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute);
|
||||
const string ps = nameof(ServiceManager.ProvidedServiceAttribute);
|
||||
var errmsg =
|
||||
$"{typeof(T)} is a {bels}; it can only depend on {bels} and {ps}.\nOffending dependencies:\n" +
|
||||
string.Join("\n", offenders.Select(x => $"\t* {x.Name}"));
|
||||
#if DEBUG
|
||||
Util.Fatal(errmsg, "Service Dependency Check");
|
||||
#else
|
||||
ServiceManager.Log.Error(errmsg);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return dependencyServices = deps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="additionalProvidedTypedObjects">Additional objects available to constructors.</param>
|
||||
/// <returns>The loader task.</returns>
|
||||
internal static Task<T> StartLoader(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
if (instanceTcs.Task.IsCompleted)
|
||||
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
|
||||
|
||||
var attr = ServiceAttribute.GetType();
|
||||
if (attr.IsAssignableTo(typeof(ServiceManager.EarlyLoadedServiceAttribute)) != true)
|
||||
throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService");
|
||||
|
||||
return Task.Run(Timings.AttachTimingHandle(async () =>
|
||||
{
|
||||
var ctorArgs = new List<object>(additionalProvidedTypedObjects.Count + 1);
|
||||
ctorArgs.AddRange(additionalProvidedTypedObjects);
|
||||
ctorArgs.Add(
|
||||
new ServiceManager.RegisterUnloadAfterDelegate(
|
||||
(additionalDependencies, justification) =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (ServiceManager.CurrentConstructorServiceType.Value != typeof(T))
|
||||
throw new InvalidOperationException("Forbidden.");
|
||||
#endif
|
||||
dependencyServicesForUnload ??= new(GetDependencyServices(false));
|
||||
dependencyServicesForUnload.AddRange(additionalDependencies);
|
||||
|
||||
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||
_ = justification;
|
||||
}));
|
||||
|
||||
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
|
||||
try
|
||||
{
|
||||
var instance = await ConstructObject(ctorArgs).ConfigureAwait(false);
|
||||
instanceTcs.SetResult(instance);
|
||||
|
||||
List<Task>? tasks = null;
|
||||
foreach (var method in typeof(T).GetMethods(
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
if (method.GetCustomAttribute<ServiceManager.CallWhenServicesReady>(true) == null)
|
||||
continue;
|
||||
|
||||
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
|
||||
var args = await ResolveInjectedParameters(
|
||||
method.GetParameters(),
|
||||
Array.Empty<object>()).ConfigureAwait(false);
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ServiceManager.Log.Warning(
|
||||
"Service<{0}>: Method {1} does not have any arguments. Consider merging it with the ctor.",
|
||||
typeof(T).Name,
|
||||
method.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (method.Invoke(instance, args) is Task task)
|
||||
{
|
||||
tasks ??= new();
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tasks ??= new();
|
||||
tasks.Add(Task.FromException(e));
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks is not null)
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
|
||||
return instance;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
|
||||
instanceTcs.SetException(e);
|
||||
throw;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
private static void Unset()
|
||||
{
|
||||
if (!instanceTcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
switch (instanceTcs.Task.Result)
|
||||
{
|
||||
case IInternalDisposableService d:
|
||||
ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name);
|
||||
try
|
||||
{
|
||||
d.DisposeService();
|
||||
ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
ServiceManager.CheckServiceTypeContracts(typeof(T));
|
||||
ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name);
|
||||
break;
|
||||
}
|
||||
|
||||
instanceTcs = new TaskCompletionSource<T>();
|
||||
instanceTcs.SetException(new UnloadedException());
|
||||
}
|
||||
|
||||
private static ConstructorInfo? GetServiceConstructor()
|
||||
{
|
||||
const BindingFlags ctorBindingFlags =
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding;
|
||||
return typeof(T)
|
||||
.GetConstructors(ctorBindingFlags)
|
||||
.SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
|
||||
}
|
||||
|
||||
private static async Task<T> ConstructObject(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
var ctor = GetServiceConstructor();
|
||||
if (ctor == null)
|
||||
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
|
||||
|
||||
var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects)
|
||||
.ConfigureAwait(false);
|
||||
using (Timings.Start($"{typeof(T).Name} Construct"))
|
||||
{
|
||||
#if DEBUG
|
||||
ServiceManager.CurrentConstructorServiceType.Value = typeof(T);
|
||||
try
|
||||
{
|
||||
return (T)ctor.Invoke(args)!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ServiceManager.CurrentConstructorServiceType.Value = null;
|
||||
}
|
||||
#else
|
||||
return (T)ctor.Invoke(args)!;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<object[]> ResolveInjectedParameters(
|
||||
IReadOnlyList<ParameterInfo> argDefs,
|
||||
IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
var argTasks = new Task<object>[argDefs.Count];
|
||||
for (var i = 0; i < argDefs.Count; i++)
|
||||
{
|
||||
var argType = argDefs[i].ParameterType;
|
||||
ref var argTask = ref argTasks[i];
|
||||
|
||||
if (argType.GetCustomAttribute<ServiceManager.InjectableTypeAttribute>() is not null)
|
||||
{
|
||||
argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType));
|
||||
continue;
|
||||
}
|
||||
|
||||
argTask = (Task<object>)typeof(Service<>)
|
||||
.MakeGenericType(argType)
|
||||
.InvokeMember(
|
||||
nameof(GetAsyncAsObject),
|
||||
BindingFlags.InvokeMethod |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.NonPublic,
|
||||
null,
|
||||
null,
|
||||
null)!;
|
||||
}
|
||||
|
||||
return Task.WhenAll(argTasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pull the instance out of the service locator, waiting if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The object.</returns>
|
||||
private static Task<object> GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result);
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
|
||||
/// </summary>
|
||||
public class UnloadedException : InvalidOperationException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnloadedException"/> class.
|
||||
/// </summary>
|
||||
public UnloadedException()
|
||||
: base("Service is unloaded.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper functions for services.
|
||||
/// </summary>
|
||||
internal static class ServiceHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
|
||||
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The dependencies for this service.</param>
|
||||
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||
/// <returns>A list of dependencies.</returns>
|
||||
public static IReadOnlyCollection<Type> GetDependencies(Type serviceType, bool includeUnloadDependencies)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected an instance of {nameof(Service<IServiceType>)}<>",
|
||||
nameof(serviceType));
|
||||
}
|
||||
#endif
|
||||
|
||||
return (IReadOnlyCollection<Type>)serviceType.InvokeMember(
|
||||
nameof(Service<IServiceType>.GetDependencyServices),
|
||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
||||
null,
|
||||
null,
|
||||
new object?[] { includeUnloadDependencies }) ?? new List<Type>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="Service{T}"/> type for a given service type.
|
||||
/// This will throw if the service type is not a valid service.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to obtain a <see cref="Service{T}"/> for.</param>
|
||||
/// <returns>The <see cref="Service{T}"/>.</returns>
|
||||
public static Type GetAsService(Type type)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!type.IsAssignableTo(typeof(IServiceType)))
|
||||
throw new ArgumentException($"Expected an instance of {nameof(IServiceType)}", nameof(type));
|
||||
#endif
|
||||
|
||||
return typeof(Service<>).MakeGenericType(type);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue