diff --git a/Pal.Client/DependencyInjectionLoader.cs b/Pal.Client/DependencyContextInitializer.cs similarity index 64% rename from Pal.Client/DependencyInjectionLoader.cs rename to Pal.Client/DependencyContextInitializer.cs index 9466eab..db1c679 100644 --- a/Pal.Client/DependencyInjectionLoader.cs +++ b/Pal.Client/DependencyContextInitializer.cs @@ -16,7 +16,6 @@ using Pal.Client.Configuration; using Pal.Client.Configuration.Legacy; using Pal.Client.Database; using Pal.Client.DependencyInjection; -using Pal.Client.Properties; using Pal.Client.Windows; namespace Pal.Client @@ -25,80 +24,56 @@ namespace Pal.Client /// Takes care of async plugin init - this is mostly everything that requires either the config or the database to /// be available. /// - internal sealed class DependencyInjectionLoader + internal sealed class DependencyContextInitializer { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - public DependencyInjectionLoader(ILogger logger, IServiceProvider serviceProvider) + public DependencyContextInitializer(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; } - public ELoadState LoadState { get; private set; } = ELoadState.Initializing; - - public event Action? InitCompleted; - public async Task InitializeAsync(CancellationToken cancellationToken) { using IDisposable? logScope = _logger.BeginScope("AsyncInit"); - Chat? chat = null; - try - { - _logger.LogInformation("Starting async init"); - chat = _serviceProvider.GetService(); + _logger.LogInformation("Starting async init"); - await RemoveOldBackups(); - await CreateBackups(); - cancellationToken.ThrowIfCancellationRequested(); + await RemoveOldBackups(); + await CreateBackups(); + cancellationToken.ThrowIfCancellationRequested(); - await RunMigrations(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + await RunMigrations(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - await RunCleanup(_logger); - cancellationToken.ThrowIfCancellationRequested(); + await RunCleanup(); + cancellationToken.ThrowIfCancellationRequested(); - // v1 migration: config migration for import history, json migration for markers - _serviceProvider.GetRequiredService().Migrate(); - await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - // windows that have logic to open on startup - _serviceProvider.GetRequiredService(); + // windows that have logic to open on startup + _serviceProvider.GetRequiredService(); - // initialize components that are mostly self-contained/self-registered - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + // initialize components that are mostly self-contained/self-registered + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); - // eager load any commands to find errors now, not when running them - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); - _serviceProvider.GetRequiredService(); + // eager load any commands to find errors now, not when running them + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - LoadState = ELoadState.Loaded; - InitCompleted?.Invoke(null); - _logger.LogInformation("Async init complete"); - } - catch (ObjectDisposedException) - { - InitCompleted?.Invoke(null); - LoadState = ELoadState.Error; - } - catch (Exception e) - { - _logger.LogError(e, "Async load failed"); - InitCompleted?.Invoke(() => - chat?.Error(string.Format(Localization.Error_LoadFailed, $"{e.GetType()} - {e.Message}"))); - - LoadState = ELoadState.Error; - } + _logger.LogInformation("Async init complete"); } private async Task RemoveOldBackups() @@ -186,7 +161,7 @@ namespace Pal.Client _logger.LogInformation("Completed database migrations"); } - private async Task RunCleanup(ILogger logger) + private async Task RunCleanup() { await using var scope = _serviceProvider.CreateAsyncScope(); await using var dbContext = scope.ServiceProvider.GetRequiredService(); @@ -196,13 +171,5 @@ namespace Pal.Client await dbContext.SaveChangesAsync(); } - - - public enum ELoadState - { - Initializing, - Loaded, - Error - } } } diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index 1ce57e5..00c36cb 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -1,6 +1,5 @@ -using System.Globalization; +using System; using System.IO; -using System.Threading; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -32,20 +31,18 @@ namespace Pal.Client /// /// DI-aware Plugin. /// - // ReSharper disable once UnusedType.Global - internal sealed class DependencyInjectionContext : IDalamudPlugin + internal sealed class DependencyInjectionContext : IDisposable { public static DalamudLoggerProvider LoggerProvider { get; } = new(); /// /// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. /// - private readonly ILogger _logger = LoggerProvider.CreateLogger(); + private ILogger _logger = LoggerProvider.CreateLogger(); private readonly string _sqliteConnectionString; - private readonly CancellationTokenSource _initCts = new(); + private readonly ServiceCollection _serviceCollection = new(); private ServiceProvider? _serviceProvider; - private Plugin? _plugin; public string Name => Localization.Palace_Pal; @@ -58,9 +55,10 @@ namespace Pal.Client Framework framework, Condition condition, CommandManager commandManager, - DataManager dataManager) + DataManager dataManager, + Plugin plugin) { - _logger.LogInformation("Building service container for {Assembly}", + _logger.LogInformation("Building dalamud service container for {Assembly}", typeof(DependencyInjectionContext).Assembly.FullName); // set up legacy services @@ -69,8 +67,7 @@ namespace Pal.Client #pragma warning restore CS0612 // set up logging - IServiceCollection services = new ServiceCollection(); - services.AddLogging(builder => + _serviceCollection.AddLogging(builder => builder.AddFilter("Pal", LogLevel.Trace) .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) .AddFilter("Grpc", LogLevel.Debug) @@ -78,70 +75,78 @@ namespace Pal.Client .AddProvider(LoggerProvider)); // dalamud - services.AddSingleton(this); - services.AddSingleton(pluginInterface); - services.AddSingleton(clientState); - services.AddSingleton(gameGui); - services.AddSingleton(chatGui); - services.AddSingleton(); - services.AddSingleton(objectTable); - services.AddSingleton(framework); - services.AddSingleton(condition); - services.AddSingleton(commandManager); - services.AddSingleton(dataManager); - services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); + _serviceCollection.AddSingleton(plugin); + _serviceCollection.AddSingleton(pluginInterface); + _serviceCollection.AddSingleton(clientState); + _serviceCollection.AddSingleton(gameGui); + _serviceCollection.AddSingleton(chatGui); + _serviceCollection.AddSingleton(); + _serviceCollection.AddSingleton(objectTable); + _serviceCollection.AddSingleton(framework); + _serviceCollection.AddSingleton(condition); + _serviceCollection.AddSingleton(commandManager); + _serviceCollection.AddSingleton(dataManager); + _serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); - // EF core _sqliteConnectionString = $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; - services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); - services.AddTransient(); - services.AddScoped(); + } + + public IServiceProvider BuildServiceContainer() + { + _logger.LogInformation("Building async service container for {Assembly}", + typeof(DependencyInjectionContext).Assembly.FullName); + + // EF core + _serviceCollection.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + _serviceCollection.AddTransient(); + _serviceCollection.AddScoped(); // plugin-specific - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(sp => sp.GetRequiredService().Load()); - services.AddTransient(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(sp => + sp.GetRequiredService().Load()); + _serviceCollection.AddTransient(); // commands - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // territory & marker related services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // windows & related services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // rendering - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); + _serviceCollection.AddScoped(); // queue handling - services.AddTransient, QueuedImport.Handler>(); - services.AddTransient, QueuedUndoImport.Handler>(); - services.AddTransient, QueuedConfigUpdate.Handler>(); - services.AddTransient, QueuedSyncResponse.Handler>(); - - // set up the current UI language before creating anything - Localization.Culture = new CultureInfo(pluginInterface.UiLanguage); + _serviceCollection.AddTransient, QueuedImport.Handler>(); + _serviceCollection + .AddTransient, QueuedUndoImport.Handler>(); + _serviceCollection + .AddTransient, QueuedConfigUpdate.Handler>(); + _serviceCollection + .AddTransient, QueuedSyncResponse.Handler>(); // build - _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + _serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true, @@ -165,34 +170,19 @@ namespace Pal.Client // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for // config stuff. _logger = _serviceProvider.GetRequiredService>(); - _logger.LogInformation("Service container built, creating plugin"); - _plugin = new Plugin(pluginInterface, _serviceProvider, _initCts.Token); + _logger.LogInformation("Service container built"); + + return _serviceProvider; } public void Dispose() { - _initCts.Cancel(); + _logger.LogInformation("Disposing DI Context"); + _serviceProvider?.Dispose(); - // ensure we're not calling dispose recursively on ourselves - if (_serviceProvider != null) - { - _logger.LogInformation("Disposing DI Context"); - - ServiceProvider serviceProvider = _serviceProvider; - _serviceProvider = null; - - _plugin?.Dispose(); - _plugin = null; - serviceProvider.Dispose(); - - // ensure we're not keeping the file open longer than the plugin is loaded - using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) - SqliteConnection.ClearPool(sqliteConnection); - } - else - { - _logger.LogDebug("DI context is already disposed"); - } + // ensure we're not keeping the file open longer than the plugin is loaded + using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) + SqliteConnection.ClearPool(sqliteConnection); } } } diff --git a/Pal.Client/Hooks.cs b/Pal.Client/Hooks.cs index 17707e6..cf4b373 100644 --- a/Pal.Client/Hooks.cs +++ b/Pal.Client/Hooks.cs @@ -1,6 +1,5 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Memory; using Dalamud.Utility.Signatures; using System; @@ -32,8 +31,11 @@ namespace Pal.Client _territoryState = territoryState; _frameworkService = frameworkService; + _logger.LogDebug("Initializing game hooks"); SignatureHelper.Initialise(this); ActorVfxCreateHook.Enable(); + + _logger.LogDebug("Game hooks initialized"); } /// @@ -82,6 +84,7 @@ namespace Pal.Client { if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") { + _logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position); _frameworkService.NextUpdateObjects.Enqueue(obj.Address); } } @@ -96,6 +99,7 @@ namespace Pal.Client public void Dispose() { + _logger.LogDebug("Disposing game hooks"); ActorVfxCreateHook.Dispose(); } } diff --git a/Pal.Client/Plugin.cs b/Pal.Client/Plugin.cs index c4b3503..0adafc0 100644 --- a/Pal.Client/Plugin.cs +++ b/Pal.Client/Plugin.cs @@ -1,14 +1,15 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Pal.Client.Rendering; -using Pal.Client.Windows; using System; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Command; +using Dalamud.Game.Gui; using Pal.Client.Properties; using ECommons; using Microsoft.Extensions.DependencyInjection; @@ -24,52 +25,101 @@ namespace Pal.Client /// need to be sent to different receivers depending on priority or configuration . /// /// - internal sealed class Plugin : IDisposable + internal sealed class Plugin : IDalamudPlugin { + private readonly CancellationTokenSource _initCts = new(); + private readonly DalamudPluginInterface _pluginInterface; - private readonly ILogger _logger; private readonly CommandManager _commandManager; - private readonly Chat _chat; - private readonly WindowSystem _windowSystem; private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + private readonly Framework _framework; - private readonly IServiceScope _rootScope; - private readonly DependencyInjectionLoader _loader; + private readonly TaskCompletionSource _rootScopeCompletionSource = new(); + private ELoadState _loadState = ELoadState.Initializing; + private DependencyInjectionContext? _dependencyInjectionContext; + private ILogger _logger = DependencyInjectionContext.LoggerProvider.CreateLogger(); + private WindowSystem? _windowSystem; + private IServiceScope? _rootScope; private Action? _loginAction; public Plugin( DalamudPluginInterface pluginInterface, - IServiceProvider serviceProvider, - CancellationToken cancellationToken) + CommandManager commandManager, + ClientState clientState, + ChatGui chatGui, + Framework framework) { _pluginInterface = pluginInterface; - _logger = serviceProvider.GetRequiredService>(); - _commandManager = serviceProvider.GetRequiredService(); - _chat = serviceProvider.GetRequiredService(); - _windowSystem = serviceProvider.GetRequiredService(); - _clientState = serviceProvider.GetRequiredService(); + _commandManager = commandManager; + _clientState = clientState; + _chatGui = chatGui; + _framework = framework; - _rootScope = serviceProvider.CreateScope(); - _loader = _rootScope.ServiceProvider.GetRequiredService(); - _loader.InitCompleted += InitCompleted; - var _ = Task.Run(async () => await _loader.InitializeAsync(cancellationToken)); - - pluginInterface.UiBuilder.Draw += Draw; - pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; - pluginInterface.LanguageChanged += LanguageChanged; - _clientState.Login += Login; + // set up the current UI language before creating anything + Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage); _commandManager.AddHandler("/pal", new CommandInfo(OnCommand) { HelpMessage = Localization.Command_pal_HelpText }); + + Task.Run(async () => await CreateDependencyContext()); } - private void InitCompleted(Action? loginAction) - { - LanguageChanged(_pluginInterface.UiLanguage); + public string Name => Localization.Palace_Pal; + private async Task CreateDependencyContext() + { + try + { + _dependencyInjectionContext = _pluginInterface.Create(this) + ?? throw new Exception("Could not create DI root context class"); + var serviceProvider = _dependencyInjectionContext.BuildServiceContainer(); + _initCts.Token.ThrowIfCancellationRequested(); + + _logger = serviceProvider.GetRequiredService>(); + _windowSystem = serviceProvider.GetRequiredService(); + _rootScope = serviceProvider.CreateScope(); + + var loader = _rootScope.ServiceProvider.GetRequiredService(); + await loader.InitializeAsync(_initCts.Token); + + await _framework.RunOnFrameworkThread(() => + { + _pluginInterface.UiBuilder.Draw += Draw; + _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; + _pluginInterface.LanguageChanged += LanguageChanged; + _clientState.Login += Login; + }); + _rootScopeCompletionSource.SetResult(_rootScope); + _loadState = ELoadState.Loaded; + } + catch (ObjectDisposedException e) + { + _rootScopeCompletionSource.SetException(e); + _loadState = ELoadState.Error; + } + catch (OperationCanceledException e) + { + _rootScopeCompletionSource.SetException(e); + _loadState = ELoadState.Error; + } + catch (Exception e) + { + _rootScopeCompletionSource.SetException(e); + _logger.LogError(e, "Async load failed"); + ShowErrorOnLogin(() => + new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed, + $"{e.GetType()} - {e.Message}"))); + + _loadState = ELoadState.Error; + } + } + + private void ShowErrorOnLogin(Action? loginAction) + { if (_clientState.IsLoggedIn) { loginAction?.Invoke(); @@ -89,84 +139,106 @@ namespace Pal.Client { arguments = arguments.Trim(); - IPalacePalConfiguration configuration = - _rootScope.ServiceProvider.GetRequiredService(); - if (configuration.FirstUse && arguments != "" && arguments != "config") + Task.Run(async () => { - _chat.Error(Localization.Error_FirstTimeSetupRequired); - return; - } - - try - { - var sp = _rootScope.ServiceProvider; - - switch (arguments) + IServiceScope rootScope; + try { - case "": - case "config": - sp.GetRequiredService().Execute(); - break; - - case "stats": - sp.GetRequiredService().Execute(); - break; - - case "tc": - case "test-connection": - sp.GetRequiredService().Execute(); - break; - - case "near": - case "tnear": - case "hnear": - sp.GetRequiredService().Execute(arguments); - break; - - default: - _chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, - command)); - break; + rootScope = await _rootScopeCompletionSource.Task; } - } - catch (Exception e) - { - _chat.Error(e.ToString()); - } + catch (Exception e) + { + _logger.LogError(e, "Could not wait for command root scope"); + return; + } + + IPalacePalConfiguration configuration = + rootScope.ServiceProvider.GetRequiredService(); + Chat chat = rootScope.ServiceProvider.GetRequiredService(); + if (configuration.FirstUse && arguments != "" && arguments != "config") + { + chat.Error(Localization.Error_FirstTimeSetupRequired); + return; + } + + try + { + var sp = rootScope.ServiceProvider; + + switch (arguments) + { + case "": + case "config": + sp.GetRequiredService().Execute(); + break; + + case "stats": + sp.GetRequiredService().Execute(); + break; + + case "tc": + case "test-connection": + sp.GetRequiredService().Execute(); + break; + + case "near": + case "tnear": + case "hnear": + sp.GetRequiredService().Execute(arguments); + break; + + default: + chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, + command)); + break; + } + } + catch (Exception e) + { + chat.Error(e.ToString()); + } + }); } private void OpenConfigUi() - => _rootScope.ServiceProvider.GetRequiredService().Execute(); + => _rootScope!.ServiceProvider.GetRequiredService().Execute(); private void LanguageChanged(string languageCode) { _logger.LogInformation("Language set to '{Language}'", languageCode); Localization.Culture = new CultureInfo(languageCode); - _windowSystem.Windows.OfType() + _windowSystem!.Windows.OfType() .Each(w => w.LanguageChanged()); } private void Draw() { - if (_loader.LoadState == DependencyInjectionLoader.ELoadState.Loaded) - { - _rootScope.ServiceProvider.GetRequiredService().DrawLayers(); - _windowSystem.Draw(); - } + _rootScope!.ServiceProvider.GetRequiredService().DrawLayers(); + _windowSystem!.Draw(); } public void Dispose() { _commandManager.RemoveHandler("/pal"); - _pluginInterface.UiBuilder.Draw -= Draw; - _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; - _pluginInterface.LanguageChanged -= LanguageChanged; - _clientState.Login -= Login; + if (_loadState == ELoadState.Loaded) + { + _pluginInterface.UiBuilder.Draw -= Draw; + _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; + _pluginInterface.LanguageChanged -= LanguageChanged; + _clientState.Login -= Login; + } - _loader.InitCompleted -= InitCompleted; - _rootScope.Dispose(); + _initCts.Cancel(); + _rootScope?.Dispose(); + } + + private enum ELoadState + { + Initializing, + Loaded, + Error } } } diff --git a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs index c86ad36..5397be3 100644 --- a/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs +++ b/Pal.Client/Scheduled/IQueueOnFrameworkThread.cs @@ -27,7 +27,7 @@ namespace Pal.Client.Scheduled { if (queued is T t) { - _logger.LogInformation("Handling {QueuedType}", queued.GetType()); + _logger.LogDebug("Handling {QueuedType}", queued.GetType()); Run(t, ref recreateLayout); } else