master
cpp 488 lines 15.6 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCUserSettings.cpp
8
9 Abstract:
10
11 Implementation of UserSettings — YAML loading and validation.
12
13 --*/
14 #include "precomp.h"
15 #include "WSLCUserSettings.h"
16 #include "filesystem.hpp"
17 #include "string.hpp"
18 #include "wslutil.h"
19
20 #pragma warning(push)
21 #pragma warning(disable : 4251 4275)
22 #include <yaml-cpp/yaml.h>
23 #pragma warning(pop)
24 #include <algorithm>
25 #include <format>
26 #include <fstream>
27 #include <set>
28
29 using namespace wsl::windows::common::string;
30
31 namespace wsl::windows::wslc::settings {
32
33 // All entries are commented out; the values shown are the built-in defaults.
34 // TODO: localization for comments needed?
35 static constexpr std::string_view s_DefaultSettingsTemplate =
36 "# wslc user settings\n"
37 "# https://aka.ms/wslc-settings\n"
38 "# All settings support string value \"default\" which uses built-in defaults.\n"
39 "\n"
40 "session:\n"
41 " # Number of virtual CPUs allocated to the session (e.g. 4 default: all available CPUs)\n"
42 " # cpuCount: default\n"
43 "\n"
44 " # Memory limit for the session (e.g. 2GB default: half of available memory)\n"
45 " # memorySize: default\n"
46 "\n"
47 " # Maximum disk image size (e.g. 500GB default: 1TB)\n"
48 " # maxStorageSize: default\n"
49 "\n"
50 " # Base directory for the default session's storage; the session VHD is created at\n"
51 " # <storagePath>\\wslc\\sessions\\<session>\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: "
52 "%LOCALAPPDATA%). Changing this after a session already exists does not move existing storage, containers, or\n"
53 " # images; the previous location is left in place and a new empty session is created at the new path.\n"
54 " # storagePath: default\n"
55 "\n"
56 " # Default host address that published ports bind to when 'container run -p' is\n"
57 " # used without an explicit address (default: 127.0.0.1)\n"
58 " # defaultBindingAddress: default\n"
59 "\n"
60 " # DNS name that resolves to the host loopback address (default: host.wslc.internal).\n"
61 " # Set to \"none\" to disable the entry.\n"
62 " # hostLoopback: default\n"
63 "\n"
64 " # Seconds an idle session VM stays running before it is torn down (default: 30)\n"
65 " # idleTimeout: default\n"
66 "\n"
67 "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n"
68 "# credentialStore: wincred\n";
69
70 // Validate individual setting specializations
71 namespace details {
72
73 std::optional<uint32_t> ParseSettingsMemoryValue(const std::string& value)
74 {
75
76 // WSLC settings accept leading whitespace for compatibility with existing settings files.
77 const auto wideValue = MultiByteToWide(value);
78 const auto parsed = ParseStorageSize(StripLeadingWhitespace(wideValue), StorageSizeUnit::Binary);
79 const auto converted = parsed.has_value() ? *parsed / _1MB : 0;
80 if (converted == 0 || converted > std::numeric_limits<uint32_t>::max())
81 {
82 return std::nullopt;
83 }
84
85 return static_cast<uint32_t>(converted);
86 }
87
88 #define WSLC_VALIDATE_SETTING(_setting_) \
89 std::optional<SettingMapping<Setting::_setting_>::value_t> SettingMapping<Setting::_setting_>::Validate( \
90 const SettingMapping<Setting::_setting_>::yaml_t& value)
91
92 WSLC_VALIDATE_SETTING(SessionCpuCount)
93 {
94 return value > 0 ? std::optional{value} : std::nullopt;
95 }
96
97 WSLC_VALIDATE_SETTING(SessionMemoryMb)
98 {
99 return ParseSettingsMemoryValue(value);
100 }
101
102 WSLC_VALIDATE_SETTING(SessionStorageSizeMb)
103 {
104 return ParseSettingsMemoryValue(value);
105 }
106
107 WSLC_VALIDATE_SETTING(SessionNetworkingMode)
108 {
109 if (value == "none")
110 {
111 return WSLCNetworkingModeNone;
112 }
113 if (value == "nat")
114 {
115 return WSLCNetworkingModeNAT;
116 }
117 if (value == "consomme")
118 {
119 return WSLCNetworkingModeConsomme;
120 }
121
122 return std::nullopt;
123 }
124
125 WSLC_VALIDATE_SETTING(SessionHostFileShareMode)
126 {
127 if (value == "plan9")
128 {
129 return HostFileShareMode::Plan9;
130 }
131 if (value == "virtiofs")
132 {
133 return HostFileShareMode::VirtioFs;
134 }
135
136 return std::nullopt;
137 }
138
139 WSLC_VALIDATE_SETTING(SessionDnsTunneling)
140 {
141 return value;
142 }
143
144 WSLC_VALIDATE_SETTING(SessionHostLoopback)
145 {
146 if (value == "none")
147 {
148 return std::string{};
149 }
150
151 return !value.empty() ? std::optional{value} : std::nullopt;
152 }
153
154 WSLC_VALIDATE_SETTING(SessionPortRelay)
155 {
156 if (value == "virtionet")
157 {
158 return PortRelayType::VirtioNet;
159 }
160 if (value == "wslrelay")
161 {
162 return PortRelayType::WslRelay;
163 }
164
165 return std::nullopt;
166 }
167
168 WSLC_VALIDATE_SETTING(SessionDefaultBindingAddress)
169 {
170 // The default binding address only applies to IPv4 ports (IPv6 bindings are always
171 // explicit), so it must parse as a valid IPv4 literal.
172 in_addr address{};
173 if (inet_pton(AF_INET, value.c_str(), &address) != 1)
174 {
175 return std::nullopt;
176 }
177
178 return value;
179 }
180
181 WSLC_VALIDATE_SETTING(SessionStoragePath)
182 {
183 if (value.empty() || !std::filesystem::path(value).is_absolute())
184 {
185 return std::nullopt;
186 }
187
188 return value;
189 }
190
191 WSLC_VALIDATE_SETTING(SessionIdleTimeout)
192 {
193 return value > 0 ? std::optional{value} : std::nullopt;
194 }
195
196 WSLC_VALIDATE_SETTING(CredentialStore)
197 {
198 if (value == "wincred")
199 {
200 return CredentialStoreType::WinCred;
201 }
202 if (value == "file")
203 {
204 return CredentialStoreType::File;
205 }
206
207 return std::nullopt;
208 }
209
210 #undef WSLC_VALIDATE_SETTING
211
212 } // namespace details
213
214 // Helpers
215 namespace {
216
217 // Traverses a dot-separated path (e.g. "session.cpuCount") through a YAML node tree.
218 // Returns nullopt if any segment is invalid or missing.
219 std::optional<YAML::Node> NavigateYamlPath(const YAML::Node& root, std::string_view path)
220 {
221 YAML::Node current = root;
222 auto subPaths = wsl::shared::string::Split(std::string{path}, '.');
223 for (auto const& subPath : subPaths)
224 {
225 if (current.IsDefined() && current.IsMap())
226 {
227 // Use the const operator[] to avoid yaml-cpp's AssignNode/set_ref side-effect,
228 // which mutates the shared detail::node and corrupts subsequent lookups.
229 // Then use reset() to rebind 'current' without triggering set_ref.
230 auto child = static_cast<const YAML::Node&>(current)[subPath];
231 if (!child.IsDefined())
232 {
233 return std::nullopt;
234 }
235 current.reset(child);
236 }
237 else
238 {
239 return std::nullopt;
240 }
241 }
242 return current;
243 }
244
245 // Validates and stores a single setting from the YAML document.
246 template <Setting S>
247 void ValidateSetting(const YAML::Node& root, SettingsMap& map, const std::wstring& filePath, std::vector<Warning>& warnings)
248 {
249 constexpr auto path = details::SettingMapping<S>::YamlPath;
250 auto node = NavigateYamlPath(root, path);
251
252 if (!node || !node->IsDefined() || node->IsNull())
253 {
254 // Key absent — silently use the built-in default.
255 return;
256 }
257
258 // Check "default"
259 try
260 {
261 if (node->IsScalar() && node->as<std::string>() == "default")
262 {
263 return;
264 }
265 }
266 catch (...)
267 {
268 }
269
270 try
271 {
272 auto rawValue = node->as<typename details::SettingMapping<S>::yaml_t>();
273 auto validated = details::SettingMapping<S>::Validate(rawValue);
274 if (validated.has_value())
275 {
276 map.Add<S>(std::move(validated.value()));
277 }
278 else
279 {
280 const auto widePath = MultiByteToWide(path);
281 warnings.push_back(
282 {wsl::shared::Localization::WSLCUserSettings_Warning_InvalidValue(widePath, filePath, node->Mark().line + 1), widePath});
283 }
284 }
285 catch (...)
286 {
287 const auto widePath = MultiByteToWide(path);
288 warnings.push_back(
289 {wsl::shared::Localization::WSLCUserSettings_Warning_InvalidType(widePath, filePath, node->Mark().line + 1), widePath});
290 }
291 }
292
293 // Validates all settings via a fold over the Setting enum index sequence.
294 template <size_t... S>
295 void ValidateAll(const YAML::Node& root, SettingsMap& map, const std::wstring& filePath, std::vector<Warning>& warnings, std::index_sequence<S...>)
296 {
297 (ValidateSetting<static_cast<Setting>(S)>(root, map, filePath, warnings), ...);
298 }
299
300 // Collects the set of known dot-separated YAML paths from all SettingMapping specializations.
301 template <size_t... S>
302 std::set<std::string> CollectKnownPaths(std::index_sequence<S...>)
303 {
304 std::set<std::string> paths;
305 (paths.insert(std::string(details::SettingMapping<static_cast<Setting>(S)>::YamlPath)), ...);
306 return paths;
307 }
308
309 // Derives the set of all prefixes from the known paths.
310 // e.g. "a.b.c" contributes both "a" and "a.b" as known prefixes.
311 std::set<std::string> CollectKnownPrefixes(const std::set<std::string>& knownPaths)
312 {
313 std::set<std::string> prefixes;
314 for (const auto& path : knownPaths)
315 {
316 for (size_t pos = path.find('.'); pos != std::string::npos; pos = path.find('.', pos + 1))
317 {
318 prefixes.insert(path.substr(0, pos));
319 }
320 }
321 return prefixes;
322 }
323
324 // Iteratively walks the YAML tree and warns about keys not in the known set.
325 void WarnUnknownKeys(
326 const YAML::Node& root,
327 const std::set<std::string>& knownPaths,
328 const std::set<std::string>& knownPrefixes,
329 const std::wstring& filePath,
330 std::vector<Warning>& warnings)
331 {
332 // Stack of (node, prefix) pairs to process.
333 std::vector<std::pair<YAML::Node, std::string>> stack;
334 stack.emplace_back(root, std::string{});
335
336 while (!stack.empty())
337 {
338 auto [node, prefix] = std::move(stack.back());
339 stack.pop_back();
340
341 for (auto it = node.begin(); it != node.end(); ++it)
342 {
343 std::string key;
344 try
345 {
346 key = it->first.as<std::string>();
347 }
348 catch (...)
349 {
350 auto location = prefix.empty() ? std::wstring(L"root") : MultiByteToWide(prefix);
351 warnings.push_back(
352 {wsl::shared::Localization::WSLCUserSettings_Warning_NonStringKey(location, filePath, it->first.Mark().line + 1), location});
353 continue;
354 }
355
356 auto fullPath = prefix.empty() ? key : prefix + '.' + key;
357
358 if (it->second.IsMap())
359 {
360 if (knownPrefixes.count(fullPath))
361 {
362 // Known section — add to stack to traverse.
363 stack.emplace_back(it->second, fullPath);
364 }
365 else
366 {
367 // Unknown section — warn once, don't traverse.
368 const auto widePath = MultiByteToWide(fullPath);
369 warnings.push_back(
370 {wsl::shared::Localization::WSLCUserSettings_Warning_UnknownSection(
371 widePath, filePath, it->first.Mark().line + 1),
372 widePath});
373 }
374 }
375 else if (!knownPaths.count(fullPath) && !knownPrefixes.count(fullPath))
376 {
377 // Unknown setting
378 const auto widePath = MultiByteToWide(fullPath);
379 warnings.push_back(
380 {wsl::shared::Localization::WSLCUserSettings_Warning_UnknownKey(widePath, filePath, it->first.Mark().line + 1), widePath});
381 }
382 }
383 }
384 }
385
386 // Attempts to parse a YAML document from the given file path.
387 // Returns an empty optional and pushes a warning if the file exists but fails to parse.
388 std::optional<YAML::Node> TryLoadYaml(const std::filesystem::path& path, std::vector<Warning>& warnings)
389 {
390 std::ifstream stream(path);
391 if (!stream.is_open())
392 {
393 auto err = errno;
394 // If the file exists but cannot be opened (permissions, sharing violation, etc.),
395 // emit a warning so the user understands why settings were ignored.
396 if (err != ENOENT)
397 {
398 warnings.push_back({wsl::shared::Localization::WSLCUserSettings_Warning_FailedToOpen(path.wstring(), err), {}});
399 }
400
401 return std::nullopt;
402 }
403
404 try
405 {
406 return YAML::Load(stream);
407 }
408 catch (const std::exception& e)
409 {
410 warnings.push_back(
411 {wsl::shared::Localization::WSLCUserSettings_Warning_ParseError(path.wstring(), MultiByteToWide(e.what())), {}});
412 return std::nullopt;
413 }
414 }
415
416 const std::filesystem::path& SettingsDir()
417 {
418 static const std::filesystem::path dir = wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc";
419 return dir;
420 }
421 } // namespace
422
423 UserSettings const& UserSettings::Instance()
424 {
425 static UserSettings instance;
426 return instance;
427 }
428
429 UserSettings::UserSettings() : UserSettings(SettingsDir())
430 {
431 }
432
433 UserSettings::UserSettings(const std::filesystem::path& settingsDir)
434 {
435 m_settingsPath = settingsDir / L"settings.yaml";
436
437 auto root = TryLoadYaml(m_settingsPath, m_warnings);
438 if (root.has_value())
439 {
440 m_type = UserSettingsType::Standard;
441 const auto filePath = m_settingsPath.wstring();
442
443 if (root->IsMap())
444 {
445 constexpr auto settingCount = static_cast<size_t>(Setting::Max);
446 ValidateAll(root.value(), m_settings, filePath, m_warnings, std::make_index_sequence<settingCount>());
447
448 constexpr auto indexSeq = std::make_index_sequence<settingCount>();
449 auto knownPaths = CollectKnownPaths(indexSeq);
450 auto knownPrefixes = CollectKnownPrefixes(knownPaths);
451 WarnUnknownKeys(root.value(), knownPaths, knownPrefixes, filePath, m_warnings);
452 }
453 else
454 {
455 m_warnings.push_back({wsl::shared::Localization::WSLCUserSettings_Warning_InvalidStructure(filePath), {}});
456 }
457 }
458
459 // Emit any settings load warnings.
460 for (const auto& warning : m_warnings)
461 {
462 wsl::windows::common::wslutil::PrintMessage(warning.Message, stderr);
463 }
464 }
465
466 void UserSettings::Reset() const
467 {
468 std::filesystem::create_directories(m_settingsPath.parent_path());
469 std::ofstream file(m_settingsPath);
470 THROW_HR_IF_MSG(E_UNEXPECTED, !file.is_open(), "Failed to create settings file");
471 file << s_DefaultSettingsTemplate;
472 }
473
474 void UserSettings::PrepareToShellExecuteFile() const
475 {
476 if (m_type == UserSettingsType::Default && !std::filesystem::exists(m_settingsPath))
477 {
478 // First run — create the directory and write the commented-out defaults template.
479 Reset();
480 }
481 }
482
483 std::filesystem::path UserSettings::SettingsFilePath() const
484 {
485 return m_settingsPath;
486 }
487
488 } // namespace wsl::windows::wslc::settings