From ad42b29a444ee5283a7e4e435639d74de4a3ac2f Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sat, 30 Nov 2024 18:09:18 +0100 Subject: [PATCH] rework configuration save, load configuration backups when available and config cannot be read fix unnecessary config reload on save --- MareAPI | 2 +- .../ConfigurationSaveService.cs | 137 ++++++++++++++++++ .../ConfigurationServiceBase.cs | 99 +++++++------ .../MareConfiguration/IConfigService.cs | 12 ++ .../MareConfiguration/MareConfigService.cs | 2 +- .../MareConfiguration/NotesConfigService.cs | 2 +- .../PlayerPerformanceConfigService.cs | 2 +- .../ServerBlockConfigService.cs | 2 +- .../MareConfiguration/ServerConfigService.cs | 2 +- .../ServerTagConfigService.cs | 2 +- .../SyncshellConfigService.cs | 2 +- .../TransientConfigService.cs | 2 +- .../XivDataStorageService.cs | 2 +- MareSynchronos/Plugin.cs | 13 ++ MareSynchronos/UI/SyncshellAdminUI.cs | 2 +- 15 files changed, 225 insertions(+), 58 deletions(-) create mode 100644 MareSynchronos/MareConfiguration/ConfigurationSaveService.cs create mode 100644 MareSynchronos/MareConfiguration/IConfigService.cs diff --git a/MareAPI b/MareAPI index 9387f02..9c9b7d9 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 9387f020dc21ac7795d2300ed99dea4704e76e2c +Subproject commit 9c9b7d90c1d242bfae3d1df6083409ed8786c841 diff --git a/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs b/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs new file mode 100644 index 0000000..64a8ea1 --- /dev/null +++ b/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs @@ -0,0 +1,137 @@ +using MareSynchronos.MareConfiguration.Configurations; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; +using System.Text.Json; + +namespace MareSynchronos.MareConfiguration; + +public class ConfigurationSaveService : IHostedService +{ + private readonly HashSet _configsToSave = []; + private readonly ILogger _logger; + private readonly SemaphoreSlim _configSaveSemaphore = new(1, 1); + private readonly CancellationTokenSource _configSaveCheckCts = new(); + public const string BackupFolder = "config_backup"; + private readonly MethodInfo _saveMethod; + + public ConfigurationSaveService(ILogger logger, IEnumerable> configs) + { + foreach (var config in configs) + { + config.ConfigSave += OnConfigurationSave; + } + _logger = logger; +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + _saveMethod = GetType().GetMethod(nameof(SaveConfig), BindingFlags.Instance | BindingFlags.NonPublic)!; +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + } + + private void OnConfigurationSave(object? sender, EventArgs e) + { + _configSaveSemaphore.Wait(); + _configsToSave.Add(sender!); + _configSaveSemaphore.Release(); + } + + private async Task PeriodicSaveCheck(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await SaveConfigs().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during SaveConfigs"); + } + + await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); + } + } + + private async Task SaveConfigs() + { + if (_configsToSave.Count == 0) return; + + await _configSaveSemaphore.WaitAsync().ConfigureAwait(false); + var configList = _configsToSave.ToList(); + _configsToSave.Clear(); + _configSaveSemaphore.Release(); + + foreach (var config in configList) + { + var expectedType = config.GetType().BaseType!.GetGenericArguments()[0]; + var save = _saveMethod.MakeGenericMethod(expectedType); + await ((Task)save.Invoke(this, [config])!).ConfigureAwait(false); + } + } + + private async Task SaveConfig(IConfigService config) where T : IMareConfiguration + { + _logger.LogTrace("Saving {configName}", config.ConfigurationName); + var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty); + + try + { + var configBackupFolder = Path.Join(configDir, BackupFolder); + if (!Directory.Exists(configBackupFolder)) + Directory.CreateDirectory(configBackupFolder); + + var configNameSplit = config.ConfigurationName.Split("."); + var existingConfigs = Directory.EnumerateFiles( + configBackupFolder, + configNameSplit[0] + "*") + .Select(c => new FileInfo(c)) + .OrderByDescending(c => c.LastWriteTime).ToList(); + if (existingConfigs.Skip(10).Any()) + { + foreach (var oldBak in existingConfigs.Skip(10).ToList()) + { + oldBak.Delete(); + } + } + + string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]); + _logger.LogTrace("Backing up current config to {backupPath}", backupPath); + File.Copy(config.ConfigurationPath, backupPath, overwrite: true); + FileInfo fi = new(backupPath); + fi.LastWriteTimeUtc = DateTime.UtcNow; + } + catch (Exception ex) + { + // ignore if file cannot be backupped + _logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath); + } + + var temp = config.ConfigurationPath + ".tmp"; + try + { + await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions() + { + WriteIndented = true + })).ConfigureAwait(false); + File.Move(temp, config.ConfigurationPath, true); + config.UpdateLastWriteTime(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during config save of {config}", config.ConfigurationName); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Run(() => PeriodicSaveCheck(_configSaveCheckCts.Token)); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _configSaveCheckCts.CancelAsync().ConfigureAwait(false); + _configSaveCheckCts.Dispose(); + + await SaveConfigs().ConfigureAwait(false); + } +} diff --git a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs index 4e81655..65cf435 100644 --- a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs +++ b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs @@ -3,27 +3,28 @@ using System.Text.Json; namespace MareSynchronos.MareConfiguration; -public abstract class ConfigurationServiceBase : IDisposable where T : IMareConfiguration +public abstract class ConfigurationServiceBase : IConfigService where T : IMareConfiguration { private readonly CancellationTokenSource _periodicCheckCts = new(); - private bool _configIsDirty = false; private DateTime _configLastWriteTime; private Lazy _currentConfigInternal; + private bool _disposed = false; - protected ConfigurationServiceBase(string configurationDirectory) + public event EventHandler? ConfigSave; + + protected ConfigurationServiceBase(string configDirectory) { - ConfigurationDirectory = configurationDirectory; + ConfigurationDirectory = configDirectory; _ = Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token); - _ = Task.Run(CheckForDirtyConfigInternal, _periodicCheckCts.Token); _currentConfigInternal = LazyConfig(); } public string ConfigurationDirectory { get; init; } public T Current => _currentConfigInternal.Value; - protected abstract string ConfigurationName { get; } - protected string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName); + public abstract string ConfigurationName { get; } + public string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName); public void Dispose() { @@ -33,14 +34,20 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC public void Save() { - _configIsDirty = true; + ConfigSave?.Invoke(this, EventArgs.Empty); + } + + public void UpdateLastWriteTime() + { + _configLastWriteTime = GetConfigLastWriteTime(); } protected virtual void Dispose(bool disposing) { + if (!disposing || _disposed) return; + _disposed = true; _periodicCheckCts.Cancel(); _periodicCheckCts.Dispose(); - if (_configIsDirty) SaveDirtyConfig(); } protected T LoadConfig() @@ -48,8 +55,12 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC T? config; if (!File.Exists(ConfigurationPath)) { - config = (T)Activator.CreateInstance(typeof(T))!; - Save(); + config = AttemptToLoadBackup(); + if (Equals(config, default(T))) + { + config = (T)Activator.CreateInstance(typeof(T))!; + Save(); + } } else { @@ -60,9 +71,10 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC catch { // config failed to load for some reason - config = default; + config = AttemptToLoadBackup(); } - if (config == null) + + if (config == null || Equals(config, default(T))) { config = (T)Activator.CreateInstance(typeof(T))!; Save(); @@ -73,35 +85,36 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC return config; } - protected void SaveDirtyConfig() + private T? AttemptToLoadBackup() { - _configIsDirty = false; - var existingConfigs = Directory.EnumerateFiles(ConfigurationDirectory, ConfigurationName + ".bak.*").Select(c => new FileInfo(c)) - .OrderByDescending(c => c.LastWriteTime).ToList(); - if (existingConfigs.Skip(10).Any()) + var configBackupFolder = Path.Join(ConfigurationDirectory, ConfigurationSaveService.BackupFolder); + var configNameSplit = ConfigurationName.Split("."); + if (!Directory.Exists(configBackupFolder)) + return default; + + var existingBackups = Directory.EnumerateFiles(configBackupFolder, configNameSplit[0] + "*").OrderByDescending(f => new FileInfo(f).LastWriteTimeUtc); + foreach (var file in existingBackups) { - foreach (var config in existingConfigs.Skip(10).ToList()) + try { - config.Delete(); + var config = JsonSerializer.Deserialize(File.ReadAllText(file)); + if (Equals(config, default(T))) + { + File.Delete(file); + } + + File.Copy(file, ConfigurationPath, true); + return config; } + catch + { + // couldn't load backup, might as well delete it + File.Delete(file); + } + } - try - { - File.Copy(ConfigurationPath, ConfigurationPath + ".bak." + DateTime.Now.ToString("yyyyMMddHHmmss"), overwrite: true); - } - catch - { - // ignore if file cannot be backupped once - } - - var temp = ConfigurationPath + ".tmp"; - File.WriteAllText(temp, JsonSerializer.Serialize(Current, new JsonSerializerOptions() - { - WriteIndented = true - })); - File.Move(temp, ConfigurationPath, true); - _configLastWriteTime = new FileInfo(ConfigurationPath).LastWriteTimeUtc; + return default; } private async Task CheckForConfigUpdatesInternal() @@ -118,20 +131,12 @@ public abstract class ConfigurationServiceBase : IDisposable where T : IMareC } } - private async Task CheckForDirtyConfigInternal() + private DateTime GetConfigLastWriteTime() { - while (!_periodicCheckCts.IsCancellationRequested) - { - if (_configIsDirty) - { - SaveDirtyConfig(); - } - - await Task.Delay(TimeSpan.FromSeconds(1), _periodicCheckCts.Token).ConfigureAwait(false); - } + try { return new FileInfo(ConfigurationPath).LastWriteTimeUtc; } + catch { return DateTime.MinValue; } } - private DateTime GetConfigLastWriteTime() => new FileInfo(ConfigurationPath).LastWriteTimeUtc; private Lazy LazyConfig() { diff --git a/MareSynchronos/MareConfiguration/IConfigService.cs b/MareSynchronos/MareConfiguration/IConfigService.cs new file mode 100644 index 0000000..a45917a --- /dev/null +++ b/MareSynchronos/MareConfiguration/IConfigService.cs @@ -0,0 +1,12 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public interface IConfigService : IDisposable where T : IMareConfiguration +{ + T Current { get; } + string ConfigurationName { get; } + string ConfigurationPath { get; } + public event EventHandler? ConfigSave; + void UpdateLastWriteTime(); +} diff --git a/MareSynchronos/MareConfiguration/MareConfigService.cs b/MareSynchronos/MareConfiguration/MareConfigService.cs index 7e60baf..39a0599 100644 --- a/MareSynchronos/MareConfiguration/MareConfigService.cs +++ b/MareSynchronos/MareConfiguration/MareConfigService.cs @@ -10,5 +10,5 @@ public class MareConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/NotesConfigService.cs b/MareSynchronos/MareConfiguration/NotesConfigService.cs index ed6cab8..bf8c00b 100644 --- a/MareSynchronos/MareConfiguration/NotesConfigService.cs +++ b/MareSynchronos/MareConfiguration/NotesConfigService.cs @@ -10,5 +10,5 @@ public class NotesConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs b/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs index e07eca1..6140760 100644 --- a/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs +++ b/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs @@ -7,5 +7,5 @@ public class PlayerPerformanceConfigService : ConfigurationServiceBase ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs b/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs index 9aac787..5c85f5d 100644 --- a/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs +++ b/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs @@ -10,5 +10,5 @@ public class ServerBlockConfigService : ConfigurationServiceBase ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerConfigService.cs b/MareSynchronos/MareConfiguration/ServerConfigService.cs index c45120e..185e2fe 100644 --- a/MareSynchronos/MareConfiguration/ServerConfigService.cs +++ b/MareSynchronos/MareConfiguration/ServerConfigService.cs @@ -10,5 +10,5 @@ public class ServerConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerTagConfigService.cs b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs index 2f868b3..fc78403 100644 --- a/MareSynchronos/MareConfiguration/ServerTagConfigService.cs +++ b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs @@ -10,5 +10,5 @@ public class ServerTagConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/SyncshellConfigService.cs b/MareSynchronos/MareConfiguration/SyncshellConfigService.cs index c564b9c..4d34e5a 100644 --- a/MareSynchronos/MareConfiguration/SyncshellConfigService.cs +++ b/MareSynchronos/MareConfiguration/SyncshellConfigService.cs @@ -10,5 +10,5 @@ public class SyncshellConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/TransientConfigService.cs b/MareSynchronos/MareConfiguration/TransientConfigService.cs index 409407a..cae9d02 100644 --- a/MareSynchronos/MareConfiguration/TransientConfigService.cs +++ b/MareSynchronos/MareConfiguration/TransientConfigService.cs @@ -10,5 +10,5 @@ public class TransientConfigService : ConfigurationServiceBase { } - protected override string ConfigurationName => ConfigName; + public override string ConfigurationName => ConfigName; } diff --git a/MareSynchronos/MareConfiguration/XivDataStorageService.cs b/MareSynchronos/MareConfiguration/XivDataStorageService.cs index 96e08bf..4cddfd0 100644 --- a/MareSynchronos/MareConfiguration/XivDataStorageService.cs +++ b/MareSynchronos/MareConfiguration/XivDataStorageService.cs @@ -8,5 +8,5 @@ public class XivDataStorageService : ConfigurationServiceBase ConfigName; + public override string ConfigurationName => ConfigName; } \ No newline at end of file diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 5284c0c..f3a9709 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -7,6 +7,7 @@ using MareSynchronos.FileCache; using MareSynchronos.Interop; using MareSynchronos.Interop.Ipc; using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Configurations; using MareSynchronos.PlayerData.Export; using MareSynchronos.PlayerData.Factories; using MareSynchronos.PlayerData.Pairs; @@ -140,7 +141,18 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); + collection.AddSingleton>(s => s.GetRequiredService()); collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); // add scoped services @@ -168,6 +180,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddScoped(); collection.AddScoped(); + collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/MareSynchronos/UI/SyncshellAdminUI.cs b/MareSynchronos/UI/SyncshellAdminUI.cs index 0092f56..07fc067 100644 --- a/MareSynchronos/UI/SyncshellAdminUI.cs +++ b/MareSynchronos/UI/SyncshellAdminUI.cs @@ -351,7 +351,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase using var pushId = ImRaii.PushId(bannedUser.UID); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) { - _ = _apiController.GroupUnbanUser(bannedUser); + _apiController.GroupUnbanUser(bannedUser); _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); } }