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
18 changes: 18 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2721,6 +2721,24 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_VolumeListLongDesc" xml:space="preserve">
<value>Lists all volumes in the session.</value>
</data>
<data name="WSLCCLI_VolumePruneAllArgDescription" xml:space="preserve">
<value>Remove all unused volumes, not just anonymous ones.</value>
</data>
<data name="WSLCCLI_VolumePruneDeleted" 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_VolumePruneDesc" xml:space="preserve">
<value>Remove unused volumes.</value>
</data>
<data name="WSLCCLI_VolumePruneLongDesc" xml:space="preserve">
<value>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.</value>
<comment>{Locked="--all "}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_VolumePruneSpaceReclaimed" 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_VolumeNameArgDescription" xml:space="preserve">
<value>Volume name</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/VolumeCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ std::vector<std::unique_ptr<Command>> VolumeCommand::GetCommands() const
commands.push_back(std::make_unique<VolumeRemoveCommand>(FullName()));
commands.push_back(std::make_unique<VolumeInspectCommand>(FullName()));
commands.push_back(std::make_unique<VolumeListCommand>(FullName()));
commands.push_back(std::make_unique<VolumePruneCommand>(FullName()));
return commands;
}

Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/commands/VolumeCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;

protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
} // namespace wsl::windows::wslc
51 changes: 51 additions & 0 deletions src/windows/wslc/commands/VolumePruneCommand.cpp
Original file line number Diff line number Diff line change
@@ -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<Argument> VolumePruneCommand::GetArguments() const
{
return {
Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()),
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 help text in the new E2E test expects -a,--all, but the argument registration passes std::nullopt for both names. If ArgType::All does not implicitly define -a/--all in this codebase, the command will not expose the intended flags and help output will diverge. To make the CLI contract explicit, define the short/long forms directly here (or adjust the help expectations to match the actual registered flags).

Suggested change
Argument::Create(ArgType::All, std::nullopt, std::nullopt, Localization::WSLCCLI_VolumePruneAllArgDescription()),
Argument::Create(ArgType::All, L"a", L"all", Localization::WSLCCLI_VolumePruneAllArgDescription()),

Copilot uses AI. Check for mistakes.
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
6 changes: 6 additions & 0 deletions src/windows/wslc/services/VolumeModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ struct CreateVolumeOptions
std::vector<std::pair<std::string, std::string>> Labels{};
};

struct PruneVolumesResult
{
std::vector<std::string> DeletedVolumes;
ULONGLONG SpaceReclaimed{};
};

} // namespace wsl::windows::wslc::models
18 changes: 18 additions & 0 deletions src/windows/wslc/services/VolumeService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<wsl::windows::common::wslc_schema::InspectVolume>(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
1 change: 1 addition & 0 deletions src/windows/wslc/services/VolumeService.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ struct VolumeService
static void Delete(models::Session& session, const std::string& name);
static std::vector<WSLCVolumeInformation> 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
18 changes: 18 additions & 0 deletions src/windows/wslc/tasks/VolumeTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<Data::Session>();

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"");
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.

A blank line is always printed even when no volumes are deleted. This creates avoidable output churn and makes CLI output harder to parse consistently. Consider only printing the separator line when result.DeletedVolumes is non-empty (or otherwise aligning output formatting with the rest of the CLI’s conventions for empty results).

Suggested change
PrintMessage(L"");
if (!result.DeletedVolumes.empty())
{
PrintMessage(L"");
}

Copilot uses AI. Check for mistakes.
PrintMessage(Localization::WSLCCLI_VolumePruneSpaceReclaimed(static_cast<double>(result.SpaceReclaimed) / WSLC_IMAGE_1MB));
}
} // namespace wsl::windows::wslc::task
1 change: 1 addition & 0 deletions src/windows/wslc/tasks/VolumeTasks.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
184 changes: 184 additions & 0 deletions test/windows/wslc/e2e/WSLCE2EVolumePruneTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*++

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);
}
Comment on lines +66 to +80
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 E2E suite covers volume prune --all but does not cover the default behavior of volume prune without --all (i.e., that named volumes should not be removed when --all is omitted). Adding a test that creates a named volume, runs volume prune (no flags), and verifies the volume remains would exercise the non---all path and prevent regressions in default semantics.

Copilot uses AI. Check for mistakes.

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