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([&]() {