@samitouri / QOSAMI-WSL / commits / 5a61e777

CLI: Add container stats command (#40500)

David Bennett committed May 13, 2026 at 23:20 UTC 5a61e777d1ef910d252aed5b068d809860d01c37
16 files changed +631 -18
localization/strings/en-US/Resources.resw
+42
@@ -2423,6 +2423,12 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2423 <data name="WSLCCLI_ContainerStartLongDesc" xml:space="preserve">
2424 <value>Starts a container.</value>
2425 </data>
2426 + <data name="WSLCCLI_ContainerStatsDesc" xml:space="preserve">
2427 + <value>Display container resource usage statistics.</value>
2428 + </data>
2429 + <data name="WSLCCLI_ContainerStatsLongDesc" xml:space="preserve">
2430 + <value>Display a snapshot of a running container's resource usage: CPU, memory, network I/O, block I/O, and PIDs.</value>
2431 + </data>
2432 <data name="WSLCCLI_ContainerStopDesc" xml:space="preserve">
2433 <value>Stop containers.</value>
2434 </data>
@@ -2989,6 +2995,42 @@ On first run, creates the file with all settings commented out at their defaults
2995 <value>Object not found: {}</value>
2996 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2997 </data>
2998 + <data name="WSLCCLI_TableHeaderContainerId" xml:space="preserve">
2999 + <value>CONTAINER ID</value>
3000 + </data>
3001 + <data name="WSLCCLI_TableHeaderName" xml:space="preserve">
3002 + <value>NAME</value>
3003 + </data>
3004 + <data name="WSLCCLI_TableHeaderImage" xml:space="preserve">
3005 + <value>IMAGE</value>
3006 + </data>
3007 + <data name="WSLCCLI_TableHeaderCreated" xml:space="preserve">
3008 + <value>CREATED</value>
3009 + </data>
3010 + <data name="WSLCCLI_TableHeaderStatus" xml:space="preserve">
3011 + <value>STATUS</value>
3012 + </data>
3013 + <data name="WSLCCLI_TableHeaderPorts" xml:space="preserve">
3014 + <value>PORTS</value>
3015 + </data>
3016 + <data name="WSLCCLI_TableHeaderCpuPercent" xml:space="preserve">
3017 + <value>CPU %</value>
3018 + </data>
3019 + <data name="WSLCCLI_TableHeaderMemUsageLimit" xml:space="preserve">
3020 + <value>MEM USAGE / LIMIT</value>
3021 + </data>
3022 + <data name="WSLCCLI_TableHeaderMemPercent" xml:space="preserve">
3023 + <value>MEM %</value>
3024 + </data>
3025 + <data name="WSLCCLI_TableHeaderNetIo" xml:space="preserve">
3026 + <value>NET I/O</value>
3027 + </data>
3028 + <data name="WSLCCLI_TableHeaderBlockIo" xml:space="preserve">
3029 + <value>BLOCK I/O</value>
3030 + </data>
3031 + <data name="WSLCCLI_TableHeaderPids" xml:space="preserve">
3032 + <value>PIDS</value>
3033 + </data>
3034 <data name="WSLCUserSettings_Warning_InvalidValue" xml:space="preserve">
3035 <value>Warning: Invalid value for setting '{}' in {}:{}.</value>
3036 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslc/commands/ContainerCommand.cpp
+2 -1
@@ -32,6 +32,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
32 commands.push_back(std::make_unique<ContainerRemoveCommand>(FullName()));
33 commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
34 commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
35 + commands.push_back(std::make_unique<ContainerStatsCommand>(FullName()));
36 commands.push_back(std::make_unique<ContainerStopCommand>(FullName()));
37 return commands;
38 }
@@ -55,4 +56,4 @@ void ContainerCommand::ExecuteInternal(CLIExecutionContext& context) const
56 {
57 OutputHelp();
58 }
58 -} // namespace wsl::windows::wslc
\ No newline at end of file
59 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerCommand.h
+17 -1
@@ -183,6 +183,22 @@ protected:
183 void ExecuteInternal(CLIExecutionContext& context) const override;
184 };
185
186 +// Stats Command
187 +struct ContainerStatsCommand final : public Command
188 +{
189 + constexpr static std::wstring_view CommandName = L"stats";
190 + ContainerStatsCommand(const std::wstring& parent) : Command(CommandName, parent)
191 + {
192 + }
193 + std::vector<Argument> GetArguments() const override;
194 + std::wstring ShortDescription() const override;
195 + std::wstring LongDescription() const override;
196 +
197 +protected:
198 + void ValidateArgumentsInternal(const ArgMap& execArgs) const override;
199 + void ExecuteInternal(CLIExecutionContext& context) const override;
200 +};
201 +
202 // Stop Command
203 struct ContainerStopCommand final : public Command
204 {
@@ -197,4 +213,4 @@ struct ContainerStopCommand final : public Command
213 protected:
214 void ExecuteInternal(CLIExecutionContext& context) const override;
215 };
200 -} // namespace wsl::windows::wslc
\ No newline at end of file
216 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/ContainerStatsCommand.cpp new
+66
@@ -0,0 +1,66 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + ContainerStatsCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of stats command execution logic.
12 +
13 +--*/
14 +
15 +#include "ContainerCommand.h"
16 +#include "CLIExecutionContext.h"
17 +#include "ContainerTasks.h"
18 +#include "SessionTasks.h"
19 +#include "Task.h"
20 +
21 +using namespace wsl::windows::wslc::execution;
22 +using namespace wsl::windows::wslc::task;
23 +using namespace wsl::shared;
24 +using namespace wsl::shared::string;
25 +
26 +namespace wsl::windows::wslc {
27 +std::vector<Argument> ContainerStatsCommand::GetArguments() const
28 +{
29 + return {
30 + Argument::Create(ArgType::ContainerId, false, NO_LIMIT),
31 + Argument::Create(ArgType::All),
32 + Argument::Create(ArgType::Format),
33 + Argument::Create(ArgType::NoTrunc),
34 + Argument::Create(ArgType::Session),
35 + };
36 +}
37 +
38 +std::wstring ContainerStatsCommand::ShortDescription() const
39 +{
40 + return Localization::WSLCCLI_ContainerStatsDesc();
41 +}
42 +
43 +std::wstring ContainerStatsCommand::LongDescription() const
44 +{
45 + return Localization::WSLCCLI_ContainerStatsLongDesc();
46 +}
47 +
48 +void ContainerStatsCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const
49 +{
50 + if (execArgs.Contains(ArgType::Format))
51 + {
52 + auto format = execArgs.Get<ArgType::Format>();
53 + if (!IsEqual(format, L"json") && !IsEqual(format, L"table"))
54 + {
55 + throw CommandException(Localization::WSLCCLI_InvalidFormatError());
56 + }
57 + }
58 +}
59 +
60 +void ContainerStatsCommand::ExecuteInternal(CLIExecutionContext& context) const
61 +{
62 + context //
63 + << CreateSession //
64 + << ShowContainerStats;
65 +}
66 +} // namespace wsl::windows::wslc
src/windows/wslc/commands/RootCommand.cpp
+1
@@ -56,6 +56,7 @@ std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
56 commands.push_back(std::make_unique<ContainerRunCommand>(FullName()));
57 commands.push_back(std::make_unique<ImageSaveCommand>(FullName()));
58 commands.push_back(std::make_unique<ContainerStartCommand>(FullName()));
59 + commands.push_back(std::make_unique<ContainerStatsCommand>(FullName()));
60 commands.push_back(std::make_unique<ContainerStopCommand>(FullName()));
61 commands.push_back(std::make_unique<ImageTagCommand>(FullName()));
62 commands.push_back(std::make_unique<VersionCommand>(FullName()));
src/windows/wslc/core/TableOutput.h
+12 -4
@@ -96,6 +96,10 @@ struct TableOutput
96
97 static constexpr size_t DefaultColumnPadding = 3; // Docker-like spacing between columns
98
99 + // For redirected console the receiver controls the width. This should be a large value but not
100 + // too large. A few thousand should be reasonable and prevents potential arithmetic issues later.
101 + static constexpr size_t DefaultRedirectedConsoleWidth = 2000;
102 +
103 // Constructor with default behavior (no column limits)
104 TableOutput(header_t&& header, size_t sizingBuffer = 50, size_t columnPadding = DefaultColumnPadding) :
105 m_sizingBuffer(sizingBuffer), m_limitColumnWidths(false), m_columnPadding(columnPadding), m_outputFn(DefaultOutputFn())
@@ -223,6 +227,7 @@ private:
227 bool m_limitColumnWidths = false;
228 bool m_alwaysShowHeader = true;
229 bool m_showHeader = true;
230 + bool m_dropEmptyColumns = false;
231 std::wstringstream m_stream;
232 OutputFn m_outputFn;
233 size_t m_consoleWidthOverride = 0;
@@ -269,8 +274,9 @@ private:
274 return static_cast<size_t>(consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
275 }
276
272 - // Default to 80 columns if console info is unavailable
273 - return 80;
277 + // stdout is not a real console (e.g. redirected/piped). Return a large value
278 + // so column shrinking is not applied — the receiver controls its own display width.
279 + return DefaultRedirectedConsoleWidth;
280 }
281
282 void OutputHeaderOnly()
@@ -319,10 +325,12 @@ private:
325 }
326 }
327
322 - // If there are actually columns with data, then also bring in the minimum size
328 + // If there are actually columns with data, then also bring in the minimum size.
329 + // When m_dropEmptyColumns is false, always apply MinLength so empty columns
330 + // still render at least as wide as their header.
331 for (size_t i = 0; i < FieldCount; ++i)
332 {
325 - if (m_columns[i].MaxLength)
333 + if (m_columns[i].MaxLength || !m_dropEmptyColumns)
334 {
335 m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, m_columns[i].MinLength);
336 }
src/windows/wslc/services/ContainerService.cpp
+9
@@ -518,4 +518,13 @@ void ContainerService::Logs(Session& session, const std::string& id, bool follow
518 // TODO: Handle ctrl-c.
519 io.Run({});
520 }
521 +
522 +wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id)
523 +{
524 + wil::com_ptr<IWSLCContainer> container;
525 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
526 + wil::unique_cotaskmem_ansistring output;
527 + THROW_IF_FAILED(container->Stats(&output));
528 + return wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
529 +}
530 } // namespace wsl::windows::wslc::services
src/windows/wslc/services/ContainerService.h
+2
@@ -14,6 +14,7 @@ Abstract:
14 #pragma once
15 #include "SessionModel.h"
16 #include "ContainerModel.h"
17 +#include <docker_schema.h>
18 #include <wslc_schema.h>
19
20 namespace wsl::windows::wslc::services {
@@ -33,5 +34,6 @@ struct ContainerService
34 static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
35 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
36 static void Logs(models::Session& session, const std::string& id, bool follow, ULONGLONG tail = 0);
37 + static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
38 };
39 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/ContainerTasks.cpp
+224 -9
@@ -32,6 +32,36 @@ using namespace wsl::windows::wslc::execution;
32 using namespace wsl::windows::wslc::models;
33 using namespace wsl::windows::wslc::services;
34
35 +namespace {
36 +
37 +std::string FormatBytes(uint64_t bytes)
38 +{
39 + constexpr uint64_t c_kib = 1024;
40 + constexpr uint64_t c_mib = 1024 * c_kib;
41 + constexpr uint64_t c_gib = 1024 * c_mib;
42 +
43 + if (bytes >= c_gib)
44 + {
45 + return std::format("{:.2f} GiB", static_cast<double>(bytes) / static_cast<double>(c_gib));
46 + }
47 + else if (bytes >= c_mib)
48 + {
49 + return std::format("{:.2f} MiB", static_cast<double>(bytes) / static_cast<double>(c_mib));
50 + }
51 + else if (bytes >= c_kib)
52 + {
53 + return std::format("{:.2f} KiB", static_cast<double>(bytes) / static_cast<double>(c_kib));
54 + }
55 + else
56 + {
57 + // Bytes are always whole numbers, so decimal places are intentionally omitted here.
58 + // This matches the behaviour of `docker stats`.
59 + return std::format("{} B", bytes);
60 + }
61 +}
62 +
63 +} // namespace
64 +
65 namespace wsl::windows::wslc::task {
66
67 static bool TryInspectContainer(Session& session, const std::string& containerId, std::optional<wslc_schema::InspectContainer>& inspectData)
@@ -170,15 +200,20 @@ void ListContainers(CLIExecutionContext& context)
200 bool trunc = !context.Args.Contains(ArgType::NoTrunc);
201
202 // Create table with or without column limits based on --no-trunc flag
173 - auto table =
174 - trunc ? wsl::windows::wslc::TableOutput<6>(
175 - {{{L"CONTAINER ID", {Config::NoLimit, 12, false}},
176 - {L"NAME", {Config::NoLimit, 20, true}},
177 - {L"IMAGE", {Config::NoLimit, 20, false}},
178 - {L"CREATED", {Config::NoLimit, Config::NoLimit, false}},
179 - {L"STATUS", {Config::NoLimit, Config::NoLimit, false}},
180 - {L"PORTS", {Config::NoLimit, Config::NoLimit, false}}}})
181 - : wsl::windows::wslc::TableOutput<6>({L"CONTAINER ID", L"NAME", L"IMAGE", L"CREATED", L"STATUS", L"PORTS"});
203 + auto table = trunc ? wsl::windows::wslc::TableOutput<6>(
204 + {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
205 + {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
206 + {Localization::WSLCCLI_TableHeaderImage(), {Config::NoLimit, 20, false}},
207 + {Localization::WSLCCLI_TableHeaderCreated(), {Config::NoLimit, Config::NoLimit, false}},
208 + {Localization::WSLCCLI_TableHeaderStatus(), {Config::NoLimit, Config::NoLimit, false}},
209 + {Localization::WSLCCLI_TableHeaderPorts(), {Config::NoLimit, Config::NoLimit, false}}}})
210 + : wsl::windows::wslc::TableOutput<6>(
211 + {Localization::WSLCCLI_TableHeaderContainerId(),
212 + Localization::WSLCCLI_TableHeaderName(),
213 + Localization::WSLCCLI_TableHeaderImage(),
214 + Localization::WSLCCLI_TableHeaderCreated(),
215 + Localization::WSLCCLI_TableHeaderStatus(),
216 + Localization::WSLCCLI_TableHeaderPorts()});
217
218 // Add each container as a row
219 for (const auto& container : containers)
@@ -414,6 +449,186 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
449 context.Data.Add<Data::ContainerOptions>(std::move(options));
450 }
451
452 +void ShowContainerStats(CLIExecutionContext& context)
453 +{
454 + WI_ASSERT(context.Data.Contains(Data::Session));
455 + auto& session = context.Data.Get<Data::Session>();
456 +
457 + auto containers = context.Args.GetAll<ArgType::ContainerId>();
458 +
459 + // If any are specified we use those, otherwise we show all containers.
460 + const bool userSpecifiedContainers = !containers.empty();
461 + if (!userSpecifiedContainers)
462 + {
463 + GetContainers(context);
464 + const auto& allContainers = context.Data.Get<Data::Containers>();
465 + for (const auto& container : allContainers)
466 + {
467 + // Skip non-running containers unless --all is specified.
468 + if (!context.Args.Contains(ArgType::All) && container.State != WSLCContainerState::WslcContainerStateRunning)
469 + {
470 + continue;
471 + }
472 +
473 + containers.push_back(MultiByteToWide(container.Id));
474 + }
475 + }
476 +
477 + // Build stats as a json array first for later filtering or display either as json or table format.
478 + nlohmann::json statsJson = nlohmann::json::array();
479 + for (const auto& containerId : containers)
480 + {
481 + wsl::windows::common::docker_schema::ContainerStats stats;
482 + try
483 + {
484 + stats = ContainerService::Stats(session, WideToMultiByte(containerId));
485 + }
486 + catch (const wil::ResultException& ex)
487 + {
488 + if (!userSpecifiedContainers)
489 + {
490 + // If the user did not explicitly specify a container then there may be expected
491 + // race conditions between listing containers and querying stats.
492 + switch (ex.GetErrorCode())
493 + {
494 + case RPC_E_DISCONNECTED:
495 + case WSLC_E_CONTAINER_NOT_FOUND:
496 + continue;
497 + }
498 + }
499 +
500 + LOG_HR_MSG(ex.GetErrorCode(), "Failed to get stats for container %ws", containerId.c_str());
501 + throw;
502 + }
503 +
504 + // Calculate CPU %
505 + double cpuPercent = 0.0;
506 + const auto cpuDelta = static_cast<double>(stats.cpu_stats.cpu_usage.total_usage) -
507 + static_cast<double>(stats.precpu_stats.cpu_usage.total_usage);
508 + const auto systemDelta =
509 + static_cast<double>(stats.cpu_stats.system_cpu_usage) - static_cast<double>(stats.precpu_stats.system_cpu_usage);
510 + const auto onlineCpus = stats.cpu_stats.online_cpus > 0 ? stats.cpu_stats.online_cpus : 1u;
511 + if (systemDelta > 0.0 && cpuDelta >= 0.0)
512 + {
513 + cpuPercent = (cpuDelta / systemDelta) * static_cast<double>(onlineCpus) * 100.0;
514 + }
515 +
516 + // Calculate memory %
517 + double memPercent = 0.0;
518 + if (stats.memory_stats.limit > 0)
519 + {
520 + memPercent = (static_cast<double>(stats.memory_stats.usage) / static_cast<double>(stats.memory_stats.limit)) * 100.0;
521 + }
522 +
523 + // Aggregate network I/O
524 + uint64_t netRxBytes = 0;
525 + uint64_t netTxBytes = 0;
526 + if (stats.networks.has_value())
527 + {
528 + for (const auto& [iface, netStats] : *stats.networks)
529 + {
530 + netRxBytes += netStats.rx_bytes;
531 + netTxBytes += netStats.tx_bytes;
532 + }
533 + }
534 +
535 + // Aggregate block I/O
536 + uint64_t blkReadBytes = 0;
537 + uint64_t blkWriteBytes = 0;
538 + if (stats.blkio_stats.io_service_bytes_recursive.has_value())
539 + {
540 + for (const auto& entry : *stats.blkio_stats.io_service_bytes_recursive)
541 + {
542 + if (_stricmp(entry.op.c_str(), "read") == 0)
543 + {
544 + blkReadBytes += entry.value;
545 + }
546 + else if (_stricmp(entry.op.c_str(), "write") == 0)
547 + {
548 + blkWriteBytes += entry.value;
549 + }
550 + }
551 + }
552 +
553 + const auto& containerName = stats.name.empty() ? stats.id : stats.name;
554 + const auto cpuPercentStr = std::format("{:.2f}%", cpuPercent);
555 + const auto memPercentStr = std::format("{:.2f}%", memPercent);
556 + const auto memUsage = std::format("{} / {}", FormatBytes(stats.memory_stats.usage), FormatBytes(stats.memory_stats.limit));
557 + const auto netIo = std::format("{} / {}", FormatBytes(netRxBytes), FormatBytes(netTxBytes));
558 + const auto blkIo = std::format("{} / {}", FormatBytes(blkReadBytes), FormatBytes(blkWriteBytes));
559 +
560 + statsJson.push_back({
561 + {"ID", stats.id},
562 + {"Name", containerName},
563 + {"CPUPerc", cpuPercentStr},
564 + {"MemUsage", memUsage},
565 + {"MemPerc", memPercentStr},
566 + {"NetIO", netIo},
567 + {"BlockIO", blkIo},
568 + {"PIDs", stats.pids_stats.current},
569 + });
570 + }
571 +
572 + FormatType format = FormatType::Table; // Default is table
573 + if (context.Args.Contains(ArgType::Format))
574 + {
575 + format = validation::GetFormatTypeFromString(context.Args.Get<ArgType::Format>());
576 + }
577 +
578 + switch (format)
579 + {
580 + case FormatType::Json:
581 + {
582 + PrintMessage(MultiByteToWide(statsJson.dump(c_jsonPrettyPrintIndent)));
583 + break;
584 + }
585 + case FormatType::Table:
586 + {
587 + using Config = wsl::windows::wslc::ColumnWidthConfig;
588 + bool trunc = !context.Args.Contains(ArgType::NoTrunc);
589 +
590 + auto table = trunc ? wsl::windows::wslc::TableOutput<8>(
591 + {{{Localization::WSLCCLI_TableHeaderContainerId(), {Config::NoLimit, 12, false}},
592 + {Localization::WSLCCLI_TableHeaderName(), {Config::NoLimit, 20, true}},
593 + {Localization::WSLCCLI_TableHeaderCpuPercent(), {Config::NoLimit, Config::NoLimit, false}},
594 + {Localization::WSLCCLI_TableHeaderMemUsageLimit(), {Config::NoLimit, Config::NoLimit, false}},
595 + {Localization::WSLCCLI_TableHeaderMemPercent(), {Config::NoLimit, Config::NoLimit, false}},
596 + {Localization::WSLCCLI_TableHeaderNetIo(), {Config::NoLimit, Config::NoLimit, false}},
597 + {Localization::WSLCCLI_TableHeaderBlockIo(), {Config::NoLimit, Config::NoLimit, false}},
598 + {Localization::WSLCCLI_TableHeaderPids(), {Config::NoLimit, Config::NoLimit, false}}}})
599 + : wsl::windows::wslc::TableOutput<8>(
600 + {Localization::WSLCCLI_TableHeaderContainerId(),
601 + Localization::WSLCCLI_TableHeaderName(),
602 + Localization::WSLCCLI_TableHeaderCpuPercent(),
603 + Localization::WSLCCLI_TableHeaderMemUsageLimit(),
604 + Localization::WSLCCLI_TableHeaderMemPercent(),
605 + Localization::WSLCCLI_TableHeaderNetIo(),
606 + Localization::WSLCCLI_TableHeaderBlockIo(),
607 + Localization::WSLCCLI_TableHeaderPids()});
608 +
609 + for (const auto& entry : statsJson)
610 + {
611 + const auto id = entry["ID"].get<std::string>();
612 + table.OutputLine({
613 + MultiByteToWide(trunc ? TruncateId(id) : id),
614 + MultiByteToWide(entry["Name"].get<std::string>()),
615 + MultiByteToWide(entry["CPUPerc"].get<std::string>()),
616 + MultiByteToWide(entry["MemUsage"].get<std::string>()),
617 + MultiByteToWide(entry["MemPerc"].get<std::string>()),
618 + MultiByteToWide(entry["NetIO"].get<std::string>()),
619 + MultiByteToWide(entry["BlockIO"].get<std::string>()),
620 + std::to_wstring(entry["PIDs"].get<uint64_t>()),
621 + });
622 + }
623 +
624 + table.Complete();
625 + break;
626 + }
627 + default:
628 + THROW_HR(E_UNEXPECTED);
629 + }
630 +}
631 +
632 void StartContainer(CLIExecutionContext& context)
633 {
634 WI_ASSERT(context.Data.Contains(Data::Session));
src/windows/wslc/tasks/ContainerTasks.h
+1
@@ -39,6 +39,7 @@ void ListContainers(CLIExecutionContext& context);
39 void RemoveContainers(CLIExecutionContext& context);
40 void RunContainer(CLIExecutionContext& context);
41 void SetContainerOptionsFromArgs(CLIExecutionContext& context);
42 +void ShowContainerStats(CLIExecutionContext& context);
43 void StartContainer(CLIExecutionContext& context);
44 void StopContainers(CLIExecutionContext& context);
45 void ViewContainerLogs(CLIExecutionContext& context);
src/windows/wslcsession/WSLCContainer.cpp
+6
@@ -1840,6 +1840,12 @@ void WSLCContainerImpl::Stats(LPSTR* Output) const
1840 try
1841 {
1842 auto stats = m_dockerClient.ContainerStats(m_id);
1843 +
1844 + // Always inject the authoritative id and name from this instance.
1845 + // The response may omit them or use inconsistent casing.
1846 + stats.id = m_id;
1847 + stats.name = m_name;
1848 +
1849 std::string json = wsl::shared::ToJson(stats);
1850 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
1851 }
test/windows/WSLCTests.cpp
-2
@@ -8196,7 +8196,6 @@ class WSLCTests
8196 WSLCContainerLauncher launcher("debian:latest", "wslc-test-stats", {"sleep", "60"}, {}, WSLCContainerNetworkTypeBridged);
8197
8198 auto runningContainer = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
8199 - auto cleanup = wil::scope_exit([&]() { runningContainer.SetDeleteOnClose(true); });
8199
8200 wil::com_ptr<IWSLCContainer> container;
8201 VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-test-stats", &container));
@@ -8279,7 +8278,6 @@ class WSLCTests
8278 {
8279 WSLCContainerLauncher launcher("debian:latest", "wslc-test-stats-null", {"sleep", "60"}, {}, WSLCContainerNetworkTypeBridged);
8280 auto runningContainer = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
8282 - auto cleanup = wil::scope_exit([&]() { runningContainer.SetDeleteOnClose(true); });
8281
8282 wil::com_ptr<IWSLCContainer> container;
8283 VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-test-stats-null", &container));
test/windows/wslc/CommandLineTestCases.h
+7
@@ -147,6 +147,13 @@ COMMAND_LINE_TEST_CASE(L"rm cont1", L"remove", true)
147 COMMAND_LINE_TEST_CASE(L"container rm cont1 cont2", L"remove", true)
148 COMMAND_LINE_TEST_CASE(L"container attach cont", L"attach", true)
149 COMMAND_LINE_TEST_CASE(L"container attach", L"attach", false)
150 +// Stats command tests
151 +COMMAND_LINE_TEST_CASE(L"stats", L"stats", true)
152 +COMMAND_LINE_TEST_CASE(L"container stats", L"stats", true)
153 +COMMAND_LINE_TEST_CASE(L"container stats cont1", L"stats", true)
154 +COMMAND_LINE_TEST_CASE(L"container stats cont1 cont2", L"stats", true)
155 +COMMAND_LINE_TEST_CASE(L"container stats --no-trunc cont1", L"stats", true)
156 +COMMAND_LINE_TEST_CASE(L"container stats --all", L"stats", true)
157
158 // Logs command
159 COMMAND_LINE_TEST_CASE(L"logs cont1", L"logs", true)
test/windows/wslc/e2e/WSLCE2EContainerStatsTests.cpp new
+239
@@ -0,0 +1,239 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +/*++
4 +
5 +Module Name:
6 +
7 + WSLCE2EContainerStatsTests.cpp
8 +
9 +Abstract:
10 +
11 + This file contains end-to-end tests for the WSLC container stats command.
12 +--*/
13 +
14 +#include "precomp.h"
15 +#include "windows/Common.h"
16 +#include "WSLCExecutor.h"
17 +#include "WSLCE2EHelpers.h"
18 +
19 +namespace WSLCE2ETests {
20 +
21 +using namespace wsl::shared;
22 +using namespace wsl::windows::common::string;
23 +
24 +class WSLCE2EContainerStatsTests
25 +{
26 + WSLC_TEST_CLASS(WSLCE2EContainerStatsTests)
27 +
28 + TEST_CLASS_SETUP(ClassSetup)
29 + {
30 + EnsureImageIsLoaded(DebianImage);
31 + return true;
32 + }
33 +
34 + TEST_CLASS_CLEANUP(ClassCleanup)
35 + {
36 + EnsureContainerDoesNotExist(WslcContainerName);
37 + EnsureImageIsDeleted(DebianImage);
38 + return true;
39 + }
40 +
41 + TEST_METHOD_SETUP(TestMethodSetup)
42 + {
43 + EnsureContainerDoesNotExist(WslcContainerName);
44 + return true;
45 + }
46 +
47 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_HelpCommand)
48 + {
49 + auto result = RunWslc(L"container stats --help");
50 + result.Verify({.Stderr = L"", .ExitCode = 0});
51 + }
52 +
53 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_NoContainers)
54 + {
55 + // With no running containers, stats should produce no output rows (header only or empty).
56 + auto result = RunWslc(L"container stats");
57 + result.Verify({.Stderr = L"", .ExitCode = 0});
58 + VERIFY_ARE_EQUAL(
59 + static_cast<size_t>(1), result.GetStdoutLines().size(), L"Expected only the header row when there are no containers");
60 + }
61 +
62 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_RunningContainer_HasExpectedColumns)
63 + {
64 + auto runResult =
65 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
66 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
67 +
68 + auto result = RunWslc(L"container stats");
69 + result.Verify({.Stderr = L"", .ExitCode = 0});
70 +
71 + const auto lines = result.GetStdoutLines();
72 + VERIFY_IS_TRUE(lines.size() >= 2, L"Expected header row and at least one data row");
73 +
74 + // Verify the header row contains all expected column titles.
75 + const auto& header = lines[0];
76 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderContainerId()));
77 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderName()));
78 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderCpuPercent()));
79 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderMemUsageLimit()));
80 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderMemPercent()));
81 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderNetIo()));
82 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderBlockIo()));
83 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, header.find(Localization::WSLCCLI_TableHeaderPids()));
84 + }
85 +
86 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_RunningContainer_ContainerIdAndNamePresent)
87 + {
88 + auto runResult =
89 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
90 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
91 + const auto containerId = TruncateId(runResult.GetStdoutOneLine());
92 + VERIFY_IS_FALSE(containerId.empty());
93 +
94 + auto result = RunWslc(L"container stats");
95 + result.Verify({.Stderr = L"", .ExitCode = 0});
96 +
97 + // The data row must contain the truncated container ID and the container name.
98 + bool foundContainer = false;
99 + for (const auto& line : result.GetStdoutLines())
100 + {
101 + if (line.find(containerId) != std::wstring::npos)
102 + {
103 + foundContainer = true;
104 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(WslcContainerName));
105 + break;
106 + }
107 + }
108 +
109 + VERIFY_IS_TRUE(foundContainer, L"Container ID not found in stats output");
110 + }
111 +
112 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_RunningContainer_NoTrunc)
113 + {
114 + auto runResult =
115 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
116 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
117 + const auto fullContainerId = runResult.GetStdoutOneLine();
118 + VERIFY_IS_FALSE(fullContainerId.empty());
119 +
120 + auto result = RunWslc(L"container stats --no-trunc");
121 + result.Verify({.Stderr = L"", .ExitCode = 0});
122 +
123 + // With --no-trunc the full container ID must appear.
124 + bool foundContainer = false;
125 + for (const auto& line : result.GetStdoutLines())
126 + {
127 + if (line.find(fullContainerId) != std::wstring::npos)
128 + {
129 + foundContainer = true;
130 + break;
131 + }
132 + }
133 +
134 + VERIFY_IS_TRUE(foundContainer, L"Full container ID not found in stats --no-trunc output");
135 + }
136 +
137 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_SpecificContainerId)
138 + {
139 + auto runResult =
140 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
141 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
142 + const auto containerId = TruncateId(runResult.GetStdoutOneLine());
143 + VERIFY_IS_FALSE(containerId.empty());
144 +
145 + // Pass the container ID explicitly — only that container's row should appear.
146 + auto result = RunWslc(std::format(L"container stats {}", containerId));
147 + result.Verify({.Stderr = L"", .ExitCode = 0});
148 +
149 + const auto lines = result.GetStdoutLines();
150 + // Header + exactly one data row.
151 + VERIFY_ARE_EQUAL(2u, lines.size());
152 + VERIFY_ARE_NOT_EQUAL(std::wstring::npos, lines[1].find(containerId));
153 + }
154 +
155 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_StoppedContainerExcluded)
156 + {
157 + // Create (but do not start) a container.
158 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
159 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
160 + const auto containerId = TruncateId(createResult.GetStdoutOneLine());
161 + VERIFY_IS_FALSE(containerId.empty());
162 +
163 + // Stats without --all must not show non-running containers.
164 + auto result = RunWslc(L"container stats");
165 + result.Verify({.Stderr = L"", .ExitCode = 0});
166 +
167 + for (const auto& line : result.GetStdoutLines())
168 + {
169 + VERIFY_ARE_EQUAL(std::wstring::npos, line.find(containerId));
170 + }
171 + }
172 +
173 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_AllFlag_IncludesStoppedContainers)
174 + {
175 + // Create (but do not start) a container.
176 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
177 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
178 + const auto containerId = TruncateId(createResult.GetStdoutOneLine());
179 + VERIFY_IS_FALSE(containerId.empty());
180 +
181 + // Stats with --all must include non-running containers.
182 + auto result = RunWslc(L"container stats --all");
183 + result.Verify({.Stderr = L"", .ExitCode = 0});
184 +
185 + bool foundContainer = false;
186 + for (const auto& line : result.GetStdoutLines())
187 + {
188 + if (line.find(containerId) != std::wstring::npos)
189 + {
190 + foundContainer = true;
191 + break;
192 + }
193 + }
194 +
195 + VERIFY_IS_TRUE(foundContainer, L"Stopped container not found in stats --all output");
196 + }
197 +
198 + WSLC_TEST_METHOD(WSLCE2E_Container_Stats_JsonFormat)
199 + {
200 + // Run a container in the background
201 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
202 + result.Verify({.Stderr = L"", .ExitCode = 0});
203 + const auto containerId = result.GetStdoutOneLine();
204 + VERIFY_IS_FALSE(containerId.empty());
205 +
206 + // Get stats in JSON format
207 + result = RunWslc(std::format(L"container stats --no-trunc --format json {}", containerId));
208 + result.Verify({.Stderr = L"", .ExitCode = 0});
209 +
210 + // Parse and validate the JSON output
211 + const auto json = nlohmann::json::parse(WideToMultiByte(result.Stdout.value()));
212 + VERIFY_IS_TRUE(json.is_array());
213 + VERIFY_IS_GREATER_THAN_OR_EQUAL(json.size(), 1U);
214 +
215 + const auto& entry = json[0];
216 + VERIFY_IS_TRUE(entry.contains("ID"));
217 + VERIFY_IS_TRUE(entry.contains("Name"));
218 + VERIFY_IS_TRUE(entry.contains("CPUPerc"));
219 + VERIFY_IS_TRUE(entry.contains("MemUsage"));
220 + VERIFY_IS_TRUE(entry.contains("MemPerc"));
221 + VERIFY_IS_TRUE(entry.contains("NetIO"));
222 + VERIFY_IS_TRUE(entry.contains("BlockIO"));
223 + VERIFY_IS_TRUE(entry.contains("PIDs"));
224 +
225 + VERIFY_ARE_EQUAL(containerId, MultiByteToWide(entry["ID"].get<std::string>()));
226 + VERIFY_IS_TRUE(entry["CPUPerc"].is_string());
227 + VERIFY_IS_TRUE(entry["MemUsage"].is_string());
228 + VERIFY_IS_TRUE(entry["MemPerc"].is_string());
229 + VERIFY_IS_TRUE(entry["NetIO"].is_string());
230 + VERIFY_IS_TRUE(entry["BlockIO"].is_string());
231 + VERIFY_IS_TRUE(entry["PIDs"].is_number_unsigned());
232 + }
233 +
234 +private:
235 + const std::wstring WslcContainerName = L"wslc-stats-test";
236 + const TestImage& DebianImage = DebianTestImage();
237 +};
238 +
239 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+2 -1
@@ -79,6 +79,7 @@ private:
79 {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()},
80 {L"run", Localization::WSLCCLI_ContainerRunDesc()},
81 {L"start", Localization::WSLCCLI_ContainerStartDesc()},
82 + {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
83 {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
84 };
85
@@ -106,4 +107,4 @@ private:
107 return options.str();
108 }
109 };
109 -} // namespace WSLCE2ETests
\ No newline at end of file
110 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+1
@@ -549,6 +549,7 @@ private:
549 {L"run", Localization::WSLCCLI_ContainerRunDesc()},
550 {L"save", Localization::WSLCCLI_ImageSaveDesc()},
551 {L"start", Localization::WSLCCLI_ContainerStartDesc()},
552 + {L"stats", Localization::WSLCCLI_ContainerStatsDesc()},
553 {L"stop", Localization::WSLCCLI_ContainerStopDesc()},
554 {L"tag", Localization::WSLCCLI_ImageTagDesc()},
555 {L"version", Localization::WSLCCLI_VersionDesc()},