Add container list filters (#40513)

Kevin Vega committed May 14, 2026 at 10:35 UTC 17aca9185691613bb2fe299ebcfb7381cc960bc8
19 files changed +720 -183
localization/strings/en-US/Resources.resw
+12
@@ -2693,6 +2693,12 @@ On first run, creates the file with all settings commented out at their defaults
2693 <value>Path to the Dockerfile (use "-" to read from stdin)</value>
2694 <comment>{Locked="Dockerfile"}{Locked="stdin"}{Locked="-"}Command line arguments should not be translated</comment>
2695 </data>
2696 + <data name="WSLCCLI_FilterArgDescription" xml:space="preserve">
2697 + <value>Filter output based on conditions provided</value>
2698 + </data>
2699 + <data name="WSLCCLI_InvalidFilterError" xml:space="preserve">
2700 + <value>Invalid argument "{}" for '-f, --filter' flag: bad format of filter (expected name=value)</value>
2701 + <comment>{FixedPlaceholder="{}"}{Locked="--filter'"}Command line arguments, file names and string inserts should not be translated</comment> </data>
2702 <data name="WSLCCLI_FollowArgDescription" xml:space="preserve">
2703 <value>Follow log output</value>
2704 </data>
@@ -2729,6 +2735,12 @@ On first run, creates the file with all settings commented out at their defaults
2735 <data name="WSLCCLI_LabelKeyEmptyError" xml:space="preserve">
2736 <value>Label key cannot be empty</value>
2737 </data>
2738 + <data name="WSLCCLI_LastArgDescription" xml:space="preserve">
2739 + <value>Show n last created containers (includes all states)</value>
2740 + </data>
2741 + <data name="WSLCCLI_LatestArgDescription" xml:space="preserve">
2742 + <value>Show the latest created container (includes all states)</value>
2743 + </data>
2744 <data name="WSLCCLI_HostnameArgDescription" xml:space="preserve">
2745 <value>Container host name</value>
2746 </data>
src/windows/common/wslutil.cpp
+17
@@ -1544,3 +1544,20 @@ std::map<std::string, std::string> wsl::windows::common::wslutil::ParseKeyValueP
1544
1545 return result;
1546 }
1547 +
1548 +std::map<std::string, std::vector<std::string>> wsl::windows::common::wslutil::ParseKeyMultiValuePairs(const KeyValuePair* pairs, ULONG count)
1549 +{
1550 + THROW_HR_IF(E_POINTER, count > 0 && pairs == nullptr);
1551 +
1552 + std::map<std::string, std::vector<std::string>> result;
1553 +
1554 + for (ULONG i = 0; i < count; i++)
1555 + {
1556 + THROW_HR_IF_NULL_MSG(E_POINTER, pairs[i].Key, "Key at index %lu is null", i);
1557 + THROW_HR_IF_NULL_MSG(E_POINTER, pairs[i].Value, "Value at index %lu is null", i);
1558 +
1559 + result[pairs[i].Key].emplace_back(pairs[i].Value);
1560 + }
1561 +
1562 + return result;
1563 +}
src/windows/common/wslutil.h
+1
@@ -342,5 +342,6 @@ std::string BuildRegistryAuthHeader(const std::string& username, const std::stri
342 std::string BuildRegistryAuthHeader(const std::string& identityToken);
343
344 std::map<std::string, std::string> ParseKeyValuePairs(_In_reads_opt_(count) const KeyValuePair* pairs, ULONG count, _In_opt_ LPCSTR reservedKey = nullptr);
345 +std::map<std::string, std::vector<std::string>> ParseKeyMultiValuePairs(_In_reads_opt_(count) const KeyValuePair* pairs, ULONG count);
346
347 } // namespace wsl::windows::common::wslutil
src/windows/service/inc/wslc.idl
+22 -5
@@ -155,6 +155,7 @@ typedef struct _KeyValuePair
155
156 typedef KeyValuePair WSLCLabel;
157 typedef KeyValuePair WSLCDriverOption;
158 +typedef KeyValuePair WSLCFilter;
159
160 typedef KeyValuePairInformation WSLCLabelInformation;
161 typedef KeyValuePairInformation WSLCDriverOptionInformation;
@@ -679,6 +680,25 @@ typedef struct _WSLCPruneImagesOptions
680 ULONG LabelsCount;
681 } WSLCPruneImagesOptions;
682
683 +typedef enum _WSLCListContainersFlags
684 +{
685 + WSLCListContainersFlagsNone = 0,
686 + WSLCListContainersFlagsAll = 1,
687 +} WSLCListContainersFlags;
688 +
689 +cpp_quote("#define WSLCListContainersFlagsValid (WSLCListContainersFlagsAll)")
690 +
691 +cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCListContainersFlags);")
692 +
693 +typedef struct _WSLCListContainersOptions
694 +{
695 + DWORD Flags; // WSLCListContainersFlags
696 + LONG Limit;
697 +
698 + [unique, size_is(FiltersCount)] const WSLCFilter* Filters;
699 + ULONG FiltersCount;
700 +} WSLCListContainersOptions;
701 +
702 typedef enum _WSLCSessionState
703 {
704 WSLCSessionStateRunning = 0,
@@ -726,11 +746,8 @@ interface IWSLCSession : IUnknown
746 // Container management.
747 HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [out] IWSLCContainer** Container);
748 HRESULT OpenContainer([in, ref] LPCSTR Id, [out] IWSLCContainer** Container);
729 - HRESULT ListContainers([out, size_is(, *Count)] WSLCContainerEntry** Containers,
730 - [out] ULONG* Count,
731 - [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports,
732 - [out] ULONG* PortsCount);
733 - HRESULT PruneContainers([in, unique, size_is(FiltersCount)] WSLCPruneLabelFilter* Filters, [in] DWORD FiltersCount, [in] ULONGLONG Until, [out] WSLCPruneContainersResults* Result);
749 + HRESULT ListContainers([in, unique] const WSLCListContainersOptions* Options,[out, size_is(, *Count)] WSLCContainerEntry** Containers,[out] ULONG* Count, [out, size_is(, *PortsCount)] WSLCContainerPortMapping** Ports, [out] ULONG* PortsCount);
750 + HRESULT PruneContainers([in, unique, size_is(FiltersCount)] const WSLCFilter* Filters, [in] ULONG FiltersCount, [out] WSLCPruneContainersResults* Result);
751
752 // Create a process at the VM level. This is meant for debugging.
753 HRESULT CreateRootNamespaceProcess([in, ref] LPCSTR Executable, [in, ref] const WSLCProcessOptions* Options, [out] IWSLCProcess** Process, [out] int* Errno);
src/windows/wslc/arguments/ArgumentDefinitions.h
+3
@@ -53,6 +53,7 @@ _(Entrypoint, "entrypoint", NO_ALIAS, Kind::Value, L
53 _(Env, "env", L"e", Kind::Value, Localization::WSLCCLI_EnvArgDescription()) \
54 _(EnvFile, "env-file", NO_ALIAS, Kind::Value, Localization::WSLCCLI_EnvFileArgDescription()) \
55 _(File, "file", L"f", Kind::Value, Localization::WSLCCLI_FileArgDescription()) \
56 +_(Filter, "filter", L"f", Kind::Value, Localization::WSLCCLI_FilterArgDescription()) \
57 _(Follow, "follow", L"f", Kind::Flag, Localization::WSLCCLI_FollowArgDescription()) \
58 _(Format, "format", NO_ALIAS, Kind::Value, Localization::WSLCCLI_FormatArgDescription()) \
59 _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, Localization::WSLCCLI_ForwardArgsDescription()) \
@@ -66,6 +67,8 @@ _(ImportFile, "file", NO_ALIAS, Kind::Positional, L
67 _(Input, "input", L"i", Kind::Value, Localization::WSLCCLI_InputArgDescription()) \
68 _(Interactive, "interactive", L"i", Kind::Flag, Localization::WSLCCLI_InteractiveArgDescription()) \
69 _(Label, "label", L"l", Kind::Value, Localization::WSLCCLI_LabelArgDescription()) \
70 +_(Last, "last", L"n", Kind::Value, Localization::WSLCCLI_LastArgDescription()) \
71 +_(Latest, "latest", L"l", Kind::Flag, Localization::WSLCCLI_LatestArgDescription()) \
72 _(Name, "name", NO_ALIAS, Kind::Value, Localization::WSLCCLI_NameArgDescription()) \
73 /*_(NoDNS, "no-dns", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoDNSArgDescription())*/ \
74 _(NoCache, "no-cache", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoCacheArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+21
@@ -57,6 +57,14 @@ void Argument::Validate(const ArgMap& execArgs) const
57 validation::ValidateIntegerFromString<LONGLONG>(execArgs.GetAll<ArgType::Time>(), m_name);
58 break;
59
60 + case ArgType::Last:
61 + validation::ValidateIntegerFromString<int>(execArgs.GetAll<ArgType::Last>(), m_name);
62 + break;
63 +
64 + case ArgType::Filter:
65 + validation::ValidateFilter(execArgs.GetAll<ArgType::Filter>());
66 + break;
67 +
68 case ArgType::Gpus:
69 validation::ValidateGpus(execArgs.GetAll<ArgType::Gpus>(), m_name);
70 break;
@@ -115,6 +123,19 @@ void ValidateVolumeMount(const std::vector<std::wstring>& values)
123 }
124 }
125
126 +// Validates that each --filter argument is in the form "key=value". Rejects entries without an '=';
127 +// the runtime validates the key and value for specific objects.
128 +void ValidateFilter(const std::vector<std::wstring>& values)
129 +{
130 + for (const auto& value : values)
131 + {
132 + if (value.find(L'=') == std::wstring::npos)
133 + {
134 + throw ArgumentException(Localization::WSLCCLI_InvalidFilterError(value));
135 + }
136 + }
137 +}
138 +
139 // Convert string to WSLCSignal enum - accepts either signal name (e.g., "SIGKILL") or number (e.g., "9")
140 WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring& argName)
141 {
src/windows/wslc/arguments/ArgumentValidation.h
+1
@@ -72,5 +72,6 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri
72
73 void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
74 void ValidateVolumeMount(const std::vector<std::wstring>& values);
75 +void ValidateFilter(const std::vector<std::wstring>& values);
76
77 } // namespace wsl::windows::wslc::validation
src/windows/wslc/commands/ContainerCommand.h
+1 -1
@@ -119,8 +119,8 @@ struct ContainerListCommand final : public Command
119 std::wstring LongDescription() const override;
120
121 protected:
122 - void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
122 void ExecuteInternal(CLIExecutionContext& context) const override;
123 + void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
124 };
125
126 // Logs Command
src/windows/wslc/commands/ContainerListCommand.cpp
+11 -12
@@ -29,7 +29,10 @@ std::vector<Argument> ContainerListCommand::GetArguments() const
29 {
30 return {
31 Argument::Create(ArgType::All),
32 + Argument::Create(ArgType::Filter, false, NO_LIMIT),
33 Argument::Create(ArgType::Format),
34 + Argument::Create(ArgType::Last),
35 + Argument::Create(ArgType::Latest),
36 Argument::Create(ArgType::NoTrunc),
37 Argument::Create(ArgType::Quiet),
38 Argument::Create(ArgType::Session),
@@ -46,18 +49,6 @@ std::wstring ContainerListCommand::LongDescription() const
49 return Localization::WSLCCLI_ContainerListLongDesc();
50 }
51
49 -void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
50 -{
51 - if (execArgs.Contains(ArgType::Format))
52 - {
53 - auto format = execArgs.Get<ArgType::Format>();
54 - if (!IsEqual(format, L"json") && !IsEqual(format, L"table"))
55 - {
56 - throw CommandException(Localization::WSLCCLI_InvalidFormatError());
57 - }
58 - }
59 -}
60 -
52 // clang-format off
53 void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const
54 {
@@ -67,4 +58,12 @@ void ContainerListCommand::ExecuteInternal(CLIExecutionContext& context) const
58 << ListContainers;
59 }
60 // clang-format on
61 +
62 +void ContainerListCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
63 +{
64 + if (execArgs.Contains(ArgType::Last) && execArgs.Contains(ArgType::Latest))
65 + {
66 + throw CommandException(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest"));
67 + }
68 +}
69 } // namespace wsl::windows::wslc
\ No newline at end of file
src/windows/wslc/services/ContainerService.cpp
+19 -3
@@ -430,12 +430,28 @@ void ContainerService::Delete(Session& session, const std::string& id, bool forc
430 THROW_IF_FAILED(container->Delete(force ? WSLCDeleteFlagsForce : WSLCDeleteFlagsNone));
431 }
432
433 -std::vector<ContainerInformation> ContainerService::List(Session& session)
433 +std::vector<ContainerInformation> ContainerService::List(
434 + Session& session, bool all, int limit, const std::vector<std::pair<std::string, std::string>>& filters)
435 {
435 - std::vector<ContainerInformation> result;
436 + std::vector<WSLCFilter> filterEntries;
437 + filterEntries.reserve(filters.size());
438 + for (const auto& [key, value] : filters)
439 + {
440 + filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
441 + }
442 +
443 + WSLCListContainersOptions options{};
444 + options.Flags = all ? WSLCListContainersFlagsAll : WSLCListContainersFlagsNone;
445 + options.Limit = limit;
446 + options.Filters = filterEntries.data();
447 + options.FiltersCount = static_cast<ULONG>(filterEntries.size());
448 +
449 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
450 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
438 - THROW_IF_FAILED(session.Get()->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
451 + THROW_IF_FAILED(
452 + session.Get()->ListContainers(&options, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
453 +
454 + std::vector<ContainerInformation> result;
455
456 for (const auto& current : containers)
457 {
src/windows/wslc/services/ContainerService.h
+3 -1
@@ -30,7 +30,9 @@ struct ContainerService
30 static void Stop(models::Session& session, const std::string& id, models::StopContainerOptions options);
31 static void Kill(models::Session& session, const std::string& id, WSLCSignal signal = WSLCSignalSIGKILL);
32 static void Delete(models::Session& session, const std::string& id, bool force);
33 - static std::vector<models::ContainerInformation> List(models::Session& session);
33 + static std::vector<models::ContainerInformation> List(
34 + models::Session& session, bool all = false, int limit = -1, const std::vector<std::pair<std::string, std::string>>& filters = {});
35 +
36 static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
37 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
38 static void Logs(models::Session& session, const std::string& id, bool follow, ULONGLONG tail = 0);
src/windows/wslc/tasks/ContainerTasks.cpp
+29 -9
@@ -112,7 +112,33 @@ void GetContainers(CLIExecutionContext& context)
112 {
113 WI_ASSERT(context.Data.Contains(Data::Session));
114 auto& session = context.Data.Get<Data::Session>();
115 - context.Data.Add<Data::Containers>(ContainerService::List(session));
115 +
116 + int limit = -1;
117 +
118 + if (context.Args.Contains(ArgType::Last))
119 + {
120 + limit = validation::GetIntegerFromString<int>(context.Args.Get<ArgType::Last>(), L"--last");
121 + }
122 + else if (context.Args.Contains(ArgType::Latest))
123 + {
124 + limit = 1;
125 + }
126 +
127 + // Filter syntax (`key=value`) is enforced upstream; here we just split on the first '='.
128 + std::vector<std::pair<std::string, std::string>> filters;
129 + if (context.Args.Contains(ArgType::Filter))
130 + {
131 + for (const auto& wideValue : context.Args.GetAll<ArgType::Filter>())
132 + {
133 + std::string raw = WideToMultiByte(wideValue);
134 + const auto eq = raw.find('=');
135 + WI_ASSERT(eq != std::string::npos);
136 +
137 + filters.emplace_back(raw.substr(0, eq), raw.substr(eq + 1));
138 + }
139 + }
140 +
141 + context.Data.Add<Data::Containers>(ContainerService::List(session, context.Args.Contains(ArgType::All), limit, filters));
142 }
143
144 void InspectContainers(CLIExecutionContext& context)
@@ -160,14 +186,8 @@ void ListContainers(CLIExecutionContext& context)
186 WI_ASSERT(context.Data.Contains(Data::Containers));
187 auto& containers = context.Data.Get<Data::Containers>();
188
163 - // Filter by running state if --all is not specified
164 - if (!context.Args.Contains(ArgType::All))
165 - {
166 - auto shouldRemove = [](const ContainerInformation& container) {
167 - return container.State != WSLCContainerState::WslcContainerStateRunning;
168 - };
169 - containers.erase(std::remove_if(containers.begin(), containers.end(), shouldRemove), containers.end());
170 - }
189 + // Note: --all and --filter status= are honored by the Docker daemon when
190 + // GetContainers ran; no post-filtering needed here.
191
192 if (context.Args.Contains(ArgType::Quiet))
193 {
src/windows/wslcsession/DockerHTTPClient.cpp
+12 -4
@@ -297,10 +297,18 @@ docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const PruneImagesF
297 return Transaction<docker_schema::EmptyRequest, docker_schema::PruneImageResult>(verb::post, url);
298 }
299
300 -std::vector<docker_schema::ContainerInfo> DockerHTTPClient::ListContainers(bool all)
300 +std::vector<docker_schema::ContainerInfo> DockerHTTPClient::ListContainers(
301 + bool all, int limit, const std::map<std::string, std::vector<std::string>>& filters)
302 {
303 auto url = URL::Create("/containers/json");
304 url.SetParameter("all", all);
305 + url.SetParameter("limit", std::to_string(limit));
306 +
307 + if (!filters.empty())
308 + {
309 + nlohmann::json filtersJson = filters;
310 + url.SetParameter("filters", filtersJson.dump());
311 + }
312
313 return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::ContainerInfo>>(verb::get, url);
314 }
@@ -503,13 +511,13 @@ wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLo
511 return std::move(socket);
512 }
513
506 -docker_schema::PruneContainerResult DockerHTTPClient::PruneContainers(const PruneContainersFilters& filters)
514 +docker_schema::PruneContainerResult DockerHTTPClient::PruneContainers(const std::map<std::string, std::vector<std::string>>& filters)
515 {
516 auto url = URL::Create("/containers/prune");
517
510 - auto filtersJson = PruneFiltersToJson(filters);
511 - if (!filtersJson.empty())
518 + if (!filters.empty())
519 {
520 + nlohmann::json filtersJson = filters;
521 url.SetParameter("filters", filtersJson.dump());
522 }
523
src/windows/wslcsession/DockerHTTPClient.h
+3 -9
@@ -122,14 +122,8 @@ public:
122 DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE ExitingEvent, GUID VmId, ULONG ConnectTimeoutMs);
123
124 // Container management.
125 - struct PruneContainersFilters
126 - {
127 - std::optional<std::uint64_t> until;
128 - std::vector<std::string> presentLabels;
129 - std::vector<std::string> absentLabels;
130 - };
131 -
132 - std::vector<common::docker_schema::ContainerInfo> ListContainers(bool all = false);
125 + std::vector<common::docker_schema::ContainerInfo> ListContainers(
126 + bool all = false, int limit = -1, const std::map<std::string, std::vector<std::string>>& filters = {});
127 common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional<std::string>& Name);
128 void StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
129 void StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<ULONG> TimeoutSeconds);
@@ -142,7 +136,7 @@ public:
136 void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
137 wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail);
138 std::pair<uint32_t, wil::unique_socket> ExportContainer(const std::string& ContainerID);
145 - common::docker_schema::PruneContainerResult PruneContainers(const PruneContainersFilters& filters = {});
139 + common::docker_schema::PruneContainerResult PruneContainers(const std::map<std::string, std::vector<std::string>>& filters = {});
140
141 // Volume management.
142 common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request);
src/windows/wslcsession/WSLCSession.cpp
+70 -52
@@ -1704,7 +1704,7 @@ try
1704 for (int attempt = 0; attempt < c_maxNameRetries; attempt++)
1705 {
1706 auto randomName = GenerateContainerName(attempt);
1707 - if (std::ranges::none_of(m_containers, [&](const auto& c) { return c->Name() == randomName; }))
1707 + if (std::ranges::none_of(m_containers, [&](const auto& entry) { return entry.second->Name() == randomName; }))
1708 {
1709 containerName = randomName;
1710 break;
@@ -1721,7 +1721,7 @@ try
1721 }
1722 }
1723
1724 - auto& it = m_containers.emplace_back(WSLCContainerImpl::Create(
1724 + auto container = WSLCContainerImpl::Create(
1725 *containerOptions,
1726 containerName,
1727 *this,
@@ -1730,9 +1730,13 @@ try
1730 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1731 m_eventTracker.value(),
1732 m_dockerClient.value(),
1733 - m_ioRelay));
1733 + m_ioRelay);
1734
1735 - it->CopyTo(Container);
1735 + // Key the map by Docker's container ID, which is set in the WSLCContainerImpl constructor and stable for its lifetime.
1736 + auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
1737 + WI_ASSERT(inserted);
1738 +
1739 + it->second->CopyTo(Container);
1740
1741 return S_OK;
1742 }
@@ -1763,8 +1767,8 @@ try
1767 std::lock_guard containersLock{m_containersLock};
1768
1769 // Purge containers that were auto-deleted via OnEvent (--rm).
1766 - std::erase_if(m_containers, [](const auto& e) { return e->State() == WslcContainerStateDeleted; });
1767 - auto it = std::ranges::find_if(m_containers, [Id](const auto& e) { return e->ID() == Id; });
1770 + std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });
1771 + auto it = m_containers.find(Id);
1772
1773 // If no match is found, call Inspect() so that partial IDs and names are matched.
1774 if (it == m_containers.end())
@@ -1784,12 +1788,12 @@ try
1788 THROW_HR_MSG(E_FAIL, "Unexpected error inspecting container '%hs': %hs", Id, e.what());
1789 }
1790
1787 - it = std::ranges::find_if(m_containers, [&](const auto& e) { return e->ID() == inspectResult.Id; });
1791 + it = m_containers.find(inspectResult.Id);
1792 RETURN_HR_IF_MSG(
1793 E_UNEXPECTED, it == m_containers.end(), "Resolved container ID (%hs -> %hs) not found", Id, inspectResult.Id.c_str());
1794 }
1795
1792 - auto result = wil::ResultFromException([&]() { (*it)->CopyTo(Container); });
1796 + auto result = wil::ResultFromException([&]() { it->second->CopyTo(Container); });
1797
1798 // Return WSLC_E_CONTAINER_NOT_FOUND if the container was found, but is being deleted for consistency.
1799 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerNotFound(Id), result == RPC_E_DISCONNECTED);
@@ -1798,28 +1802,71 @@ try
1802 }
1803 CATCH_RETURN();
1804
1801 -HRESULT WSLCSession::ListContainers(WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount)
1805 +HRESULT WSLCSession::ListContainers(
1806 + const WSLCListContainersOptions* Options, WSLCContainerEntry** Containers, ULONG* Count, WSLCContainerPortMapping** Ports, ULONG* PortsCount)
1807 try
1808 {
1809 COMServiceExecutionContext context;
1810
1811 + RETURN_HR_IF_NULL(E_POINTER, Containers);
1812 + RETURN_HR_IF_NULL(E_POINTER, Count);
1813 + RETURN_HR_IF_NULL(E_POINTER, Ports);
1814 + RETURN_HR_IF_NULL(E_POINTER, PortsCount);
1815 +
1816 *Count = 0;
1817 *Containers = nullptr;
1818 *Ports = nullptr;
1819 *PortsCount = 0;
1820
1821 + bool all = false;
1822 + int limit = -1;
1823 + std::map<std::string, std::vector<std::string>> filters;
1824 +
1825 + if (Options != nullptr)
1826 + {
1827 + THROW_HR_IF_MSG(
1828 + E_INVALIDARG,
1829 + WI_IsAnyFlagSet(static_cast<WSLCListContainersFlags>(Options->Flags), ~WSLCListContainersFlagsValid),
1830 + "Invalid flags: 0x%x",
1831 + Options->Flags);
1832 +
1833 + all = WI_IsFlagSet(Options->Flags, WSLCListContainersFlagsAll);
1834 + limit = static_cast<int>(Options->Limit);
1835 +
1836 + filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Options->Filters, Options->FiltersCount);
1837 + }
1838 +
1839 auto lock = m_lock.lock_shared();
1840 + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1841 +
1842 + std::vector<docker_schema::ContainerInfo> dockerContainers;
1843 + try
1844 + {
1845 + dockerContainers = m_dockerClient->ListContainers(all, limit, filters);
1846 + }
1847 + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list containers");
1848 +
1849 std::lock_guard containersLock{m_containersLock};
1850
1851 // Purge containers that were auto-deleted via OnEvent (--rm).
1815 - std::erase_if(m_containers, [](const auto& e) { return e->State() == WslcContainerStateDeleted; });
1852 + std::erase_if(m_containers, [](const auto& entry) { return entry.second->State() == WslcContainerStateDeleted; });
1853
1817 - auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(m_containers.size());
1854 + // Allocate up to the Docker result count. The actual count (tracked via index) may be smaller
1855 + // if some IDs returned by Docker aren't in m_containers (e.g. created externally), but in the
1856 + // common case the two should match.
1857 + auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
1858 std::vector<WSLCContainerPortMapping> allPorts;
1859
1860 size_t index = 0;
1821 - for (const auto& e : m_containers)
1861 + for (const auto& dockerContainer : dockerContainers)
1862 {
1863 + auto it = m_containers.find(dockerContainer.Id);
1864 + if (it == m_containers.end())
1865 + {
1866 + continue;
1867 + }
1868 +
1869 + auto* e = it->second.get();
1870 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, e->Image().c_str()) != 0);
1871 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, e->Name().c_str()) != 0);
1872 THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Id, e->ID().c_str()) != 0);
@@ -1843,7 +1890,7 @@ try
1890 index++;
1891 }
1892
1846 - *Count = static_cast<ULONG>(m_containers.size());
1893 + *Count = static_cast<ULONG>(index);
1894 *Containers = output.release();
1895
1896 if (!allPorts.empty())
@@ -1858,7 +1905,7 @@ try
1905 }
1906 CATCH_RETURN();
1907
1861 -HRESULT WSLCSession::PruneContainers(_In_opt_ WSLCPruneLabelFilter* Filters, _In_ DWORD FiltersCount, _In_ ULONGLONG Until, _Out_ WSLCPruneContainersResults* Result)
1908 +HRESULT WSLCSession::PruneContainers(_In_opt_ const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCPruneContainersResults* Result)
1909 try
1910 {
1911 COMServiceExecutionContext context;
@@ -1866,38 +1913,7 @@ try
1913 RETURN_HR_IF_NULL(E_POINTER, Result);
1914 ZeroMemory(Result, sizeof(*Result));
1915
1869 - DockerHTTPClient::PruneContainersFilters filters;
1870 -
1871 - if (FiltersCount > 0)
1872 - {
1873 - THROW_HR_IF(E_POINTER, FiltersCount > 0 && Filters == nullptr);
1874 -
1875 - for (DWORD i = 0; i < FiltersCount; ++i)
1876 - {
1877 - THROW_HR_IF_MSG(E_POINTER, Filters[i].Key == nullptr, "Filter key cannot be null (index %lu)", i);
1878 - std::string labelFilter = Filters[i].Key;
1879 -
1880 - if (Filters[i].Value != nullptr)
1881 - {
1882 - labelFilter += '=';
1883 - labelFilter += Filters[i].Value;
1884 - }
1885 -
1886 - if (Filters[i].Present)
1887 - {
1888 - filters.presentLabels.emplace_back(std::move(labelFilter));
1889 - }
1890 - else
1891 - {
1892 - filters.absentLabels.emplace_back(std::move(labelFilter));
1893 - }
1894 - }
1895 - }
1896 -
1897 - if (Until > 0)
1898 - {
1899 - filters.until = Until;
1900 - }
1916 + auto filters = wsl::windows::common::wslutil::ParseKeyMultiValuePairs(Filters, FiltersCount);
1917
1918 auto lock = m_lock.lock_shared();
1919 RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
@@ -1917,11 +1933,12 @@ try
1933 if (pruneResult.ContainersDeleted.has_value() && pruneResult.ContainersDeleted->size() > 0)
1934 {
1935 // Remove deleted containers from m_containers.
1920 - auto pred = [&](const auto& e) {
1921 - return std::ranges::find(pruneResult.ContainersDeleted.value(), e->ID()) != pruneResult.ContainersDeleted->end();
1922 - };
1936 + size_t erased = 0;
1937 + for (const auto& deletedId : pruneResult.ContainersDeleted.value())
1938 + {
1939 + erased += m_containers.erase(deletedId);
1940 + }
1941
1924 - auto erased = std::erase_if(m_containers, pred);
1942 LOG_HR_IF_MSG(
1943 E_UNEXPECTED,
1944 erased != pruneResult.ContainersDeleted->size(),
@@ -2726,7 +2743,7 @@ void WSLCSession::OnContainerDeleted(const WSLCContainerImpl* Container)
2743 auto lock = m_lock.lock_shared();
2744 std::lock_guard containersLock(m_containersLock);
2745
2729 - WI_VERIFY(std::erase_if(m_containers, [Container](const auto& e) { return e.get() == Container; }) == 1);
2746 + WI_VERIFY(m_containers.erase(Container->ID()) == 1);
2747 }
2748
2749 HRESULT WSLCSession::GetState(_Out_ WSLCSessionState* State)
@@ -2757,7 +2774,8 @@ void WSLCSession::RecoverExistingContainers()
2774 m_dockerClient.value(),
2775 m_ioRelay);
2776
2760 - m_containers.emplace_back(std::move(container));
2777 + auto [it, inserted] = m_containers.emplace(container->ID(), std::move(container));
2778 + WI_ASSERT(inserted);
2779 }
2780 catch (...)
2781 {
src/windows/wslcsession/WSLCSession.h
+8 -3
@@ -111,8 +111,13 @@ public:
111 // Container management.
112 IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _Out_ IWSLCContainer** Container) override;
113 IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override;
114 - IFACEMETHOD(ListContainers)(_Out_ WSLCContainerEntry** Containers, _Out_ ULONG* Count, _Out_ WSLCContainerPortMapping** Ports, _Out_ ULONG* PortsCount) override;
115 - IFACEMETHOD(PruneContainers)(_In_opt_ WSLCPruneLabelFilter* Filters, _In_ DWORD FiltersCount, _In_ ULONGLONG Until, _Out_ WSLCPruneContainersResults* Result) override;
114 + IFACEMETHOD(ListContainers)(
115 + _In_opt_ const WSLCListContainersOptions* Options,
116 + _Out_ WSLCContainerEntry** Containers,
117 + _Out_ ULONG* Count,
118 + _Out_ WSLCContainerPortMapping** Ports,
119 + _Out_ ULONG* PortsCount) override;
120 + IFACEMETHOD(PruneContainers)(_In_opt_ const WSLCFilter* Filters, _In_ ULONG FiltersCount, _Out_ WSLCPruneContainersResults* Result) override;
121
122 // VM management.
123 IFACEMETHOD(CreateRootNamespaceProcess)(
@@ -202,7 +207,7 @@ private:
207 // This allows independent operations to proceed while container bookkeeping remains synchronized.
208 // WSLCVolumes has its own internal srwlock and does not require m_lock.
209 std::mutex m_containersLock;
205 - std::vector<std::unique_ptr<WSLCContainerImpl>> m_containers;
210 + std::unordered_map<std::string, std::unique_ptr<WSLCContainerImpl>> m_containers;
211 std::optional<WSLCVolumes> m_volumes;
212 std::mutex m_networksLock;
213 std::unordered_map<std::string, NetworkEntry> m_networks;
test/windows/WSLCTests.cpp
+173 -54
@@ -102,7 +102,7 @@ class WSLCTests
102 }
103
104 PruneResult result;
105 - VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
105 + VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, &result.result));
106 if (result.result.ContainersCount > 0)
107 {
108 LogInfo("Pruned %lu containers", result.result.ContainersCount);
@@ -183,6 +183,30 @@ class WSLCTests
183 return RunningWSLCContainer(std::move(rawContainer), {});
184 }
185
186 + struct ListContainersResult
187 + {
188 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> Containers;
189 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> Ports;
190 + };
191 +
192 + // Issues IWSLCSession::ListContainers with WSLCListContainersFlagsAll (all containers, no filter).
193 + // If a future caller needs a different flag set, add a parameter.
194 + ListContainersResult ListContainers(IWSLCSession* session)
195 + {
196 + WSLCListContainersOptions options{};
197 + options.Flags = WSLCListContainersFlagsAll;
198 +
199 + ListContainersResult result;
200 + VERIFY_SUCCEEDED(session->ListContainers(
201 + &options,
202 + result.Containers.addressof(),
203 + result.Containers.size_address<ULONG>(),
204 + result.Ports.addressof(),
205 + result.Ports.size_address<ULONG>()));
206 +
207 + return result;
208 + }
209 +
210 std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, USHORT port = 5000)
211 {
212 std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
@@ -2079,11 +2103,7 @@ class WSLCTests
2103 });
2104
2105 // Validate that the session is correctly restarted.
2082 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
2083 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
2084 -
2085 - VERIFY_SUCCEEDED(
2086 - m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
2106 + auto [containers, ports] = ListContainers(m_defaultSession.get());
2107
2108 VERIFY_ARE_EQUAL(containers.size(), 1);
2109 VERIFY_ARE_EQUAL(containers[0].Id, containerId);
@@ -3954,7 +3974,7 @@ class WSLCTests
3974 // Prune containers on exit so this test doesn't leak "wslc-test-container-vhd" on exit.
3975 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
3976 PruneResult result;
3957 - LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
3977 + LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, &result.result));
3978 });
3979
3980 WSLCVolumeOptions volumeOptions{};
@@ -5255,11 +5275,7 @@ class WSLCTests
5275 WSLC_TEST_METHOD(ContainerState)
5276 {
5277 auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
5258 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
5259 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
5260 -
5261 - VERIFY_SUCCEEDED(
5262 - m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5278 + auto [containers, ports] = ListContainers(m_defaultSession.get());
5279 VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
5280
5281 for (size_t i = 0; i < expectedContainers.size(); i++)
@@ -5301,10 +5317,7 @@ class WSLCTests
5317 ULONGLONG runningStateChangedAt{};
5318 ULONGLONG runningCreatedAt{};
5319 {
5304 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
5305 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
5306 - VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
5307 - &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5320 + auto [containers, ports] = ListContainers(m_defaultSession.get());
5321 VERIFY_ARE_EQUAL(containers.size(), 1);
5322 runningStateChangedAt = containers[0].StateChangedAt;
5323 runningCreatedAt = containers[0].CreatedAt;
@@ -5330,10 +5343,7 @@ class WSLCTests
5343
5344 // Verify that StateChangedAt was updated after the state transition.
5345 {
5333 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
5334 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
5335 - VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
5336 - &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5346 + auto [containers, ports] = ListContainers(m_defaultSession.get());
5347 VERIFY_ARE_EQUAL(containers.size(), 1);
5348
5349 auto now = static_cast<ULONGLONG>(time(nullptr));
@@ -5574,14 +5584,123 @@ class WSLCTests
5584 }
5585 }
5586
5577 - WSLC_TEST_METHOD(ContainerNetwork)
5587 + WSLC_TEST_METHOD(ContainerListFilter)
5588 {
5579 - auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
5589 + // Lists containers with the given filter options and returns the names as a set.
5590 + auto listContainers = [&](DWORD flags, std::initializer_list<std::pair<std::string, std::string>> filterPairs) {
5591 + std::vector<std::pair<std::string, std::string>> storage(filterPairs.begin(), filterPairs.end());
5592 + std::vector<WSLCFilter> filters;
5593 + filters.reserve(storage.size());
5594 + for (const auto& [k, v] : storage)
5595 + {
5596 + filters.push_back({.Key = k.c_str(), .Value = v.c_str()});
5597 + }
5598 +
5599 + WSLCListContainersOptions options{};
5600 + options.Flags = flags;
5601 + options.Filters = filters.data();
5602 + options.FiltersCount = static_cast<ULONG>(filters.size());
5603 +
5604 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
5605 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
5606 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
5607 + &options, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5608 +
5609 + std::set<std::string> names;
5610 + for (const auto& c : containers)
5611 + {
5612 + names.insert(c.Name);
5613 + }
5614 + return names;
5615 + };
5616 +
5617 + auto expectContainers =
5618 + [&](DWORD flags, std::initializer_list<std::pair<std::string, std::string>> filterPairs, std::set<std::string> expected) {
5619 + VERIFY_ARE_EQUAL(expected, listContainers(flags, filterPairs));
5620 + };
5621 +
5622 + // Set up: one running container, one exited container, one created container.
5623 + WSLCContainerLauncher runningLauncher("debian:latest", "filter-running", {"sleep", "99999"});
5624 + runningLauncher.AddLabel("filter.test", "yes");
5625 + runningLauncher.AddLabel("filter.role", "primary");
5626 + auto runningContainer = runningLauncher.Launch(*m_defaultSession);
5627 + VERIFY_ARE_EQUAL(runningContainer.State(), WslcContainerStateRunning);
5628 + std::string runningId = runningContainer.Id();
5629
5583 - VERIFY_SUCCEEDED(
5584 - m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5630 + WSLCContainerLauncher exitedLauncher("debian:latest", "filter-exited", {"true"});
5631 + exitedLauncher.AddLabel("filter.test", "yes");
5632 + auto exitedContainer = exitedLauncher.Launch(*m_defaultSession);
5633 + exitedContainer.GetInitProcess().Wait();
5634 + VERIFY_ARE_EQUAL(exitedContainer.State(), WslcContainerStateExited);
5635 +
5636 + WSLCContainerLauncher createdLauncher("debian:latest", "filter-created", {"echo", "hi"});
5637 + auto createdContainer = createdLauncher.Create(*m_defaultSession);
5638 + VERIFY_ARE_EQUAL(createdContainer.State(), WslcContainerStateCreated);
5639 +
5640 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
5641 + LOG_IF_FAILED(runningContainer.Get().Delete(WSLCDeleteFlagsForce));
5642 + LOG_IF_FAILED(exitedContainer.Get().Delete(WSLCDeleteFlagsForce));
5643 + LOG_IF_FAILED(createdContainer.Get().Delete(WSLCDeleteFlagsForce));
5644 + });
5645 +
5646 + // Default (Flags=None, no filters) -> only running containers visible.
5647 + expectContainers(WSLCListContainersFlagsNone, {}, {"filter-running"});
5648 +
5649 + // --all (Flags=All, no filters) -> all three visible.
5650 + expectContainers(WSLCListContainersFlagsAll, {}, {"filter-running", "filter-exited", "filter-created"});
5651 +
5652 + // status=exited
5653 + expectContainers(WSLCListContainersFlagsAll, {{"status", "exited"}}, {"filter-exited"});
5654 +
5655 + // status=running OR status=created (multiple values for the same key are OR'd by Docker).
5656 + expectContainers(
5657 + WSLCListContainersFlagsAll, {{"status", "running"}, {"status", "created"}}, {"filter-running", "filter-created"});
5658 +
5659 + // name=filter-running
5660 + expectContainers(WSLCListContainersFlagsAll, {{"name", "filter-running"}}, {"filter-running"});
5661 +
5662 + // id prefix match
5663 + expectContainers(WSLCListContainersFlagsAll, {{"id", runningId.substr(0, 12)}}, {"filter-running"});
5664 +
5665 + // label=filter.test (key-only) matches running and exited (both have the label).
5666 + expectContainers(WSLCListContainersFlagsAll, {{"label", "filter.test"}}, {"filter-running", "filter-exited"});
5667 +
5668 + // label=filter.role=primary (key=value) matches only the running container.
5669 + expectContainers(WSLCListContainersFlagsAll, {{"label", "filter.role=primary"}}, {"filter-running"});
5670 +
5671 + // Multiple keys are AND'd: status=exited AND label=filter.test.
5672 + expectContainers(WSLCListContainersFlagsAll, {{"status", "exited"}, {"label", "filter.test"}}, {"filter-exited"});
5673 +
5674 + // before=filter-exited -> only containers created before filter-exited are visible.
5675 + expectContainers(WSLCListContainersFlagsAll, {{"before", "filter-exited"}}, {"filter-running"});
5676 +
5677 + // since=filter-running -> only containers created after filter-running are visible.
5678 + expectContainers(WSLCListContainersFlagsAll, {{"since", "filter-running"}}, {"filter-exited", "filter-created"});
5679 +
5680 + // exited=0 -> only the exited container that completed successfully.
5681 + expectContainers(WSLCListContainersFlagsAll, {{"exited", "0"}}, {"filter-exited"});
5682 +
5683 + // Limit caps the result count.
5684 + {
5685 + WSLCListContainersOptions options{};
5686 + options.Flags = WSLCListContainersFlagsAll;
5687 + options.Limit = 1;
5688 +
5689 + wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
5690 + wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
5691 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
5692 + &options, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
5693 +
5694 + // Docker returns at most one container; we intersect with the
5695 + // session list so the actual count should also be at most one.
5696 + VERIFY_IS_TRUE(containers.size() <= 1u);
5697 + }
5698 + }
5699 +
5700 + WSLC_TEST_METHOD(ContainerNetwork)
5701 + {
5702 + auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
5703 + auto [containers, ports] = ListContainers(m_defaultSession.get());
5704 VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
5705
5706 for (size_t i = 0; i < expectedContainers.size(); i++)
@@ -6515,7 +6634,8 @@ class WSLCTests
6634 {
6635 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
6636 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
6518 - VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6637 + VERIFY_SUCCEEDED(session.ListContainers(
6638 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6639
6640 // Find the container ID for "test-ports"
6641 std::string testPortsId;
@@ -6559,7 +6679,8 @@ class WSLCTests
6679
6680 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
6681 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
6562 - VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6682 + VERIFY_SUCCEEDED(session.ListContainers(
6683 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6684
6685 std::string createdId = createdContainer.Id();
6686 for (const auto& port : ports)
@@ -6585,7 +6706,8 @@ class WSLCTests
6706 {
6707 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
6708 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
6588 - VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6709 + VERIFY_SUCCEEDED(session.ListContainers(
6710 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
6711
6712 std::string stoppedId = container.Id();
6713 for (const auto& port : ports)
@@ -7441,9 +7563,7 @@ class WSLCTests
7563 VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7564
7565 // Capture StateChangedAt and CreatedAt before the session is destroyed.
7444 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
7445 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
7446 - VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
7566 + auto [containers, ports] = ListContainers(session.get());
7567 VERIFY_ARE_EQUAL(containers.size(), 1);
7568 originalStateChangedAt = containers[0].StateChangedAt;
7569 originalCreatedAt = containers[0].CreatedAt;
@@ -7459,9 +7579,7 @@ class WSLCTests
7579 VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7580
7581 // Verify that StateChangedAt was correctly restored from the Docker timestamp.
7462 - wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
7463 - wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
7464 - VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
7582 + auto [containers, ports] = ListContainers(session.get());
7583 VERIFY_ARE_EQUAL(containers.size(), 1);
7584
7585 // StateChangedAt may differ by ~1s between live (event time) and recovery (FinishedAt).
@@ -8472,8 +8590,8 @@ class WSLCTests
8590
8591 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
8592 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
8475 - VERIFY_SUCCEEDED(
8476 - m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8593 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
8594 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8595 VERIFY_ARE_EQUAL(containers.size(), 0);
8596 }
8597 }
@@ -8507,7 +8625,8 @@ class WSLCTests
8625
8626 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
8627 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
8510 - VERIFY_SUCCEEDED(m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8628 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
8629 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8630 VERIFY_ARE_EQUAL(containers.size(), 0);
8631 }
8632
@@ -8637,8 +8756,8 @@ class WSLCTests
8756 {
8757 wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
8758 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
8640 - VERIFY_SUCCEEDED(
8641 - m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8759 + VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
8760 + nullptr, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
8761
8762 if (containers.size() > 0)
8763 {
@@ -8738,7 +8857,7 @@ class WSLCTests
8857
8858 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
8859 PruneResult result;
8741 - LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
8860 + LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, &result.result));
8861 });
8862
8863 // Use overlapped write pipe so the server-side WriteFile doesn't block synchronously.
@@ -8855,19 +8974,19 @@ class WSLCTests
8974 {
8975 auto expectPrune = [this](
8976 const std::vector<std::string>& expectedIds = {},
8858 - const std::map<std::string, std::pair<const char*, bool>>& labels = {},
8859 - uint64_t until = 0,
8977 + const std::vector<std::pair<std::string, std::string>>& filterPairs = {},
8978 const std::source_location& source = std::source_location::current()) {
8979 PruneResult result;
8980
8863 - std::vector<WSLCPruneLabelFilter> labelsFilter;
8864 - for (const auto& e : labels)
8981 + std::vector<WSLCFilter> filters;
8982 + filters.reserve(filterPairs.size());
8983 + for (const auto& [key, value] : filterPairs)
8984 {
8866 - labelsFilter.push_back({e.first.c_str(), e.second.first, e.second.second});
8985 + filters.push_back({key.c_str(), value.c_str()});
8986 }
8987
8988 VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(
8870 - labels.empty() ? nullptr : labelsFilter.data(), static_cast<DWORD>(labelsFilter.size()), until, &result.result));
8989 + filters.empty() ? nullptr : filters.data(), static_cast<ULONG>(filters.size()), &result.result));
8990
8991 std::vector<std::string> prunedContainers;
8992 for (size_t i = 0; i < result.result.ContainersCount; i++)
@@ -8931,16 +9050,16 @@ class WSLCTests
9050 auto testPrune4 = RunAndWait(testPrune4Launcher);
9051
9052 // Expect testPrune1 to be selected via key=value.
8934 - expectPrune({testPrune1.Id()}, {{"key", {"value", true}}});
9053 + expectPrune({testPrune1.Id()}, {{"label", "key=value"}});
9054
9055 // Expect testPrune2 to be selected via key being present.
8937 - expectPrune({testPrune2.Id()}, {{"key", {nullptr, true}}});
9056 + expectPrune({testPrune2.Id()}, {{"label", "key"}});
9057
9058 // Prune by absence of 'anotherKey' label.
8940 - expectPrune({testPrune4.Id()}, {{"anotherKey", {nullptr, false}}});
9059 + expectPrune({testPrune4.Id()}, {{"label!", "anotherKey"}});
9060
9061 // Prune by label inequality.
8943 - expectPrune({testPrune3.Id()}, {{"anotherKey", {"someValue", false}}});
9062 + expectPrune({testPrune3.Id()}, {{"label!", "anotherKey=someValue"}});
9063 }
9064
9065 // Validate that the 'until' filter works.
@@ -8951,17 +9070,17 @@ class WSLCTests
9070
9071 auto now = time(nullptr);
9072
8954 - expectPrune({}, {}, now - 3600);
8955 - expectPrune({container.Id()}, {}, now + 3600);
9073 + expectPrune({}, {{"until", std::to_string(now - 3600)}});
9074 + expectPrune({container.Id()}, {{"until", std::to_string(now + 3600)}});
9075 }
9076
9077 // Validate error paths.
9078 {
8960 - WSLCPruneLabelFilter filter{.Key = nullptr, .Value = nullptr, .Present = false};
9079 + WSLCFilter filter{.Key = nullptr, .Value = nullptr};
9080 PruneResult result;
9081
8963 - VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, 0, &result.result), E_POINTER);
8964 - VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, 0, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
9082 + VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, &result.result), E_POINTER);
9083 + VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
9084 }
9085 }
9086
test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp
+294 -6
@@ -200,6 +200,291 @@ class WSLCE2EContainerListTests
200 VERIFY_IS_TRUE(findContainer(containers, containerId2));
201 }
202
203 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidKey)
204 + {
205 + // Filter keys are validated by the Docker daemon, which rejects unknown keys.
206 + const auto result = RunWslc(L"container list --filter color=blue");
207 + VERIFY_ARE_EQUAL(1, result.ExitCode);
208 + VERIFY_IS_TRUE(result.Stderr.has_value());
209 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter 'color'"));
210 + }
211 +
212 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_MalformedValue)
213 + {
214 + // Filter values must be of the form key=value; bare keys are rejected by the CLI.
215 + const auto result = RunWslc(L"container list --filter status");
216 + result.Verify({.Stdout = GetHelpMessage(), .Stderr = Localization::WSLCCLI_InvalidFilterError(L"status") + L"\r\n", .ExitCode = 1});
217 + }
218 +
219 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_InvalidStatusValue)
220 + {
221 + // Status values are validated by the Docker daemon, which rejects unknown values.
222 + const auto result = RunWslc(L"container list --filter status=bogus");
223 + VERIFY_ARE_EQUAL(1, result.ExitCode);
224 + VERIFY_IS_TRUE(result.Stderr.has_value());
225 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, result.Stderr->find(L"invalid filter 'status=bogus'"));
226 + }
227 +
228 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Name)
229 + {
230 + VerifyContainerIsNotListed(WslcContainerName);
231 + VerifyContainerIsNotListed(WslcContainerName2);
232 +
233 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
234 + result.Verify({.Stderr = L"", .ExitCode = 0});
235 + const auto containerId = result.GetStdoutOneLine();
236 +
237 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
238 + result.Verify({.Stderr = L"", .ExitCode = 0});
239 + const auto containerId2 = result.GetStdoutOneLine();
240 +
241 + result = RunWslc(std::format(L"container list --all --format json --filter name={}", WslcContainerName2));
242 + result.Verify({.Stderr = L"", .ExitCode = 0});
243 +
244 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
245 + VERIFY_ARE_EQUAL(1U, containers.size());
246 + VERIFY_ARE_EQUAL(WideToMultiByte(WslcContainerName2), std::string(containers[0].Name));
247 + }
248 +
249 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Status)
250 + {
251 + VerifyContainerIsNotListed(WslcContainerName);
252 + VerifyContainerIsNotListed(WslcContainerName2);
253 +
254 + // Created (never started) container.
255 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
256 + result.Verify({.Stderr = L"", .ExitCode = 0});
257 +
258 + // Running container.
259 + result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName2, DebianImage.NameAndTag()));
260 + result.Verify({.Stderr = L"", .ExitCode = 0});
261 +
262 + auto listNames = [&](const std::wstring& filterArgs) {
263 + auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs));
264 + r.Verify({.Stderr = L"", .ExitCode = 0});
265 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(r.Stdout.value().c_str());
266 + std::set<std::string> names;
267 + for (const auto& c : containers)
268 + {
269 + names.insert(c.Name);
270 + }
271 + return names;
272 + };
273 +
274 + // status=created
275 + {
276 + const auto names = listNames(L"--filter status=created");
277 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
278 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2)));
279 + }
280 +
281 + // status=running
282 + {
283 + const auto names = listNames(L"--filter status=running");
284 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName)));
285 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2)));
286 + }
287 +
288 + // Multiple --filter status= values are OR'd.
289 + {
290 + const auto names = listNames(L"--filter status=created --filter status=running");
291 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
292 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2)));
293 + }
294 + }
295 +
296 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Label)
297 + {
298 + VerifyContainerIsNotListed(WslcContainerName);
299 + VerifyContainerIsNotListed(WslcContainerName2);
300 +
301 + // First container has both labels; second has only one of them.
302 + auto result = RunWslc(std::format(
303 + L"container create --name {} --label filter.test=yes --label filter.role=primary {}", WslcContainerName, DebianImage.NameAndTag()));
304 + result.Verify({.Stderr = L"", .ExitCode = 0});
305 +
306 + result = RunWslc(std::format(L"container create --name {} --label filter.test=yes {}", WslcContainerName2, DebianImage.NameAndTag()));
307 + result.Verify({.Stderr = L"", .ExitCode = 0});
308 +
309 + auto listNames = [&](const std::wstring& filterArgs) {
310 + auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs));
311 + r.Verify({.Stderr = L"", .ExitCode = 0});
312 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(r.Stdout.value().c_str());
313 + std::set<std::string> names;
314 + for (const auto& c : containers)
315 + {
316 + names.insert(c.Name);
317 + }
318 + return names;
319 + };
320 +
321 + // label=<key> matches both since both have filter.test set (any value).
322 + {
323 + const auto names = listNames(L"--filter label=filter.test");
324 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
325 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2)));
326 + }
327 +
328 + // label=<key>=<value> matches only the first.
329 + {
330 + const auto names = listNames(L"--filter label=filter.role=primary");
331 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
332 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2)));
333 + }
334 +
335 + // Multiple --filter label= entries are AND'd.
336 + {
337 + const auto names = listNames(L"--filter label=filter.test --filter label=filter.role=primary");
338 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
339 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2)));
340 + }
341 + }
342 +
343 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Id)
344 + {
345 + VerifyContainerIsNotListed(WslcContainerName);
346 + VerifyContainerIsNotListed(WslcContainerName2);
347 +
348 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
349 + result.Verify({.Stderr = L"", .ExitCode = 0});
350 + const auto containerId = result.GetStdoutOneLine();
351 +
352 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
353 + result.Verify({.Stderr = L"", .ExitCode = 0});
354 +
355 + // Filter by id (full id) should return exactly one container.
356 + result = RunWslc(std::format(L"container list --all --format json --filter id={}", containerId));
357 + result.Verify({.Stderr = L"", .ExitCode = 0});
358 +
359 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
360 + VERIFY_ARE_EQUAL(1U, containers.size());
361 + VERIFY_ARE_EQUAL(WideToMultiByte(containerId), std::string(containers[0].Id));
362 + }
363 +
364 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_Exited)
365 + {
366 + VerifyContainerIsNotListed(WslcContainerName);
367 + VerifyContainerIsNotListed(WslcContainerName2);
368 +
369 + // First container exits with code 0.
370 + auto result = RunWslc(std::format(L"container run --name {} {} true", WslcContainerName, DebianImage.NameAndTag()));
371 + result.Verify({.ExitCode = 0});
372 +
373 + // Second container exits with non-zero code.
374 + result = RunWslc(std::format(L"container run --name {} {} sh -c \"exit 7\"", WslcContainerName2, DebianImage.NameAndTag()));
375 + // run with non-zero container exit code is allowed; we don't assert exit here.
376 +
377 + auto listNames = [&](const std::wstring& filterArgs) {
378 + auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs));
379 + r.Verify({.Stderr = L"", .ExitCode = 0});
380 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(r.Stdout.value().c_str());
381 + std::set<std::string> names;
382 + for (const auto& c : containers)
383 + {
384 + names.insert(c.Name);
385 + }
386 + return names;
387 + };
388 +
389 + // exited=0 should match the first container only.
390 + {
391 + const auto names = listNames(L"--filter exited=0");
392 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
393 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2)));
394 + }
395 +
396 + // exited=7 should match the second container only.
397 + {
398 + const auto names = listNames(L"--filter exited=7");
399 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName)));
400 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2)));
401 + }
402 + }
403 +
404 + WSLC_TEST_METHOD(WSLCE2E_Container_List_Filter_BeforeSince)
405 + {
406 + VerifyContainerIsNotListed(WslcContainerName);
407 + VerifyContainerIsNotListed(WslcContainerName2);
408 +
409 + // Create the first container, then the second so that ordering is deterministic.
410 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
411 + result.Verify({.Stderr = L"", .ExitCode = 0});
412 +
413 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
414 + result.Verify({.Stderr = L"", .ExitCode = 0});
415 +
416 + auto listNames = [&](const std::wstring& filterArgs) {
417 + auto r = RunWslc(std::format(L"container list --all --format json {}", filterArgs));
418 + r.Verify({.Stderr = L"", .ExitCode = 0});
419 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(r.Stdout.value().c_str());
420 + std::set<std::string> names;
421 + for (const auto& c : containers)
422 + {
423 + names.insert(c.Name);
424 + }
425 + return names;
426 + };
427 +
428 + // before=<container2 name> -> only container1 (created earlier) is visible.
429 + {
430 + const auto names = listNames(std::format(L"--filter before={}", WslcContainerName2));
431 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName)));
432 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName2)));
433 + }
434 +
435 + // since=<container1 name> -> only container2 (created later) is visible.
436 + {
437 + const auto names = listNames(std::format(L"--filter since={}", WslcContainerName));
438 + VERIFY_IS_FALSE(names.contains(WideToMultiByte(WslcContainerName)));
439 + VERIFY_IS_TRUE(names.contains(WideToMultiByte(WslcContainerName2)));
440 + }
441 + }
442 +
443 + WSLC_TEST_METHOD(WSLCE2E_Container_List_LastAndLatest)
444 + {
445 + VerifyContainerIsNotListed(WslcContainerName);
446 + VerifyContainerIsNotListed(WslcContainerName2);
447 +
448 + // Create container1 then container2 (deterministic order).
449 + auto result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
450 + result.Verify({.Stderr = L"", .ExitCode = 0});
451 +
452 + result = RunWslc(std::format(L"container create --name {} {}", WslcContainerName2, DebianImage.NameAndTag()));
453 + result.Verify({.Stderr = L"", .ExitCode = 0});
454 +
455 + // --latest is shorthand for --last 1; should return only container2 (most recent).
456 + // Implies --all so created-but-not-running containers are visible.
457 + {
458 + result = RunWslc(L"container list --latest --format json");
459 + result.Verify({.Stderr = L"", .ExitCode = 0});
460 +
461 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
462 + VERIFY_ARE_EQUAL(1U, containers.size());
463 + VERIFY_ARE_EQUAL(WideToMultiByte(WslcContainerName2), std::string(containers[0].Name));
464 + }
465 +
466 + // --last 2 should cap output at 2 containers.
467 + {
468 + result = RunWslc(L"container list --last 2 --format json");
469 + result.Verify({.Stderr = L"", .ExitCode = 0});
470 +
471 + const auto containers = wsl::shared::FromJson<std::vector<ContainerInformation>>(result.Stdout.value().c_str());
472 + VERIFY_IS_TRUE(containers.size() <= 2u);
473 + }
474 +
475 + // --last and --latest are mutually exclusive.
476 + {
477 + result = RunWslc(L"container list --last 1 --latest");
478 + VERIFY_ARE_EQUAL(1, result.ExitCode);
479 + }
480 +
481 + // --last requires an integer.
482 + {
483 + result = RunWslc(L"container list --last bogus");
484 + VERIFY_ARE_EQUAL(1, result.ExitCode);
485 + }
486 + }
487 +
488 private:
489 const std::wstring WslcContainerName = L"wslc-test-container";
490 const std::wstring WslcContainerName2 = L"wslc-test-container-2";
@@ -235,12 +520,15 @@ private:
520 {
521 std::wstringstream options;
522 options << L"The following options are available:\r\n"
238 - << L" -a,--all Show all regardless of state.\r\n"
239 - << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
240 - << L" --no-trunc Do not truncate output\r\n"
241 - << L" -q,--quiet Outputs the container IDs only\r\n"
242 - << L" --session Specify the session to use\r\n"
243 - << L" -?,--help Shows help about the selected command\r\n"
523 + << L" -a,--all Show all regardless of state.\r\n"
524 + << L" -f,--filter " << Localization::WSLCCLI_FilterArgDescription() << L"\r\n"
525 + << L" --format " << Localization::WSLCCLI_FormatArgDescription() << L"\r\n"
526 + << L" -n,--last " << Localization::WSLCCLI_LastArgDescription() << L"\r\n"
527 + << L" -l,--latest " << Localization::WSLCCLI_LatestArgDescription() << L"\r\n"
528 + << L" --no-trunc Do not truncate output\r\n"
529 + << L" -q,--quiet Outputs the container IDs only\r\n"
530 + << L" --session Specify the session to use\r\n"
531 + << L" -?,--help Shows help about the selected command\r\n"
532 << L"\r\n";
533 return options.str();
534 }
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+20 -24
@@ -278,35 +278,31 @@ wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName)
278
279 void EnsureContainerDoesNotExist(const std::wstring& containerName)
280 {
281 - auto listResult = RunWslc(L"container list --no-trunc --all");
282 - listResult.Verify({.Stderr = L"", .ExitCode = 0});
281 + const auto name = wsl::shared::string::WideToMultiByte(containerName);
282 + const auto containers = ListAllContainers();
283 + auto it = std::ranges::find_if(containers, [&](const auto& c) { return c.Name == name; });
284 + if (it == containers.end())
285 + {
286 + return;
287 + }
288
284 - auto stdoutLines = listResult.GetStdoutLines();
285 - for (const auto& line : stdoutLines)
289 + if (it->State == WSLCContainerState::WslcContainerStateRunning)
290 {
287 - if (line.find(containerName) != std::wstring::npos)
291 + auto result = RunWslc(std::format(L"container kill {}", containerName));
292 + // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already stopped/removed
293 + if (result.ExitCode != 0 &&
294 + (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
295 {
289 - if (line.find(L"running") != std::wstring::npos)
290 - {
291 - auto result = RunWslc(std::format(L"container kill {}", containerName));
292 - // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already stopped/removed
293 - if (result.ExitCode != 0 &&
294 - (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
295 - {
296 - result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
297 - }
298 - }
299 -
300 - auto result = RunWslc(std::format(L"container remove --force {}", containerName));
301 - // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already removed
302 - if (result.ExitCode != 0 &&
303 - (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
304 - {
305 - result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
306 - }
307 - break;
296 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
297 }
298 }
299 +
300 + auto result = RunWslc(std::format(L"container remove --force {}", containerName));
301 + // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already removed
302 + if (result.ExitCode != 0 && (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
303 + {
304 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
305 + }
306 }
307
308 std::vector<wsl::windows::wslc::models::ContainerInformation> ListAllContainers()