Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2242,6 +2242,20 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<data name="WSLCCLI_ContainerLogsLongDesc" xml:space="preserve">
<value>View logs for a container.</value>
</data>
<data name="WSLCCLI_ContainerPruneDeleted" xml:space="preserve">
<value>Deleted: {}</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ContainerPruneDesc" xml:space="preserve">
<value>Remove stopped containers.</value>
</data>
<data name="WSLCCLI_ContainerPruneLongDesc" xml:space="preserve">
<value>Removes all stopped containers.</value>
</data>
<data name="WSLCCLI_ContainerPruneSpaceReclaimed" xml:space="preserve">
<value>Total reclaimed space: {:.2f} MB</value>
<comment>{FixedPlaceholder="{:.2f}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ContainerRemoveDesc" xml:space="preserve">
<value>Remove containers.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
commands.push_back(std::make_unique<ContainerKillCommand>(FullName()));
commands.push_back(std::make_unique<ContainerLogsCommand>(FullName()));
commands.push_back(std::make_unique<ContainerListCommand>(FullName()));
commands.push_back(std::make_unique<ContainerPruneCommand>(FullName()));
commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/commands/ContainerCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;

protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
Expand Down
50 changes: 50 additions & 0 deletions src/windows/wslc/commands/ContainerPruneCommand.cpp
Original file line number Diff line number Diff line change
@@ -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<Argument> 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
6 changes: 6 additions & 0 deletions src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ struct VolumeMount
}
};

struct PruneContainersResult
{
std::vector<std::string> DeletedContainers;
ULONGLONG SpaceReclaimed{};
};

struct TmpfsMount
{
std::string ContainerPath() const
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Comment on lines +496 to +506
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes results.Containers is valid and populated after calling PruneContainers(nullptr, 0, 0, &results), but no buffer is allocated/provided for container IDs. If the underlying API expects a caller-provided buffer (common for COM-style “get required size then allocate then call again”), this will read from an uninitialized pointer and/or produce incomplete results. If the API allocates memory, there’s also no ownership/cleanup shown, which risks leaking. Please update this to follow the API’s documented allocation pattern (e.g., two-pass call to obtain required size then allocate, or wrap returned memory in an owning RAII type and free it appropriately) and only iterate when the container list pointer is valid for the duration needed.

Copilot uses AI. Check for mistakes.

return result;
}
} // namespace wsl::windows::wslc::services
1 change: 1 addition & 0 deletions src/windows/wslc/services/ContainerService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<Data::Session>();

auto result = ContainerService::Prune(session);

for (const auto& container : result.DeletedContainers)
{
PrintMessage(Localization::WSLCCLI_ContainerPruneDeleted(container));
Comment thread
AmelBawa-msft marked this conversation as resolved.
}

PrintMessage(L"");
PrintMessage(Localization::WSLCCLI_ContainerPruneSpaceReclaimed(static_cast<double>(result.SpaceReclaimed) / WSLC_IMAGE_1MB));
}
} // namespace wsl::windows::wslc::task
1 change: 1 addition & 0 deletions src/windows/wslc/tasks/ContainerTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
180 changes: 180 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerPruneTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*++

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:");
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test asserts English output directly. Since prune output strings are now localized (and this test already uses Localization::... for help text), this can become brittle if tests run under a non-en-US locale or if the localized string changes. Prefer asserting using the localized resource (or a helper that derives the expected prefix from Localization::WSLCCLI_ContainerPruneSpaceReclaimed(...)) rather than hardcoding L"Total reclaimed space:".

Copilot uses AI. Check for mistakes.
}

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 [<options>]\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
1 change: 1 addition & 0 deletions test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()},
Expand Down