diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index d95f6a646..d18c65645 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2417,6 +2417,10 @@ For privacy information about this product please visit https://aka.ms/privacy.< Cannot use '{}' as session storage because it is not a directory {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Creating session storage at '{}' as configured by session.storagePath. If you later change or remove this setting, data stored here will no longer be used by the default session and can be deleted to reclaim space. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated. {Locked="session.storagePath"}"session.storagePath" is a configuration setting name and should not be translated. + Invalid image tag format: '{}'. Expected format is 'name:tag' {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/common/WSLCSessionDefaults.h b/src/windows/common/WSLCSessionDefaults.h index 5d502c6ad..e288d4179 100644 --- a/src/windows/common/WSLCSessionDefaults.h +++ b/src/windows/common/WSLCSessionDefaults.h @@ -20,6 +20,7 @@ namespace wsl::windows::wslc { inline constexpr const wchar_t DefaultSessionName[] = L"wslc-cli"; inline constexpr const wchar_t DefaultAdminSessionName[] = L"wslc-cli-admin"; inline constexpr const wchar_t DefaultStorageSubPath[] = L"wslc\\sessions"; +inline constexpr const wchar_t DefaultStorageVhdName[] = L"storage.vhdx"; inline constexpr uint32_t DefaultBootTimeoutMs = 30000; } // namespace wsl::windows::wslc diff --git a/src/windows/common/WSLCUserSettings.cpp b/src/windows/common/WSLCUserSettings.cpp index f05610f79..b0c3e1e86 100644 --- a/src/windows/common/WSLCUserSettings.cpp +++ b/src/windows/common/WSLCUserSettings.cpp @@ -47,6 +47,12 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # Maximum disk image size (e.g. 500GB default: 1TB)\n" " # maxStorageSize: default\n" "\n" + " # Base directory for the default session's storage; the session VHD is created at\n" + " # \\wslc\\sessions\\\\storage.vhdx. Must be an absolute path (e.g. D:\\data default: " + "%LOCALAPPDATA%). Changing this after a session already exists does not move existing storage, containers, or\n" + " # images; the previous location is left in place and a new empty session is created at the new path.\n" + " # storagePath: default\n" + "\n" " # Default host address that published ports bind to when 'container run -p' is\n" " # used without an explicit address (default: 127.0.0.1)\n" " # defaultBindingAddress: default\n" @@ -147,6 +153,16 @@ namespace details { return value; } + WSLC_VALIDATE_SETTING(SessionStoragePath) + { + if (value.empty() || !std::filesystem::path(value).is_absolute()) + { + return std::nullopt; + } + + return value; + } + WSLC_VALIDATE_SETTING(CredentialStore) { if (value == "wincred") diff --git a/src/windows/common/WSLCUserSettings.h b/src/windows/common/WSLCUserSettings.h index 2f006b05b..30e3f3f9a 100644 --- a/src/windows/common/WSLCUserSettings.h +++ b/src/windows/common/WSLCUserSettings.h @@ -44,6 +44,7 @@ enum class Setting : size_t CredentialStore, SessionPortRelay, SessionDefaultBindingAddress, + SessionStoragePath, Max }; @@ -99,6 +100,7 @@ namespace details { DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore") DEFINE_SETTING_MAPPING(SessionPortRelay, std::string, PortRelayType, PortRelayType::VirtioNet, "experimental.portRelay") DEFINE_SETTING_MAPPING(SessionDefaultBindingAddress, std::string, std::string, std::string{}, "session.defaultBindingAddress") + DEFINE_SETTING_MAPPING(SessionStoragePath, std::string, std::string, std::string{}, "session.storagePath") #undef DEFINE_SETTING_MAPPING // clang-format on diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp index f13ff8269..e5e0540e9 100644 --- a/src/windows/service/exe/WSLCSessionManager.cpp +++ b/src/windows/service/exe/WSLCSessionManager.cpp @@ -86,12 +86,20 @@ struct SessionSettings static std::unique_ptr Default(HANDLE UserToken, const std::wstring& ResolvedName) { auto userSettings = LoadUserSettings(UserToken); - auto localAppData = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); - auto storagePath = (localAppData / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName).wstring(); + auto configuredStorageBase = userSettings.Get(); + const bool customConfigured = !configuredStorageBase.empty(); + const std::filesystem::path defaultBase = wsl::windows::common::filesystem::GetLocalAppDataPath(UserToken); + const std::filesystem::path storageBase = + customConfigured ? std::filesystem::path(wsl::shared::string::MultiByteToWide(configuredStorageBase)) : defaultBase; + + const auto storageDir = storageBase / wsl::windows::wslc::DefaultStorageSubPath / ResolvedName; + + // wslcsession emits the custom-location warning when it actually creates the VHD, so the notice + // fires once at creation without a service-side callback that could stall CreateSession. + const auto storageFlags = customConfigured ? WSLCSessionStorageFlagsWarnCustomLocation : WSLCSessionStorageFlagsNone; - return std::unique_ptr( - new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings)); + return std::unique_ptr(new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings)); } // Custom session: caller provides name and storage path. diff --git a/src/windows/service/inc/WSLCShared.idl b/src/windows/service/inc/WSLCShared.idl index d02849aa0..b25fbc053 100644 --- a/src/windows/service/inc/WSLCShared.idl +++ b/src/windows/service/inc/WSLCShared.idl @@ -165,7 +165,8 @@ typedef enum _WSLCSessionStorageFlags { WSLCSessionStorageFlagsNone = 0, WSLCSessionStorageFlagsNoCreate = 1, // Open an existing storage path, but don't create a new one. - WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate + WSLCSessionStorageFlagsWarnCustomLocation = 2, // Warn when creating a new VHD at a session.storagePath-configured location. + WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate | WSLCSessionStorageFlagsWarnCustomLocation } WSLCSessionStorageFlags; cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionStorageFlags);") diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index eefba3048..a1bdd4f6a 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -21,6 +21,7 @@ Module Name: #include "ServiceProcessLauncher.h" #include "WindowsCertStore.h" #include "WslCoreFilesystem.h" +#include "WSLCSessionDefaults.h" #include "wslpolicies.h" #include "APICompat.h" @@ -38,7 +39,7 @@ using wsl::windows::service::wslc::WSLCVirtualMachine; constexpr auto c_containerdStorage = "/var/lib/docker"; constexpr auto c_containerdSocket = "/run/containerd/containerd.sock"; constexpr auto c_dockerdReadyLogLine = "API listen on /var/run/docker.sock"; -constexpr auto c_storageVhdFilename = L"storage.vhdx"; +constexpr auto c_storageVhdFilename = wsl::windows::wslc::DefaultStorageVhdName; constexpr DWORD c_processTerminateTimeoutMs = 30 * 1000; constexpr DWORD c_processKillTimeoutMs = 10 * 1000; @@ -504,6 +505,11 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID // If the VHD wasn't found, create it. WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath")); + if (WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsWarnCustomLocation)) + { + EMIT_USER_WARNING(Localization::MessageWslcSessionStorageCustomLocation(storagePath.c_str())); + } + std::filesystem::create_directories(storagePath); wsl::core::filesystem::CreateVhd(m_storageVhdPath.c_str(), Settings.MaximumStorageSizeMb * _1MB, UserSid, false, false); vhdCreated = true; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 8baad20ab..63b293ee4 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -456,7 +456,7 @@ class WSLCTests // Reject invalid storage flags. { auto settings = GetDefaultSessionSettings(L"invalid-storage-flags"); - settings.StorageFlags = static_cast(0x2); + settings.StorageFlags = static_cast(0x4); wil::com_ptr session; VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG); } diff --git a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp index e316c0d33..cd7ae1d44 100644 --- a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp +++ b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp @@ -524,6 +524,7 @@ class WSLCCLISettingsUnitTests " networkingMode: nat\n" " hostFileShareMode: virtiofs\n" " dnsTunneling: true\n" + " storagePath: C:\\wslc-data\n" "experimental:\n" " portRelay: wslrelay\n" "credentialStore: wincred\n"); @@ -629,6 +630,71 @@ class WSLCCLISettingsUnitTests Loc::WSLCUserSettings_Warning_InvalidValue(L"session.defaultBindingAddress", s.SettingsFilePath().wstring(), 2), s.GetWarnings().front().Message); } + + // ----------------------------------------------------------------------- + // session.storagePath + // ----------------------------------------------------------------------- + + // When unset, the storage path falls back to the empty built-in default (caller uses %LOCALAPPDATA%). + TEST_METHOD(Validation_StoragePath_Absent_UsesEmptyDefault) + { + auto dir = UniqueTempDir(); + WriteFile(dir / L"settings.yaml", "session:\n cpuCount: 4\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + } + + // A valid absolute path loads without warnings. + TEST_METHOD(Validation_StoragePath_AbsoluteValue) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: D:\\wslc\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string("D:\\wslc"), s.Get()); + } + + // "default" magic string uses the built-in (empty) default with no warning. + TEST_METHOD(Validation_StoragePath_DefaultString) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: default\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(0u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + } + + // A relative path is rejected: the default is used and an invalid-value warning emitted. + TEST_METHOD(Validation_StoragePath_RelativeValue_UsesDefaultAndWarns) + { + auto dir = UniqueTempDir(); + WriteFile( + dir / L"settings.yaml", + "session:\n" + " storagePath: wslc\\data\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(std::string{}, s.Get()); + VERIFY_ARE_EQUAL(1u, s.GetWarnings().size()); + VERIFY_ARE_EQUAL( + Loc::WSLCUserSettings_Warning_InvalidValue(L"session.storagePath", s.SettingsFilePath().wstring(), 2), + s.GetWarnings().front().Message); + VERIFY_ARE_EQUAL(std::wstring(L"session.storagePath"), s.GetWarnings().front().SettingPath); + } }; } // namespace WSLCCLISettingsUnitTests