Skip to content

Align container list format with Docker specifications - #41375

Merged
ggarzia-MSFT merged 16 commits into
masterfrom
user/ggarzia/container-list-output-parity
Aug 31, 2026
Merged

Align container list format with Docker specifications#41375
ggarzia-MSFT merged 16 commits into
masterfrom
user/ggarzia/container-list-output-parity

Conversation

@ggarzia-MSFT

@ggarzia-MSFT ggarzia-MSFT commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary of the Pull Request

Brings wslc container list to output parity with docker 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:

  • Table — column order now matches docker (CONTAINER ID / IMAGE / COMMAND / CREATED / STATUS / PORTS / NAMES), adding the missing COMMAND column and moving NAMES last. STATUS now 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 nested Platform object. This matches the convention already used by wslc image list and wslc network list.
  • --quiet — truncates to docker's 12-character short ID, with --no-trunc for the full ID.

The service now supplies the additional data docker reports (Command, Status, Labels, Networks, Mounts, LocalVolumes), so WSLCContainerEntry gains those fields.

PR Checklist

  • Closes: Link to issue #xxx
  • Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • Tests: Added/updated if needed and all pass
  • Localization: All end user facing strings can be localized
  • Dev docs: Added/updated if needed
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

JSON output is a breaking change

container list --format json previously 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.

ggarzia-MSFT and others added 3 commits August 18, 2026 14:22
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>
Copilot AI lite review requested due to automatic review settings August 18, 2026 22:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-trunc behavior.
  • 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.

Comment thread src/windows/wslc/services/ContainerModel.h Outdated
Comment thread src/windows/wslcsession/WSLCSession.cpp Outdated
…iner json macro

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 23:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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];

@ggarzia-MSFT
ggarzia-MSFT marked this pull request as ready for review August 19, 2026 16:15
@ggarzia-MSFT
ggarzia-MSFT requested review from a team as code owners August 19, 2026 16:15
Comment thread src/windows/wslc/services/ContainerService.cpp Outdated
Comment thread src/windows/wslc/services/ContainerService.cpp Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/windows/wslc/services/ContainerService.cpp Outdated
Comment thread src/windows/wslc/tasks/ContainerTasks.cpp Outdated
Comment thread src/windows/wslc/tasks/ContainerTasks.cpp Outdated
Comment thread src/windows/wslc/services/ContainerService.cpp
Comment thread src/windows/wslc/services/ContainerService.cpp
Comment thread src/windows/wslc/tasks/ContainerTasks.cpp Outdated
Comment thread src/windows/wslcsession/WSLCSession.cpp Outdated
Comment thread src/windows/wslc/services/ContainerService.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • output is allocated with make_unique_cotaskmem and not value-initialized, but the freeStrings scope_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 via continue), FreeContainerEntryStrings(output[i]) can call CoTaskMemFree on 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]);

Comment thread src/windows/common/WSLCContainerEntry.h Outdated
ggarzia-MSFT and others added 2 commits August 24, 2026 12:04
…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>
Copilot AI review requested due to automatic review settings August 24, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • output is allocated via CoTaskMem and then freeStrings calls FreeContainerEntryStrings(&output[i]) for every element. Because the array is not zero-initialized, any entries (or fields) that aren't populated may contain garbage pointers, and CoTaskMemFree on 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]);

Copilot AI review requested due to automatic review settings August 24, 2026 21:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • output is allocated with make_unique_cotaskmem and then freeStrings unconditionally calls FreeContainerEntryStrings(&output[i]) for every element. If the allocation is not zero-initialized, unpopulated entries will contain garbage pointers and CoTaskMemFree may 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]);

Comment thread src/windows/wslc/services/ContainerService.cpp Outdated
…ner-list-output-parity

# Conflicts:
#	test/windows/StringUnitTests.cpp
Copilot AI review requested due to automatic review settings August 25, 2026 18:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/windows/wslcsession/WSLCSession.cpp
…ner-list-output-parity

# Conflicts:
#	test/windows/StringUnitTests.cpp
Copilot AI review requested due to automatic review settings August 28, 2026 22:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • output is allocated with wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size()), and freeStrings unconditionally calls FreeContainerEntryStrings(&output[i]) for every element. If the array is not zero-initialized (and/or fewer than dockerContainers.size() entries are populated), those LPSTR fields can be uninitialized garbage pointers, making CoTaskMemFree unsafe 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]);

Comment thread src/windows/common/WSLCContainerEntry.h

@ranm-msft ranm-msft left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copilot AI review requested due to automatic review settings August 31, 2026 20:26
@craigloewen-msft

Copy link
Copy Markdown
Member

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.
When we go GA we will have a note about localization (As some of our error codes will not be localized) and I will make sure to include this behaviour in that note.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_array is defined as wil::unique_cotaskmem_array_ptr<unique_container_entry>, but ListContainers returns a CoTaskMem-allocated array of raw WSLCContainerEntry and the COM signature expects WSLCContainerEntry**. Storing that in an array of wil::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 freeStrings scope_exit currently iterates i < dockerContainers.size(). If an exception occurs before all entries are populated (e.g., during label/network/mount joining/allocations), this will call FreeContainerEntryStrings on uninitialized WSLCContainerEntry elements and can pass garbage pointers to CoTaskMemFree.

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]);

@ggarzia-MSFT
ggarzia-MSFT enabled auto-merge (squash) August 31, 2026 21:04
@ggarzia-MSFT
ggarzia-MSFT merged commit 46be5bb into master Aug 31, 2026
12 checks passed
@ggarzia-MSFT
ggarzia-MSFT deleted the user/ggarzia/container-list-output-parity branch August 31, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants