1
+ using Google . Protobuf . WellKnownTypes ;
1
2
using System ;
2
3
using System . Collections . Generic ;
3
4
using System . IO ;
4
5
using System . Text . Json ;
5
6
using System . Text . Json . Serialization ;
7
+ using System . Threading ;
8
+ using System . Threading . Tasks ;
9
+ using System . Xml . Linq ;
6
10
7
11
namespace Coder . Desktop . App . Services ;
8
12
9
13
/// <summary>
10
14
/// Settings contract exposing properties for app settings.
11
15
/// </summary>
12
- public interface ISettingsManager
16
+ public interface ISettingsManager < T > where T : ISettings , new ( )
13
17
{
14
18
/// <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.
16
22
/// </summary>
17
- bool StartOnLogin { get ; set ; }
18
-
23
+ /// <param name="ct"></param>
24
+ /// <returns></returns>
25
+ public Task < T > Read ( CancellationToken ct = 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
+ public Task Write ( T settings , CancellationToken ct = default ) ;
19
33
/// <summary>
20
- /// Returnsthe value of theConnectOnLaunch setting. Returns <c>false</c> if the key is notfound .
34
+ /// Returnsnull if thesettings are not cached or notavailable .
21
35
/// </summary>
22
- bool ConnectOnLaunch { get ; set ; }
36
+ /// <returns></returns>
37
+ public T ? GetFromCache ( ) ;
23
38
}
24
39
25
40
/// <summary>
26
41
/// Implemention of <see cref="ISettingsManager"/> that persists settings to a JSON file
27
42
/// located in the user's local application data folder.
28
43
/// </summary>
29
- public sealed class SettingsManager : ISettingsManager
44
+ public sealed class SettingsManager < T > : ISettingsManager < T > where T : ISettings , new ( )
30
45
{
31
46
private readonly string _settingsFilePath ;
32
- private Settings _settings ;
33
- private readonly string _fileName = "app-settings.json" ;
34
47
private readonly string _appName = "CoderDesktop" ;
48
+ private string _fileName ;
35
49
private readonly object _lock = new ( ) ;
36
50
37
- public const string ConnectOnLaunchKey = "ConnectOnLaunch" ;
38
- public const string StartOnLoginKey = "StartOnLogin" ;
51
+ private T ? _cachedSettings ;
39
52
40
- public bool StartOnLogin
41
- {
42
- get
43
- {
44
- return Read ( StartOnLoginKey , false ) ;
45
- }
46
- set
47
- {
48
- Save ( StartOnLoginKey , value ) ;
49
- }
50
- }
51
-
52
- public bool ConnectOnLaunch
53
- {
54
- get
55
- {
56
- return Read ( ConnectOnLaunchKey , false ) ;
57
- }
58
- set
59
- {
60
- Save ( ConnectOnLaunchKey , value ) ;
61
- }
62
- }
53
+ private readonly SemaphoreSlim _gate = new ( 1 , 1 ) ;
54
+ private static readonly TimeSpan LockTimeout = TimeSpan . FromSeconds ( 3 ) ;
63
55
64
56
/// <param name="settingsFilePath">
65
57
/// For unit‑tests you can pass an absolute path that already exists.
@@ -81,109 +73,129 @@ public SettingsManager(string? settingsFilePath = null)
81
73
_appName ) ;
82
74
83
75
Directory . CreateDirectory ( folder ) ;
76
+
77
+ _fileName = T . SettingsFileName ;
84
78
_settingsFilePath = Path . Combine ( folder , _fileName ) ;
79
+ }
85
80
86
- if ( ! File . Exists ( _settingsFilePath ) )
81
+ public async Task < T > Read ( CancellationToken ct = default )
82
+ {
83
+ // try to get the lock with short timeout
84
+ if ( ! await _gate . WaitAsync ( LockTimeout , ct ) . ConfigureAwait ( false ) )
85
+ throw new InvalidOperationException (
86
+ $ "Could not acquire the settings lock within{ LockTimeout . TotalSeconds } s.") ;
87
+
88
+ try
87
89
{
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
+ return new ( ) ;
92
+
93
+ var json = await File . ReadAllTextAsync ( _settingsFilePath , ct )
94
+ . ConfigureAwait ( false ) ;
95
+
96
+ // deserialize; fall back to default(T) if empty or malformed
97
+ var result = JsonSerializer . Deserialize < T > ( json ) ! ;
98
+ _cachedSettings = result ;
99
+ return result ;
91
100
}
92
- else
101
+ catch ( OperationCanceledException )
93
102
{
94
- _settings = Load ( ) ;
103
+ throw ; // propagate caller-requested cancellation
95
104
}
96
- }
97
-
98
- private void Save ( string name , bool value )
99
- {
100
- lock ( _lock )
105
+ catch ( Exception ex )
101
106
{
102
- try
103
- {
104
- // We lock the file for the entire operation to prevent concurrent writes
105
- using var fs = new FileStream ( _settingsFilePath ,
106
- FileMode . OpenOrCreate ,
107
- FileAccess . ReadWrite ,
108
- FileShare . None ) ;
109
-
110
- // Ensure cache is loaded before saving
111
- var freshCache = 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
- throw new InvalidOperationException ( $ "Failed to persist settings to{ _settingsFilePath } . The file may be corrupted, malformed or locked.") ;
125
- }
107
+ throw new InvalidOperationException (
108
+ $ "Failed to read settings from{ _settingsFilePath } . "+
109
+ "The file may be corrupted, malformed or locked." , ex ) ;
126
110
}
127
- }
128
-
129
- private bool Read ( string name , bool defaultValue )
130
- {
131
- lock ( _lock )
111
+ finally
132
112
{
133
- if ( _settings . Options . TryGetValue ( name , out var element ) )
134
- {
135
- try
136
- {
137
- return element . Deserialize < bool ? > ( ) ?? defaultValue ;
138
- }
139
- catch
140
- {
141
- // malformed value – return default value
142
- return defaultValue ;
143
- }
144
- }
145
- return defaultValue ; // key not found – return default value
113
+ _gate . Release ( ) ;
146
114
}
147
115
}
148
116
149
- private Settings Load ( )
117
+ public async Task Write ( T settings , CancellationToken ct = default )
150
118
{
119
+ // try to get the lock with short timeout
120
+ if ( ! await _gate . WaitAsync ( LockTimeout , ct ) . ConfigureAwait ( false ) )
121
+ throw new InvalidOperationException (
122
+ $ "Could not acquire the settings lock within{ LockTimeout . TotalSeconds } s.") ;
123
+
151
124
try
152
125
{
153
- using var fs = File . OpenRead ( _settingsFilePath ) ;
154
- return JsonSerializer . Deserialize ( fs , SettingsJsonContext . Default . Settings ) ?? new ( ) ;
126
+ // overwrite the settings file with the new settings
127
+ var json = JsonSerializer . Serialize (
128
+ settings , new JsonSerializerOptions ( ) { WriteIndented = true } ) ;
129
+ _cachedSettings = settings ; // cache the settings
130
+ await File . WriteAllTextAsync ( _settingsFilePath , json , ct )
131
+ . ConfigureAwait ( false ) ;
132
+ }
133
+ catch ( OperationCanceledException )
134
+ {
135
+ throw ; // let callers observe cancellation
155
136
}
156
137
catch ( Exception ex )
157
138
{
158
- throw new InvalidOperationException ( $ "Failed to load settings from{ _settingsFilePath } . The file may be corrupted or malformed. Exception:{ ex . Message } ") ;
139
+ throw new InvalidOperationException (
140
+ $ "Failed to persist settings to{ _settingsFilePath } . "+
141
+ "The file may be corrupted, malformed or locked." , ex ) ;
142
+ }
143
+ finally
144
+ {
145
+ _gate . Release ( ) ;
159
146
}
160
147
}
148
+
149
+ public T ? GetFromCache ( )
150
+ {
151
+ return _cachedSettings ;
152
+ }
161
153
}
162
154
163
- public class Settings
155
+ public interface ISettings
164
156
{
165
157
/// <summary>
166
- /// User settings version. Increment this when the settings schema changes.
158
+ /// Gets the version of the settings schema.
159
+ /// </summary>
160
+ int Version { get ; }
161
+
162
+ /// <summary>
163
+ /// FileName where the settings are stored.
164
+ /// </summary>
165
+ static abstract string SettingsFileName { get ; }
166
+ }
167
+
168
+ /// <summary>
169
+ /// CoderConnect settings class that holds the settings for the CoderConnect feature.
170
+ /// </summary>
171
+ public class CoderConnectSettings : ISettings
172
+ {
173
+ /// <summary>
174
+ /// CoderConnect settings version. Increment this when the settings schema changes.
167
175
/// In future iterations we will be able to handle migrations when the user has
168
176
/// an older version.
169
177
/// </summary>
170
178
public int Version { get ; set ; }
171
- public Dictionary < string , JsonElement > Options { get ; set ; }
179
+ public bool ConnectOnLaunch { get ; set ; }
180
+ public static string SettingsFileName { get ; } = "coder-connect-settings.json" ;
172
181
173
182
private const int VERSION = 1 ; // Default version for backward compatibility
174
- public Settings ( )
183
+ public CoderConnectSettings ( )
175
184
{
176
185
Version = VERSION ;
177
- Options = [ ] ;
186
+ ConnectOnLaunch = false ;
178
187
}
179
188
180
- public Settings ( int ? version , Dictionary < string , JsonElement > options )
189
+ public CoderConnectSettings ( int ? version , bool connectOnLogin )
181
190
{
182
191
Version = version ?? VERSION ;
183
- Options = options ;
192
+ ConnectOnLaunch = connectOnLogin ;
184
193
}
185
- }
186
194
187
- [ JsonSerializable ( typeof ( Settings ) ) ]
188
- [ JsonSourceGenerationOptions ( WriteIndented = true ) ]
189
- public partial class SettingsJsonContext : JsonSerializerContext ;
195
+ public CoderConnectSettings Clone ( )
196
+ {
197
+ return new CoderConnectSettings ( Version , ConnectOnLaunch ) ;
198
+ }
199
+
200
+
201
+ }