diff --git a/Endpoints/EngineEndpoints.cs b/Endpoints/EngineEndpoints.cs index 541111f..2171859 100644 --- a/Endpoints/EngineEndpoints.cs +++ b/Endpoints/EngineEndpoints.cs @@ -21,14 +21,14 @@ public static void MapEngineEndpoints(this WebApplication app) }); }); - app.MapGet("/api/settings", (ISettingsService settingsService) => + app.MapGet("/api/settings", async (ISettingsService settingsService, CancellationToken ct) => { - return Results.Ok(settingsService.LoadSettings()); + return Results.Ok(await settingsService.LoadSettingsAsync(ct)); }); - app.MapPost("/api/settings", (AppSettings newSettings, ISettingsService settingsService) => + app.MapPost("/api/settings", async (AppSettings newSettings, ISettingsService settingsService, CancellationToken ct) => { - settingsService.SaveSettings(newSettings); + await settingsService.SaveSettingsAsync(newSettings, ct); return Results.Ok(newSettings); }); diff --git a/LocalLLMServerManager.Shared/Interfaces/ISettingsService.cs b/LocalLLMServerManager.Shared/Interfaces/ISettingsService.cs index dbf76a0..7fadc8b 100644 --- a/LocalLLMServerManager.Shared/Interfaces/ISettingsService.cs +++ b/LocalLLMServerManager.Shared/Interfaces/ISettingsService.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using LocalLLMServerManager.Shared.Models; namespace LocalLLMServerManager.Shared.Interfaces; @@ -7,4 +9,10 @@ public interface ISettingsService string SettingsFilePath(); AppSettings LoadSettings(); void SaveSettings(AppSettings settings); + Task LoadSettingsAsync(CancellationToken cancellationToken = default) => Task.FromResult(LoadSettings()); + Task SaveSettingsAsync(AppSettings settings, CancellationToken cancellationToken = default) + { + SaveSettings(settings); + return Task.CompletedTask; + } } diff --git a/LocalLLMServerManager.Tests/AppSettingsTests.cs b/LocalLLMServerManager.Tests/AppSettingsTests.cs index 160921e..6d437ac 100644 --- a/LocalLLMServerManager.Tests/AppSettingsTests.cs +++ b/LocalLLMServerManager.Tests/AppSettingsTests.cs @@ -135,4 +135,39 @@ public void SettingsService_UsesCachedInstance_AfterInitialLoad() Assert.Same(firstLoad, secondLoad); } + + [Fact] + public async Task SettingsService_SaveAndLoadAsync_RoundTripsSuccessfully() + { + var service = new SettingsService(); + var original = await service.LoadSettingsAsync(); + + try + { + var customSettings = new AppSettings( + ForgeModelsPath: @"C:\CustomForgeAsync", + ComfyModelsPath: @"C:\CustomComfyAsync" + ); + await service.SaveSettingsAsync(customSettings); + + var loaded = await service.LoadSettingsAsync(); + Assert.Equal(@"C:\CustomForgeAsync", loaded.ForgeModelsPath); + Assert.Equal(@"C:\CustomComfyAsync", loaded.ComfyModelsPath); + } + finally + { + await service.SaveSettingsAsync(original); + } + } + + [Fact] + public async Task SettingsService_SyncAndAsync_ShareCachedInstance() + { + var service = new SettingsService(); + var syncLoad = service.LoadSettings(); + var asyncLoad = await service.LoadSettingsAsync(); + + Assert.Same(syncLoad, asyncLoad); + } } + diff --git a/Program.cs b/Program.cs index 5bd85e6..7ff31e4 100644 --- a/Program.cs +++ b/Program.cs @@ -99,6 +99,10 @@ public static AppBuilder BuildAvaloniaApp() public static void SaveSettings(AppSettings settings) => new SettingsService().SaveSettings(settings); + public static Task LoadSettingsAsync(CancellationToken cancellationToken = default) => new SettingsService().LoadSettingsAsync(cancellationToken); + + public static Task SaveSettingsAsync(AppSettings settings, CancellationToken cancellationToken = default) => new SettingsService().SaveSettingsAsync(settings, cancellationToken); + public static string ResolvePath(string? rawPath, string fallbackRelativePath = "") { var target = string.IsNullOrWhiteSpace(rawPath) ? fallbackRelativePath : rawPath; diff --git a/Services/SettingsService.cs b/Services/SettingsService.cs index ace7049..1acc84a 100644 --- a/Services/SettingsService.cs +++ b/Services/SettingsService.cs @@ -1,11 +1,13 @@ using System.IO; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; namespace LocalLLMServerManager.Services; public class SettingsService : ISettingsService { - private static readonly object SettingsLock = new(); + private static readonly SemaphoreSlim SettingsSemaphore = new(1, 1); private static AppSettings? _cachedSettings; public string SettingsFilePath() @@ -15,7 +17,8 @@ public string SettingsFilePath() public AppSettings LoadSettings() { - lock (SettingsLock) + SettingsSemaphore.Wait(); + try { if (_cachedSettings != null) { @@ -37,18 +40,26 @@ public AppSettings LoadSettings() } catch { - Thread.Sleep(50); + if (i < 4) + { + Thread.Sleep(50); + } } } - + _cachedSettings = new AppSettings(); return _cachedSettings; } + finally + { + SettingsSemaphore.Release(); + } } public void SaveSettings(AppSettings settings) { - lock (SettingsLock) + SettingsSemaphore.Wait(); + try { for (int i = 0; i < 5; i++) { @@ -57,16 +68,90 @@ public void SaveSettings(AppSettings settings) var path = SettingsFilePath(); var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(path, json); - + // Update cache after successful save _cachedSettings = settings; break; } catch { - Thread.Sleep(50); + if (i < 4) + { + Thread.Sleep(50); + } } } } + finally + { + SettingsSemaphore.Release(); + } + } + + public async Task LoadSettingsAsync(CancellationToken cancellationToken = default) + { + await SettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_cachedSettings != null) + { + return _cachedSettings; + } + + for (int i = 0; i < 5; i++) + { + try + { + var path = SettingsFilePath(); + if (File.Exists(path)) + { + var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + _cachedSettings = JsonSerializer.Deserialize(json) ?? new AppSettings(); + return _cachedSettings; + } + break; + } + catch when (i < 4) + { + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + } + + _cachedSettings = new AppSettings(); + return _cachedSettings; + } + finally + { + SettingsSemaphore.Release(); + } + } + + public async Task SaveSettingsAsync(AppSettings settings, CancellationToken cancellationToken = default) + { + await SettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + for (int i = 0; i < 5; i++) + { + try + { + var path = SettingsFilePath(); + var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(path, json, cancellationToken).ConfigureAwait(false); + + // Update cache after successful save + _cachedSettings = settings; + break; + } + catch when (i < 4) + { + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + } + } + finally + { + SettingsSemaphore.Release(); + } } }