Skip to content

Commit 7bef041

Browse files
committed
settings manager moved from generic to explicit settings
1 parent e7a9d70 commit 7bef041

File tree

5 files changed

+183
-141
lines changed

5 files changed

+183
-141
lines changed

App/App.xaml.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public App()
9292
// FileSyncListMainPage is created by FileSyncListWindow.
9393
services.AddTransient<FileSyncListWindow>();
9494

95-
services.AddSingleton<ISettingsManager>(_ => new SettingsManager("CoderDesktop"));
95+
services.AddSingleton<ISettingsManager, SettingsManager>();
96+
services.AddSingleton<IStartupManager, StartupManager>();
9697
// SettingsWindow views and view models
9798
services.AddTransient<SettingsViewModel>();
9899
// SettingsMainPage is created by SettingsWindow.
@@ -159,10 +160,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
159160

160161
// Start connecting to the manager in the background.
161162
var rpcController = _services.GetRequiredService<IRpcController>();
162-
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
163-
// Passing in a CT with no cancellation is desired here, because
164-
// the named pipe open will block until the pipe comes up.
165-
_logger.LogDebug("reconnecting with VPN service");
166163
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
167164
{
168165
if (t.Exception != null)
@@ -172,22 +169,18 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
172169
Debug.WriteLine(t.Exception);
173170
Debugger.Break();
174171
#endif
175-
} else
172+
return;
173+
}
174+
if (_settingsManager.ConnectOnLaunch)
176175
{
177-
if (rpcController.GetState().VpnLifecycle == VpnLifecycle.Stopped)
176+
_logger.LogInformation("RPC lifecycle is disconnected, but ConnectOnLaunch is enabled; attempting to connect");
177+
_ = rpcController.StartVpn(CancellationToken.None).ContinueWith(connectTask =>
178178
{
179-
if (_settingsManager.Read(SettingsManager.ConnectOnLaunchKey, false))
179+
if (connectTask.Exception != null)
180180
{
181-
_logger.LogInformation("RPC lifecycle is disconnected, but ConnectOnLaunch is enabled; attempting to connect");
182-
_ = rpcController.StartVpn(CancellationToken.None).ContinueWith(connectTask =>
183-
{
184-
if (connectTask.Exception != null)
185-
{
186-
_logger.LogError(connectTask.Exception, "failed to connect on launch");
187-
}
188-
});
181+
_logger.LogError(connectTask.Exception, "failed to connect on launch");
189182
}
190-
}
183+
});
191184
}
192185
});
193186

App/Services/SettingsManager.cs

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,110 +4,150 @@
44
using System.Text.Json;
55

66
namespace Coder.Desktop.App.Services;
7+
78
/// <summary>
8-
/// Generic persistence contract for simple key/value settings.
9+
/// Settings contract exposing properties for app settings.
910
/// </summary>
1011
public interface ISettingsManager
1112
{
1213
/// <summary>
13-
/// Saves <paramref name="value"/> under <paramref name="name"/> and returns the value.
14+
/// Returns the value of the StartOnLogin setting. Returns <c>false</c> if the key is not found.
1415
/// </summary>
15-
T Save<T>(string name, T value);
16+
bool StartOnLogin { get; set; }
1617

1718
/// <summary>
18-
/// Reads the setting or returns <paramref name="defaultValue"/> when the key is missing.
19+
/// Returns the value of the ConnectOnLaunch setting. Returns <c>false</c> if the key is not found.
1920
/// </summary>
20-
T Read<T>(string name, T defaultValue);
21+
bool ConnectOnLaunch { get; set; }
2122
}
23+
2224
/// <summary>
23-
/// JSON‑file implementation that works in unpackaged Win32/WinUI 3 apps.
25+
/// Implemention of <see cref="ISettingsManager"/> that persists settings to a JSON file
26+
/// located in the user's local application data folder.
2427
/// </summary>
2528
public sealed class SettingsManager : ISettingsManager
2629
{
2730
private readonly string _settingsFilePath;
2831
private readonly string _fileName = "app-settings.json";
32+
private readonly string _appName = "CoderDesktop";
2933
private readonly object _lock = new();
3034
private Dictionary<string, JsonElement> _cache;
3135

32-
public static readonly string ConnectOnLaunchKey = "ConnectOnLaunch";
33-
public static readonly string StartOnLoginKey = "StartOnLogin";
36+
public const string ConnectOnLaunchKey = "ConnectOnLaunch";
37+
public const string StartOnLoginKey = "StartOnLogin";
3438

35-
/// <param name="appName">
36-
/// Sub‑folder under %LOCALAPPDATA% (e.g. "CoderDesktop").
37-
/// If <c>null</c> the folder name defaults to the executable name.
39+
public bool StartOnLogin
40+
{
41+
get
42+
{
43+
return Read(StartOnLoginKey, false);
44+
}
45+
set
46+
{
47+
Save(StartOnLoginKey, value);
48+
}
49+
}
50+
51+
public bool ConnectOnLaunch
52+
{
53+
get
54+
{
55+
return Read(ConnectOnLaunchKey, false);
56+
}
57+
set
58+
{
59+
Save(ConnectOnLaunchKey, value);
60+
}
61+
}
62+
63+
/// <param name="settingsFilePath">
3864
/// For unit‑tests you can pass an absolute path that already exists.
65+
/// Otherwise the settings file will be created in the user's local application data folder.
3966
/// </param>
40-
public SettingsManager(string? appName = null)
67+
public SettingsManager(string? settingsFilePath = null)
4168
{
42-
// Allow unit‑tests to inject a fully‑qualified path.
43-
if (appName is not null && Path.IsPathRooted(appName))
69+
if (settingsFilePath is null)
4470
{
45-
_settingsFilePath = Path.Combine(appName, _fileName);
46-
Directory.CreateDirectory(Path.GetDirectoryName(_settingsFilePath)!);
71+
settingsFilePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
4772
}
48-
else
73+
else if (!Path.IsPathRooted(settingsFilePath))
4974
{
50-
string folder = Path.Combine(
51-
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
52-
appName ?? AppDomain.CurrentDomain.FriendlyName.ToLowerInvariant());
53-
Directory.CreateDirectory(folder);
54-
_settingsFilePath = Path.Combine(folder, _fileName);
75+
throw new ArgumentException("settingsFilePath must be an absolute path if provided", nameof(settingsFilePath));
76+
}
77+
78+
string folder = Path.Combine(
79+
settingsFilePath,
80+
_appName);
81+
82+
Directory.CreateDirectory(folder);
83+
_settingsFilePath = Path.Combine(folder, _fileName);
84+
85+
if(!File.Exists(_settingsFilePath))
86+
{
87+
// Create the settings file if it doesn't exist
88+
string emptyJson = JsonSerializer.Serialize(new { });
89+
File.WriteAllText(_settingsFilePath, emptyJson);
5590
}
5691

5792
_cache = Load();
5893
}
5994

60-
public T Save<T>(string name, T value)
95+
private void Save(string name, bool value)
6196
{
6297
lock (_lock)
6398
{
64-
_cache[name] = JsonSerializer.SerializeToElement(value);
65-
Persist();
66-
return value;
99+
try
100+
{
101+
// Ensure cache is loaded before saving
102+
using var fs = new FileStream(_settingsFilePath,
103+
FileMode.OpenOrCreate,
104+
FileAccess.ReadWrite,
105+
FileShare.None);
106+
107+
var currentCache = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fs) ?? new();
108+
_cache = currentCache;
109+
_cache[name] = JsonSerializer.SerializeToElement(value);
110+
fs.Position = 0; // Reset stream position to the beginning before writing to override the file
111+
var options = new JsonSerializerOptions { WriteIndented = true};
112+
JsonSerializer.Serialize(fs, _cache, options);
113+
}
114+
catch
115+
{
116+
throw new InvalidOperationException($"Failed to persist settings to {_settingsFilePath}. The file may be corrupted, malformed or locked.");
117+
}
67118
}
68119
}
69120

70-
public T Read<T>(string name, T defaultValue)
121+
private bool Read(string name, bool defaultValue)
71122
{
72123
lock (_lock)
73124
{
74125
if (_cache.TryGetValue(name, out var element))
75126
{
76127
try
77128
{
78-
return element.Deserialize<T>() ?? defaultValue;
129+
return element.Deserialize<bool?>() ?? defaultValue;
79130
}
80131
catch
81132
{
82-
// Malformed value – fall back.
133+
// malformed value – return default value
83134
return defaultValue;
84135
}
85136
}
86-
return defaultValue; // key not found – return caller‑supplied default (false etc.)
137+
return defaultValue; // key not found – return default value
87138
}
88139
}
89140

90141
private Dictionary<string, JsonElement> Load()
91142
{
92-
if (!File.Exists(_settingsFilePath))
93-
return new();
94-
95143
try
96144
{
97145
using var fs = File.OpenRead(_settingsFilePath);
98146
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(fs) ?? new();
99147
}
100-
catch
148+
catch (Exception ex)
101149
{
102-
// Corrupted file – start fresh.
103-
return new();
150+
throw new InvalidOperationException($"Failed to load settings from {_settingsFilePath}. The file may be corrupted or malformed. Exception: {ex.Message}");
104151
}
105152
}
106-
107-
private void Persist()
108-
{
109-
using var fs = File.Create(_settingsFilePath);
110-
var options = new JsonSerializerOptions { WriteIndented = true };
111-
JsonSerializer.Serialize(fs, _cache, options);
112-
}
113153
}

App/Services/StartupManager.cs

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,30 @@
44
using System.Security;
55

66
namespace Coder.Desktop.App.Services;
7-
public static class StartupManager
7+
8+
public interface IStartupManager
9+
{
10+
/// <summary>
11+
/// Adds the current executable to the per‑user Run key. Returns <c>true</c> if successful.
12+
/// Fails (returns <c>false</c>) when blocked by policy or lack of permissions.
13+
/// </summary>
14+
bool Enable();
15+
/// <summary>
16+
/// Removes the value from the Run key (no-op if missing).
17+
/// </summary>
18+
void Disable();
19+
/// <summary>
20+
/// Checks whether the value exists in the Run key.
21+
/// </summary>
22+
bool IsEnabled();
23+
/// <summary>
24+
/// Detects whether group policy disables per‑user startup programs.
25+
/// Mirrors <see cref="Windows.ApplicationModel.StartupTaskState.DisabledByPolicy"/>.
26+
/// </summary>
27+
bool IsDisabledByPolicy();
28+
}
29+
30+
public class StartupManager : IStartupManager
831
{
932
private const string RunKey = @"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
1033
private const string PoliciesExplorerUser = @"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer";
@@ -14,11 +37,7 @@ public static class StartupManager
1437

1538
private const string _defaultValueName = "CoderDesktopApp";
1639

17-
/// <summary>
18-
/// Adds the current executable to the per‑user Run key. Returns <c>true</c> if successful.
19-
/// Fails (returns <c>false</c>) when blocked by policy or lack of permissions.
20-
/// </summary>
21-
public static bool Enable()
40+
public bool Enable()
2241
{
2342
if (IsDisabledByPolicy())
2443
return false;
@@ -35,25 +54,19 @@ public static bool Enable()
3554
catch (SecurityException) { return false; }
3655
}
3756

38-
/// <summary>Removes the value from the Run key (no-op if missing).</summary>
39-
public static void Disable()
57+
public void Disable()
4058
{
4159
using var key = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
4260
key?.DeleteValue(_defaultValueName, throwOnMissingValue: false);
4361
}
4462

45-
/// <summary>Checks whether the value exists in the Run key.</summary>
46-
public static bool IsEnabled()
63+
public bool IsEnabled()
4764
{
4865
using var key = Registry.CurrentUser.OpenSubKey(RunKey);
4966
return key?.GetValue(_defaultValueName) != null;
5067
}
5168

52-
/// <summary>
53-
/// Detects whether group policy disables per‑user startup programs.
54-
/// Mirrors <see cref="Windows.ApplicationModel.StartupTaskState.DisabledByPolicy"/>.
55-
/// </summary>
56-
public static bool IsDisabledByPolicy()
69+
public bool IsDisabledByPolicy()
5770
{
5871
// User policy – HKCU
5972
using (var keyUser = Registry.CurrentUser.OpenSubKey(PoliciesExplorerUser))
@@ -65,8 +78,6 @@ public static bool IsDisabledByPolicy()
6578
{
6679
if ((int?)keyMachine?.GetValue(DisableLocalMachineRun) == 1) return true;
6780
}
68-
69-
// Some non‑desktop SKUs report DisabledByPolicy implicitly
7081
return false;
7182
}
7283
}

0 commit comments

Comments
 (0)