DI: Load entire DI container in the background

This commit is contained in:
Liza 2023-02-22 22:20:50 +01:00
parent dbe6abd1db
commit 8d17c02186
5 changed files with 258 additions and 225 deletions

View File

@ -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,30 +24,22 @@ namespace Pal.Client
/// Takes care of async plugin init - this is mostly everything that requires either the config or the database to
/// be available.
/// </summary>
internal sealed class DependencyInjectionLoader
internal sealed class DependencyContextInitializer
{
private readonly ILogger<DependencyInjectionLoader> _logger;
private readonly ILogger<DependencyContextInitializer> _logger;
private readonly IServiceProvider _serviceProvider;
public DependencyInjectionLoader(ILogger<DependencyInjectionLoader> logger, IServiceProvider serviceProvider)
public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public ELoadState LoadState { get; private set; } = ELoadState.Initializing;
public event Action<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<Chat>();
await RemoveOldBackups();
await CreateBackups();
@ -57,7 +48,7 @@ namespace Pal.Client
await RunMigrations(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await RunCleanup(_logger);
await RunCleanup();
cancellationToken.ThrowIfCancellationRequested();
// v1 migration: config migration for import history, json migration for markers
@ -82,24 +73,8 @@ namespace Pal.Client
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;
}
}
private async Task RemoveOldBackups()
{
@ -186,7 +161,7 @@ namespace Pal.Client
_logger.LogInformation("Completed database migrations");
}
private async Task RunCleanup(ILogger<DependencyInjectionLoader> logger)
private async Task RunCleanup()
{
await using var scope = _serviceProvider.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
@ -196,13 +171,5 @@ namespace Pal.Client
await dbContext.SaveChangesAsync();
}
public enum ELoadState
{
Initializing,
Loaded,
Error
}
}
}

View File

@ -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
/// <summary>
/// DI-aware Plugin.
/// </summary>
// ReSharper disable once UnusedType.Global
internal sealed class DependencyInjectionContext : IDalamudPlugin
internal sealed class DependencyInjectionContext : IDisposable
{
public static DalamudLoggerProvider LoggerProvider { get; } = new();
/// <summary>
/// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes.
/// </summary>
private readonly ILogger _logger = LoggerProvider.CreateLogger<DependencyInjectionContext>();
private ILogger _logger = LoggerProvider.CreateLogger<DependencyInjectionContext>();
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<IDalamudPlugin>(this);
services.AddSingleton(pluginInterface);
services.AddSingleton(clientState);
services.AddSingleton(gameGui);
services.AddSingleton(chatGui);
services.AddSingleton<Chat>();
services.AddSingleton(objectTable);
services.AddSingleton(framework);
services.AddSingleton(condition);
services.AddSingleton(commandManager);
services.AddSingleton(dataManager);
services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
_serviceCollection.AddSingleton<IDalamudPlugin>(plugin);
_serviceCollection.AddSingleton(pluginInterface);
_serviceCollection.AddSingleton(clientState);
_serviceCollection.AddSingleton(gameGui);
_serviceCollection.AddSingleton(chatGui);
_serviceCollection.AddSingleton<Chat>();
_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<PalClientContext>(o => o.UseSqlite(_sqliteConnectionString));
services.AddTransient<JsonMigration>();
services.AddScoped<Cleanup>();
}
public IServiceProvider BuildServiceContainer()
{
_logger.LogInformation("Building async service container for {Assembly}",
typeof(DependencyInjectionContext).Assembly.FullName);
// EF core
_serviceCollection.AddDbContext<PalClientContext>(o => o.UseSqlite(_sqliteConnectionString));
_serviceCollection.AddTransient<JsonMigration>();
_serviceCollection.AddScoped<Cleanup>();
// plugin-specific
services.AddScoped<DependencyInjectionLoader>();
services.AddScoped<DebugState>();
services.AddScoped<Hooks>();
services.AddScoped<RemoteApi>();
services.AddScoped<ConfigurationManager>();
services.AddScoped<IPalacePalConfiguration>(sp => sp.GetRequiredService<ConfigurationManager>().Load());
services.AddTransient<RepoVerification>();
_serviceCollection.AddScoped<DependencyContextInitializer>();
_serviceCollection.AddScoped<DebugState>();
_serviceCollection.AddScoped<Hooks>();
_serviceCollection.AddScoped<RemoteApi>();
_serviceCollection.AddScoped<ConfigurationManager>();
_serviceCollection.AddScoped<IPalacePalConfiguration>(sp =>
sp.GetRequiredService<ConfigurationManager>().Load());
_serviceCollection.AddTransient<RepoVerification>();
// commands
services.AddScoped<PalConfigCommand>();
services.AddScoped<PalNearCommand>();
services.AddScoped<PalStatsCommand>();
services.AddScoped<PalTestConnectionCommand>();
_serviceCollection.AddScoped<PalConfigCommand>();
_serviceCollection.AddScoped<PalNearCommand>();
_serviceCollection.AddScoped<PalStatsCommand>();
_serviceCollection.AddScoped<PalTestConnectionCommand>();
// territory & marker related services
services.AddScoped<TerritoryState>();
services.AddScoped<FrameworkService>();
services.AddScoped<ChatService>();
services.AddScoped<FloorService>();
services.AddScoped<ImportService>();
_serviceCollection.AddScoped<TerritoryState>();
_serviceCollection.AddScoped<FrameworkService>();
_serviceCollection.AddScoped<ChatService>();
_serviceCollection.AddScoped<FloorService>();
_serviceCollection.AddScoped<ImportService>();
// windows & related services
services.AddScoped<AgreementWindow>();
services.AddScoped<ConfigWindow>();
services.AddScoped<StatisticsService>();
services.AddScoped<StatisticsWindow>();
_serviceCollection.AddScoped<AgreementWindow>();
_serviceCollection.AddScoped<ConfigWindow>();
_serviceCollection.AddScoped<StatisticsService>();
_serviceCollection.AddScoped<StatisticsWindow>();
// rendering
services.AddScoped<SimpleRenderer>();
services.AddScoped<SplatoonRenderer>();
services.AddScoped<RenderAdapter>();
_serviceCollection.AddScoped<SimpleRenderer>();
_serviceCollection.AddScoped<SplatoonRenderer>();
_serviceCollection.AddScoped<RenderAdapter>();
// queue handling
services.AddTransient<IQueueOnFrameworkThread.Handler<QueuedImport>, QueuedImport.Handler>();
services.AddTransient<IQueueOnFrameworkThread.Handler<QueuedUndoImport>, QueuedUndoImport.Handler>();
services.AddTransient<IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>, QueuedConfigUpdate.Handler>();
services.AddTransient<IQueueOnFrameworkThread.Handler<QueuedSyncResponse>, QueuedSyncResponse.Handler>();
// set up the current UI language before creating anything
Localization.Culture = new CultureInfo(pluginInterface.UiLanguage);
_serviceCollection.AddTransient<IQueueOnFrameworkThread.Handler<QueuedImport>, QueuedImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedUndoImport>, QueuedUndoImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>, QueuedConfigUpdate.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedSyncResponse>, 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<ILogger<DependencyInjectionContext>>();
_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();
// 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();
_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");
}
}
}
}

View File

@ -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");
}
/// <summary>
@ -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();
}
}

View File

@ -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 .
/// </summary>
/// <see cref="DependencyInjectionContext"/>
internal sealed class Plugin : IDisposable
internal sealed class Plugin : IDalamudPlugin
{
private readonly CancellationTokenSource _initCts = new();
private readonly DalamudPluginInterface _pluginInterface;
private readonly ILogger<Plugin> _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<IServiceScope> _rootScopeCompletionSource = new();
private ELoadState _loadState = ELoadState.Initializing;
private DependencyInjectionContext? _dependencyInjectionContext;
private ILogger _logger = DependencyInjectionContext.LoggerProvider.CreateLogger<Plugin>();
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<ILogger<Plugin>>();
_commandManager = serviceProvider.GetRequiredService<CommandManager>();
_chat = serviceProvider.GetRequiredService<Chat>();
_windowSystem = serviceProvider.GetRequiredService<WindowSystem>();
_clientState = serviceProvider.GetRequiredService<ClientState>();
_commandManager = commandManager;
_clientState = clientState;
_chatGui = chatGui;
_framework = framework;
_rootScope = serviceProvider.CreateScope();
_loader = _rootScope.ServiceProvider.GetRequiredService<DependencyInjectionLoader>();
_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<DependencyInjectionContext>(this)
?? throw new Exception("Could not create DI root context class");
var serviceProvider = _dependencyInjectionContext.BuildServiceContainer();
_initCts.Token.ThrowIfCancellationRequested();
_logger = serviceProvider.GetRequiredService<ILogger<Plugin>>();
_windowSystem = serviceProvider.GetRequiredService<WindowSystem>();
_rootScope = serviceProvider.CreateScope();
var loader = _rootScope.ServiceProvider.GetRequiredService<DependencyContextInitializer>();
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,17 +139,31 @@ namespace Pal.Client
{
arguments = arguments.Trim();
Task.Run(async () =>
{
IServiceScope rootScope;
try
{
rootScope = await _rootScopeCompletionSource.Task;
}
catch (Exception e)
{
_logger.LogError(e, "Could not wait for command root scope");
return;
}
IPalacePalConfiguration configuration =
_rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
Chat chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
if (configuration.FirstUse && arguments != "" && arguments != "config")
{
_chat.Error(Localization.Error_FirstTimeSetupRequired);
chat.Error(Localization.Error_FirstTimeSetupRequired);
return;
}
try
{
var sp = _rootScope.ServiceProvider;
var sp = rootScope.ServiceProvider;
switch (arguments)
{
@ -124,49 +188,57 @@ namespace Pal.Client
break;
default:
_chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments,
chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, arguments,
command));
break;
}
}
catch (Exception e)
{
_chat.Error(e.ToString());
chat.Error(e.ToString());
}
});
}
private void OpenConfigUi()
=> _rootScope.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
private void LanguageChanged(string languageCode)
{
_logger.LogInformation("Language set to '{Language}'", languageCode);
Localization.Culture = new CultureInfo(languageCode);
_windowSystem.Windows.OfType<ILanguageChanged>()
_windowSystem!.Windows.OfType<ILanguageChanged>()
.Each(w => w.LanguageChanged());
}
private void Draw()
{
if (_loader.LoadState == DependencyInjectionLoader.ELoadState.Loaded)
{
_rootScope.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers();
_windowSystem.Draw();
}
_rootScope!.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers();
_windowSystem!.Draw();
}
public void Dispose()
{
_commandManager.RemoveHandler("/pal");
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
}
}
}

View File

@ -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