Align container list format with Docker specifications - #41375
Align container list format with Docker specifications#41375ggarzia-MSFT wants to merge 14 commits into
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());
Pooja Trivedi (ptrivedi)
left a comment
There was a problem hiding this comment.
thank you for the changes, a few minor comments
…strings, health status 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 15 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/windows/wslcsession/WSLCSession.cpp:2563
freeStringsassumes everyWSLCContainerEntryhas pointer fields initialized tonullptr(soCoTaskMemFree(nullptr)is safe), butwil::make_unique_cotaskmem<WSLCContainerEntry[]>(...)does not guarantee zero-initialization. If an exception is thrown before all entries are populated,FreeContainerEntryStrings(output[i])can attempt to free uninitialized garbage pointers and crash.
Explicitly zero-initialize the allocated array before installing the scope-exit cleanup (or otherwise ensure the pointer members are set to null before any possible throw).
auto output = wil::make_unique_cotaskmem<WSLCContainerEntry[]>(dockerContainers.size());
auto freeStrings = wil::scope_exit([&] {
for (size_t i = 0; i < dockerContainers.size(); ++i)
{
…ner-list-output-parity
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: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();
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.