@samitouri / QOSAMI-WSL / commits / d367d52e

fix: implement IDisposable on WslConfigService to dispose FileSystemWatcher (#40249)

* fix: implement IDisposable on WslConfigService to dispose FileSystemWatcher FileSystemWatcher holds unmanaged OS resources and implements IDisposable. Without disposing it, the watcher leaks handles. Implement the standard Dispose pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: implement IDisposable on WslConfigService to dispose FileSystemWatcher Synchronize Dispose with _wslCoreConfigInterfaceLockObj to prevent racing with OnWslConfigFileChanged/SetWslConfigSetting. Use _wslConfigFileSystemWatcher null check instead of a separate _disposed flag so the watcher is always cleaned up regardless of Dispose(true) vs Dispose(false) ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Ben Hillis committed Apr 20, 2026 at 15:25 UTC d367d52e2df5fb2776782afd2b91e5827b50d62d
1 file changed +228 -204
src/windows/wslsettings/Services/WslConfigService.cs
+228 -204
@@ -1,205 +1,229 @@
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
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 - public WslConfigService()
16 - {
17 - string filePath = WslCoreConfigInterface.GetWslConfigFilePath();
18 - _wslConfig = WslCoreConfigInterface.CreateWslConfig(filePath);
19 - _wslConfigDefaults = WslCoreConfigInterface.CreateWslConfig(null);
20 - _wslCoreConfigInterfaceLockObj = new object();
21 - _wslConfigFileSystemWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath) ?? string.Empty, Path.GetFileName(filePath));
22 -
23 - _wslConfigFileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
24 -
25 - _wslConfigFileSystemWatcher.Changed += OnWslConfigFileChanged;
26 - _wslConfigFileSystemWatcher.Deleted += OnWslConfigFileChanged;
27 - _wslConfigFileSystemWatcher.Renamed += OnWslConfigFileChanged;
28 -
29 - _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
30 - }
31 -
32 - ~WslConfigService()
33 - {
34 - WslCoreConfigInterface.FreeWslConfig(_wslConfig);
35 - WslCoreConfigInterface.FreeWslConfig(_wslConfigDefaults);
36 - }
37 -
38 - public IWslConfigSetting GetWslConfigSetting(WslConfigEntry wslConfigEntry, bool defaultSetting)
39 - {
40 - WslConfigSettingManaged? wslConfigSetting = null;
41 - lock (_wslCoreConfigInterfaceLockObj!)
42 - {
43 - wslConfigSetting = new WslConfigSettingManaged(WslCoreConfigInterface.GetWslConfigSetting(defaultSetting ? _wslConfigDefaults : _wslConfig, wslConfigEntry));
44 - }
45 -
46 - return wslConfigSetting;
47 - }
48 -
49 - public uint SetWslConfigSetting(IWslConfigSetting wslConfigSetting)
50 - {
51 - var wslConfigSettingsManaged = wslConfigSetting as WslConfigSettingManaged;
52 - if (wslConfigSettingsManaged == null)
53 - {
54 - throw new ArgumentNullException("wslConfigSetting");
55 - }
56 -
57 - uint result = 0;
58 - lock (_wslCoreConfigInterfaceLockObj!)
59 - {
60 - _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
61 - result = WslCoreConfigInterface.SetWslConfigSetting(_wslConfig, wslConfigSettingsManaged.ConfigSetting);
62 - _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
63 - }
64 -
65 - return result;
66 - }
67 -
68 - private WslConfigChangedEventHandler? _onWslConfigChangedHandler = null;
69 - public event WslConfigChangedEventHandler WslConfigChanged
70 - {
71 - add
72 - {
73 - _onWslConfigChangedHandler += value;
74 - }
75 - remove
76 - {
77 - _onWslConfigChangedHandler -= value;
78 - }
79 - }
80 -
81 - private void OnWslConfigFileChanged(object sender, FileSystemEventArgs e)
82 - {
83 - lock (_wslCoreConfigInterfaceLockObj!)
84 - {
85 - _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
86 - WslCoreConfigInterface.FreeWslConfig(_wslConfig);
87 - _wslConfig = WslCoreConfigInterface.CreateWslConfig(WslCoreConfigInterface.GetWslConfigFilePath());
88 - _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
89 - }
90 -
91 - _onWslConfigChangedHandler?.Invoke();
92 - }
93 -}
94 -
95 -public partial class WslConfigSettingManaged : IWslConfigSetting
96 -{
97 - public WslConfigSettingManaged(WslConfigSetting wslConfigSetting)
98 - {
99 - ConfigSetting = wslConfigSetting;
100 - }
101 -
102 - ~WslConfigSettingManaged()
103 - {
104 - ConfigSetting.Dispose();
105 - }
106 -
107 - public WslConfigSetting ConfigSetting { get; init; }
108 - public WslConfigEntry ConfigEntry { get { return ConfigSetting.ConfigEntry; } }
109 - public string StringValue { get { return ConfigSetting.StringValue; } }
110 - public ulong UInt64Value { get { return ConfigSetting.UInt64Value; } }
111 - public int Int32Value { get { return ConfigSetting.Int32Value; } }
112 - public bool BoolValue { get { return ConfigSetting.BoolValue; } }
113 - public NetworkingConfiguration NetworkingConfigurationValue { get { return ConfigSetting.NetworkingConfigurationValue; } }
114 - public MemoryReclaimMode MemoryReclaimModeValue { get { return ConfigSetting.MemoryReclaimModeValue; } }
115 -
116 -#nullable enable
117 - public uint SetValue(object? value)
118 - {
119 - if (value == null)
120 - {
121 - throw new ArgumentNullException("value");
122 - }
123 -
124 - if ("".GetType() == value.GetType())
125 - {
126 - ConfigSetting.StringValue = (string)value;
127 - }
128 - else if (ConfigSetting.UInt64Value.GetType() == value.GetType())
129 - {
130 - ConfigSetting.UInt64Value = (ulong)value;
131 - }
132 - else if (ConfigSetting.Int32Value.GetType() == value.GetType())
133 - {
134 - ConfigSetting.Int32Value = (int)value;
135 - }
136 - else if (ConfigSetting.BoolValue.GetType() == value.GetType())
137 - {
138 - ConfigSetting.BoolValue = (bool)value;
139 - }
140 - else if (ConfigSetting.NetworkingConfigurationValue.GetType() == value.GetType())
141 - {
142 - ConfigSetting.NetworkingConfigurationValue = (NetworkingConfiguration)value;
143 - }
144 - else if (ConfigSetting.MemoryReclaimModeValue.GetType() == value.GetType())
145 - {
146 - ConfigSetting.MemoryReclaimModeValue = (MemoryReclaimMode)value;
147 - }
148 - else
149 - {
150 - throw new InvalidDataException();
151 - }
152 -
153 - return App.GetService<IWslConfigService>().SetWslConfigSetting(this);
154 - }
155 -
156 - public override bool Equals(object? value)
157 - {
158 - if (value == null)
159 - {
160 - throw new ArgumentNullException("value");
161 - }
162 -
163 - // Special handling for byte values. Compare using MB since in the UI the user works with MB.
164 - if (ConfigSetting.ConfigEntry == WslConfigEntry.MemorySizeBytes ||
165 - ConfigSetting.ConfigEntry == WslConfigEntry.SwapSizeBytes ||
166 - ConfigSetting.ConfigEntry == WslConfigEntry.VhdSizeBytes)
167 - {
168 - return ((ulong)value / Constants.MB) == (UInt64Value / Constants.MB);
169 - }
170 -
171 - if ("".GetType() == value.GetType())
172 - {
173 - return ConfigSetting.StringValue == (string)value;
174 - }
175 - else if (ConfigSetting.UInt64Value.GetType() == value.GetType())
176 - {
177 - return ConfigSetting.UInt64Value == (ulong)value;
178 - }
179 - else if (ConfigSetting.Int32Value.GetType() == value.GetType())
180 - {
181 - return ConfigSetting.Int32Value == (int)value;
182 - }
183 - else if (ConfigSetting.BoolValue.GetType() == value.GetType())
184 - {
185 - return ConfigSetting.BoolValue == (bool)value;
186 - }
187 - else if (ConfigSetting.NetworkingConfigurationValue.GetType() == value.GetType())
188 - {
189 - return ConfigSetting.NetworkingConfigurationValue == (NetworkingConfiguration)value;
190 - }
191 - else if (ConfigSetting.MemoryReclaimModeValue.GetType() == value.GetType())
192 - {
193 - return ConfigSetting.MemoryReclaimModeValue == (MemoryReclaimMode)value;
194 - }
195 - else
196 - {
197 - throw new InvalidDataException();
198 - }
199 - }
200 -
201 - public override int GetHashCode()
202 - {
203 - return base.GetHashCode();
204 - }
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 + public WslConfigService()
16 + {
17 + string filePath = WslCoreConfigInterface.GetWslConfigFilePath();
18 + _wslConfig = WslCoreConfigInterface.CreateWslConfig(filePath);
19 + _wslConfigDefaults = WslCoreConfigInterface.CreateWslConfig(null);
20 + _wslCoreConfigInterfaceLockObj = new object();
21 + _wslConfigFileSystemWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath) ?? string.Empty, Path.GetFileName(filePath));
22 +
23 + _wslConfigFileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
24 +
25 + _wslConfigFileSystemWatcher.Changed += OnWslConfigFileChanged;
26 + _wslConfigFileSystemWatcher.Deleted += OnWslConfigFileChanged;
27 + _wslConfigFileSystemWatcher.Renamed += OnWslConfigFileChanged;
28 +
29 + _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
30 + }
31 +
32 + ~WslConfigService()
33 + {
34 + Dispose(false);
35 + }
36 +
37 + public void Dispose()
38 + {
39 + Dispose(true);
40 + GC.SuppressFinalize(this);
41 + }
42 +
43 + protected virtual void Dispose(bool disposing)
44 + {
45 + lock (_wslCoreConfigInterfaceLockObj!)
46 + {
47 + if (disposing && _wslConfigFileSystemWatcher != null)
48 + {
49 + _wslConfigFileSystemWatcher.EnableRaisingEvents = false;
50 + _wslConfigFileSystemWatcher.Changed -= OnWslConfigFileChanged;
51 + _wslConfigFileSystemWatcher.Deleted -= OnWslConfigFileChanged;
52 + _wslConfigFileSystemWatcher.Renamed -= OnWslConfigFileChanged;
53 + _wslConfigFileSystemWatcher.Dispose();
54 + _wslConfigFileSystemWatcher = null;
55 + }
56 +
57 + WslCoreConfigInterface.FreeWslConfig(_wslConfig);
58 + WslCoreConfigInterface.FreeWslConfig(_wslConfigDefaults);
59 + }
60 + }
61 +
62 + public IWslConfigSetting GetWslConfigSetting(WslConfigEntry wslConfigEntry, bool defaultSetting)
63 + {
64 + WslConfigSettingManaged? wslConfigSetting = null;
65 + lock (_wslCoreConfigInterfaceLockObj!)
66 + {
67 + wslConfigSetting = new WslConfigSettingManaged(WslCoreConfigInterface.GetWslConfigSetting(defaultSetting ? _wslConfigDefaults : _wslConfig, wslConfigEntry));
68 + }
69 +
70 + return wslConfigSetting;
71 + }
72 +
73 + public uint SetWslConfigSetting(IWslConfigSetting wslConfigSetting)
74 + {
75 + var wslConfigSettingsManaged = wslConfigSetting as WslConfigSettingManaged;
76 + if (wslConfigSettingsManaged == null)
77 + {
78 + throw new ArgumentNullException("wslConfigSetting");
79 + }
80 +
81 + uint result = 0;
82 + lock (_wslCoreConfigInterfaceLockObj!)
83 + {
84 + _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
85 + result = WslCoreConfigInterface.SetWslConfigSetting(_wslConfig, wslConfigSettingsManaged.ConfigSetting);
86 + _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
87 + }
88 +
89 + return result;
90 + }
91 +
92 + private WslConfigChangedEventHandler? _onWslConfigChangedHandler = null;
93 + public event WslConfigChangedEventHandler WslConfigChanged
94 + {
95 + add
96 + {
97 + _onWslConfigChangedHandler += value;
98 + }
99 + remove
100 + {
101 + _onWslConfigChangedHandler -= value;
102 + }
103 + }
104 +
105 + private void OnWslConfigFileChanged(object sender, FileSystemEventArgs e)
106 + {
107 + lock (_wslCoreConfigInterfaceLockObj!)
108 + {
109 + _wslConfigFileSystemWatcher!.EnableRaisingEvents = false;
110 + WslCoreConfigInterface.FreeWslConfig(_wslConfig);
111 + _wslConfig = WslCoreConfigInterface.CreateWslConfig(WslCoreConfigInterface.GetWslConfigFilePath());
112 + _wslConfigFileSystemWatcher!.EnableRaisingEvents = true;
113 + }
114 +
115 + _onWslConfigChangedHandler?.Invoke();
116 + }
117 +}
118 +
119 +public partial class WslConfigSettingManaged : IWslConfigSetting
120 +{
121 + public WslConfigSettingManaged(WslConfigSetting wslConfigSetting)
122 + {
123 + ConfigSetting = wslConfigSetting;
124 + }
125 +
126 + ~WslConfigSettingManaged()
127 + {
128 + ConfigSetting.Dispose();
129 + }
130 +
131 + public WslConfigSetting ConfigSetting { get; init; }
132 + public WslConfigEntry ConfigEntry { get { return ConfigSetting.ConfigEntry; } }
133 + public string StringValue { get { return ConfigSetting.StringValue; } }
134 + public ulong UInt64Value { get { return ConfigSetting.UInt64Value; } }
135 + public int Int32Value { get { return ConfigSetting.Int32Value; } }
136 + public bool BoolValue { get { return ConfigSetting.BoolValue; } }
137 + public NetworkingConfiguration NetworkingConfigurationValue { get { return ConfigSetting.NetworkingConfigurationValue; } }
138 + public MemoryReclaimMode MemoryReclaimModeValue { get { return ConfigSetting.MemoryReclaimModeValue; } }
139 +
140 +#nullable enable
141 + public uint SetValue(object? value)
142 + {
143 + if (value == null)
144 + {
145 + throw new ArgumentNullException("value");
146 + }
147 +
148 + if ("".GetType() == value.GetType())
149 + {
150 + ConfigSetting.StringValue = (string)value;
151 + }
152 + else if (ConfigSetting.UInt64Value.GetType() == value.GetType())
153 + {
154 + ConfigSetting.UInt64Value = (ulong)value;
155 + }
156 + else if (ConfigSetting.Int32Value.GetType() == value.GetType())
157 + {
158 + ConfigSetting.Int32Value = (int)value;
159 + }
160 + else if (ConfigSetting.BoolValue.GetType() == value.GetType())
161 + {
162 + ConfigSetting.BoolValue = (bool)value;
163 + }
164 + else if (ConfigSetting.NetworkingConfigurationValue.GetType() == value.GetType())
165 + {
166 + ConfigSetting.NetworkingConfigurationValue = (NetworkingConfiguration)value;
167 + }
168 + else if (ConfigSetting.MemoryReclaimModeValue.GetType() == value.GetType())
169 + {
170 + ConfigSetting.MemoryReclaimModeValue = (MemoryReclaimMode)value;
171 + }
172 + else
173 + {
174 + throw new InvalidDataException();
175 + }
176 +
177 + return App.GetService<IWslConfigService>().SetWslConfigSetting(this);
178 + }
179 +
180 + public override bool Equals(object? value)
181 + {
182 + if (value == null)
183 + {
184 + throw new ArgumentNullException("value");
185 + }
186 +
187 + // Special handling for byte values. Compare using MB since in the UI the user works with MB.
188 + if (ConfigSetting.ConfigEntry == WslConfigEntry.MemorySizeBytes ||
189 + ConfigSetting.ConfigEntry == WslConfigEntry.SwapSizeBytes ||
190 + ConfigSetting.ConfigEntry == WslConfigEntry.VhdSizeBytes)
191 + {
192 + return ((ulong)value / Constants.MB) == (UInt64Value / Constants.MB);
193 + }
194 +
195 + if ("".GetType() == value.GetType())
196 + {
197 + return ConfigSetting.StringValue == (string)value;
198 + }
199 + else if (ConfigSetting.UInt64Value.GetType() == value.GetType())
200 + {
201 + return ConfigSetting.UInt64Value == (ulong)value;
202 + }
203 + else if (ConfigSetting.Int32Value.GetType() == value.GetType())
204 + {
205 + return ConfigSetting.Int32Value == (int)value;
206 + }
207 + else if (ConfigSetting.BoolValue.GetType() == value.GetType())
208 + {
209 + return ConfigSetting.BoolValue == (bool)value;
210 + }
211 + else if (ConfigSetting.NetworkingConfigurationValue.GetType() == value.GetType())
212 + {
213 + return ConfigSetting.NetworkingConfigurationValue == (NetworkingConfiguration)value;
214 + }
215 + else if (ConfigSetting.MemoryReclaimModeValue.GetType() == value.GetType())
216 + {
217 + return ConfigSetting.MemoryReclaimModeValue == (MemoryReclaimMode)value;
218 + }
219 + else
220 + {
221 + throw new InvalidDataException();
222 + }
223 + }
224 +
225 + public override int GetHashCode()
226 + {
227 + return base.GetHashCode();
228 + }
229 }
\ No newline at end of file