using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Utility.Timing; using JetBrains.Annotations; namespace Dalamud; /// /// Basic service locator. /// /// /// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI. /// /// The class you want to store in the service locator. internal static class Service where T : IServiceType { private static TaskCompletionSource instanceTcs = new(); static Service() { var exposeToPlugins = typeof(T).GetCustomAttribute() != null; if (exposeToPlugins) ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", typeof(T).Name); else ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); if (exposeToPlugins) Service.Get().RegisterSingleton(instanceTcs.Task); } /// /// Specifies how to handle the cases of failed services when calling . /// public enum ExceptionPropagationMode { /// /// Propagate all exceptions. /// PropagateAll, /// /// Propagate all exceptions, except for . /// PropagateNonUnloaded, /// /// Treat all exceptions as null. /// None, } /// /// Sets the type in the service locator to the given object. /// /// Object to set. public static void Provide(T obj) { instanceTcs.SetResult(obj); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); } /// /// Sets the service load state to failure. /// /// The exception. public static void ProvideException(Exception exception) { ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name); instanceTcs.SetException(exception); } /// /// Pull the instance out of the service locator, waiting if necessary. /// /// The object. public static T Get() { if (!instanceTcs.Task.IsCompleted) instanceTcs.Task.Wait(); return instanceTcs.Task.Result; } /// /// Pull the instance out of the service locator, waiting if necessary. /// /// The object. [UsedImplicitly] public static Task GetAsync() => instanceTcs.Task; /// /// Attempt to pull the instance out of the service locator. /// /// Specifies which exceptions to propagate. /// The object if registered, null otherwise. 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; } /// /// Gets an enumerable containing Service<T>s that are required for this Service to initialize without blocking. /// /// List of dependency services. [UsedImplicitly] public static List GetDependencyServices() { var res = new List(); res.AddRange(GetServiceConstructor() .GetParameters() .Select(x => x.ParameterType)); res.AddRange(typeof(T) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Select(x => x.FieldType) .Where(x => x.GetCustomAttribute(true) != null)); res.AddRange(typeof(T) .GetCustomAttributes() .OfType() .Select(x => x.GetType().GetGenericArguments().First())); return res .Distinct() .Select(x => typeof(Service<>).MakeGenericType(x)) .ToList(); } [UsedImplicitly] private static Task StartLoader() { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); var attr = typeof(T).GetCustomAttribute(true)?.GetType(); if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); return Task.Run(Timings.AttachTimingHandle(async () => { ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); try { var instance = await ConstructObject(); instanceTcs.SetResult(instance); foreach (var method in typeof(T).GetMethods( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { if (method.GetCustomAttribute(true) == null) continue; ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); var args = await Task.WhenAll(method.GetParameters().Select( x => ResolveServiceFromTypeAsync(x.ParameterType))); method.Invoke(instance, args); } 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; var instance = instanceTcs.Task.Result; if (instance is IDisposable disposable) { ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); try { disposable.Dispose(); ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); } catch (Exception e) { ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); } } else { ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); } instanceTcs = new TaskCompletionSource(); instanceTcs.SetException(new UnloadedException()); } private static async Task ResolveServiceFromTypeAsync(Type type) { var task = (Task)typeof(Service<>) .MakeGenericType(type) .InvokeMember( "GetAsync", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null)!; await task; return typeof(Task<>).MakeGenericType(type) .GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)! .GetValue(task); } private static ConstructorInfo GetServiceConstructor() { const BindingFlags ctorBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.OptionalParamBinding; return typeof(T) .GetConstructors(ctorBindingFlags) .Single(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); } private static async Task ConstructObject() { var ctor = GetServiceConstructor(); var args = await Task.WhenAll( ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); using (Timings.Start($"{typeof(T).Name} Construct")) { return (T)ctor.Invoke(args)!; } } /// /// Exception thrown when service is attempted to be retrieved when it's unloaded. /// public class UnloadedException : InvalidOperationException { /// /// Initializes a new instance of the class. /// public UnloadedException() : base("Service is unloaded.") { } } }