diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index e81766f37..99293088d 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -2421,6 +2421,16 @@ For privacy information about this product please visit https://aka.ms/privacy.<
View logs for a container.
+
+ Remove all 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 ff919ebd5..20ca7a58f 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 312816488..51865a44b 100644
--- a/src/windows/wslc/commands/ContainerCommand.h
+++ b/src/windows/wslc/commands/ContainerCommand.h
@@ -210,6 +210,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 47e052788..94a283032 100644
--- a/src/windows/wslc/services/ContainerModel.h
+++ b/src/windows/wslc/services/ContainerModel.h
@@ -68,6 +68,12 @@ struct StopContainerOptions
LONG Timeout = DefaultTimeout;
};
+struct PruneContainersResult
+{
+ std::vector PrunedContainers;
+ ULONGLONG SpaceReclaimed{};
+};
+
struct KillContainerOptions
{
int Signal = WSLCSignalSIGKILL;
diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp
index 961fa467e..81b985311 100644
--- a/src/windows/wslc/services/ContainerService.cpp
+++ b/src/windows/wslc/services/ContainerService.cpp
@@ -543,4 +543,20 @@ wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Sess
THROW_IF_FAILED(container->Stats(&output));
return wsl::shared::FromJson(output.get());
}
+
+PruneContainersResult ContainerService::Prune(Session& session)
+{
+ PruneResult result;
+ THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result));
+
+ PruneContainersResult pruneResult;
+ pruneResult.SpaceReclaimed = result.result.SpaceReclaimed;
+ pruneResult.PrunedContainers.reserve(result.result.ContainersCount);
+ for (ULONG i = 0; i < result.result.ContainersCount; i++)
+ {
+ pruneResult.PrunedContainers.push_back(result.result.Containers[i]);
+ }
+
+ return pruneResult;
+}
} // namespace wsl::windows::wslc::services
diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h
index 87f002a7e..23b0c11bd 100644
--- a/src/windows/wslc/services/ContainerService.h
+++ b/src/windows/wslc/services/ContainerService.h
@@ -37,5 +37,6 @@ struct ContainerService
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, ULONGLONG tail = 0);
static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
+ 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 47d8b44c8..744e74964 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"
@@ -694,4 +695,20 @@ void ViewContainerLogs(CLIExecutionContext& context)
ContainerService::Logs(session, WideToMultiByte(containerId), follow, tail);
}
+
+void PruneContainers(CLIExecutionContext& context)
+{
+ WI_ASSERT(context.Data.Contains(Data::Session));
+ auto& session = context.Data.Get();
+
+ auto result = ContainerService::Prune(session);
+
+ for (const auto& containerId : result.PrunedContainers)
+ {
+ PrintMessage(MultiByteToWide(containerId));
+ }
+
+ 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 2af19d4ae..a8fadfeb0 100644
--- a/src/windows/wslc/tasks/ContainerTasks.h
+++ b/src/windows/wslc/tasks/ContainerTasks.h
@@ -36,6 +36,7 @@ void GetContainers(CLIExecutionContext& context);
void InspectContainers(CLIExecutionContext& context);
void KillContainers(CLIExecutionContext& context);
void ListContainers(CLIExecutionContext& context);
+void PruneContainers(CLIExecutionContext& context);
void RemoveContainers(CLIExecutionContext& context);
void RunContainer(CLIExecutionContext& context);
void SetContainerOptionsFromArgs(CLIExecutionContext& context);
diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h
index 70a0e097c..955effad7 100644
--- a/test/windows/wslc/CommandLineTestCases.h
+++ b/test/windows/wslc/CommandLineTestCases.h
@@ -56,6 +56,8 @@ COMMAND_LINE_TEST_CASE(L"container list -qa", L"list", true)
COMMAND_LINE_TEST_CASE(L"container list --format json", L"list", true)
COMMAND_LINE_TEST_CASE(L"container list --format table", L"list", true)
COMMAND_LINE_TEST_CASE(L"container list --format badformat", L"list", false)
+COMMAND_LINE_TEST_CASE(L"container prune", L"prune", true)
+COMMAND_LINE_TEST_CASE(L"container prune --session foo", L"prune", true)
COMMAND_LINE_TEST_CASE(L"run ubuntu", L"run", true)
COMMAND_LINE_TEST_CASE(L"run --rm -it --entrypoint bash archlinux:latest -c \"echo 123\"", L"run", true)
COMMAND_LINE_TEST_CASE(L"run --rm --entrypoint /bin/bash debian:latest -c ls", L"run", true)
diff --git a/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
new file mode 100644
index 000000000..968a3f07f
--- /dev/null
+++ b/test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
@@ -0,0 +1,157 @@
+/*++
+
+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)
+
+ TEST_CLASS_SETUP(ClassSetup)
+ {
+ EnsureImageIsLoaded(DebianImage);
+
+ // Clean up any leftover containers from previous failed runs
+ EnsureContainerDoesNotExist(L"prune-test-container");
+ EnsureContainerDoesNotExist(L"prune-running-test");
+ EnsureContainerDoesNotExist(L"prune-multi-1");
+ EnsureContainerDoesNotExist(L"prune-multi-2");
+ return true;
+ }
+
+ TEST_CLASS_CLEANUP(ClassCleanup)
+ {
+ // Clean up any leftover containers
+ RunWslc(L"container prune");
+ 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)
+ {
+ // Prune when no stopped containers exist should succeed with zero reclaimed space
+ const auto result = RunWslc(L"container prune");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(0.0)));
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Prune_StoppedContainer)
+ {
+ // Create and stop a container, then prune it
+ auto createResult = RunWslc(std::format(L"container create --name prune-test-container {}", DebianImage.NameAndTag()));
+ createResult.Verify({.Stderr = L"", .ExitCode = 0});
+ auto containerId = createResult.GetStdoutOneLine();
+
+ auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
+
+ // The created container is in stopped state, so prune should remove it
+ const auto result = RunWslc(L"container prune");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Verify pruned container ID is in output
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId));
+
+ // Verify the container is actually removed
+ VerifyContainerIsNotListed(L"prune-test-container");
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Prune_RunningContainerNotPruned)
+ {
+ // Start a running container, verify prune does NOT remove it
+ auto runResult = RunWslc(std::format(L"container run --detach --name prune-running-test {} sleep 300", DebianImage.NameAndTag()));
+ runResult.Verify({.Stderr = L"", .ExitCode = 0});
+
+ auto cleanup = wil::scope_exit([&]() {
+ RunWslc(L"container kill prune-running-test");
+ RunWslc(L"container remove --force prune-running-test");
+ });
+
+ // Prune should not remove a running container
+ const auto pruneResult = RunWslc(L"container prune");
+ pruneResult.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Verify the running container is still present
+ VerifyContainerIsListed(L"prune-running-test", L"running");
+ }
+
+ WSLC_TEST_METHOD(WSLCE2E_Container_Prune_MultipleStopped)
+ {
+ // Create multiple stopped containers and verify all are pruned
+ auto create1 = RunWslc(std::format(L"container create --name prune-multi-1 {}", DebianImage.NameAndTag()));
+ create1.Verify({.Stderr = L"", .ExitCode = 0});
+ auto containerId1 = create1.GetStdoutOneLine();
+
+ auto create2 = RunWslc(std::format(L"container create --name prune-multi-2 {}", DebianImage.NameAndTag()));
+ create2.Verify({.Stderr = L"", .ExitCode = 0});
+ auto containerId2 = create2.GetStdoutOneLine();
+
+ auto cleanup = wil::scope_exit([&]() { RunWslc(L"container prune"); });
+
+ const auto result = RunWslc(L"container prune");
+ result.Verify({.Stderr = L"", .ExitCode = 0});
+
+ // Verify pruned container IDs are in output
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId1));
+ VERIFY_IS_TRUE(result.StdoutContainsSubstring(containerId2));
+
+ // Verify both containers are removed
+ VerifyContainerIsNotListed(L"prune-multi-1");
+ VerifyContainerIsNotListed(L"prune-multi-2");
+ }
+
+private:
+ const TestImage& DebianImage = DebianTestImage();
+
+ 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 a9ef20db5..d1f5dc569 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()},
diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp
index 5711713ae..4a2166b21 100644
--- a/test/windows/wslc/e2e/WSLCExecutor.cpp
+++ b/test/windows/wslc/e2e/WSLCExecutor.cpp
@@ -141,6 +141,12 @@ bool WSLCExecutionResult::StdoutContainsLine(const std::wstring& expectedLine) c
return false;
}
+bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) const
+{
+ VERIFY_IS_TRUE(Stdout.has_value());
+ return Stdout.value().find(substring) != std::wstring::npos;
+}
+
WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType)
{
auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h
index c7a4fccba..98cc45de9 100644
--- a/test/windows/wslc/e2e/WSLCExecutor.h
+++ b/test/windows/wslc/e2e/WSLCExecutor.h
@@ -44,6 +44,7 @@ struct WSLCExecutionResult
std::vector GetStdoutLines() const;
std::wstring GetStdoutOneLine() const;
bool StdoutContainsLine(const std::wstring& expectedLine) const;
+ bool StdoutContainsSubstring(const std::wstring& substring) const;
};
// Interactive session for testing wslc commands that require stdin/stdout interaction.