From 09f3ecb326dc7aea6c63bdd255e40da90cb43b80 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:10:48 -0700 Subject: [PATCH 1/2] Init volume prune --- localization/strings/en-US/Resources.resw | 18 ++ src/windows/wslc/commands/VolumeCommand.cpp | 1 + src/windows/wslc/commands/VolumeCommand.h | 15 ++ .../wslc/commands/VolumePruneCommand.cpp | 51 +++++ src/windows/wslc/services/VolumeModel.h | 6 + src/windows/wslc/services/VolumeService.cpp | 18 ++ src/windows/wslc/services/VolumeService.h | 1 + src/windows/wslc/tasks/VolumeTasks.cpp | 18 ++ src/windows/wslc/tasks/VolumeTasks.h | 1 + .../wslc/e2e/WSLCE2EVolumePruneTests.cpp | 186 ++++++++++++++++++ test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp | 1 + 11 files changed, 316 insertions(+) create mode 100644 src/windows/wslc/commands/VolumePruneCommand.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..fff63d0e3 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2721,6 +2721,24 @@ On first run, creates the file with all settings commented out at their defaults Lists all volumes in the session. + + Remove all unused volumes, not just anonymous ones. + + + Deleted: {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + + Remove unused volumes. + + + Removes all unused local volumes. Unused volumes are those which are not referenced by any containers. If --all is specified, removes all unused volumes, not just anonymous ones. + {Locked="--all "}Command line arguments, file names and string inserts should not be translated + + + Total reclaimed space: {:.2f} MB + {FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated + Volume name diff --git a/src/windows/wslc/commands/VolumeCommand.cpp b/src/windows/wslc/commands/VolumeCommand.cpp index 9c171a349..dd3ded298 100644 --- a/src/windows/wslc/commands/VolumeCommand.cpp +++ b/src/windows/wslc/commands/VolumeCommand.cpp @@ -26,6 +26,7 @@ std::vector> VolumeCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); return commands; } diff --git a/src/windows/wslc/commands/VolumeCommand.h b/src/windows/wslc/commands/VolumeCommand.h index 19a2b0634..e8cabdf5e 100644 --- a/src/windows/wslc/commands/VolumeCommand.h +++ b/src/windows/wslc/commands/VolumeCommand.h @@ -92,4 +92,19 @@ struct VolumeListCommand final : public Command void ValidateArgumentsInternal(const ArgMap& execArgs) const override; void ExecuteInternal(CLIExecutionContext& context) const override; }; + +// Prune Command +struct VolumePruneCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"prune"; + VolumePruneCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; } // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/VolumePruneCommand.cpp b/src/windows/wslc/commands/VolumePruneCommand.cpp new file mode 100644 index 000000000..89b5614a6 --- /dev/null +++ b/src/windows/wslc/commands/VolumePruneCommand.cpp @@ -0,0 +1,51 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + VolumePruneCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "VolumeCommand.h" +#include "CLIExecutionContext.h" +#include "VolumeTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Volume Prune Command +std::vector VolumePruneCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()), + Argument::Create(ArgType::Session), + }; +} + +std::wstring VolumePruneCommand::ShortDescription() const +{ + return Localization::WSLCCLI_VolumePruneDesc(); +} + +std::wstring VolumePruneCommand::LongDescription() const +{ + return Localization::WSLCCLI_VolumePruneLongDesc(); +} + +void VolumePruneCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << PruneVolumes; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/VolumeModel.h b/src/windows/wslc/services/VolumeModel.h index e31fd1ec3..906adb181 100644 --- a/src/windows/wslc/services/VolumeModel.h +++ b/src/windows/wslc/services/VolumeModel.h @@ -27,4 +27,10 @@ struct CreateVolumeOptions std::vector> Labels{}; }; +struct PruneVolumesResult +{ + std::vector DeletedVolumes; + ULONGLONG SpaceReclaimed{}; +}; + } // namespace wsl::windows::wslc::models diff --git a/src/windows/wslc/services/VolumeService.cpp b/src/windows/wslc/services/VolumeService.cpp index bbeb55605..be5ad9ae4 100644 --- a/src/windows/wslc/services/VolumeService.cpp +++ b/src/windows/wslc/services/VolumeService.cpp @@ -81,4 +81,22 @@ wsl::windows::common::wslc_schema::InspectVolume VolumeService::Inspect(models:: THROW_IF_FAILED(session.Get()->InspectVolume(name.c_str(), &output)); return FromJson(output.get()); } + +models::PruneVolumesResult VolumeService::Prune(models::Session& session, bool all) +{ + WSLCPruneVolumesOptions options{}; + options.All = all; + + WSLCPruneVolumesResults results{}; + THROW_IF_FAILED(session.Get()->PruneVolumes(&options, &results)); + + models::PruneVolumesResult result; + result.SpaceReclaimed = results.SpaceReclaimed; + for (ULONG i = 0; i < results.VolumesCount; ++i) + { + result.DeletedVolumes.push_back(results.Volumes[i]); + } + + return result; +} } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/VolumeService.h b/src/windows/wslc/services/VolumeService.h index bd0fe5441..daea6ee70 100644 --- a/src/windows/wslc/services/VolumeService.h +++ b/src/windows/wslc/services/VolumeService.h @@ -24,5 +24,6 @@ struct VolumeService static void Delete(models::Session& session, const std::string& name); static std::vector List(models::Session& session); static wsl::windows::common::wslc_schema::InspectVolume Inspect(models::Session& session, const std::string& name); + static models::PruneVolumesResult Prune(models::Session& session, bool all); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/VolumeTasks.cpp b/src/windows/wslc/tasks/VolumeTasks.cpp index 352153b7a..cef7bf40d 100644 --- a/src/windows/wslc/tasks/VolumeTasks.cpp +++ b/src/windows/wslc/tasks/VolumeTasks.cpp @@ -14,6 +14,7 @@ Module Name: #include "Argument.h" #include "ArgumentValidation.h" #include "CLIExecutionContext.h" +#include "ImageModel.h" #include "VolumeModel.h" #include "VolumeService.h" #include "VolumeTasks.h" @@ -203,4 +204,21 @@ void ListVolumes(CLIExecutionContext& context) THROW_HR(E_UNEXPECTED); } } + +void PruneVolumes(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + + bool all = context.Args.Contains(ArgType::All); + auto result = VolumeService::Prune(session, all); + + for (const auto& volume : result.DeletedVolumes) + { + PrintMessage(Localization::WSLCCLI_VolumePruneDeleted(volume)); + } + + PrintMessage(L""); + PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(static_cast(result.SpaceReclaimed) / WSLC_IMAGE_1MB)); +} } // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/VolumeTasks.h b/src/windows/wslc/tasks/VolumeTasks.h index d6fe39479..bb2e3b7a2 100644 --- a/src/windows/wslc/tasks/VolumeTasks.h +++ b/src/windows/wslc/tasks/VolumeTasks.h @@ -21,4 +21,5 @@ void DeleteVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void GetVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void InspectVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); void ListVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); +void PruneVolumes(wsl::windows::wslc::execution::CLIExecutionContext& context); } // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp new file mode 100644 index 000000000..d923e36c3 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -0,0 +1,186 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EVolumePruneTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC volume prune command. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EVolumePruneTests +{ + WSLC_TEST_CLASS(WSLCE2EVolumePruneTests) + + static constexpr auto TestVolumeName1 = L"prune-test-vol-1"; + static constexpr auto TestVolumeName2 = L"prune-test-vol-2"; + static constexpr auto TestContainerName = L"prune-vol-test-ctr"; + + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureContainerDoesNotExist(TestContainerName); + EnsureVolumeDoesNotExist(TestVolumeName1); + EnsureVolumeDoesNotExist(TestVolumeName2); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureContainerDoesNotExist(TestContainerName); + EnsureVolumeDoesNotExist(TestVolumeName1); + EnsureVolumeDoesNotExist(TestVolumeName2); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_HelpCommand) + { + const auto result = RunWslc(L"volume prune --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_NoUnusedVolumes) + { + const auto result = RunWslc(L"volume prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Total reclaimed space:"); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_AllFlag) + { + // Create a named volume, then prune with --all + RunWslc(std::format(L"volume create --name {}", TestVolumeName1)).Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsListed(TestVolumeName1); + + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Deleted:"); + VerifyStdoutContains(result, L"Total reclaimed space:"); + + // Verify the volume was actually removed + VerifyVolumeIsNotListed(TestVolumeName1); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_MultipleVolumes) + { + // Create two named volumes, prune with --all + RunWslc(std::format(L"volume create --name {}", TestVolumeName1)).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"volume create --name {}", TestVolumeName2)).Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyVolumeIsListed(TestVolumeName1); + VerifyVolumeIsListed(TestVolumeName2); + + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Total reclaimed space:"); + + // Verify both volumes were removed + VerifyVolumeIsNotListed(TestVolumeName1); + VerifyVolumeIsNotListed(TestVolumeName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_VolumeInUseNotPruned) + { + // Create a volume and a container using it + RunWslc(std::format(L"volume create --name {}", TestVolumeName1)).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container run -d --name {} -v {}:/data {} sleep infinity", + TestContainerName, + TestVolumeName1, + DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + auto cleanup = wil::scope_exit([&]() { + EnsureContainerDoesNotExist(TestContainerName); + EnsureVolumeDoesNotExist(TestVolumeName1); + }); + + // Prune with --all should not remove the volume in use + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Volume should still exist because it is mounted by a running container + VerifyVolumeIsListed(TestVolumeName1); + } + + WSLC_TEST_METHOD(WSLCE2E_Volume_Prune_IdempotentSecondPrune) + { + // Create a volume, prune it, then prune again + RunWslc(std::format(L"volume create --name {}", TestVolumeName1)).Verify({.Stderr = L"", .ExitCode = 0}); + + RunWslc(L"volume prune --all").Verify({.Stderr = L"", .ExitCode = 0}); + VerifyVolumeIsNotListed(TestVolumeName1); + + // Second prune should succeed with nothing to prune + const auto result = RunWslc(L"volume prune --all"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Total reclaimed space:"); + } + +private: + const TestImage& DebianImage = DebianTestImage(); + + static void VerifyStdoutContains(const WSLCExecutionResult& result, const std::wstring& substring) + { + for (const auto& line : result.GetStdoutLines()) + { + if (line.find(substring) != std::wstring::npos) + { + return; + } + } + + VERIFY_FAIL(std::format(L"Expected stdout to contain '{}'", substring).c_str()); + } + + std::wstring GetHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() // + << GetDescription() // + << GetUsage() // + << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetDescription() const + { + return Localization::WSLCCLI_VolumePruneLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetUsage() const + { + return L"Usage: wslc volume prune []\r\n\r\n"; + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << L"The following options are available:\r\n" + << L" -a,--all " << Localization::WSLCCLI_VolumePruneAllArgDescription() << L"\r\n" + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" + << L" -?,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp index 123a801ab..d271922f9 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumeTests.cpp @@ -71,6 +71,7 @@ class WSLCE2EVolumeTests {L"remove", Localization::WSLCCLI_VolumeRemoveDesc()}, {L"inspect", Localization::WSLCCLI_VolumeInspectDesc()}, {L"list", Localization::WSLCCLI_VolumeListDesc()}, + {L"prune", Localization::WSLCCLI_VolumePruneDesc()}, }; size_t maxLen = 0; From 5f98cda2aa8ccf0d1c51c5d4045eb0add80b914c Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:44:24 -0700 Subject: [PATCH 2/2] Clang format --- test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp index d923e36c3..6dc440ccf 100644 --- a/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp @@ -102,10 +102,8 @@ class WSLCE2EVolumePruneTests { // Create a volume and a container using it RunWslc(std::format(L"volume create --name {}", TestVolumeName1)).Verify({.Stderr = L"", .ExitCode = 0}); - RunWslc(std::format(L"container run -d --name {} -v {}:/data {} sleep infinity", - TestContainerName, - TestVolumeName1, - DebianImage.NameAndTag())) + RunWslc(std::format( + L"container run -d --name {} -v {}:/data {} sleep infinity", TestContainerName, TestVolumeName1, DebianImage.NameAndTag())) .Verify({.Stderr = L"", .ExitCode = 0}); auto cleanup = wil::scope_exit([&]() {