diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index ef6809b3e..e950e1b2a 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -493,6 +493,9 @@ Arguments for managing Windows Subsystem for Linux: --resize <MemoryString> Resize the disk of the distribution to the specified size. + --compact + Compact the VHDX file of a stopped WSL 2 distribution. + --mount <Disk> Attaches and mounts a physical or virtual disk in all WSL 2 distributions. @@ -611,7 +614,8 @@ Arguments for managing distributions in Windows Subsystem for Linux: "}{Locked="--from-file "}{Locked="--legacy "}{Locked="--location "}{Locked="--name "}{Locked="--no-distribution "}{Locked="--no-launch,"}{Locked="--version "}{Locked="--vhd-size "}{Locked="--web-download -"}{Locked="--manage "}{Locked="--move "}{Locked="--set-sparse,"}{Locked="--set-default-user "}{Locked="--resize "}{Locked="--mount "}{Locked="--vhd +"}{Locked="--manage "}{Locked="--move "}{Locked="--set-sparse,"}{Locked="--set-default-user "}{Locked="--resize "}{Locked="--compact +"}{Locked="--mount "}{Locked="--vhd "}{Locked="--bare "}{Locked="--name "}{Locked="--type "}{Locked="--options "}{Locked="--partition "}{Locked="--set-default-version "}{Locked="--shutdown "}{Locked="--force diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp index 2dc05b57e..e81ef72d5 100644 --- a/src/windows/common/WslClient.cpp +++ b/src/windows/common/WslClient.cpp @@ -765,6 +765,10 @@ int ListDistributionsHelper(_In_ ListOptions options) state = L"Exporting"; break; + case LxssDistributionStateCompacting: + state = L"Compacting"; + break; + default: break; } @@ -790,7 +794,8 @@ int ListDistributionsHelper(_In_ ListOptions options) std::erase_if(distros, [&](const auto& entry) { return ( (entry.State == LxssDistributionStateInstalling) || (entry.State == LxssDistributionStateUninstalling) || - (entry.State == LxssDistributionStateConverting) || (entry.State == LxssDistributionStateExporting)); + (entry.State == LxssDistributionStateConverting) || (entry.State == LxssDistributionStateExporting) || + (entry.State == LxssDistributionStateCompacting)); }); } @@ -887,6 +892,7 @@ int Manage(_In_ std::wstring_view commandLine) std::optional move; std::optional defaultUser; std::optional resize; + bool compact = false; bool allowUnsafe = false; ArgumentParser parser(std::wstring{commandLine}, WSL_BINARY_NAME, 0); @@ -895,6 +901,7 @@ int Manage(_In_ std::wstring_view commandLine) parser.AddArgument(AbsolutePath(move), WSL_MANAGE_ARG_MOVE_OPTION_LONG, WSL_MANAGE_ARG_MOVE_OPTION); parser.AddArgument(defaultUser, WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG); parser.AddArgument(SizeString(resize), WSL_MANAGE_ARG_RESIZE_OPTION_LONG, WSL_MANAGE_ARG_RESIZE_OPTION); + parser.AddArgument(compact, WSL_MANAGE_ARG_COMPACT_OPTION_LONG); parser.AddArgument(allowUnsafe, WSL_MANAGE_ARG_ALLOW_UNSAFE); parser.Parse(); @@ -903,7 +910,7 @@ int Manage(_In_ std::wstring_view commandLine) wsl::windows::common::SvcComm service; auto distroGuid = service.GetDistributionId(distribution); - if (sparse.has_value() + move.has_value() + defaultUser.has_value() + resize.has_value() != 1) + if (sparse.has_value() + move.has_value() + defaultUser.has_value() + resize.has_value() + compact != 1) { THROW_HR(WSL_E_INVALID_USAGE); } @@ -950,6 +957,10 @@ int Manage(_In_ std::wstring_view commandLine) { THROW_IF_FAILED(service.ResizeDistribution(&distroGuid, resize.value())); } + else if (compact) + { + THROW_IF_FAILED(service.CompactDistribution(&distroGuid)); + } wsl::windows::common::wslutil::PrintSystemError(ERROR_SUCCESS); return 0; diff --git a/src/windows/common/WslCoreFilesystem.cpp b/src/windows/common/WslCoreFilesystem.cpp index 989eee13c..b99270e1c 100644 --- a/src/windows/common/WslCoreFilesystem.cpp +++ b/src/windows/common/WslCoreFilesystem.cpp @@ -92,6 +92,16 @@ wil::unique_handle wsl::core::filesystem::OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUA return disk; } +void wsl::core::filesystem::CompactVhd(_In_ LPCWSTR Path) +{ + auto diskHandle = OpenVhd(Path, VIRTUAL_DISK_ACCESS_GET_INFO | VIRTUAL_DISK_ACCESS_METAOPS); + + COMPACT_VIRTUAL_DISK_PARAMETERS compact{}; + compact.Version = COMPACT_VIRTUAL_DISK_VERSION_1; + + THROW_IF_WIN32_ERROR(CompactVirtualDisk(diskHandle.get(), COMPACT_VIRTUAL_DISK_FLAG_NONE, &compact, nullptr)); +} + void wsl::core::filesystem::ResizeExistingVhd(_In_ HANDLE diskHandle, _In_ ULONGLONG maximumSize, _In_ RESIZE_VIRTUAL_DISK_FLAG resizeFlag) { RESIZE_VIRTUAL_DISK_PARAMETERS resize{}; diff --git a/src/windows/common/WslCoreFilesystem.h b/src/windows/common/WslCoreFilesystem.h index c093720ff..fca8aae6e 100644 --- a/src/windows/common/WslCoreFilesystem.h +++ b/src/windows/common/WslCoreFilesystem.h @@ -37,6 +37,8 @@ void CreateVhd(_In_ LPCWSTR target, _In_ ULONGLONG maximumSize, _In_ PSID userSi wil::unique_handle OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUAL_DISK_ACCESS_MASK Mask); +void CompactVhd(_In_ LPCWSTR Path); + void ResizeExistingVhd(_In_ HANDLE diskHandle, _In_ ULONGLONG maximumSize, _In_ RESIZE_VIRTUAL_DISK_FLAG resizeFlag); ULONGLONG GetDiskSize(_In_ HANDLE diskHandle); diff --git a/src/windows/common/svccomm.cpp b/src/windows/common/svccomm.cpp index d3ae7f803..0c5721ea7 100644 --- a/src/windows/common/svccomm.cpp +++ b/src/windows/common/svccomm.cpp @@ -652,6 +652,13 @@ wsl::windows::common::SvcComm::ResizeDistribution(_In_ LPCGUID DistroGuid, _In_ RETURN_HR(result); } +HRESULT +wsl::windows::common::SvcComm::CompactDistribution(_In_ LPCGUID DistroGuid) const +{ + ClientExecutionContext context; + RETURN_HR(m_userSession->CompactDistribution(DistroGuid, context.OutError())); +} + HRESULT wsl::windows::common::SvcComm::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version) const { diff --git a/src/windows/common/svccomm.hpp b/src/windows/common/svccomm.hpp index 983134c94..47e13d50a 100644 --- a/src/windows/common/svccomm.hpp +++ b/src/windows/common/svccomm.hpp @@ -89,6 +89,9 @@ class SvcComm HRESULT ResizeDistribution(_In_ LPCGUID DistroGuid, _In_ ULONG64 NewSize) const; + HRESULT + CompactDistribution(_In_ LPCGUID DistroGuid) const; + void SetDefaultDistribution(_In_ LPCGUID DistroGuid) const; HRESULT diff --git a/src/windows/inc/wsl.h b/src/windows/inc/wsl.h index ce4b9e430..f9bb847f5 100644 --- a/src/windows/inc/wsl.h +++ b/src/windows/inc/wsl.h @@ -75,6 +75,7 @@ Module Name: #define WSL_MANAGE_ARG_SET_SPARSE_OPTION L's' #define WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG L"--set-sparse" #define WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG L"--set-default-user" +#define WSL_MANAGE_ARG_COMPACT_OPTION_LONG L"--compact" #define WSL_MOUNT_ARG L"--mount" #define WSL_MOUNT_ARG_VHD_OPTION_LONG L"--vhd" #define WSL_MOUNT_ARG_BARE_OPTION_LONG L"--bare" diff --git a/src/windows/service/exe/LxssUserSession.cpp b/src/windows/service/exe/LxssUserSession.cpp index 831b9403d..2a5095f00 100644 --- a/src/windows/service/exe/LxssUserSession.cpp +++ b/src/windows/service/exe/LxssUserSession.cpp @@ -492,6 +492,18 @@ try } CATCH_RETURN() +HRESULT STDMETHODCALLTYPE LxssUserSession::CompactDistribution(_In_ LPCGUID DistroGuid, _Out_ LXSS_ERROR_INFO* Error) +try +{ + ServiceExecutionContext context(Error); + + const auto session = m_session.lock(); + RETURN_HR_IF(RPC_E_DISCONNECTED, !session); + + return session->CompactDistribution(DistroGuid); +} +CATCH_RETURN() + HRESULT STDMETHODCALLTYPE LxssUserSession::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version, _In_ HANDLE StdErrHandle, _Out_ LXSS_ERROR_INFO* Error) try { @@ -913,6 +925,7 @@ HRESULT LxssUserSessionImpl::MoveDistribution(_In_ LPCGUID DistroGuid, _In_ LPCW // Fail if the distribution is running. RETURN_HR_IF(WSL_E_DISTRO_NOT_STOPPED, m_runningInstances.contains(*DistroGuid)); + _EnsureNotLocked(DistroGuid); // Lookup the distribution configuration const auto lxssKey = s_OpenLxssUserKey(); @@ -1759,6 +1772,8 @@ try THROW_HR_WITH_USER_ERROR(E_INVALIDARG, wsl::shared::Localization::MessageSparseVhdDisabled()); } + _EnsureNotLocked(DistroGuid); + // Don't attempt if running RETURN_HR_IF(WSL_E_DISTRO_NOT_STOPPED, m_runningInstances.contains(*DistroGuid)); @@ -1790,6 +1805,7 @@ try const auto registration = DistributionRegistration::Open(lxssKey.get(), *DistroGuid); const auto configuration = s_GetDistributionConfiguration(registration); RETURN_HR_IF(WSL_E_WSL2_NEEDED, WI_IsFlagClear(configuration.Flags, LXSS_DISTRO_FLAGS_VM_MODE)); + _EnsureNotLocked(DistroGuid); const auto& vhdPath = configuration.VhdFilePath; if (m_utilityVm && m_utilityVm->IsVhdAttached(vhdPath.c_str())) @@ -1837,6 +1853,37 @@ try } CATCH_RETURN() +HRESULT LxssUserSessionImpl::CompactDistribution(_In_ LPCGUID DistroGuid) +try +{ + auto runAsUser = wil::CoImpersonateClient(); + std::filesystem::path vhdPath; + LXSS_DISTRO_CONFIGURATION configuration; + + { + std::lock_guard lock(m_instanceLock); + const wil::unique_hkey lxssKey = s_OpenLxssUserKey(); + const auto registration = DistributionRegistration::Open(lxssKey.get(), *DistroGuid); + configuration = s_GetDistributionConfiguration(registration); + RETURN_HR_IF(WSL_E_WSL2_NEEDED, WI_IsFlagClear(configuration.Flags, LXSS_DISTRO_FLAGS_VM_MODE)); + + vhdPath = configuration.VhdFilePath; + _ConversionBegin(configuration.DistroId, LxssDistributionStateCompacting); + } + + auto compactionComplete = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { _ConversionComplete(configuration.DistroId); }); + + const auto result = wil::ResultFromException([&] { wsl::core::filesystem::CompactVhd(vhdPath.c_str()); }); + if (result == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)) + { + THROW_HR_WITH_USER_ERROR(result, wsl::shared::Localization::MessageVhdInUse()); + } + + THROW_IF_FAILED(result); + return S_OK; +} +CATCH_RETURN() + HRESULT LxssUserSessionImpl::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version, _In_ HANDLE StderrHandle) { RETURN_HR_IF(E_INVALIDARG, ((Version != LXSS_WSL_VERSION_1) && (Version != LXSS_WSL_VERSION_2))); @@ -3108,13 +3155,13 @@ std::vector LxssUserSessionImpl::_EnumerateDistributio _Requires_lock_held_(m_instanceLock) void LxssUserSessionImpl::_EnsureNotLocked(_In_ LPCGUID DistroGuid, const std::source_location& location) { - const auto found = std::find_if(m_lockedDistributions.begin(), m_lockedDistributions.end(), [&DistroGuid](const auto& entry) { + const auto locked = std::find_if(m_lockedDistributions.begin(), m_lockedDistributions.end(), [&DistroGuid](const auto& entry) { return IsEqualGUID(entry.first, *DistroGuid); }); THROW_HR_IF_MSG( E_ILLEGAL_STATE_CHANGE, - (found != m_lockedDistributions.end()), + locked != m_lockedDistributions.end(), "%hs, %hs:%u", location.function_name(), location.file_name(), diff --git a/src/windows/service/exe/LxssUserSession.h b/src/windows/service/exe/LxssUserSession.h index 6e2d41687..418bc8d58 100644 --- a/src/windows/service/exe/LxssUserSession.h +++ b/src/windows/service/exe/LxssUserSession.h @@ -190,6 +190,11 @@ class DECLSPEC_UUID("a9b7a1b9-0671-405c-95f1-e0612cb4ce7e") LxssUserSession /// IFACEMETHOD(ResizeDistribution)(_In_ LPCGUID DistroGuid, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize, _Out_ LXSS_ERROR_INFO* Error) override; + /// + /// Compacts the virtual disk of a distribution. + /// + IFACEMETHOD(CompactDistribution)(_In_ LPCGUID DistroGuid, _Out_ LXSS_ERROR_INFO* Error) override; + /// /// Sets the default distribution. /// @@ -467,6 +472,12 @@ class LxssUserSessionImpl HRESULT ResizeDistribution(_In_ LPCGUID DistroGuid, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize); + /// + /// Compacts the disk of a distribution. + /// + HRESULT + CompactDistribution(_In_ LPCGUID DistroGuid); + /// /// Sets the default distribution. /// diff --git a/src/windows/service/inc/wslservice.idl b/src/windows/service/inc/wslservice.idl index 2b3f42b02..ebf26dbe0 100644 --- a/src/windows/service/inc/wslservice.idl +++ b/src/windows/service/inc/wslservice.idl @@ -39,7 +39,8 @@ typedef enum _LxssDistributionState LxssDistributionStateInstalling, LxssDistributionStateUninstalling, LxssDistributionStateConverting, - LxssDistributionStateExporting + LxssDistributionStateExporting, + LxssDistributionStateCompacting } LxssDistributionState; typedef @@ -344,6 +345,10 @@ interface ILxssUserSession : IUnknown [in] LPCGUID DistroGuid, [in] LPCWSTR DistributionName, [ in, out ] LXSS_ERROR_INFO * Error); + + HRESULT CompactDistribution( + [in] LPCGUID DistroGuid, + [in, out] LXSS_ERROR_INFO* Error); }; diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 281dacf85..69fad2fd4 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -1237,16 +1237,39 @@ class UnitTests } } - static void VerifyOutput(const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExpectedExitCode = 0, LPCWSTR EntryPoint = WSL_BINARY_NAME) - { - auto [output, _] = LxsstuLaunchWslAndCaptureOutput( - Cmd.c_str(), ExpectedExitCode, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint); - - VERIFY_ARE_EQUAL(output, ExpectedOutput); - } - - TEST_METHOD(ErrorMessages) - { + static void VerifyOutput(const std::wstring& Cmd, const std::wstring& ExpectedOutput, int ExpectedExitCode = 0, LPCWSTR EntryPoint = WSL_BINARY_NAME) + { + auto [output, _] = LxsstuLaunchWslAndCaptureOutput( + Cmd.c_str(), ExpectedExitCode, nullptr, nullptr, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, EntryPoint); + + VERIFY_ARE_EQUAL(output, ExpectedOutput); + } + + static std::wstring ExpectedUsageMessage() + { + std::wstring expectedUsageMessage; + for (auto e : wsl::shared::Localization::MessageWslUsage()) + { + if (e == L'\n') + { + expectedUsageMessage += L'\r'; + } + + expectedUsageMessage += e; + } + + return expectedUsageMessage + L"\r\n"; + } + + static void VerifyInvalidUsage(const std::wstring& Cmd) + { + auto [output, error] = LxsstuLaunchWslAndCaptureOutput(Cmd.c_str(), -1); + VERIFY_ARE_EQUAL(ExpectedUsageMessage(), output); + VERIFY_ARE_EQUAL(error, L""); + } + + TEST_METHOD(ErrorMessages) + { if (LxsstuVmMode()) // wsl --mount and bridged networking only exist in WSL2. { if (!wsl::shared::Arm64 && wsl::windows::common::helpers::GetWindowsVersion().BuildNumber >= 27653) @@ -1324,6 +1347,12 @@ class UnitTests L"--manage test_distro --resize 10GB", L"This operation is only supported by WSL2.", L"Wsl/Service/WSL_E_WSL2_NEEDED"); + + // wsl.exe --manage --compact requires WSL2. + ValidateErrorMessage( + L"--manage test_distro --compact", + L"This operation is only supported by WSL2.", + L"Wsl/Service/WSL_E_WSL2_NEEDED"); } ValidateErrorMessage( @@ -1436,25 +1465,12 @@ class UnitTests {}, L"bash.exe"); - VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n"); - - { - std::wstring expectedUsageMessage; - for (auto e : wsl::shared::Localization::MessageWslUsage()) - { - if (e == L'\n') - { - expectedUsageMessage += L'\r'; - } - - expectedUsageMessage += e; - } - - VerifyOutput(L"--manage --move .", expectedUsageMessage + L"\r\n", -1); - } - } + VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n"); + + VerifyInvalidUsage(L"--manage --move ."); + } - TEST_METHOD(CommandLineParsing) + TEST_METHOD(CommandLineParsing) { VerifyOutput(L"echo -n \\\"", L"\""); VerifyOutput(L"echo -n \\\'", L"\'"); @@ -1474,10 +1490,15 @@ class UnitTests VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\""); VerifyOutput(L"--exec echo -n \\\"a\\\"", L"\"a\""); VerifyOutput(L"--exec echo -n \"a\"\"b\"", L"a\"b"); - VerifyOutput(L"--exec echo -n \\\"", L"\""); - } - - // This test validates that the help messages for wsl.exe and wsl.config are correctly displayed. + VerifyOutput(L"--exec echo -n \\\"", L"\""); + } + + TEST_METHOD(ManageInvalidUsage) + { + VerifyInvalidUsage(L"--manage " LXSS_DISTRO_NAME_TEST_L L" --compact --resize 10GB"); + } + + // This test validates that the help messages for wsl.exe and wsl.config are correctly displayed. // Notes: // - This test will fail if the help messages are changed. If that's the case, simply update the below strings // - This test assumes that English is the configured language. @@ -1583,6 +1604,9 @@ Arguments for managing Windows Subsystem for Linux: --resize Resize the disk of the distribution to the specified size. + --compact + Compact the VHDX file of a stopped WSL 2 distribution. + --mount Attaches and mounts a physical or virtual disk in all WSL 2 distributions. @@ -2957,16 +2981,40 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND validateDistro(L"500G", L"492G"); validateDistro(L"1M", nullptr, L"Failed to resize disk.\r\nError code: Wsl/Service/E_FAIL\r\n"); - { - WslKeepAlive keepAlive; - auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1); - VERIFY_ARE_EQUAL( - L"The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe " - L"--shutdown\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_STOPPED\r\n", - out); - } - } - + { + WslKeepAlive keepAlive; + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage test_distro --resize 1500GB", -1); + VERIFY_ARE_EQUAL( + L"The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe " + L"--shutdown\r\nError code: Wsl/Service/WSL_E_DISTRO_NOT_STOPPED\r\n", + out); + } + + } + + WSL2_TEST_METHOD(Compact) + { + constexpr auto name = L"compact-test-distro"; + + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L); + WslShutdown(); + + auto cleanupName = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); }); + + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name)); + VERIFY_ARE_EQUAL(err, L""); + + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name)); + VERIFY_ARE_EQUAL(err, L""); + + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name)); + VERIFY_ARE_EQUAL(out, L"ok\n"); + VERIFY_ARE_EQUAL(err, L""); + WslShutdown(); + + } + WSL2_TEST_METHOD(FileOffsets) { auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"output.txt"); });