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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy