Align container list format with Docker specifications - #41375
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates wslc container list to better match docker container list output, including table column order, --format json shape, and --quiet truncation behavior. It also extends the service/container models to provide additional Docker-parity fields needed by both the table and JSON views.
Changes:
- Reworks container list rendering to use a shared
ContainerOutputInformation(table + NDJSON) with Docker-aligned fields, and updates--quiet/--no-truncbehavior. - Extends the service and schema models to carry Docker-reported container metadata (e.g.,
Command,Status,Labels,Networks,Mounts,LocalVolumes). - Updates and adds unit/E2E tests to validate the new table layout, JSON shape, and truncation rules.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLIContainerCommandUnitTests.cpp | Adds unit tests for FormatCommand and FormatStatus. |
| test/windows/wslc/e2e/WSLCE2EHelpers.h | Updates helper API to return ContainerOutputInformation. |
| test/windows/wslc/e2e/WSLCE2EHelpers.cpp | Updates helpers for new JSON shape/field names and status matching. |
| test/windows/wslc/e2e/WSLCE2EContainerListTests.cpp | Adds/updates E2E coverage for Docker-aligned table headers, JSON shape, and ID truncation. |
| src/windows/wslcsession/WSLCSession.cpp | Populates newly added service-side WSLCContainerEntry fields from Docker data. |
| src/windows/wslc/tasks/ContainerTasks.cpp | Implements Docker-shape NDJSON and new table column order via ToContainerOutput(). |
| src/windows/wslc/services/ContainerService.h | Adds APIs for invariant state names and Docker-like command/status formatting. |
| src/windows/wslc/services/ContainerService.cpp | Implements FormatCommand, FormatStatus, and splits invariant vs localized state naming. |
| src/windows/wslc/services/ContainerModel.h | Adds ContainerOutputInformation and extends ContainerInformation to carry new fields. |
| src/windows/service/inc/wslc.idl | Extends WSLCContainerEntry and introduces max-length constants for new fields. |
| src/windows/inc/docker_schema.h | Extends docker schema parsing to include Command and Status. |
| localization/strings/en-US/Resources.resw | Adds table headers (COMMAND, NAMES) and container state strings. |
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2603
- These strncpy_s calls only throw when the return value is EINVAL, but other non-zero return values would be ignored. Since truncation is expected with _TRUNCATE (STRUNCATE), it’s safer to throw on anything that is neither 0 nor STRUNCATE.
THROW_HR_IF(E_UNEXPECTED, strncpy_s(output[index].Labels, std::size(output[index].Labels), joinedLabels.c_str(), _TRUNCATE) == EINVAL);
THROW_HR_IF(E_UNEXPECTED, strncpy_s(output[index].Networks, std::size(output[index].Networks), joinedNetworks.c_str(), _TRUNCATE) == EINVAL);
THROW_HR_IF(E_UNEXPECTED, strncpy_s(output[index].Mounts, std::size(output[index].Mounts), joinedMounts.c_str(), _TRUNCATE) == EINVAL);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…iner json macro Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/windows/service/inc/wslc.idl:390
- WSLCContainerEntry now includes several large fixed-size char buffers (e.g. 4K Command/Labels/Mounts). Because ListContainers returns an array of WSLCContainerEntry over COM/RPC, this significantly increases the marshaled payload and client-side allocation per entry even when the strings are short, which can slow down
container list(and any callers) for large container counts.
Consider switching these fields to variable-length marshaled strings (e.g. [string] LPSTR) and allocating per-value, or gating the extended fields behind an option flag (so --quiet/minimal listings don’t pay the cost).
char Command[WSLC_MAX_CONTAINER_COMMAND_LENGTH + 1];
char Status[WSLC_MAX_CONTAINER_STATUS_LENGTH + 1];
char Labels[WSLC_MAX_CONTAINER_LABELS_LENGTH + 1];
char Networks[WSLC_MAX_CONTAINER_NETWORKS_LENGTH + 1];
char Mounts[WSLC_MAX_CONTAINER_MOUNTS_LENGTH + 1];
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
test/windows/wslc/e2e/WSLCE2EHelpers.cpp:186
- The test assumes the STATUS column always contains Docker-style runtime prefixes (e.g. "Up ", "Exited ("), but ContainerService::FormatStatus has a fallback that can emit plain state strings (e.g. "running", "exited") if the runtime doesn't supply a status description. This can make the test fail even when the container is correctly listed. Consider accepting either the Docker-style prefix or the original logical state string when validating the line.
const std::wstring message = L"Container '" + containerNameOrId + L"' found in container list output but status '" +
expectedStatus + L"' was not found in the same line";
VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(expectedStatus), message.c_str());
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2561
outputis allocated withmake_unique_cotaskmemand not value-initialized, but thefreeStringsscope_exit assumes every entry's new pointer fields are either valid or null. If an exception occurs before all entries are populated (including entries skipped viacontinue),FreeContainerEntryStrings(output[i])can callCoTaskMemFreeon uninitialized garbage pointers. Initialize the array elements to{}(or zero memory) before installing the cleanup scope so partially-populated entries have null pointers.
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
FreeContainerEntryStrings(output[i]);
…ner-list-output-parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… functor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2554
outputis allocated via CoTaskMem and thenfreeStringscallsFreeContainerEntryStrings(&output[i])for every element. Because the array is not zero-initialized, any entries (or fields) that aren't populated may contain garbage pointers, andCoTaskMemFreeon those is undefined behavior/crash-prone.
Zero-initialize the array immediately after allocation so unassigned LPSTR fields are guaranteed null before the scope-exit cleanup runs.
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
wsl::windows::common::wslc::FreeContainerEntryStrings(&output[i]);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2554
outputis allocated withmake_unique_cotaskmemand thenfreeStringsunconditionally callsFreeContainerEntryStrings(&output[i])for every element. If the allocation is not zero-initialized, unpopulated entries will contain garbage pointers andCoTaskMemFreemay crash during exception unwinding. Consider explicitly zero-initializing the array before installing the scope guard so uninitialized string fields are guaranteed null.
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
wsl::windows::common::wslc::FreeContainerEntryStrings(&output[i]);
…ner-list-output-parity # Conflicts: # test/windows/StringUnitTests.cpp
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2612
- Labels/Networks/Mounts are formatted as comma-separated strings, but this currently allocates CoTaskMem strings even when the joined value is empty. Setting these to nullptr when empty avoids unnecessary allocations and matches the struct comment that these fields may be null.
output[index].Labels = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedLabels.c_str()).release();
output[index].Networks = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedNetworks.c_str()).release();
output[index].Mounts = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(joinedMounts.c_str()).release();
…ner-list-output-parity # Conflicts: # test/windows/StringUnitTests.cpp
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2555
outputis allocated withwil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size()), andfreeStringsunconditionally callsFreeContainerEntryStrings(&output[i])for every element. If the array is not zero-initialized (and/or fewer thandockerContainers.size()entries are populated), thoseLPSTRfields can be uninitialized garbage pointers, makingCoTaskMemFreeunsafe on exception paths. Zero-initialize the array immediately after allocation (or otherwise ensure the string pointer fields are set to nullptr for all elements) before the scope-exit cleanup runs.
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
wsl::windows::common::wslc::FreeContainerEntryStrings(&output[i]);
ranm-msft
left a comment
There was a problem hiding this comment.
Nice work on this - I went through the earlier feedback and it all looks genuinely addressed at head, not just papered over: Ellipsis() does real display-width truncation via ICU East Asian width, the quoting is a faithful strconv.Quote port (including IsPrint semantics and surrogate handling), FormatMounts respects --no-trunc, health is derived from the status suffix, and the service side dropped the fixed-size strncpy_s copies in favour of allocations with a scope_exit guard so a throw mid-loop no longer leaks.
One thing I think is still open:
--format json can emit localized values. ToContainerOutput() fills RunningFor from FormatRelativeTime(), which resolves to Localization::WSLCCLI_RelativeTime*(), and Status from FormatStatus(), whose empty-runtime-status fallback goes through LocalizedContainerStateName() plus the same relative-time helper. So the same container can serialize different Status / RunningFor values depending on UI language. That cuts against the parity goal - docker's formatter renders those strings in invariant English - and the new invariant/localized split currently only protects State.
Sharing one representation between the table and JSON is the right instinct, but these two fields are exactly where the two outputs need to differ. Could the JSON path use invariant relative-time and fallback-status formatters?
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
ranm-msft I agree with these concerns. However I think we should keep this not localized for now as there could be programs who might be keying off these values, and so I'd want to remain similar to other container tools. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/windows/common/WSLCContainerEntry.h:27
unique_container_entry_arrayis defined aswil::unique_cotaskmem_array_ptr<unique_container_entry>, butListContainersreturns a CoTaskMem-allocated array of rawWSLCContainerEntryand the COM signature expectsWSLCContainerEntry**. Storing that in an array ofwil::unique_struct<WSLCContainerEntry,...>is not ABI-compatible and will at best fail to compile (type mismatch for the out-param) and at worst be UB if it compiles via implicit conversions.
Prefer a wrapper that owns a raw WSLCContainerEntry* plus Count, frees per-entry strings, and then frees the array block with CoTaskMemFree, while still supporting the existing &containers and containers.size_address<ULONG>() call sites.
using unique_container_entry = wil::unique_struct<WSLCContainerEntry, decltype(&FreeContainerEntryStrings), FreeContainerEntryStrings>;
using unique_container_entry_array = wil::unique_cotaskmem_array_ptr<unique_container_entry>;
src/windows/wslcsession/WSLCSession.cpp:2555
- The
freeStringsscope_exit currently iteratesi < dockerContainers.size(). If an exception occurs before all entries are populated (e.g., during label/network/mount joining/allocations), this will callFreeContainerEntryStringson uninitializedWSLCContainerEntryelements and can pass garbage pointers toCoTaskMemFree.
Track how many entries were actually populated and only free up to that count (and declare index before the scope_exit so it can be captured).
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
wsl::windows::common::wslc::FreeContainerEntryStrings(&output[i]);
Summary of the Pull Request
Brings
wslc container listto output parity withdocker container list, for both the table and--format json. This was the largest remaining gap found in a systematic wslc-vs-docker output comparison across 81 command pairs.Three changes:
CONTAINER ID / IMAGE / COMMAND / CREATED / STATUS / PORTS / NAMES), adding the missingCOMMANDcolumn and movingNAMESlast.STATUSnow reports the runtime's own description (Up 5 minutes,Exited (0) 2 hours ago) instead of a locally derived string.--format json— emits docker's 16-key shape, with every value a pre-rendered string apart from the nestedPlatformobject. This matches the convention already used bywslc image listandwslc network list.--quiet— truncates to docker's 12-character short ID, with--no-truncfor the full ID.The service now supplies the additional data docker reports (
Command,Status,Labels,Networks,Mounts,LocalVolumes), soWSLCContainerEntrygains those fields.PR Checklist
Detailed Description of the Pull Request / Additional comments
JSON output is a breaking change
container list --format jsonpreviously emitted a raw-data view of the internal model:{"Id":"<64 chars>","Name":"web","Image":"alpine:3.20","State":1,"StateChangedAt":1755543210,"CreatedAt":1755543200,"Ports":[...]}It now emits docker's shape:
{"Command":"\"sleep 3600\"","CreatedAt":"2026-08-18 12:06:50 -0700 PDT","HealthStatus":"none","ID":"0dba0f244a97","Image":"alpine:3.20","Labels":"...","LocalVolumes":"0","Mounts":"","Names":"web","Networks":"bridge","Platform":{"architecture":"amd64","os":"linux"},"Ports":"","RunningFor":"28 seconds ago","Size":"0B","State":"running","Status":"Up 27 seconds"}Notably Id → ID, Name → Names, integer timestamps → formatted strings, the Ports array → a rendered string, and IDs truncated unless --no-trunc . This mirrors the rename already made deliberately for network list, and is what makes docker-oriented tooling and Go-template-style consumers work unchanged.
Design
ToContainerOutput() builds a single ContainerOutputInformation consumed by both the table and the JSON renderer, so the two cannot drift. This is the same pattern as ToImageOutput() / ImageOutputInformation .
ContainerOutputInformation is kept separate from ContainerInformation on purpose: the former mirrors docker's all-string output shape, the latter mirrors the service's native types.
Container state names exist in two forms — ContainerStateName() returns invariant English because it feeds the machine-readable JSON, while LocalizedContainerStateName() is the display form used by the table.
Validation Steps Performed
• Added WSLCE2E_Container_List_TableFormat_MatchesDockerColumnOrder , asserting the header row matches docker's column order.
• Added WSLCE2E_Container_List_JsonFormat_MatchesDockerShape , asserting the exact 16-key set, that every value is a string apart from Platform , and that IDs are 12 characters by default.
• Rewrote the --quiet E2E test to cover both truncated and --no-trunc output.
• Added 10 unit tests for FormatCommand (quoting, escaping, 20-character shortening, code-point counting for multi-byte input) and FormatStatus (runtime description preferred, fallback when empty).
• Migrated the ~12 existing E2E call sites and the shared ListAllContainers() helper to the new JSON shape.
• Expected output was validated against real docker container list output captured from a parity harness run covering 81 wslc/docker command pairs.
• clang-format 19.1.5 clean across all files changed on the branch.