@samitouri / QOSAMI-WSL / commits / 51541ceb

Draft: add manual VHDX compaction command for stopped WSL distros (#40481)

* Add manual VHDX compaction command * Address compact command review feedback * Tighten compact command validation * Use existing user session interface for compact * Avoid holding instance lock during compact * Serialize compact with VHD operations * Restore package WiX trailing newline * Use locked distribution state for compact * Remove allow-unsafe parser restriction * Use conversion lock for compact * Format compact command source * Verify compact reclaims VHD space in test * Reject VHD compaction with clear error * Add compact telemetry * Show progress while compacting * Fix compact help locked token * Reduce compact test data cost * Update compact help text * Undo package.wix.in change per review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37ad1fd7-a452-4056-a8cc-e91cf7870a60 * Address review: reword compact-not-supported message, merge scope_exits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37ad1fd7-a452-4056-a8cc-e91cf7870a60 * Align compact help text with shipped resw and fix locked token - HelpMessages test expected 'stopped WSL 2 distribution' but the resw help ships 'WSL 2 distribution'; drop 'stopped' so the test matches (compaction auto-terminates the distro, and sibling --resize/--move help omit it too). - Add missing trailing space to the --compact locked token in the help comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37ad1fd7-a452-4056-a8cc-e91cf7870a60 * Trim the filesystem before compacting a distribution's VHD WSL2 does not mount ext4 with 'discard' and does not run fsck at boot, so blocks freed inside the guest stay marked as allocated in the VHD. A bare 'wsl --manage <distro> --compact' therefore reclaims very little space. Before compacting, attach the stopped distribution's VHD to the utility VM and run an offline 'e2fsck -f -y -E discard' on the raw device. This issues the same block-discard requests that 'fstrim' would on a mounted filesystem, so freed blocks are marked unused and the subsequent host-side compaction can reclaim them. Running e2fsck offline (rather than fstrim on a mount) reuses the plumbing already used by ResizeDistribution, needs no mount, relies only on e2fsprogs (guaranteed present in the utility VM), and also performs the filesystem check a reviewer asked for. The trim is best-effort: any failure is logged and does not abort compaction. The Compact unit test no longer runs 'fstrim' manually inside the guest, so it now validates that '--compact' performs the trim itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37ad1fd7-a452-4056-a8cc-e91cf7870a60 * Pass explicit user token to s_OpenLxssUserKey * Remove stale VHD localization lock * Fix --compact localization lock delimiter * Default initialize distro configuration * Remove redundant compaction sharing violation handling * Remove redundant sparse setup from compact test * Simplify VHD compaction logging * Revert unrelated locked distribution rename * Remove redundant shutdown from compact test * Apply clang-format to compact changes * Fix compact command formatting --------- Co-authored-by: Ben Hillis <benhill@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37ad1fd7-a452-4056-a8cc-e91cf7870a60

Rafa Pradillo committed Aug 12, 2026 at 23:09 UTC 51541ceb625467855160e04c7cd6cf55b1ebf227
15 files changed +350 -18
localization/strings/en-US/Resources.resw
+12 -1
@@ -302,6 +302,9 @@ and 'wsl.exe --install &lt;Distro&gt;' to install.</value>
302 <data name="MessageConversionStart" xml:space="preserve">
303 <value>Conversion in progress, this may take a few minutes.</value>
304 </data>
305 + <data name="MessageCompactionStart" xml:space="preserve">
306 + <value>Compaction in progress, this may take a few minutes.</value>
307 + </data>
308 <data name="MessageAlreadyRequestedVersion" xml:space="preserve">
309 <value>The distribution is already the requested version.</value>
310 </data>
@@ -493,6 +496,9 @@ Arguments for managing Windows Subsystem for Linux:
496 --resize &lt;MemoryString&gt;
497 Resize the disk of the distribution to the specified size.
498
499 + --compact
500 + Compact the VHDX file of a WSL 2 distribution.
501 +
502 --mount &lt;Disk&gt;
503 Attaches and mounts a physical or virtual disk in all WSL 2 distributions.
504
@@ -611,7 +617,8 @@ Arguments for managing distributions in Windows Subsystem for Linux:
617 "}{Locked="--from-file "}{Locked="--legacy
618 "}{Locked="--location "}{Locked="--name "}{Locked="--no-distribution
619 "}{Locked="--no-launch,"}{Locked="--version "}{Locked="--vhd-size "}{Locked="--web-download
614 -"}{Locked="--manage "}{Locked="--move "}{Locked="--set-sparse,"}{Locked="--set-default-user "}{Locked="--resize "}{Locked="--mount "}{Locked="--vhd
620 +"}{Locked="--manage "}{Locked="--move "}{Locked="--set-sparse,"}{Locked="--set-default-user "}{Locked="--resize "}{Locked="--compact
621 +"}{Locked="--mount "}{Locked="--vhd
622 "}{Locked="--bare
623 "}{Locked="--name "}{Locked="--type "}{Locked="--options "}{Locked="--partition "}{Locked="--set-default-version "}{Locked="--shutdown
624 "}{Locked="--force
@@ -1061,6 +1068,10 @@ Falling back to NAT networking.</value>
1068 <value>The operation could not be completed because the VHD is currently in use. To force WSL to stop use: wsl.exe --shutdown</value>
1069 <comment>{Locked="--shutdown"}Command line arguments, file names and string inserts should not be translated. {Locked="VHD"}"VHD" (Virtual Hard Disk) is a technical format name and should not be translated.</comment>
1070 </data>
1071 + <data name="MessageCompactVhdNotSupported" xml:space="preserve">
1072 + <value>Compaction is only supported on .vhdx files.</value>
1073 + <comment>{Locked=".vhdx"}Command line arguments, file names and string inserts should not be translated.</comment>
1074 + </data>
1075 <data name="MessageInvalidBoolean" xml:space="preserve">
1076 <value>{} is not a valid boolean, &lt;true|false&gt;</value>
1077 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/linux/init/main.cpp
+60
@@ -2949,6 +2949,59 @@ try
2949 }
2950 CATCH_RETURN_ERRNO();
2951
2952 +int ProcessTrimDistributionMessage(gsl::span<gsl::byte> Buffer)
2953 +try
2954 +{
2955 + auto* Message = gslhelpers::try_get_struct<LX_MINI_INIT_TRIM_DISTRIBUTION_MESSAGE>(Buffer);
2956 +
2957 + if (!Message)
2958 + {
2959 + LOG_ERROR("Unexpected message size {}", Buffer.size());
2960 + return -1;
2961 + }
2962 +
2963 + wil::unique_fd SocketFd{UtilConnectVsock(LX_INIT_UTILITY_VM_INIT_PORT, true)};
2964 + if (!SocketFd)
2965 + {
2966 + return -1;
2967 + }
2968 +
2969 + const int ChildPid = UtilCreateChildProcess(
2970 + "TrimDistribution", [Message, Channel = wsl::shared::SocketChannel{std::move(SocketFd), "TrimDistribution"}]() mutable {
2971 + int ResponseCode = -1;
2972 + auto ReportStatus = wil::scope_exit([&]() {
2973 + LX_MINI_INIT_TRIM_DISTRIBUTION_RESPONSE ResponseMessage{};
2974 + ResponseMessage.ResponseCode = ResponseCode;
2975 + ResponseMessage.Header.MessageType = LxMiniInitMessageTrimDistributionResponse;
2976 + ResponseMessage.Header.MessageSize = sizeof(ResponseMessage);
2977 +
2978 + Channel.SendMessage(ResponseMessage);
2979 + });
2980 +
2981 + const auto DevicePath = GetLunDevicePath(Message->ScsiLun);
2982 +
2983 + //
2984 + // Run a full offline filesystem check and discard the free blocks so the host can reclaim
2985 + // them when the VHD is compacted. This mirrors the offline e2fsck used by
2986 + // ResizeDistribution: it runs on the detached device without mounting it, and '-E discard'
2987 + // issues the same block-discard requests that 'fstrim' would on a mounted filesystem.
2988 + //
2989 + // This is best-effort: a failure here must not prevent compaction, so the child logs the
2990 + // error but still reports success.
2991 + //
2992 + const auto CommandLine = std::format("/usr/sbin/e2fsck -f -y -E discard '{}'", DevicePath);
2993 + if (UtilExecCommandLine(CommandLine.c_str(), nullptr) < 0)
2994 + {
2995 + LOG_WARNING("Failed to trim {}", DevicePath.c_str());
2996 + }
2997 +
2998 + ResponseCode = 0;
2999 + });
3000 +
3001 + return (ChildPid < 0) ? -1 : 0;
3002 +}
3003 +CATCH_RETURN_ERRNO();
3004 +
3005 int ProcessMessage(wsl::shared::Transaction& Transaction, LX_MESSAGE_TYPE Type, gsl::span<gsl::byte> Buffer, VmConfiguration& Config)
3006
3007 /*++
@@ -3321,6 +3374,13 @@ try
3374 return 0;
3375 }
3376
3377 + case LxMiniInitMessageTrimDistribution:
3378 + {
3379 +
3380 + ProcessTrimDistributionMessage(Buffer);
3381 + return 0;
3382 + }
3383 +
3384 default:
3385 LOG_ERROR("Unexpected message type {}", Type);
3386 return -1;
src/shared/inc/lxinitshared.h
+24
@@ -415,6 +415,8 @@ typedef enum _LX_MESSAGE_TYPE
415 LxMessageWSLCListDirResult,
416 LxMessageWSLCMountVirtioFs,
417 LxMessageWSLCWriteFile,
418 + LxMiniInitMessageTrimDistribution,
419 + LxMiniInitMessageTrimDistributionResponse,
420 } LX_MESSAGE_TYPE,
421 *PLX_MESSAGE_TYPE;
422
@@ -531,6 +533,8 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
533 X(LxMessageWSLCListDirResult)
534 X(LxMessageWSLCMountVirtioFs)
535 X(LxMessageWSLCWriteFile)
536 + X(LxMiniInitMessageTrimDistribution)
537 + X(LxMiniInitMessageTrimDistributionResponse)
538
539 default:
540 return "<unexpected LX_MESSAGE_TYPE>";
@@ -1514,6 +1518,26 @@ typedef struct _LX_MINI_INIT_RESIZE_DISTRIBUTION_MESSAGE
1518 PRETTY_PRINT(FIELD(Header), FIELD(ScsiLun), FIELD(NewSize));
1519 } LX_MINI_INIT_RESIZE_DISTRIBUTION_MESSAGE, *PLX_MINI_INIT_RESIZE_DISTRIBUTION_MESSAGE;
1520
1521 +typedef struct _LX_MINI_INIT_TRIM_DISTRIBUTION_RESPONSE
1522 +{
1523 + static inline auto Type = LxMiniInitMessageTrimDistributionResponse;
1524 +
1525 + MESSAGE_HEADER Header;
1526 + uint32_t ResponseCode;
1527 +
1528 + PRETTY_PRINT(FIELD(Header), FIELD(ResponseCode));
1529 +} LX_MINI_INIT_TRIM_DISTRIBUTION_RESPONSE, *PLX_MINI_INIT_TRIM_DISTRIBUTION_RESPONSE;
1530 +
1531 +typedef struct _LX_MINI_INIT_TRIM_DISTRIBUTION_MESSAGE
1532 +{
1533 + static inline auto Type = LxMiniInitMessageTrimDistribution;
1534 +
1535 + MESSAGE_HEADER Header;
1536 + unsigned int ScsiLun;
1537 +
1538 + PRETTY_PRINT(FIELD(Header), FIELD(ScsiLun));
1539 +} LX_MINI_INIT_TRIM_DISTRIBUTION_MESSAGE, *PLX_MINI_INIT_TRIM_DISTRIBUTION_MESSAGE;
1540 +
1541 struct CREATE_PROCESS_MESSAGE
1542 {
1543 static inline auto Type = LxInitCreateProcess;
src/windows/common/WslClient.cpp
+16 -2
@@ -765,6 +765,10 @@ int ListDistributionsHelper(_In_ ListOptions options)
765 state = L"Exporting";
766 break;
767
768 + case LxssDistributionStateCompacting:
769 + state = L"Compacting";
770 + break;
771 +
772 default:
773 break;
774 }
@@ -790,7 +794,8 @@ int ListDistributionsHelper(_In_ ListOptions options)
794 std::erase_if(distros, [&](const auto& entry) {
795 return (
796 (entry.State == LxssDistributionStateInstalling) || (entry.State == LxssDistributionStateUninstalling) ||
793 - (entry.State == LxssDistributionStateConverting) || (entry.State == LxssDistributionStateExporting));
797 + (entry.State == LxssDistributionStateConverting) || (entry.State == LxssDistributionStateExporting) ||
798 + (entry.State == LxssDistributionStateCompacting));
799 });
800 }
801
@@ -887,6 +892,7 @@ int Manage(_In_ std::wstring_view commandLine)
892 std::optional<std::wstring> move;
893 std::optional<std::wstring> defaultUser;
894 std::optional<uint64_t> resize;
895 + bool compact = false;
896 bool allowUnsafe = false;
897
898 ArgumentParser parser(std::wstring{commandLine}, WSL_BINARY_NAME, 0);
@@ -895,6 +901,7 @@ int Manage(_In_ std::wstring_view commandLine)
901 parser.AddArgument(AbsolutePath(move), WSL_MANAGE_ARG_MOVE_OPTION_LONG, WSL_MANAGE_ARG_MOVE_OPTION);
902 parser.AddArgument(defaultUser, WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG);
903 parser.AddArgument(SizeString(resize), WSL_MANAGE_ARG_RESIZE_OPTION_LONG, WSL_MANAGE_ARG_RESIZE_OPTION);
904 + parser.AddArgument(compact, WSL_MANAGE_ARG_COMPACT_OPTION_LONG);
905 parser.AddArgument(allowUnsafe, WSL_MANAGE_ARG_ALLOW_UNSAFE);
906 parser.Parse();
907
@@ -903,7 +910,7 @@ int Manage(_In_ std::wstring_view commandLine)
910 wsl::windows::common::SvcComm service;
911 auto distroGuid = service.GetDistributionId(distribution);
912
906 - if (sparse.has_value() + move.has_value() + defaultUser.has_value() + resize.has_value() != 1)
913 + if (sparse.has_value() + move.has_value() + defaultUser.has_value() + resize.has_value() + compact != 1)
914 {
915 THROW_HR(WSL_E_INVALID_USAGE);
916 }
@@ -950,6 +957,13 @@ int Manage(_In_ std::wstring_view commandLine)
957 {
958 THROW_IF_FAILED(service.ResizeDistribution(&distroGuid, resize.value()));
959 }
960 + else if (compact)
961 + {
962 + auto progress = wsl::windows::common::ConsoleProgressIndicator(wsl::shared::Localization::MessageCompactionStart(), true);
963 + const auto result = service.CompactDistribution(&distroGuid);
964 + progress.End();
965 + THROW_IF_FAILED(result);
966 + }
967
968 wsl::windows::common::wslutil::PrintSystemError(ERROR_SUCCESS);
969 return 0;
src/windows/common/WslCoreFilesystem.cpp
+10
@@ -92,6 +92,16 @@ wil::unique_handle wsl::core::filesystem::OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUA
92 return disk;
93 }
94
95 +void wsl::core::filesystem::CompactVhd(_In_ LPCWSTR Path)
96 +{
97 + auto diskHandle = OpenVhd(Path, VIRTUAL_DISK_ACCESS_GET_INFO | VIRTUAL_DISK_ACCESS_METAOPS);
98 +
99 + COMPACT_VIRTUAL_DISK_PARAMETERS compact{};
100 + compact.Version = COMPACT_VIRTUAL_DISK_VERSION_1;
101 +
102 + THROW_IF_WIN32_ERROR(CompactVirtualDisk(diskHandle.get(), COMPACT_VIRTUAL_DISK_FLAG_NONE, &compact, nullptr));
103 +}
104 +
105 void wsl::core::filesystem::ResizeExistingVhd(_In_ HANDLE diskHandle, _In_ ULONGLONG maximumSize, _In_ RESIZE_VIRTUAL_DISK_FLAG resizeFlag)
106 {
107 RESIZE_VIRTUAL_DISK_PARAMETERS resize{};
src/windows/common/WslCoreFilesystem.h
+2
@@ -37,6 +37,8 @@ void CreateVhd(_In_ LPCWSTR target, _In_ ULONGLONG maximumSize, _In_ PSID userSi
37
38 wil::unique_handle OpenVhd(_In_ LPCWSTR Path, _In_ VIRTUAL_DISK_ACCESS_MASK Mask);
39
40 +void CompactVhd(_In_ LPCWSTR Path);
41 +
42 void ResizeExistingVhd(_In_ HANDLE diskHandle, _In_ ULONGLONG maximumSize, _In_ RESIZE_VIRTUAL_DISK_FLAG resizeFlag);
43
44 ULONGLONG GetDiskSize(_In_ HANDLE diskHandle);
src/windows/common/svccomm.cpp
+7
@@ -653,6 +653,13 @@ wsl::windows::common::SvcComm::ResizeDistribution(_In_ LPCGUID DistroGuid, _In_
653 RETURN_HR(result);
654 }
655
656 +HRESULT
657 +wsl::windows::common::SvcComm::CompactDistribution(_In_ LPCGUID DistroGuid) const
658 +{
659 + ClientExecutionContext context;
660 + RETURN_HR(m_userSession->CompactDistribution(DistroGuid, context.OutError()));
661 +}
662 +
663 HRESULT
664 wsl::windows::common::SvcComm::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version) const
665 {
src/windows/common/svccomm.hpp
+3
@@ -89,6 +89,9 @@ public:
89 HRESULT
90 ResizeDistribution(_In_ LPCGUID DistroGuid, _In_ ULONG64 NewSize) const;
91
92 + HRESULT
93 + CompactDistribution(_In_ LPCGUID DistroGuid) const;
94 +
95 void SetDefaultDistribution(_In_ LPCGUID DistroGuid) const;
96
97 HRESULT
src/windows/inc/wsl.h
+1
@@ -75,6 +75,7 @@ Abstract:
75 #define WSL_MANAGE_ARG_SET_SPARSE_OPTION L's'
76 #define WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG L"--set-sparse"
77 #define WSL_MANAGE_ARG_SET_DEFAULT_USER_OPTION_LONG L"--set-default-user"
78 +#define WSL_MANAGE_ARG_COMPACT_OPTION_LONG L"--compact"
79 #define WSL_MOUNT_ARG L"--mount"
80 #define WSL_MOUNT_ARG_VHD_OPTION_LONG L"--vhd"
81 #define WSL_MOUNT_ARG_BARE_OPTION_LONG L"--bare"
src/windows/service/exe/LxssUserSession.cpp
+64
@@ -492,6 +492,18 @@ try
492 }
493 CATCH_RETURN()
494
495 +HRESULT STDMETHODCALLTYPE LxssUserSession::CompactDistribution(_In_ LPCGUID DistroGuid, _Out_ LXSS_ERROR_INFO* Error)
496 +try
497 +{
498 + ServiceExecutionContext context(Error);
499 +
500 + const auto session = m_session.lock();
501 + RETURN_HR_IF(RPC_E_DISCONNECTED, !session);
502 +
503 + return session->CompactDistribution(DistroGuid);
504 +}
505 +CATCH_RETURN()
506 +
507 HRESULT STDMETHODCALLTYPE LxssUserSession::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version, _In_ HANDLE StdErrHandle, _Out_ LXSS_ERROR_INFO* Error)
508 try
509 {
@@ -1871,6 +1883,58 @@ try
1883 }
1884 CATCH_RETURN()
1885
1886 +HRESULT LxssUserSessionImpl::CompactDistribution(_In_ LPCGUID DistroGuid)
1887 +try
1888 +{
1889 + auto runAsUser = wil::CoImpersonateClient();
1890 + std::filesystem::path vhdPath;
1891 + LXSS_DISTRO_CONFIGURATION configuration{};
1892 +
1893 + {
1894 + std::lock_guard lock(m_instanceLock);
1895 + const auto userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
1896 + const wil::unique_hkey lxssKey = s_OpenLxssUserKey(userToken.get());
1897 + const auto registration = DistributionRegistration::Open(lxssKey.get(), *DistroGuid);
1898 + configuration = s_GetDistributionConfiguration(registration);
1899 + RETURN_HR_IF(WSL_E_WSL2_NEEDED, WI_IsFlagClear(configuration.Flags, LXSS_DISTRO_FLAGS_VM_MODE));
1900 +
1901 + vhdPath = configuration.VhdFilePath;
1902 + if (wsl::shared::string::IsEqual(vhdPath.extension().c_str(), wsl::windows::common::wslutil::c_vhdFileExtension, true))
1903 + {
1904 + THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), wsl::shared::Localization::MessageCompactVhdNotSupported());
1905 + }
1906 +
1907 + _ConversionBegin(configuration.DistroId, LxssDistributionStateCompacting);
1908 +
1909 + // Trim the filesystem before compaction so the host can reclaim the freed blocks.
1910 + //
1911 + // WSL2 does not mount ext4 with 'discard' and does not run fsck at boot, so blocks freed
1912 + // inside the guest are still marked as allocated in the VHD and a bare compaction reclaims
1913 + // little space. Attaching the (now stopped) distribution's VHD to the utility VM and running
1914 + // an offline fsck with block discard releases those blocks, then ejecting flushes the change
1915 + // back to the VHD before it is compacted below.
1916 + //
1917 + // This is best-effort: any failure here must not prevent compaction.
1918 + try
1919 + {
1920 + _CreateVm();
1921 + const auto lun = m_utilityVm->AttachDisk(vhdPath.c_str(), WslCoreVm::DiskType::VHD, {}, true, userToken.get());
1922 + auto ejectVhd = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { m_utilityVm->EjectVhd(vhdPath.c_str()); });
1923 + m_utilityVm->TrimDistribution(lun);
1924 + }
1925 + CATCH_LOG();
1926 + }
1927 +
1928 + auto compactionComplete = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { _ConversionComplete(configuration.DistroId); });
1929 +
1930 + THROW_IF_FAILED_MSG(
1931 + wil::ResultFromException([&] { wsl::core::filesystem::CompactVhd(vhdPath.c_str()); }),
1932 + "Failed to compact VHD: %ls",
1933 + vhdPath.c_str());
1934 + return S_OK;
1935 +}
1936 +CATCH_RETURN()
1937 +
1938 HRESULT LxssUserSessionImpl::SetVersion(_In_ LPCGUID DistroGuid, _In_ ULONG Version, _In_ HANDLE StderrHandle)
1939 {
1940 RETURN_HR_IF(E_INVALIDARG, ((Version != LXSS_WSL_VERSION_1) && (Version != LXSS_WSL_VERSION_2)));
src/windows/service/exe/LxssUserSession.h
+11
@@ -190,6 +190,11 @@ public:
190 /// </summary>
191 IFACEMETHOD(ResizeDistribution)(_In_ LPCGUID DistroGuid, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize, _Out_ LXSS_ERROR_INFO* Error) override;
192
193 + /// <summary>
194 + /// Compacts the virtual disk of a distribution.
195 + /// </summary>
196 + IFACEMETHOD(CompactDistribution)(_In_ LPCGUID DistroGuid, _Out_ LXSS_ERROR_INFO* Error) override;
197 +
198 /// <summary>
199 /// Sets the default distribution.
200 /// </summary>
@@ -467,6 +472,12 @@ public:
472 HRESULT
473 ResizeDistribution(_In_ LPCGUID DistroGuid, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize);
474
475 + /// <summary>
476 + /// Compacts the disk of a distribution.
477 + /// </summary>
478 + HRESULT
479 + CompactDistribution(_In_ LPCGUID DistroGuid);
480 +
481 /// <summary>
482 /// Sets the default distribution.
483 /// </summary>
src/windows/service/exe/WslCoreVm.cpp
+18
@@ -2489,6 +2489,24 @@ void WslCoreVm::ResizeDistribution(_In_ ULONG Lun, _In_ HANDLE OutputHandle, _In
2489 }
2490 }
2491
2492 +void WslCoreVm::TrimDistribution(_In_ ULONG Lun)
2493 +{
2494 + auto lock = m_lock.lock_exclusive();
2495 +
2496 + LX_MINI_INIT_TRIM_DISTRIBUTION_MESSAGE message;
2497 + message.Header.MessageSize = sizeof(message);
2498 + message.Header.MessageType = LxMiniInitMessageTrimDistribution;
2499 + message.ScsiLun = Lun;
2500 +
2501 + auto transaction = m_miniInitChannel.StartTransaction();
2502 + transaction.Send(message);
2503 +
2504 + wsl::shared::SocketChannel channel{AcceptConnection(m_vmConfig.KernelBootTimeout), "TrimDistribution", {m_terminatingEvent.get()}};
2505 +
2506 + const auto& resultMessage = channel.ReceiveMessage<LX_MINI_INIT_TRIM_DISTRIBUTION_RESPONSE>();
2507 + THROW_HR_IF(E_FAIL, resultMessage.ResponseCode != 0);
2508 +}
2509 +
2510 void WslCoreVm::SaveAttachedDisksState()
2511 try
2512 {
src/windows/service/exe/WslCoreVm.h
+2
@@ -119,6 +119,8 @@ public:
119
120 void ResizeDistribution(_In_ ULONG Lun, _In_ HANDLE OutputHandle, _In_ ULONG64 NewSize);
121
122 + void TrimDistribution(_In_ ULONG Lun);
123 +
124 _Requires_lock_not_held_(m_lock)
125 void SaveAttachedDisksState();
126
src/windows/service/inc/wslservice.idl
+6 -1
@@ -41,7 +41,8 @@ typedef enum _LxssDistributionState
41 LxssDistributionStateInstalling,
42 LxssDistributionStateUninstalling,
43 LxssDistributionStateConverting,
44 - LxssDistributionStateExporting
44 + LxssDistributionStateExporting,
45 + LxssDistributionStateCompacting
46 } LxssDistributionState;
47
48 typedef
@@ -346,6 +347,10 @@ interface ILxssUserSession : IUnknown
347 [in] LPCGUID DistroGuid,
348 [in] LPCWSTR DistributionName,
349 [ in, out ] LXSS_ERROR_INFO * Error);
350 +
351 + HRESULT CompactDistribution(
352 + [in] LPCGUID DistroGuid,
353 + [in, out] LXSS_ERROR_INFO* Error);
354 };
355
356
test/windows/UnitTests.cpp
+114 -14
@@ -1436,6 +1436,29 @@ class UnitTests
1436 VERIFY_ARE_EQUAL(output, ExpectedOutput);
1437 }
1438
1439 + static std::wstring ExpectedUsageMessage()
1440 + {
1441 + std::wstring expectedUsageMessage;
1442 + for (auto e : wsl::shared::Localization::MessageWslUsage())
1443 + {
1444 + if (e == L'\n')
1445 + {
1446 + expectedUsageMessage += L'\r';
1447 + }
1448 +
1449 + expectedUsageMessage += e;
1450 + }
1451 +
1452 + return expectedUsageMessage + L"\r\n";
1453 + }
1454 +
1455 + static void VerifyInvalidUsage(const std::wstring& Cmd)
1456 + {
1457 + auto [output, error] = LxsstuLaunchWslAndCaptureOutput(Cmd.c_str(), -1);
1458 + VERIFY_ARE_EQUAL(ExpectedUsageMessage(), output);
1459 + VERIFY_ARE_EQUAL(error, L"");
1460 + }
1461 +
1462 TEST_METHOD(ErrorMessages)
1463 {
1464 if (LxsstuVmMode()) // wsl --mount and bridged networking only exist in WSL2.
@@ -1515,6 +1538,10 @@ class UnitTests
1538 L"--manage test_distro --resize 10GB",
1539 L"This operation is only supported by WSL2.",
1540 L"Wsl/Service/WSL_E_WSL2_NEEDED");
1541 +
1542 + // wsl.exe --manage --compact requires WSL2.
1543 + ValidateErrorMessage(
1544 + L"--manage test_distro --compact", L"This operation is only supported by WSL2.", L"Wsl/Service/WSL_E_WSL2_NEEDED");
1545 }
1546
1547 ValidateErrorMessage(
@@ -1629,20 +1656,7 @@ class UnitTests
1656
1657 VerifyOutput(L"--install --no-distribution", L"The operation completed successfully. \r\n");
1658
1632 - {
1633 - std::wstring expectedUsageMessage;
1634 - for (auto e : wsl::shared::Localization::MessageWslUsage())
1635 - {
1636 - if (e == L'\n')
1637 - {
1638 - expectedUsageMessage += L'\r';
1639 - }
1640 -
1641 - expectedUsageMessage += e;
1642 - }
1643 -
1644 - VerifyOutput(L"--manage --move .", expectedUsageMessage + L"\r\n", -1);
1645 - }
1659 + VerifyInvalidUsage(L"--manage --move .");
1660 }
1661
1662 TEST_METHOD(CommandLineParsing)
@@ -1668,6 +1682,11 @@ class UnitTests
1682 VerifyOutput(L"--exec echo -n \\\"", L"\"");
1683 }
1684
1685 + TEST_METHOD(ManageInvalidUsage)
1686 + {
1687 + VerifyInvalidUsage(L"--manage " LXSS_DISTRO_NAME_TEST_L L" --compact --resize 10GB");
1688 + }
1689 +
1690 // This test validates that the help messages for wsl.exe and wsl.config are correctly displayed.
1691 // Notes:
1692 // - This test will fail if the help messages are changed. If that's the case, simply update the below strings
@@ -1774,6 +1793,9 @@ Arguments for managing Windows Subsystem for Linux:
1793 --resize <MemoryString>
1794 Resize the disk of the distribution to the specified size.
1795
1796 + --compact
1797 + Compact the VHDX file of a WSL 2 distribution.
1798 +
1799 --mount <Disk>
1800 Attaches and mounts a physical or virtual disk in all WSL 2 distributions.
1801
@@ -3375,6 +3397,84 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND
3397 verifyRejected(std::format(L"--manage {} --move \"{}\"", name, moveTarget));
3398 }
3399
3400 + WSL2_TEST_METHOD(Compact)
3401 + {
3402 + constexpr auto name = L"compact-test-distro";
3403 +
3404 + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--import {} . \"{}\" --version 2", name, g_testDistroPath)), 0L);
3405 + WslShutdown();
3406 +
3407 + auto cleanupName =
3408 + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [name]() { LxsstuLaunchWsl(std::format(L"--unregister {}", name)); });
3409 +
3410 + const auto distroKey = OpenDistributionKey(name);
3411 + VERIFY_IS_NOT_NULL(distroKey.get());
3412 +
3413 + const auto basePath = wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"BasePath", L"");
3414 + const auto vhdFileName =
3415 + wsl::windows::common::registry::ReadString(distroKey.get(), nullptr, L"VhdFileName", L"ext4.vhdx");
3416 + const auto vhdPath = std::filesystem::path(basePath) / vhdFileName;
3417 + VERIFY_IS_TRUE(std::filesystem::exists(vhdPath));
3418 +
3419 + auto getVhdSizeOnDisk = [](const std::filesystem::path& path) {
3420 + DWORD highPart{};
3421 + SetLastError(NO_ERROR);
3422 + const auto lowPart = GetCompressedFileSizeW(path.c_str(), &highPart);
3423 + THROW_LAST_ERROR_IF(lowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR);
3424 +
3425 + ULARGE_INTEGER size{};
3426 + size.LowPart = lowPart;
3427 + size.HighPart = highPart;
3428 + return size.QuadPart;
3429 + };
3430 +
3431 + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3432 + VERIFY_ARE_EQUAL(err, L"");
3433 +
3434 + constexpr auto minimumCompactionDelta = 32ull * 1024 * 1024;
3435 + const auto sizeBeforeWrite = getVhdSizeOnDisk(vhdPath);
3436 +
3437 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(
3438 + L"-d {} -u root -- sh -c 'mkdir -p /root/vhdx-compact-test && "
3439 + L"dd if=/dev/zero bs=1M count=128 2>/dev/null | base64 -w 0 > /root/vhdx-compact-test/nonzero.bin && sync'",
3440 + name));
3441 + VERIFY_ARE_EQUAL(err, L"");
3442 + WslShutdown();
3443 +
3444 + const auto sizeAfterWrite = getVhdSizeOnDisk(vhdPath);
3445 + VERIFY_IS_TRUE(sizeAfterWrite >= sizeBeforeWrite + minimumCompactionDelta);
3446 +
3447 + // Delete the file but do NOT trim from inside the guest: reclaiming the freed blocks now
3448 + // depends on the trim that '--compact' performs on the host before compacting the VHD.
3449 + std::tie(out, err) =
3450 + LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} -u root -- sh -c 'rm /root/vhdx-compact-test/nonzero.bin && sync'", name));
3451 + VERIFY_ARE_EQUAL(err, L"");
3452 + WslShutdown();
3453 +
3454 + const auto sizeBeforeCompact = getVhdSizeOnDisk(vhdPath);
3455 +
3456 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3457 + VERIFY_ARE_EQUAL(err, L"");
3458 +
3459 + const auto sizeAfterCompact = getVhdSizeOnDisk(vhdPath);
3460 + LogInfo(
3461 + "Compact test VHD size on disk: before write=%llu, after write=%llu, before compact=%llu, after compact=%llu",
3462 + static_cast<unsigned long long>(sizeBeforeWrite),
3463 + static_cast<unsigned long long>(sizeAfterWrite),
3464 + static_cast<unsigned long long>(sizeBeforeCompact),
3465 + static_cast<unsigned long long>(sizeAfterCompact));
3466 +
3467 + VERIFY_IS_TRUE(sizeBeforeCompact >= sizeAfterCompact);
3468 + VERIFY_IS_TRUE(sizeAfterWrite >= sizeAfterCompact + minimumCompactionDelta);
3469 +
3470 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"--manage {} --compact", name));
3471 + VERIFY_ARE_EQUAL(err, L"");
3472 +
3473 + std::tie(out, err) = LxsstuLaunchWslAndCaptureOutput(std::format(L"-d {} echo ok", name));
3474 + VERIFY_ARE_EQUAL(out, L"ok\n");
3475 + VERIFY_ARE_EQUAL(err, L"");
3476 + }
3477 +
3478 WSL2_TEST_METHOD(FileOffsets)
3479 {
3480 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { DeleteFile(L"output.txt"); });