-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathConfigService.cs
More file actions
274 lines (244 loc) · 8.36 KB
/
Copy pathConfigService.cs
File metadata and controls
274 lines (244 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace GeneralUpdate.Tools.Configuration;
/// <summary>
/// Persistent configuration service. Reads/writes <c>config.json</c> in the
/// application's AppData folder, with automatic backup and schema migration.
/// </summary>
public class ConfigService : IConfigService
{
private static readonly JsonSerializerSettings JsonSettings = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
};
private readonly SemaphoreSlim _saveLock = new(1, 1);
private readonly string _configDir;
private readonly string _configPath;
private readonly string _backupPath;
public AppConfig Config { get; private set; } = new();
public string ConfigDirectory => _configDir;
public string ConfigFilePath => _configPath;
public ConfigService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
_configDir = Path.Combine(appData, "GeneralUpdate.Tools");
_configPath = Path.Combine(_configDir, "config.json");
_backupPath = Path.Combine(_configDir, "config.json.backup");
}
/// <inheritdoc />
public void Load()
{
Directory.CreateDirectory(_configDir);
if (!File.Exists(_configPath))
{
// Try to recover from backup
if (File.Exists(_backupPath))
{
try
{
var backupJson = File.ReadAllText(_backupPath);
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
Config.Sanitize(); // repair invalid enum values etc.
Save();
return;
}
catch
{
// Backup is corrupted; fall through to defaults
}
}
Config = new AppConfig();
Save();
return;
}
try
{
var json = File.ReadAllText(_configPath);
Config = JsonConvert.DeserializeObject<AppConfig>(json, JsonSettings) ?? new AppConfig();
Config.Sanitize();
// Run schema migrations
Migrate();
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
// Config file is corrupted or inaccessible; try backup
if (File.Exists(_backupPath))
{
try
{
var backupJson = File.ReadAllText(_backupPath);
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
Config.Sanitize();
Save();
return;
}
catch
{
// Backup also corrupted; reset
}
}
Config = new AppConfig();
Save();
}
}
/// <summary>
/// Synchronous save. Blocks the calling thread until the file is written.
/// Suitable for shutdown/flush scenarios where fire-and-forget would risk
/// the process exiting before the write completes.
/// </summary>
public void Save()
{
_saveLock.Wait();
try
{
Directory.CreateDirectory(_configDir);
if (File.Exists(_configPath))
{
try { File.Copy(_configPath, _backupPath, overwrite: true); }
catch { /* Non-critical */ }
}
var json = JsonConvert.SerializeObject(Config, JsonSettings);
var tempPath = _configPath + ".tmp";
File.WriteAllText(tempPath, json);
File.Move(tempPath, _configPath, overwrite: true);
}
finally
{
_saveLock.Release();
}
}
/// <inheritdoc />
public async Task LoadAsync()
{
Directory.CreateDirectory(_configDir);
if (!File.Exists(_configPath))
{
// Try to recover from backup
if (File.Exists(_backupPath))
{
try
{
var backupJson = await File.ReadAllTextAsync(_backupPath);
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
Config.Sanitize();
await SaveAsync(); // Restore main file from backup
return;
}
catch
{
// Backup is corrupted; fall through to defaults
}
}
// First run: save defaults so the file exists
Config = new AppConfig();
await SaveAsync();
return;
}
try
{
var json = await File.ReadAllTextAsync(_configPath);
Config = JsonConvert.DeserializeObject<AppConfig>(json, JsonSettings) ?? new AppConfig();
Config.Sanitize();
// Run schema migrations
Migrate();
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
// Config file is corrupted or inaccessible; try backup
if (File.Exists(_backupPath))
{
try
{
var backupJson = await File.ReadAllTextAsync(_backupPath);
Config = JsonConvert.DeserializeObject<AppConfig>(backupJson, JsonSettings) ?? new AppConfig();
Config.Sanitize();
await SaveAsync();
return;
}
catch
{
// Backup also corrupted; reset
}
}
Config = new AppConfig();
await SaveAsync();
}
}
/// <summary>
/// Fire-and-forget safe save. Logs exceptions via System.Diagnostics.Trace
/// rather than losing them silently — no crash, no dialog, just a trace log.
/// </summary>
public static void SafeFireAndForgetSave(ConfigService service)
{
_ = Task.Run(async () =>
{
try
{
await service.SaveAsync();
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(
$"[GeneralUpdate.Tools] Config save failed: {ex.Message}");
}
});
}
/// <inheritdoc />
public async Task SaveAsync()
{
await _saveLock.WaitAsync();
try
{
Directory.CreateDirectory(_configDir);
// Create backup of existing config before overwriting
if (File.Exists(_configPath))
{
try
{
File.Copy(_configPath, _backupPath, overwrite: true);
}
catch
{
// Non-critical: backup failed, proceed with save
}
}
var json = JsonConvert.SerializeObject(Config, JsonSettings);
// Atomic write: write to temp file, then move
var tempPath = _configPath + ".tmp";
await File.WriteAllTextAsync(tempPath, json);
// On Windows, File.Move with overwrite is atomic within the same volume
File.Move(tempPath, _configPath, overwrite: true);
}
finally
{
_saveLock.Release();
}
}
/// <inheritdoc />
public void ResetToDefaults()
{
Config = new AppConfig();
}
// ── Schema Migration ──────────────────────────────────────
/// <summary>
/// Apply forward migrations based on <see cref="AppConfig.SchemaVersion"/>.
/// Add new migration steps here as the config schema evolves.
/// </summary>
private void Migrate()
{
// Schema v1 is the baseline — no migration needed yet.
// Example for future v2 migration:
//
// if (Config.SchemaVersion < 2)
// {
// Config.SomeNewField = "default value";
// Config.SchemaVersion = 2;
// }
//
// Chain further migrations in order.
}
}