master
cs 415 lines 13.9 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 using WslSettings.Contracts.Services;
4 using static WslSettings.Contracts.Services.IWslConfigService;
5
6 namespace WslSettings.Services;
7
8 public class WslConfigService : IWslConfigService, IDisposable
9 {
10 private WslConfig? _wslConfig { get; set; }
11 private WslConfig? _wslConfigDefaults { get; init; }
12 private readonly object? _wslCoreConfigInterfaceLockObj = null;
13 private FileSystemWatcher? _wslConfigFileSystemWatcher = null;
14
15 // Pending changes: stores only entries the user has changed in-app but not yet committed.
16 // Values are plain managed objects (bool, int, ulong, string, or enum) — no native clones needed.
17 private readonly Dictionary<WslConfigEntry, object> _pendingValues = new();
18
19 public WslConfigService()
20 {
21 string filePath = WslCoreConfigInterface.GetWslConfigFilePath();
22 _wslConfig = WslCoreConfigInterface.CreateWslConfig(filePath);
23 _wslConfigDefaults = WslCoreConfigInterface.CreateWslConfig(null);
24 _wslCoreConfigInterfaceLockObj = new object();
25 _wslConfigFileSystemWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath) ?? string.Empty, Path.GetFileName(filePath));
26
27 _wslConfigFileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
28
29 _wslConfigFileSystemWatcher.Changed += OnWslConfigFileChanged;
30 _wslConfigFileSystemWatcher.Deleted += OnWslConfigFileChanged;
31 _wslConfigFileSystemWatcher.Renamed += OnWslConfigFileChanged;
32
33 _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
34 }
35
36 ~WslConfigService()
37 {
38 Dispose(false);
39 }
40
41 public void Dispose()
42 {
43 Dispose(true);
44 GC.SuppressFinalize(this);
45 }
46
47 protected virtual void Dispose(bool disposing)
48 {
49 lock (_wslCoreConfigInterfaceLockObj!)
50 {
51 if (disposing && _wslConfigFileSystemWatcher != null)
52 {
53 _wslConfigFileSystemWatcher.EnableRaisingEvents = false;
54 _wslConfigFileSystemWatcher.Changed -= OnWslConfigFileChanged;
55 _wslConfigFileSystemWatcher.Deleted -= OnWslConfigFileChanged;
56 _wslConfigFileSystemWatcher.Renamed -= OnWslConfigFileChanged;
57 _wslConfigFileSystemWatcher.Dispose();
58 _wslConfigFileSystemWatcher = null;
59 }
60
61 WslCoreConfigInterface.FreeWslConfig(_wslConfig);
62 WslCoreConfigInterface.FreeWslConfig(_wslConfigDefaults);
63 }
64 }
65
66 public IWslConfigSetting GetWslConfigSetting(WslConfigEntry wslConfigEntry, bool defaultSetting)
67 {
68 if (defaultSetting)
69 {
70 return GetPersistedWslConfigSetting(wslConfigEntry, defaultSetting: true);
71 }
72
73 lock (_wslCoreConfigInterfaceLockObj!)
74 {
75 if (_pendingValues.TryGetValue(wslConfigEntry, out var pendingValue))
76 {
77 // Build a native setting from the pending managed value for the caller
78 var setting = GetPersistedWslConfigSetting(wslConfigEntry, defaultSetting: false);
79 setting.SetValueDirect(pendingValue);
80 return setting;
81 }
82
83 return GetPersistedWslConfigSetting(wslConfigEntry, defaultSetting: false);
84 }
85 }
86
87 public uint SetWslConfigSetting(IWslConfigSetting wslConfigSetting)
88 {
89 var settingManaged = wslConfigSetting as WslConfigSettingManaged;
90 if (settingManaged == null)
91 {
92 throw new ArgumentNullException(nameof(wslConfigSetting));
93 }
94
95 bool pendingStateChanged;
96 lock (_wslCoreConfigInterfaceLockObj!)
97 {
98 var hadPendingBefore = _pendingValues.Count > 0;
99
100 var persisted = GetPersistedWslConfigSetting(settingManaged.ConfigEntry, defaultSetting: false);
101 try
102 {
103 bool isChanged = !persisted.Equals(settingManaged.GetValueAsObject());
104
105 if (isChanged)
106 {
107 _pendingValues[settingManaged.ConfigEntry] = settingManaged.GetValueAsObject();
108 }
109 else
110 {
111 _pendingValues.Remove(settingManaged.ConfigEntry);
112 }
113 }
114 finally
115 {
116 persisted.ConfigSetting.Dispose();
117 }
118
119 var hasPendingAfter = _pendingValues.Count > 0;
120 pendingStateChanged = hadPendingBefore != hasPendingAfter;
121 }
122
123 if (pendingStateChanged)
124 {
125 _onPendingChangesChangedHandler?.Invoke();
126 }
127
128 return 0;
129 }
130
131 public bool HasPendingChanges
132 {
133 get
134 {
135 lock (_wslCoreConfigInterfaceLockObj!)
136 {
137 return _pendingValues.Count > 0;
138 }
139 }
140 }
141
142 public IReadOnlyList<WslConfigPendingChange> GetPendingChanges()
143 {
144 lock (_wslCoreConfigInterfaceLockObj!)
145 {
146 var changes = new List<WslConfigPendingChange>(_pendingValues.Count);
147 foreach (var (entry, value) in _pendingValues)
148 {
149 changes.Add(new WslConfigPendingChange
150 {
151 ConfigEntry = entry,
152 PendingValue = value,
153 });
154 }
155 return changes;
156 }
157 }
158
159 public uint CommitPendingChanges()
160 {
161 uint result = 0;
162 bool pendingStateChanged;
163
164 lock (_wslCoreConfigInterfaceLockObj!)
165 {
166 if (_pendingValues.Count == 0)
167 {
168 return 0;
169 }
170
171 _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
172 try
173 {
174 var committed = new List<WslConfigEntry>();
175 foreach (var (entry, value) in _pendingValues)
176 {
177 var setting = GetPersistedWslConfigSetting(entry, defaultSetting: false);
178 try
179 {
180 setting.SetValueDirect(value);
181 result = WslCoreConfigInterface.SetWslConfigSetting(_wslConfig, setting.ConfigSetting);
182 }
183 finally
184 {
185 setting.ConfigSetting.Dispose();
186 }
187
188 if (result != 0)
189 {
190 break;
191 }
192
193 committed.Add(entry);
194 }
195
196 ReloadConfig_NoLock();
197
198 if (result == 0)
199 {
200 _pendingValues.Clear();
201 }
202 else
203 {
204 // Partial failure - only remove successfully-committed entries
205 // so unapplied entries remain pending.
206 foreach (var entry in committed)
207 {
208 _pendingValues.Remove(entry);
209 }
210 }
211 }
212 finally
213 {
214 _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
215 }
216
217 // We entered with pending changes. Fire only if they're now empty (full success).
218 pendingStateChanged = _pendingValues.Count == 0;
219 }
220
221 if (pendingStateChanged)
222 {
223 _onPendingChangesChangedHandler?.Invoke();
224 }
225 _onWslConfigChangedHandler?.Invoke();
226 return result;
227 }
228
229 private WslConfigChangedEventHandler? _onWslConfigChangedHandler = null;
230 public event WslConfigChangedEventHandler WslConfigChanged
231 {
232 add
233 {
234 _onWslConfigChangedHandler += value;
235 }
236 remove
237 {
238 _onWslConfigChangedHandler -= value;
239 }
240 }
241
242 private PendingChangesChangedEventHandler? _onPendingChangesChangedHandler = null;
243 public event PendingChangesChangedEventHandler PendingChangesChanged
244 {
245 add
246 {
247 _onPendingChangesChangedHandler += value;
248 }
249 remove
250 {
251 _onPendingChangesChangedHandler -= value;
252 }
253 }
254
255 private void OnWslConfigFileChanged(object sender, FileSystemEventArgs e)
256 {
257 bool hadPending;
258 lock (_wslCoreConfigInterfaceLockObj!)
259 {
260 hadPending = _pendingValues.Count > 0;
261 _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
262 try
263 {
264 ReloadConfig_NoLock();
265 _pendingValues.Clear();
266 }
267 finally
268 {
269 _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
270 }
271 }
272
273 if (hadPending)
274 {
275 _onPendingChangesChangedHandler?.Invoke();
276 }
277 _onWslConfigChangedHandler?.Invoke();
278 }
279
280 // Read a single setting from the native config layer (either the user's .wslconfig or built-in defaults).
281 private WslConfigSettingManaged GetPersistedWslConfigSetting(WslConfigEntry wslConfigEntry, bool defaultSetting)
282 {
283 return new WslConfigSettingManaged(WslCoreConfigInterface.GetWslConfigSetting(defaultSetting ? _wslConfigDefaults : _wslConfig, wslConfigEntry));
284 }
285
286 private void ReloadConfig_NoLock()
287 {
288 WslCoreConfigInterface.FreeWslConfig(_wslConfig);
289 _wslConfig = WslCoreConfigInterface.CreateWslConfig(WslCoreConfigInterface.GetWslConfigFilePath());
290 }
291 }
292
293 public partial class WslConfigSettingManaged : IWslConfigSetting
294 {
295 public WslConfigSettingManaged(WslConfigSetting wslConfigSetting)
296 {
297 ConfigSetting = wslConfigSetting;
298 }
299
300 ~WslConfigSettingManaged()
301 {
302 ConfigSetting.Dispose();
303 }
304
305 public WslConfigSetting ConfigSetting { get; init; }
306 public WslConfigEntry ConfigEntry { get { return ConfigSetting.ConfigEntry; } }
307 public string StringValue { get { return ConfigSetting.StringValue; } }
308 public ulong UInt64Value { get { return ConfigSetting.UInt64Value; } }
309 public int Int32Value { get { return ConfigSetting.Int32Value; } }
310 public bool BoolValue { get { return ConfigSetting.BoolValue; } }
311 public NetworkingConfiguration NetworkingConfigurationValue { get { return ConfigSetting.NetworkingConfigurationValue; } }
312 public MemoryReclaimMode MemoryReclaimModeValue { get { return ConfigSetting.MemoryReclaimModeValue; } }
313
314 public object GetValueAsObject()
315 {
316 switch (ConfigSetting.ConfigEntry.GetValueKind())
317 {
318 case WslConfigValueKind.String:
319 return ConfigSetting.StringValue;
320 case WslConfigValueKind.Int32:
321 return ConfigSetting.Int32Value;
322 case WslConfigValueKind.UInt64:
323 return ConfigSetting.UInt64Value;
324 case WslConfigValueKind.NetworkingConfiguration:
325 return ConfigSetting.NetworkingConfigurationValue;
326 case WslConfigValueKind.MemoryReclaimMode:
327 return ConfigSetting.MemoryReclaimModeValue;
328 default:
329 return ConfigSetting.BoolValue;
330 }
331 }
332
333 // Apply a plain managed value to the native setting without going through the service.
334 public void SetValueDirect(object value)
335 {
336 switch (ConfigSetting.ConfigEntry.GetValueKind())
337 {
338 case WslConfigValueKind.String:
339 ConfigSetting.StringValue = (string)value;
340 break;
341 case WslConfigValueKind.Int32:
342 ConfigSetting.Int32Value = (int)value;
343 break;
344 case WslConfigValueKind.UInt64:
345 ConfigSetting.UInt64Value = (ulong)value;
346 break;
347 case WslConfigValueKind.NetworkingConfiguration:
348 ConfigSetting.NetworkingConfigurationValue = (NetworkingConfiguration)value;
349 break;
350 case WslConfigValueKind.MemoryReclaimMode:
351 ConfigSetting.MemoryReclaimModeValue = (MemoryReclaimMode)value;
352 break;
353 default:
354 ConfigSetting.BoolValue = (bool)value;
355 break;
356 }
357 }
358
359 #nullable enable
360 public uint SetValue(object? value)
361 {
362 if (value == null)
363 {
364 throw new ArgumentNullException(nameof(value));
365 }
366
367 switch (ConfigSetting.ConfigEntry.GetValueKind())
368 {
369 case WslConfigValueKind.String:
370 ConfigSetting.StringValue = (string)value;
371 break;
372 case WslConfigValueKind.Int32:
373 ConfigSetting.Int32Value = (int)value;
374 break;
375 case WslConfigValueKind.UInt64:
376 ConfigSetting.UInt64Value = (ulong)value;
377 break;
378 case WslConfigValueKind.NetworkingConfiguration:
379 ConfigSetting.NetworkingConfigurationValue = (NetworkingConfiguration)value;
380 break;
381 case WslConfigValueKind.MemoryReclaimMode:
382 ConfigSetting.MemoryReclaimModeValue = (MemoryReclaimMode)value;
383 break;
384 default:
385 ConfigSetting.BoolValue = (bool)value;
386 break;
387 }
388
389 return App.GetService<IWslConfigService>().SetWslConfigSetting(this);
390 }
391
392 public override bool Equals(object? value)
393 {
394 if (value == null)
395 {
396 throw new ArgumentNullException(nameof(value));
397 }
398
399 // Special handling for byte values. Compare using MB since in the UI the user works with MB.
400 if (ConfigSetting.ConfigEntry == WslConfigEntry.MemorySizeBytes ||
401 ConfigSetting.ConfigEntry == WslConfigEntry.SwapSizeBytes ||
402 ConfigSetting.ConfigEntry == WslConfigEntry.VhdSizeBytes)
403 {
404 return ((ulong)value / Constants.MB) == (UInt64Value / Constants.MB);
405 }
406
407 // object.Equals handles null on either side (returns true if both null, false if one null).
408 return object.Equals(GetValueAsObject(), value);
409 }
410
411 public override int GetHashCode()
412 {
413 return base.GetHashCode();
414 }
415 }