From cde1a383f0e2a704e93050792a7dab67bd7d7d51 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:05:40 -0700 Subject: [PATCH 1/2] Init container prune --- localization/strings/en-US/Resources.resw | 14 ++ .../wslc/commands/ContainerCommand.cpp | 1 + src/windows/wslc/commands/ContainerCommand.h | 15 ++ .../wslc/commands/ContainerPruneCommand.cpp | 50 +++++ src/windows/wslc/services/ContainerModel.h | 6 + .../wslc/services/ContainerService.cpp | 15 ++ src/windows/wslc/services/ContainerService.h | 1 + src/windows/wslc/tasks/ContainerTasks.cpp | 17 ++ src/windows/wslc/tasks/ContainerTasks.h | 1 + .../wslc/e2e/WSLCE2EContainerPruneTests.cpp | 184 ++++++++++++++++++ .../wslc/e2e/WSLCE2EContainerTests.cpp | 1 + 11 files changed, 305 insertions(+) create mode 100644 src/windows/wslc/commands/ContainerPruneCommand.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index de258d29d..7a3109d9d 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2242,6 +2242,20 @@ For privacy information about this product please visit https://aka.ms/privacy.< View logs for a container. + + Deleted: {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + + Remove stopped containers. + + + Removes all stopped containers. + + + Total reclaimed space: {:.2f} MB + {FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated + Remove containers. diff --git a/src/windows/wslc/commands/ContainerCommand.cpp b/src/windows/wslc/commands/ContainerCommand.cpp index c8877950a..c9f42a8ba 100644 --- a/src/windows/wslc/commands/ContainerCommand.cpp +++ b/src/windows/wslc/commands/ContainerCommand.cpp @@ -29,6 +29,7 @@ std::vector> ContainerCommand::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())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/commands/ContainerCommand.h b/src/windows/wslc/commands/ContainerCommand.h index 503618a6d..b92b001b0 100644 --- a/src/windows/wslc/commands/ContainerCommand.h +++ b/src/windows/wslc/commands/ContainerCommand.h @@ -194,6 +194,21 @@ struct ContainerStopCommand final : public Command std::wstring ShortDescription() const override; std::wstring LongDescription() const override; +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Prune Command +struct ContainerPruneCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"prune"; + ContainerPruneCommand(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; }; diff --git a/src/windows/wslc/commands/ContainerPruneCommand.cpp b/src/windows/wslc/commands/ContainerPruneCommand.cpp new file mode 100644 index 000000000..e9a550b5a --- /dev/null +++ b/src/windows/wslc/commands/ContainerPruneCommand.cpp @@ -0,0 +1,50 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ContainerPruneCommand.cpp + +Abstract: + + Implementation of command execution logic. + +--*/ + +#include "ContainerCommand.h" +#include "CLIExecutionContext.h" +#include "ContainerTasks.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 { +// Container Prune Command +std::vector ContainerPruneCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Session), + }; +} + +std::wstring ContainerPruneCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ContainerPruneDesc(); +} + +std::wstring ContainerPruneCommand::LongDescription() const +{ + return Localization::WSLCCLI_ContainerPruneLongDesc(); +} + +void ContainerPruneCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << PruneContainers; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/ContainerModel.h b/src/windows/wslc/services/ContainerModel.h index 5997a2736..a0690f2e7 100644 --- a/src/windows/wslc/services/ContainerModel.h +++ b/src/windows/wslc/services/ContainerModel.h @@ -270,6 +270,12 @@ struct VolumeMount } }; +struct PruneContainersResult +{ + std::vector DeletedContainers; + ULONGLONG SpaceReclaimed{}; +}; + struct TmpfsMount { std::string ContainerPath() const diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 90af89e87..6dbeb06dc 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -492,4 +492,19 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow // TODO: Handle ctrl-c. io.Run({}); } + +models::PruneContainersResult ContainerService::Prune(models::Session& session) +{ + WSLCPruneContainersResults results{}; + THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, 0, &results)); + + models::PruneContainersResult result; + result.SpaceReclaimed = results.SpaceReclaimed; + for (ULONG i = 0; i < results.ContainersCount; ++i) + { + result.DeletedContainers.push_back(results.Containers[i]); + } + + return result; +} } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index aeddaff9f..31eda5e1d 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -33,5 +33,6 @@ struct ContainerService static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options); static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id); static void Logs(models::Session& session, const std::string& id, bool follow); + static models::PruneContainersResult Prune(models::Session& session); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index 804f54282..b95995702 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -17,6 +17,7 @@ Module Name: #include "ContainerModel.h" #include "ContainerService.h" #include "ContainerTasks.h" +#include "ImageModel.h" #include "SessionModel.h" #include "SessionService.h" #include "TableOutput.h" @@ -394,4 +395,20 @@ void ViewContainerLogs(CLIExecutionContext& context) bool follow = context.Args.Contains(ArgType::Follow); ContainerService::Logs(session, WideToMultiByte(containerId), follow); } + +void PruneContainers(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + auto& session = context.Data.Get(); + + auto result = ContainerService::Prune(session); + + for (const auto& container : result.DeletedContainers) + { + PrintMessage(Localization::WSLCCLI_ContainerPruneDeleted(container)); + } + + PrintMessage(L""); + PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(static_cast(result.SpaceReclaimed) / WSLC_IMAGE_1MB)); +} } // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/ContainerTasks.h b/src/windows/wslc/tasks/ContainerTasks.h index 0554a9589..ba392d5c1 100644 --- a/src/windows/wslc/tasks/ContainerTasks.h +++ b/src/windows/wslc/tasks/ContainerTasks.h @@ -42,4 +42,5 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context); void StartContainer(CLIExecutionContext& context); void StopContainers(CLIExecutionContext& context); void ViewContainerLogs(CLIExecutionContext& context); +void PruneContainers(CLIExecutionContext& context); } // namespace wsl::windows::wslc::task diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp new file mode 100644 index 000000000..427d54296 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -0,0 +1,184 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EContainerPruneTests.cpp + +Abstract: + + This file contains end-to-end tests for WSLC container prune command. +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EContainerPruneTests +{ + WSLC_TEST_CLASS(WSLCE2EContainerPruneTests) + + static constexpr auto TestContainerName1 = L"prune-test-1"; + static constexpr auto TestContainerName2 = L"prune-test-2"; + static constexpr auto TestContainerRunning = L"prune-test-running"; + + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureContainerDoesNotExist(TestContainerName1); + EnsureContainerDoesNotExist(TestContainerName2); + EnsureContainerDoesNotExist(TestContainerRunning); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureContainerDoesNotExist(TestContainerName1); + EnsureContainerDoesNotExist(TestContainerName2); + EnsureContainerDoesNotExist(TestContainerRunning); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_HelpCommand) + { + const auto result = RunWslc(L"container prune --help"); + result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_NoStoppedContainers) + { + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Total reclaimed space:"); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_StoppedContainer) + { + // Create a stopped container + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyContainerIsListed(TestContainerName1, L"created"); + + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Deleted:"); + VerifyStdoutContains(result, L"Total reclaimed space:"); + + // Verify the container was actually removed + VerifyContainerIsNotListed(TestContainerName1); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStoppedContainers) + { + // Create two stopped containers + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container create --name {} {}", TestContainerName2, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyContainerIsListed(TestContainerName1, L"created"); + VerifyContainerIsListed(TestContainerName2, L"created"); + + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + VerifyStdoutContains(result, L"Total reclaimed space:"); + + // Verify both containers were removed + VerifyContainerIsNotListed(TestContainerName1); + VerifyContainerIsNotListed(TestContainerName2); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_RunningContainerNotPruned) + { + // Start a running container + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", TestContainerRunning, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + auto cleanup = wil::scope_exit([&]() { EnsureContainerDoesNotExist(TestContainerRunning); }); + + VerifyContainerIsListed(TestContainerRunning, L"running"); + + const auto result = RunWslc(L"container prune"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Running container should still exist after prune + VerifyContainerIsListed(TestContainerRunning, L"running"); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Prune_IdempotentSecondPrune) + { + // Create and prune a container, then prune again + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) + .Verify({.Stderr = L"", .ExitCode = 0}); + + RunWslc(L"container prune").Verify({.Stderr = L"", .ExitCode = 0}); + VerifyContainerIsNotListed(TestContainerName1); + + // Second prune should succeed with nothing to prune + const auto result = RunWslc(L"container prune"); + 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_ContainerPruneLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetUsage() const + { + return L"Usage: wslc container prune []\r\n\r\n"; + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << L"The following options are available:\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/WSLCE2EContainerTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp index 73ee77a90..24f11b7fb 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp @@ -76,6 +76,7 @@ class WSLCE2EContainerTests {L"kill", Localization::WSLCCLI_ContainerKillDesc()}, {L"logs", Localization::WSLCCLI_ContainerLogsDesc()}, {L"list", Localization::WSLCCLI_ContainerListDesc()}, + {L"prune", Localization::WSLCCLI_ContainerPruneDesc()}, {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()}, {L"run", Localization::WSLCCLI_ContainerRunDesc()}, {L"start", Localization::WSLCCLI_ContainerStartDesc()}, From 6454b119e93d5832941cf625a107a647846c3061 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:45:35 -0700 Subject: [PATCH 2/2] Clang format --- test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp index 427d54296..4bfdf7660 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp @@ -66,8 +66,7 @@ class WSLCE2EContainerPruneTests WSLC_TEST_METHOD(WSLCE2E_Container_Prune_StoppedContainer) { // Create a stopped container - RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) - .Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); VerifyContainerIsListed(TestContainerName1, L"created"); @@ -84,10 +83,8 @@ class WSLCE2EContainerPruneTests WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStoppedContainers) { // Create two stopped containers - RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) - .Verify({.Stderr = L"", .ExitCode = 0}); - RunWslc(std::format(L"container create --name {} {}", TestContainerName2, DebianImage.NameAndTag())) - .Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container create --name {} {}", TestContainerName2, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); VerifyContainerIsListed(TestContainerName1, L"created"); VerifyContainerIsListed(TestContainerName2, L"created"); @@ -122,8 +119,7 @@ class WSLCE2EContainerPruneTests WSLC_TEST_METHOD(WSLCE2E_Container_Prune_IdempotentSecondPrune) { // Create and prune a container, then prune again - RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())) - .Verify({.Stderr = L"", .ExitCode = 0}); + RunWslc(std::format(L"container create --name {} {}", TestContainerName1, DebianImage.NameAndTag())).Verify({.Stderr = L"", .ExitCode = 0}); RunWslc(L"container prune").Verify({.Stderr = L"", .ExitCode = 0}); VerifyContainerIsNotListed(TestContainerName1);