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