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()