Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Endpoints/EngineEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
8 changes: 8 additions & 0 deletions LocalLLMServerManager.Shared/Interfaces/ISettingsService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using LocalLLMServerManager.Shared.Models;

namespace LocalLLMServerManager.Shared.Interfaces;
Expand All @@ -7,4 +9,10 @@ public interface ISettingsService
string SettingsFilePath();
AppSettings LoadSettings();
void SaveSettings(AppSettings settings);
Task<AppSettings> LoadSettingsAsync(CancellationToken cancellationToken = default) => Task.FromResult(LoadSettings());
Task SaveSettingsAsync(AppSettings settings, CancellationToken cancellationToken = default)
{
SaveSettings(settings);
return Task.CompletedTask;
}
}
35 changes: 35 additions & 0 deletions LocalLLMServerManager.Tests/AppSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

4 changes: 4 additions & 0 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ public static AppBuilder BuildAvaloniaApp()

public static void SaveSettings(AppSettings settings) => new SettingsService().SaveSettings(settings);

public static Task<AppSettings> 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;
Expand Down
99 changes: 92 additions & 7 deletions Services/SettingsService.cs
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -15,7 +17,8 @@ public string SettingsFilePath()

public AppSettings LoadSettings()
{
lock (SettingsLock)
SettingsSemaphore.Wait();
try
{
if (_cachedSettings != null)
{
Expand All @@ -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++)
{
Expand All @@ -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<AppSettings> 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<AppSettings>(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();
}
}
}
Loading