Skip to content

Compose: Expand WSLC Compose with project identity, async operations, and listing - #41526

Draft
David Bennett (dkbennett) wants to merge 8 commits into
feature/composefrom
user/dkbennett/compose
Draft

Compose: Expand WSLC Compose with project identity, async operations, and listing#41526
David Bennett (dkbennett) wants to merge 8 commits into
feature/composefrom
user/dkbennett/compose

Conversation

@dkbennett

@dkbennett David Bennett (dkbennett) commented Sep 5, 2026

Copy link
Copy Markdown
Member

Summary

This change evolves the initial wslc compose proof of concept from a configuration-path-based session model into an identity-based, asynchronous project lifecycle model.

The primary changes are:

  • Introduce stable Compose project identity through normalized project keys and Docker-compatible resource labels.
  • Replace cached IWSLCComposeSession objects with asynchronous IComposeOperation requests.
  • Add cooperative cancellation, completion events, typed results, ordered progress callbacks, and Docker-compatible attached up cancellation.
  • Add wslc compose list / ls with table, JSON, quiet, and all-project output.
  • Add wslc compose remove with delete and rm aliases; its current project-wide behavior is equivalent to docker compose rm --force.
  • Allow start, attach, stop, and remove to address a project by either Compose file path or listed project name.
  • Report resource lifecycle progress for containers, networks, and images while hiding generic internal operation phases in the CLI.
  • Discover lifecycle state from labels so operations remain functional across CLI processes, service restarts, cancellation, and retry.
  • Expand unit and end-to-end coverage for lifecycle behavior, cancellation, validation, identity, project listing, progress, and project-name/file resolution.

These changes provide clearer ownership boundaries, safer failure behavior, and service contracts that can support further Compose functionality without continuing to expand the original path-keyed proof-of-concept model.

Sample using Compose project listing, stable identity, and asynchronous operations:

image

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

Project identity, discovery, and ownership

Compose projects now have an explicit normalized project key. The key comes from an explicit project name when provided, or from the project directory name otherwise.

Default container names follow the project and service identity:

<project>-<service>-<container-number>

Created containers receive Docker Compose identity labels:

  • com.docker.compose.project
  • com.docker.compose.service
  • com.docker.compose.container-number
  • com.docker.compose.oneoff

Created networks receive the project and network labels. Containers and networks also receive WSLC ownership and schema labels:

  • com.microsoft.wslc.compose.managed=true
  • com.microsoft.wslc.compose.metadata-version=1

This distinguishes WSLC-managed resources from unrelated Docker resources and gives the service a versioned boundary for interpreting its metadata.

Labels are now the authoritative source for project discovery. Lifecycle operations rediscover managed containers, including stopped containers, rather than depending on process-local project state. Process-local reconciliation state contains synchronization only.

Project keys and resource identity are validated before use. Missing, duplicate, invalid, or unsupported metadata is not treated as valid WSLC state.

Asynchronous operation contract

The previous cached IWSLCComposeSession interface has been replaced by:

BeginComposeOperation(request, progressCallback) -> IComposeOperation

WSLCComposeOperationRequest captures:

  • The requested Compose action.
  • Either immutable Compose documents or an existing project key.
  • Project selection data.
  • Action-specific options, such as the stop timeout.
  • A schema version for contract validation.

IComposeOperation provides:

  • GetCompletionEvent() for non-polling completion waits.
  • Cancel() for cooperative cancellation.
  • GetResult() for the final status, HRESULT, project key, and affected containers.

The service captures and validates the request before launching the worker. The worker runs asynchronously in an MTA and retains the session for its lifetime. Progress callbacks are optional and are marshaled through the COM Global Interface Table when present.

The callback contract supports status, resource progress, diagnostics, and stream notifications. Status events remain available to service clients, while the CLI hides generic validation, planning, execution, and success phases in favor of resource-level progress.

The new interfaces are registered in the MSI proxy/stub configuration.

Cancellation, partial state, and retry

Cancellation is cooperative and non-transactional. Completed mutations remain applied, pending mutations do not begin after cancellation is observed, and a retry rediscovers actual state and converges from that state.

The service checks for cancellation:

  • Before and after validation.
  • While waiting for the project mutation lock.
  • Before planning and execution.
  • Between container and network mutations.
  • Before publishing a successful result.

An operation is reported as cancelled only when execution actually returns ERROR_CANCELLED, preventing a late cancellation request from converting an already completed mutation into a cancelled result.

Attached compose up follows Docker's staged cancellation behavior:

  1. The first Ctrl+C cancels attachment and begins graceful project stop.
  2. The second Ctrl+C force-kills remaining project containers.
  3. A third Ctrl+C falls through to immediate process termination as an escape hatch.

The CLI separates arbitrary attached container output from its cancellation message and reports:

Gracefully stopping... Press Ctrl+C again to force termination.

Graceful stop remains best-effort across project containers, and failures are surfaced rather than silently discarded.

Resource progress

Compose operations emit ordered resource progress with stable operation and resource-kind codes. The current CLI renders informational stderr lines for:

  • Creating and removing containers and networks.
  • Pulling missing images.
  • Starting, stopping, and force-killing containers.

Examples:

Creating Network scratch_default (1/1)
Pulling Image python:3.12-alpine (1/1)
Starting Container wslc-compose-web (1/2)
Stopping Container wslc-compose-web (1/2)

This intentionally uses simple line-oriented output for the current iteration rather than Docker's live TTY table.

Normalization and fail-closed validation

Compose document handling has moved behind a dedicated service-side normalizer.

The request captures the document bytes and caller-resolved context before asynchronous execution. The service validates schema versions, paths, project names, project keys, document counts, and embedded NUL characters. The previous arbitrary 16 MiB document limit was removed; only the ULONG COM transport limit remains.

The parser fails closed for unsupported fields rather than silently ignoring them. The accepted top-level fields are:

  • services
  • The legacy scalar version field, treated as a compatibility no-op

The currently accepted service fields are:

  • name
  • container_name
  • image
  • environment
  • working_dir
  • command
  • volumes
  • ports

Unsupported external references are rejected before discovery or mutation, including:

  • include
  • env_file
  • label_file
  • External extends.file
  • File-backed configs
  • File-backed secrets

Profiles, explicit service selection, and dependency inclusion return E_NOTIMPL consistently until those semantics are implemented. Failing before mutation prevents partially created projects when the service cannot interpret the complete request correctly.

Lifecycle behavior

Compose lifecycle handling passes through a dedicated ComposeReconciler boundary. A per-project lock serializes mutations while allowing unrelated projects to proceed independently, and lock acquisition remains cancellation-aware.

The command behaviors are:

  • create creates project containers without starting them. If the project already has managed containers, it leaves them unchanged without evaluating configuration drift.
  • start starts existing containers and preserves their container IDs.
  • up applies the supplied document by force-removing current containers, recreating the default network, creating replacement containers, starting them, and attaching output.
  • attach attaches output from existing project containers without changing lifecycle state.
  • stop stops all project containers using the requested timeout.
  • remove / delete / rm is currently equivalent to project-wide docker compose rm --force: it removes stopped project containers without prompting, leaves running containers untouched, preserves project networks and volumes, and reports No stopped containers when nothing is eligible.

Warning

The create and up reconciliation lifecycle is intentionally incomplete in this iteration. Docker Compose reuses unchanged resources, starts existing stopped containers during up, and selectively recreates resources when their configuration or image changes. The current create path is an existing-project no-op without drift evaluation. The current up path is destructive: it force-removes the existing managed project containers and deletes the default network before creating replacements, so if network creation, image pull, or container creation fails, the previous deployment is not restored. Project volumes are preserved. Follow-up work must add the required restart and selective-reconciliation capabilities before these commands can match Docker's lifecycle behavior.

Note

The current remove behavior covers the non-interactive, project-wide docker compose rm --force case only. Follow-up PRs are required for confirmation prompts, service selection, --force, --stop, --volumes, and the related command structure.

create and up require a Compose file. start, attach, stop, and remove accept either an existing Compose file or an exact project name returned by compose list --all. Invalid values produce a localized not-found error before the service operation begins.

Note

This project-identity argument behavior does not yet match Docker Compose. Docker separates Compose files (-f / --file) from project identity (-p / --project-name) through Compose-level options inherited by its subcommands. Supporting that model requires structural changes to how the WSLC CLI handles command-group global arguments. A future PR will resolve this difference; the current dual-purpose lifecycle argument enables functional end-to-end coverage for both file-based and identity-based project operations.

Each operation returns the project key and the observed state of its affected containers. This removes the need for the CLI to retain authoritative COM object state between commands.

Compose project listing

This change adds:

wslc compose list
wslc compose ls

Supported options are:

  • Default table output with project name and aggregate status.
  • --all to include projects without running containers.
  • --format json for machine-readable output.
  • --quiet for project names only.

Project listing is reconstructed from Docker container labels rather than cached CLI state. Container states are aggregated into summaries such as created(1), running(1), and exited(1).

Discovery requires both WSLC-managed provenance and a valid Compose project label. Docker-only Compose resources are excluded. Containers with missing or unsupported WSLC metadata versions are logged and skipped without preventing other valid projects from being listed.

CLI organization

Compose commands use the standard CLI task pipeline and have separate command implementations. The subcommands are presented alphabetically:

  • attach
  • create
  • list / ls
  • remove / delete / rm
  • start
  • stop
  • up

This keeps argument declaration, session resolution, project/file resolution, document capture, service invocation, output formatting, and cancellation handling separated instead of concentrating all behavior in ComposeCommand.cpp.

Important

The CLI commands in this proof of concept are functional end-to-end lifecycle surfaces, not final Docker-compatible command contracts. Targeted follow-up PRs are expected to align inherited Compose-level options, command-specific options, prompts, service selection, output details, and lifecycle semantics with Docker Compose.

Validation Steps Performed

cmake --build . -- -m
bin\x64\Debug\test.bat /name:*Compose*

The non-fast test run redeployed the package and test distro. Result: 22 passed, 0 failed.

Coverage includes:

  • Complete create, start, up, attach, stop, list, and remove lifecycle behavior.
  • File-path and project-key execution paths for commands that support both.
  • Interactive attached up and attach behavior.
  • remove, delete, and rm command forms.
  • Project-wide docker compose rm --force behavior, including No stopped containers when all project containers are running.
  • Label-based discovery across operations and process boundaries.
  • Ordered container, network, and image progress; generic status suppression in the CLI.
  • Graceful, forced, and final escape-hatch cancellation stages.
  • Cancellation before mutation, after partial mutation, late cancellation, discoverable partial state, and retry convergence.
  • Unsupported references and selections failing before mutation.
  • Project listing filters, aliases, formats, and aggregate states.
  • Alphabetical command ordering and expected argument types.

Copilot AI lite review requested due to automatic review settings September 5, 2026 00:47

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.

🟡 Changes recommended

There are correctness and robustness issues in the new cancellation/GIT callback handling and a few user-facing/size-limit behaviors that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request refactors WSLC Compose from a path-keyed cached session model into a project-identity-based async operation model, and adds a wslc compose list/ls surface that discovers managed projects via Docker labels.

Changes:

  • Replaces IWSLCComposeSession with BeginComposeOperation/IComposeOperation (completion event, cancellation, typed result) and introduces ComposeNormalizer + ComposeReconciler.
  • Adds managed Compose project listing via IWSLCSession::ListComposeProjects and a new wslc compose list / ls CLI command with table/JSON/quiet output.
  • Updates unit + e2e tests to validate lifecycle behavior, cancellation, unsupported input rejection, and listing behavior.
File summaries
File Description
test/windows/WSLCComposeTests.cpp Refactors tests to use async compose operations, project keys, and validates ordered progress + cancellation/validation behavior.
test/windows/wslc/WSLCCLICommandUnitTests.cpp Updates CLI surface test to expect the new compose list subcommand.
test/windows/wslc/e2e/WSLCE2EComposeListTests.cpp Adds e2e coverage for compose list/ls output modes and managed-only project filtering.
src/windows/wslcsession/WSLCSession.h Replaces compose session API with compose operation + project listing; adds reconciler member.
src/windows/wslcsession/WSLCSession.cpp Implements BeginComposeOperation, ListComposeProjects, and adds compose labels + cancel-aware container creation.
src/windows/wslcsession/WSLCComposeSession.h Removes legacy cached compose session COM object.
src/windows/wslcsession/WSLCComposeSession.cpp Removes legacy cached compose session COM object implementation.
src/windows/wslcsession/WSLCComposeOperation.h Adds async compose operation COM object definition (completion event, cancel, result).
src/windows/wslcsession/WSLCComposeOperation.cpp Implements async compose worker thread, request capture/validation, progress events, and result publication.
src/windows/wslcsession/ComposeSpec.h Extends spec model (service name) and changes parsing to accept in-memory content.
src/windows/wslcsession/ComposeSpec.cpp Parses YAML from captured bytes; fails closed on unsupported fields; captures service identity.
src/windows/wslcsession/ComposeReconciler.h Introduces reconciler boundary for per-project locking and action execution.
src/windows/wslcsession/ComposeReconciler.cpp Implements create/up/start/stop semantics with cancel-aware locking and observation.
src/windows/wslcsession/ComposeNormalizer.h Adds normalization/validation boundary for documents, selection, and project key rules.
src/windows/wslcsession/ComposeNormalizer.cpp Normalizes project identity, validates selection, enforces size/NUL constraints, and generates default container names.
src/windows/wslcsession/ComposeLabels.h Centralizes Compose and WSLC-managed label keys/values.
src/windows/wslcsession/CMakeLists.txt Wires new compose sources/headers and removes the old compose session implementation.
src/windows/wslc/tasks/ComposeTasks.h Adds task entrypoints for compose lifecycle + list pipeline.
src/windows/wslc/tasks/ComposeTasks.cpp Implements CLI task pipeline for create/up/start/attach/stop and list output rendering.
src/windows/wslc/services/ComposeService.h Updates CLI service API to use async operations + adds project listing.
src/windows/wslc/services/ComposeService.cpp Implements document capture, async operation execution/wait + compose project listing formatting.
src/windows/wslc/services/ComposeProgressCallback.h Adds CLI-side progress callback interface implementation for compose operations.
src/windows/wslc/services/ComposeProgressCallback.cpp Renders compose progress events and stream forwarding (placeholder output).
src/windows/wslc/services/ComposeModel.h Adds CLI model for compose project list output (NDJSON-friendly).
src/windows/wslc/core/ExecutionContextData.h Adds execution context storage for compose project listing results.
src/windows/wslc/commands/ComposeUpCommand.cpp Moves compose up to task pipeline implementation.
src/windows/wslc/commands/ComposeStopCommand.cpp Moves compose stop to task pipeline and keeps timeout argument handling.
src/windows/wslc/commands/ComposeStartCommand.cpp Moves compose start to task pipeline implementation.
src/windows/wslc/commands/ComposeListCommand.cpp Adds new compose list / ls command and task pipeline.
src/windows/wslc/commands/ComposeCreateCommand.cpp Moves compose create to task pipeline implementation.
src/windows/wslc/commands/ComposeCommand.h Adds ComposeListCommand to the compose command set.
src/windows/wslc/commands/ComposeCommand.cpp Simplifies compose command into subcommand registration only; removes inline implementations.
src/windows/wslc/commands/ComposeAttachCommand.cpp Moves compose attach to task pipeline implementation.
src/windows/service/inc/wslc.idl Defines compose request/result/progress types and replaces IWSLCComposeSession with new async interfaces + list API.
msipackage/package.wix.in Registers new compose COM interfaces in proxy/stub MSI configuration.
localization/strings/en-US/Resources.resw Adds localized help strings for wslc compose list.
Review details
  • Files reviewed: 36/36 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 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/ComposeService.cpp Outdated
Comment thread src/windows/wslcsession/ComposeReconciler.cpp Outdated
Comment thread src/windows/wslcsession/WSLCComposeOperation.cpp
Comment thread src/windows/wslcsession/WSLCSession.cpp
Comment thread src/windows/wslc/services/ComposeProgressCallback.cpp Outdated

@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.

Went through this properly rather than skimming. Moving off the path-keyed session onto identity plus an async operation is the right call, and the labels and fail-closed parsing are the parts I was most worried about, so those are good. Two things I want resolved before it goes to Pooja.

The cancellation guarantee in the description does not actually hold. ComposeReconciler::Execute calls CheckCancelled after the switch, and RunOperation calls it again once Execute has returned. If Ctrl-C lands while the last Start or Stop is still blocked inside the container call, the mutation completes and we throw ERROR_CANCELLED on top of it. Run only assigns executionResult when RunOperation returns normally, so the caller gets status Cancelled, an empty project key and zero affected containers while the containers are genuinely up. That is precisely the case the description says is prevented. Past the mutation boundary, cancellation should lose.

Second, listing and lifecycle disagree about where truth lives. ListComposeProjects rebuilds from container labels, which is the right answer. But ComposeReconciler::m_projects is in-process only and nothing ever rehydrates it from those labels, so stop, start and attach on a project the service just listed can come back ERROR_NOT_FOUND, and up builds an empty ProjectState and never adopts or removes the containers that are already there. Either adopt by label on a miss, or say plainly in the design that lifecycle is scoped to the session instance.

Copilot AI review requested due to automatic review settings September 7, 2026 05:42

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.

🟡 Changes recommended

Ctrl-C cancellation handling can miss/race before the cancel event is created, and the service lacks a documented compose document size limit check before copying request content into memory.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/windows/wslc/core/CLIExecutionContext.cpp:23

  • RecordCancellationRequest only signals cancellation if CancelEvent is already created; if Ctrl-C arrives before (or races with) CreateCancelEvent, CancellationCount becomes 1 but the event is never signaled, and the next Ctrl-C will fall through to the default handler instead of cancelling the active operation.
bool CLIExecutionContext::RecordCancellationRequest() noexcept
{
    const auto cancellationCount = CancellationCount.fetch_add(1, std::memory_order_relaxed) + 1;
    return cancellationCount == 1 && CancelEvent && SetEvent(CancelEvent.get());
}
  • Files reviewed: 40/40 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/windows/wslc/core/CLIExecutionContext.cpp
Comment thread src/windows/wslcsession/WSLCComposeOperation.cpp
@dkbennett

Copy link
Copy Markdown
Member Author

Went through this properly rather than skimming. Moving off the path-keyed session onto identity plus an async operation is the right call, and the labels and fail-closed parsing are the parts I was most worried about, so those are good. Two things I want resolved before it goes to Pooja.

The cancellation guarantee in the description does not actually hold. ComposeReconciler::Execute calls CheckCancelled after the switch, and RunOperation calls it again once Execute has returned. If Ctrl-C lands while the last Start or Stop is still blocked inside the container call, the mutation completes and we throw ERROR_CANCELLED on top of it. Run only assigns executionResult when RunOperation returns normally, so the caller gets status Cancelled, an empty project key and zero affected containers while the containers are genuinely up. That is precisely the case the description says is prevented. Past the mutation boundary, cancellation should lose.

Second, listing and lifecycle disagree about where truth lives. ListComposeProjects rebuilds from container labels, which is the right answer. But ComposeReconciler::m_projects is in-process only and nothing ever rehydrates it from those labels, so stop, start and attach on a project the service just listed can come back ERROR_NOT_FOUND, and up builds an empty ProjectState and never adopts or removes the containers that are already there. Either adopt by label on a miss, or say plainly in the design that lifecycle is scoped to the session instance.

Thanks for the detailed review! Both issues are now addressed.

Cancellation now matches Docker Compose's non-transactional behavior: completed mutations remain applied, pending mutations do not begin after cancellation, and retrying the idempotent operation converges from discoverable partial state. A late cancellation cannot overwrite a successfully completed final mutation, while cancellation of an attached up gracefully stops the project.

Labels are indeed the truth and lifecycle operations now rediscover managed containers from Compose labels on every invocation rather than relying on process-local state, including after service or session recreation. Regression coverage was added for late cancellation, partial-state recovery, and label-based lifecycle discovery.

Copilot AI review requested due to automatic review settings September 7, 2026 19:30

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.

🟡 Changes recommended

Ctrl+C cancellation can be dropped due to event-creation timing, and the stated 16 MiB compose document limit is not currently enforced in the service normalization path.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

src/windows/wslc/core/CLIExecutionContext.cpp:33

  • RecordCancellationRequest currently returns FALSE when the corresponding event has not been created yet (CancelEvent/ForceCancelEvent are null), which means Ctrl-C may not be treated as handled and the cancellation request is effectively dropped. Since CancellationCount still increments, later Ctrl-C presses can also skip signaling the intended event, preventing graceful/force cancellation from working reliably.
    const auto cancellationCount = CancellationCount.fetch_add(1, std::memory_order_relaxed) + 1;
    if (cancellationCount == 1)
    {
        return CancelEvent && SetEvent(CancelEvent.get());
    }
  • Files reviewed: 43/43 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread src/windows/wslc/core/CLIExecutionContext.cpp
Comment thread src/windows/wslc/tasks/ComposeTasks.cpp Outdated
Comment thread src/windows/wslcsession/ComposeNormalizer.cpp
Copilot AI review requested due to automatic review settings September 7, 2026 20:01

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.

🔵 Needs a closer look

The force-cancel (second Ctrl+C) path cancels the stop operation but still requires success, and per-project lock caching currently grows unbounded in the service process.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/windows/wslc/services/ComposeService.cpp:252

  • When the force-cancel event (second Ctrl+C) is signaled, this branch calls operation->Cancel() but still requires the operation to return WSLCComposeOperationStatusSucceeded / S_OK. Since the service treats Cancel() as cooperative cancellation (likely returning ERROR_CANCELLED), this will typically throw and log an error instead of cleanly escalating to the force-kill path.

Consider treating the force-cancel event as “stop waiting and escalate” (run forceAction) without cancelling the stop operation, so it can still complete successfully after the containers are killed.
src/windows/wslcsession/ComposeReconciler.cpp:89

  • ResolveProjectLock() stores per-project locks in m_projectLocks as shared_ptrs and never removes them. Over time, running compose operations against many distinct project keys can cause unbounded growth of this map within a long-lived service process.

A simple mitigation is to prune entries whose shared_ptr use_count() is 1 (only held by the map) each time ResolveProjectLock runs, keeping the cache bounded to “in-flight” projects.

  • Files reviewed: 43/43 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 7, 2026 20:22

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.

🟡 Changes recommended

There are a couple of correctness/reliability issues in the new async compose flow (force-cancel wait behavior and an unchecked size->ULONG cast) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/windows/wslcsession/WSLCComposeOperation.cpp:105

  • GetResult() casts m_affectedContainers.size() to ULONG without checking for overflow. If the vector ever exceeds ULONG_MAX, the cast truncates and the allocated buffer size/count become inconsistent, risking memory corruption for callers.
  • Files reviewed: 43/43 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/windows/wslc/services/ComposeService.cpp

@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.

Re-reviewed against the latest push. The label-based rediscovery point from my last pass is addressed - Execute now discovers project containers up front for every action, so listing and lifecycle finally agree on where truth lives. The post-final-mutation cancellation case is improved too. Cancellation can still interrupt partway through a multi-container operation, so the partial-result concern below survives.

Three things.

compose rm reports success without doing the work. Remove filters out WslcContainerStateRunning before the delete loop, then Execute calls containers.clear() unconditionally. On an all-running project that is a successful no-op. On a mixed project it is worse: the stopped containers are deleted, the running ones are left, and the result is cleared so it reports success with zero affected containers. Docker refuses here and makes you pass --stop. I would error when any targeted container is running.

up destroys before it knows it can replace. Up force-deletes every discovered container, then CreateComposeContainers deletes the project network, and only then creates containers and pulls missing images at WSLC_E_IMAGE_NOT_FOUND. A registry hiccup, a bad tag or a port conflict therefore turns a healthy running project into nothing. The scope_exit cleanup only unwinds what this call created; it cannot bring back what Up already deleted. I know desired-versus-observed diffing is P2 and I am not asking for it here. I am asking that the destructive step not run before replacement is known to be viable, or that recreate-only semantics be stated explicitly in the description.

Small one, related to cancellation: m_projectKey is only assigned from executionResult, which stays default-constructed if Execute throws. So a cancelled up that already deleted half the project reports Cancelled with an empty project key, discarding identity we knew before we started. Worth setting the key before entering the reconciler.

@ranm-msft

Copy link
Copy Markdown

Correction to my first point above: I said Docker refuses to remove running containers and requires --stop. That was wrong. docker compose rm leaves running containers alone and removes only the eligible stopped ones, and --force just suppresses the confirmation prompt, so the eligibility rule here does match Docker's default. Apologies for the noise.

The part I'd still keep on the list is the reporting rather than the eligibility:

  • When nothing is eligible, Docker prints No stopped containers and returns success. Today this path is silent, so rm on an all-running project looks like it did something.
  • In the mixed case, where stopped containers genuinely were removed, Execute calls containers.clear() unconditionally and we report zero affected containers even though work happened. That one is independent of Docker alignment.

Prompting and the --stop / --force options seem like reasonable followups, and per the thread they're already headed that way.

Points 2 and 3 stand as written.

Copilot AI review requested due to automatic review settings September 7, 2026 22:28

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.

🔵 Needs a closer look

The new ComposeProgressCallback::OnStreamsReady mishandles non-file/pipe stream handle types (notably sockets), which can break stream relay and handle ownership/closure semantics.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/windows/wslc/services/ComposeProgressCallback.cpp:157

  • OnStreamsReady treats WSLCHandle payloads as plain Win32 HANDLEs (wil::unique_handle + ReadFile-based RelayHandle) regardless of handle type. If the service ever provides WSLCHandleTypeSocket, this will both (1) attempt ReadFile on a socket and (2) close the socket with CloseHandle instead of closesocket, leading to broken output and handle misuse. This should either reject socket streams explicitly or handle them with socket-aware IO primitives and ownership wrappers (wil::unique_socket / ReadSocketMessageHandle).
    src/windows/wslcsession/ComposeSpec.cpp:220
  • ComposeSpec::Parse currently incurs an extra full-buffer copy by doing YAML::Load(std::string{content}) where content is already a std::string in the caller (ComposeNormalizer). For large compose files this doubles peak memory and adds avoidable CPU. Consider changing ComposeSpec::Parse (and its helpers) to take const std::string& and call YAML::Load(content) directly, updating the single caller accordingly.
  • Files reviewed: 43/43 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 8, 2026 05:33

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.

🔵 Needs a closer look

It introduces a new COM surface area and async lifecycle/cancellation model, and there is at least one confirmed correctness gap in compose container identity validation/sorting that should be addressed before approval.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/windows/wslcsession/WSLCSession.cpp:2481

  • DiscoverComposeContainers() uses the Docker Compose com.docker.compose.container-number label as an opaque string and sorts by service:container-number lexicographically. This misorders containers once the container number reaches multiple digits (e.g. 10 sorts before 2) and also accepts non-numeric container-number values as valid WSLC state, which can make resource identity validation fail open.
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 8, 2026 06:14
@dkbennett

Copy link
Copy Markdown
Member Author

Feedback addressed up to this point.

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.

🔵 Needs a closer look

ComposeSpec parsing currently relies on YAML conversion exceptions for some invalid inputs (non-scalar keys / non-scalar name values), which can bypass the intended structured “invalid compose file” validation messaging and should be fixed before approval.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

src/windows/wslcsession/ComposeSpec.cpp:275

  • This loop reads setting.first.as<std::string>() without verifying the YAML key is a scalar. Non-scalar keys will throw a YAML::Exception and surface a generic parser error instead of a targeted validation message. Add an explicit IsScalar() check before converting the key.

This issue also appears on line 285 of the same file.

src/windows/wslcsession/ComposeSpec.cpp:289

  • nameNode.as<std::string>() assumes the name/container_name value is a scalar. If a user provides a non-scalar value (e.g., list/map), YAML conversion will throw and the user will get a low-level YAML error rather than a clear "invalid compose file" validation message. Validate IsScalar() before calling as<std::string>() and fail with a targeted error.
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@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.

Re-reviewed at 356465ce. All three items from my last pass are resolved, and the first one is resolved better than I asked for it.

Remove. I asked you to error when a targeted container is running. That was the wrong ask - Docker does not reject the operation, it treats running containers as ineligible - and what landed matches Docker instead: stopped containers are removed, running ones are left alone, and the CLI prints No stopped containers when nothing was eligible. The part I care about more is that the result is accurate now. Execute returns the observed entries from Remove rather than clearing the result even when eligible stopped containers had actually been removed.

Project key. Fixed at the right level: local to Run, passed by reference, assigned before the reconciler runs. So once normalization has produced the key, a later cancellation or reconciler failure still reports it.

Up. The warning block covers it. I offered documentation as the alternative to reordering, and "the previous deployment is not restored" states the accepted behavior plainly.

One question on the remove loop:

const HRESULT deleteResult = container->Delete(WSLCDeleteFlagsNone);
THROW_IF_FAILED_EXCEPT(deleteResult, RPC_E_DISCONNECTED);
result[index].State = WslcContainerStateDeleted;

Can Delete return RPC_E_DISCONNECTED here only after the container has committed Deleted? PrepareDisconnectComWrapper caches m_state as it stands at disconnect time before nulling the impl, and GetState serves that cache afterwards, so a wrapper disconnected for some other reason can still be holding Created or Exited. If that is reachable, this marks the entry Deleted without having established the container is gone - which is the narrow version of the reporting problem I raised last time, now visible through the new return value.

No objections from me otherwise. Happy to take another look after any further changes.

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.

3 participants