Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfa4fbd8

Browse files
committed
implemented a generic settings manager
1 parentfc426a8 commitfa4fbd8

File tree

4 files changed

+164
-143
lines changed

4 files changed

+164
-143
lines changed

‎App/App.xaml.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public partial class App : Application
4444
privatereadonlyILogger<App>_logger;
4545
privatereadonlyIUriHandler_uriHandler;
4646

47-
privatereadonlyISettingsManager_settingsManager;
47+
privatereadonlyISettingsManager<CoderConnectSettings>_settingsManager;
4848

4949
privatereadonlyIHostApplicationLifetime_appLifetime;
5050

@@ -94,7 +94,7 @@ public App()
9494
// FileSyncListMainPage is created by FileSyncListWindow.
9595
services.AddTransient<FileSyncListWindow>();
9696

97-
services.AddSingleton<ISettingsManager,SettingsManager>();
97+
services.AddSingleton<ISettingsManager<CoderConnectSettings>,SettingsManager<CoderConnectSettings>>();
9898
services.AddSingleton<IStartupManager,StartupManager>();
9999
// SettingsWindow views and view models
100100
services.AddTransient<SettingsViewModel>();
@@ -118,10 +118,10 @@ public App()
118118
services.AddTransient<TrayWindow>();
119119

120120
_services=services.BuildServiceProvider();
121-
_logger=(ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
122-
_uriHandler=(IUriHandler)_services.GetService(typeof(IUriHandler))!;
123-
_settingsManager=(ISettingsManager)_services.GetService(typeof(ISettingsManager))!;
124-
_appLifetime=(IHostApplicationLifetime)_services.GetRequiredService<IHostApplicationLifetime>();
121+
_logger=_services.GetRequiredService<ILogger<App>>();
122+
_uriHandler=_services.GetRequiredService<IUriHandler>();
123+
_settingsManager=_services.GetRequiredService<ISettingsManager<CoderConnectSettings>>();
124+
_appLifetime=_services.GetRequiredService<IHostApplicationLifetime>();
125125

126126
InitializeComponent();
127127
}
@@ -167,12 +167,15 @@ private async Task InitializeServicesAsync(CancellationToken cancellationToken =
167167
usingvarcredsCts=CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
168168
credsCts.CancelAfter(TimeSpan.FromSeconds(15));
169169

170-
TaskloadCredsTask=credentialManager.LoadCredentials(credsCts.Token);
171-
TaskreconnectTask=rpcController.Reconnect(cancellationToken);
170+
varloadCredsTask=credentialManager.LoadCredentials(credsCts.Token);
171+
varreconnectTask=rpcController.Reconnect(cancellationToken);
172+
varsettingsTask=_settingsManager.Read(cancellationToken);
173+
174+
vardependenciesLoaded=true;
172175

173176
try
174177
{
175-
awaitTask.WhenAll(loadCredsTask,reconnectTask);
178+
awaitTask.WhenAll(loadCredsTask,reconnectTask,settingsTask);
176179
}
177180
catch(Exception)
178181
{
@@ -184,10 +187,17 @@ private async Task InitializeServicesAsync(CancellationToken cancellationToken =
184187
_logger.LogError(reconnectTask.Exception!.GetBaseException(),
185188
"Failed to connect to VPN service");
186189

187-
return;
190+
if(settingsTask.IsFaulted)
191+
_logger.LogError(settingsTask.Exception!.GetBaseException(),
192+
"Failed to fetch Coder Connect settings");
193+
194+
// Don't attempt to connect if we failed to load credentials or reconnect.
195+
// This will prevent the app from trying to connect to the VPN service.
196+
dependenciesLoaded=false;
188197
}
189198

190-
if(_settingsManager.ConnectOnLaunch)
199+
varattemptCoderConnection=settingsTask.Result?.ConnectOnLaunch??false;
200+
if(dependenciesLoaded&&attemptCoderConnection)
191201
{
192202
try
193203
{

‎App/Services/SettingsManager.cs

Lines changed: 114 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,57 @@
1+
usingGoogle.Protobuf.WellKnownTypes;
12
usingSystem;
23
usingSystem.Collections.Generic;
34
usingSystem.IO;
45
usingSystem.Text.Json;
56
usingSystem.Text.Json.Serialization;
7+
usingSystem.Threading;
8+
usingSystem.Threading.Tasks;
9+
usingSystem.Xml.Linq;
610

711
namespaceCoder.Desktop.App.Services;
812

913
/// <summary>
1014
/// Settings contract exposing properties for app settings.
1115
/// </summary>
12-
publicinterfaceISettingsManager
16+
publicinterfaceISettingsManager<T>whereT:ISettings,new()
1317
{
1418
/// <summary>
15-
/// Returns the value of the StartOnLogin setting. Returns <c>false</c> if the key is not found.
19+
/// Reads the settings from the file system.
20+
/// Always returns the latest settings, even if they were modified by another instance of the app.
21+
/// Returned object is always a fresh instance, so it can be modified without affecting the stored settings.
1622
/// </summary>
17-
boolStartOnLogin{get;set;}
18-
23+
/// <param name="ct"></param>
24+
/// <returns></returns>
25+
publicTask<T>Read(CancellationTokenct=default);
26+
/// <summary>
27+
/// Writes the settings to the file system.
28+
/// </summary>
29+
/// <param name="settings">Object containing the settings.</param>
30+
/// <param name="ct"></param>
31+
/// <returns></returns>
32+
publicTaskWrite(Tsettings,CancellationTokenct=default);
1933
/// <summary>
20-
/// Returnsthe value oftheConnectOnLaunch setting. Returns <c>false</c> if the key isnotfound.
34+
/// Returnsnull ifthesettings are not cached ornotavailable.
2135
/// </summary>
22-
boolConnectOnLaunch{get;set;}
36+
/// <returns></returns>
37+
publicT?GetFromCache();
2338
}
2439

2540
/// <summary>
2641
/// Implemention of <see cref="ISettingsManager"/> that persists settings to a JSON file
2742
/// located in the user's local application data folder.
2843
/// </summary>
29-
publicsealedclassSettingsManager:ISettingsManager
44+
publicsealedclassSettingsManager<T>:ISettingsManager<T>whereT:ISettings,new()
3045
{
3146
privatereadonlystring_settingsFilePath;
32-
privateSettings_settings;
33-
privatereadonlystring_fileName="app-settings.json";
3447
privatereadonlystring_appName="CoderDesktop";
48+
privatestring_fileName;
3549
privatereadonlyobject_lock=new();
3650

37-
publicconststringConnectOnLaunchKey="ConnectOnLaunch";
38-
publicconststringStartOnLoginKey="StartOnLogin";
51+
privateT?_cachedSettings;
3952

40-
publicboolStartOnLogin
41-
{
42-
get
43-
{
44-
returnRead(StartOnLoginKey,false);
45-
}
46-
set
47-
{
48-
Save(StartOnLoginKey,value);
49-
}
50-
}
51-
52-
publicboolConnectOnLaunch
53-
{
54-
get
55-
{
56-
returnRead(ConnectOnLaunchKey,false);
57-
}
58-
set
59-
{
60-
Save(ConnectOnLaunchKey,value);
61-
}
62-
}
53+
privatereadonlySemaphoreSlim_gate=new(1,1);
54+
privatestaticreadonlyTimeSpanLockTimeout=TimeSpan.FromSeconds(3);
6355

6456
/// <param name="settingsFilePath">
6557
/// For unit‑tests you can pass an absolute path that already exists.
@@ -81,109 +73,129 @@ public SettingsManager(string? settingsFilePath = null)
8173
_appName);
8274

8375
Directory.CreateDirectory(folder);
76+
77+
_fileName=T.SettingsFileName;
8478
_settingsFilePath=Path.Combine(folder,_fileName);
79+
}
8580

86-
if(!File.Exists(_settingsFilePath))
81+
publicasyncTask<T>Read(CancellationTokenct=default)
82+
{
83+
// try to get the lock with short timeout
84+
if(!await_gate.WaitAsync(LockTimeout,ct).ConfigureAwait(false))
85+
thrownewInvalidOperationException(
86+
$"Could not acquire the settings lock within{LockTimeout.TotalSeconds} s.");
87+
88+
try
8789
{
88-
// Create the settings file if it doesn't exist
89-
_settings=new();
90-
File.WriteAllText(_settingsFilePath,JsonSerializer.Serialize(_settings,SettingsJsonContext.Default.Settings));
90+
if(!File.Exists(_settingsFilePath))
91+
returnnew();
92+
93+
varjson=awaitFile.ReadAllTextAsync(_settingsFilePath,ct)
94+
.ConfigureAwait(false);
95+
96+
// deserialize; fall back to default(T) if empty or malformed
97+
varresult=JsonSerializer.Deserialize<T>(json)!;
98+
_cachedSettings=result;
99+
returnresult;
91100
}
92-
else
101+
catch(OperationCanceledException)
93102
{
94-
_settings=Load();
103+
throw;// propagate caller-requested cancellation
95104
}
96-
}
97-
98-
privatevoidSave(stringname,boolvalue)
99-
{
100-
lock(_lock)
105+
catch(Exceptionex)
101106
{
102-
try
103-
{
104-
// We lock the file for the entire operation to prevent concurrent writes
105-
usingvarfs=newFileStream(_settingsFilePath,
106-
FileMode.OpenOrCreate,
107-
FileAccess.ReadWrite,
108-
FileShare.None);
109-
110-
// Ensure cache is loaded before saving
111-
varfreshCache=JsonSerializer.Deserialize(fs,SettingsJsonContext.Default.Settings)??new();
112-
_settings=freshCache;
113-
_settings.Options[name]=JsonSerializer.SerializeToElement(value);
114-
fs.Position=0;// Reset stream position to the beginning before writing
115-
116-
JsonSerializer.Serialize(fs,_settings,SettingsJsonContext.Default.Settings);
117-
118-
// This ensures the file is truncated to the new length
119-
// if the new content is shorter than the old content
120-
fs.SetLength(fs.Position);
121-
}
122-
catch
123-
{
124-
thrownewInvalidOperationException($"Failed to persist settings to{_settingsFilePath}. The file may be corrupted, malformed or locked.");
125-
}
107+
thrownewInvalidOperationException(
108+
$"Failed to read settings from{_settingsFilePath}. "+
109+
"The file may be corrupted, malformed or locked.",ex);
126110
}
127-
}
128-
129-
privateboolRead(stringname,booldefaultValue)
130-
{
131-
lock(_lock)
111+
finally
132112
{
133-
if(_settings.Options.TryGetValue(name,outvarelement))
134-
{
135-
try
136-
{
137-
returnelement.Deserialize<bool?>()??defaultValue;
138-
}
139-
catch
140-
{
141-
// malformed value – return default value
142-
returndefaultValue;
143-
}
144-
}
145-
returndefaultValue;// key not found – return default value
113+
_gate.Release();
146114
}
147115
}
148116

149-
privateSettingsLoad()
117+
publicasyncTaskWrite(Tsettings,CancellationTokenct=default)
150118
{
119+
// try to get the lock with short timeout
120+
if(!await_gate.WaitAsync(LockTimeout,ct).ConfigureAwait(false))
121+
thrownewInvalidOperationException(
122+
$"Could not acquire the settings lock within{LockTimeout.TotalSeconds} s.");
123+
151124
try
152125
{
153-
usingvarfs=File.OpenRead(_settingsFilePath);
154-
returnJsonSerializer.Deserialize(fs,SettingsJsonContext.Default.Settings)??new();
126+
// overwrite the settings file with the new settings
127+
varjson=JsonSerializer.Serialize(
128+
settings,newJsonSerializerOptions(){WriteIndented=true});
129+
_cachedSettings=settings;// cache the settings
130+
awaitFile.WriteAllTextAsync(_settingsFilePath,json,ct)
131+
.ConfigureAwait(false);
132+
}
133+
catch(OperationCanceledException)
134+
{
135+
throw;// let callers observe cancellation
155136
}
156137
catch(Exceptionex)
157138
{
158-
thrownewInvalidOperationException($"Failed to load settings from{_settingsFilePath}. The file may be corrupted or malformed. Exception:{ex.Message}");
139+
thrownewInvalidOperationException(
140+
$"Failed to persist settings to{_settingsFilePath}. "+
141+
"The file may be corrupted, malformed or locked.",ex);
142+
}
143+
finally
144+
{
145+
_gate.Release();
159146
}
160147
}
148+
149+
publicT?GetFromCache()
150+
{
151+
return_cachedSettings;
152+
}
161153
}
162154

163-
publicclassSettings
155+
publicinterfaceISettings
164156
{
165157
/// <summary>
166-
/// User settings version. Increment this when the settings schema changes.
158+
/// Gets the version of the settings schema.
159+
/// </summary>
160+
intVersion{get;}
161+
162+
/// <summary>
163+
/// FileName where the settings are stored.
164+
/// </summary>
165+
staticabstractstringSettingsFileName{get;}
166+
}
167+
168+
/// <summary>
169+
/// CoderConnect settings class that holds the settings for the CoderConnect feature.
170+
/// </summary>
171+
publicclassCoderConnectSettings:ISettings
172+
{
173+
/// <summary>
174+
/// CoderConnect settings version. Increment this when the settings schema changes.
167175
/// In future iterations we will be able to handle migrations when the user has
168176
/// an older version.
169177
/// </summary>
170178
publicintVersion{get;set;}
171-
publicDictionary<string,JsonElement>Options{get;set;}
179+
publicboolConnectOnLaunch{get;set;}
180+
publicstaticstringSettingsFileName{get;}="coder-connect-settings.json";
172181

173182
privateconstintVERSION=1;// Default version for backward compatibility
174-
publicSettings()
183+
publicCoderConnectSettings()
175184
{
176185
Version=VERSION;
177-
Options=[];
186+
ConnectOnLaunch=false;
178187
}
179188

180-
publicSettings(int?version,Dictionary<string,JsonElement>options)
189+
publicCoderConnectSettings(int?version,boolconnectOnLogin)
181190
{
182191
Version=version??VERSION;
183-
Options=options;
192+
ConnectOnLaunch=connectOnLogin;
184193
}
185-
}
186194

187-
[JsonSerializable(typeof(Settings))]
188-
[JsonSourceGenerationOptions(WriteIndented=true)]
189-
publicpartialclassSettingsJsonContext:JsonSerializerContext;
195+
publicCoderConnectSettingsClone()
196+
{
197+
returnnewCoderConnectSettings(Version,ConnectOnLaunch);
198+
}
199+
200+
201+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp