Skip to content
4 changes: 4 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2417,6 +2417,10 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<value>Cannot use '{}' as session storage because it is not a directory</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcSessionStorageCustomLocation" xml:space="preserve">
<value>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.</value>
<comment>{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.</comment>
</data>
<data name="MessageWslcTagImageInvalidFormat" xml:space="preserve">
<value>Invalid image tag format: '{}'. Expected format is 'name:tag'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
Expand Down
1 change: 1 addition & 0 deletions src/windows/common/WSLCSessionDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions src/windows/common/WSLCUserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
" # <storagePath>\\wslc\\sessions\\<session>\\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"
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/windows/common/WSLCUserSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ enum class Setting : size_t
CredentialStore,
SessionPortRelay,
SessionDefaultBindingAddress,
SessionStoragePath,

Max
};
Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/windows/service/exe/WSLCSessionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,20 @@ struct SessionSettings
static std::unique_ptr<SessionSettings> 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<settings::Setting::SessionStoragePath>();
Comment thread
benhillis marked this conversation as resolved.
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<SessionSettings>(
new SessionSettings(std::wstring(ResolvedName), std::move(storagePath), WSLCSessionStorageFlagsNone, userSettings));
return std::unique_ptr<SessionSettings>(new SessionSettings(std::wstring(ResolvedName), storageDir.wstring(), storageFlags, userSettings));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We could use std::make_unique to simplify this a bit

}

// Custom session: caller provides name and storage path.
Expand Down
3 changes: 2 additions & 1 deletion src/windows/service/inc/WSLCShared.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);")
Expand Down
8 changes: 7 additions & 1 deletion src/windows/wslcsession/WSLCSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Module Name:
#include "ServiceProcessLauncher.h"
#include "WindowsCertStore.h"
#include "WslCoreFilesystem.h"
#include "WSLCSessionDefaults.h"
#include "wslpolicies.h"
#include "APICompat.h"

Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/windows/WSLCTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ class WSLCTests
// Reject invalid storage flags.
{
auto settings = GetDefaultSessionSettings(L"invalid-storage-flags");
settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x2);
settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x4);
wil::com_ptr<IWSLCSession> session;
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), E_INVALIDARG);
}
Expand Down
66 changes: 66 additions & 0 deletions test/windows/wslc/WSLCCLISettingsUnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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<Setting::SessionStoragePath>());
}

// 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<Setting::SessionStoragePath>());
}

// "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<Setting::SessionStoragePath>());
}

// 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<Setting::SessionStoragePath>());
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