You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Compose: Expand WSLC Compose with project identity, async operations, and listing - #41526
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:
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:
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:
The first Ctrl+C cancels attachment and begins graceful project stop.
The second Ctrl+C force-kills remaining project containers.
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:
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.
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.
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.
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.
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.
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());
}
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.
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.
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.
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.
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.
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.
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.
The following review items are intentionally postponed. This PR aligns the Compose implementation with the architecture contract; these items are gaps or follow-up features rather than problems with the core flow, and they can be decided separately.
Second Ctrl+C while the cancelled up is still running (Compose: Expand WSLC Compose with project identity, async operations, and listing #41526 (comment))
The final state is correct: once the cancelled up returns, the force-cancel event is still signaled, so the stop phase force-kills immediately. The gap is responsiveness while the service is blocked in a long Docker call. Fixing it requires deciding whether the client can stop waiting on an in-flight operation, which is a separate design question.
ComposeDocument::BaseDirectory is captured and validated, but this call passes only SourcePath to the parser. Relative bind-mount sources are resolved by ParseComposeVolume against path.parent_path(), so an API caller that supplies a relative/display source path plus a separate base directory gets mounts resolved relative to the service's working directory instead of the requested Compose context. Use BaseDirectory for resolution while retaining SourcePath for diagnostics.
Changes to this point have been addressed and any outstanding ones are intentionally deferred. I think its important to restate that this is not intended to be bug-proof and fully implement everything; this is intended to get us in the right ballpark where we can make more refined changes. Its' a lot of code churn, I'm sure copilot will find something wrong every time it runs.
The reason will be displayed to describe this comment to others. Learn more.
Could this return FALSE when the force-cancel stage is not supported? attach/create/start/stop/rm create only CancelEvent; on their second Ctrl+C, !forceCancelEvent is true, so the console handler consumes the signal without cancellation or termination. Commands creating neither event similarly consume both initial presses. Three-stage cancellation makes sense for attached compose up, but this changes every command. Please preserve early-request replay for up without swallowing unsupported stages, and cover the one-event/no-event cases.
The reason will be displayed to describe this comment to others. Learn more.
The cancellation regression from my last review is fixed: unsupported stages return FALSE, and the tests cover no-event, graceful-only, and early force-event replay. The remaining Compose correctness threads still need resolution before I can sign off.
When attached up is cancelled, this cleanup starts a new stop operation from the original file path even though the completed operation has already returned the normalized project key. If the file is deleted or modified while up is attached, CapturedComposeDocuments/normalization can fail before discovery, so graceful stop—and the second Ctrl+C force-kill path—cannot clean up the containers that were just created. Prefer a ComposeProjectReference built from projectKey when it is available, using the path only as a fallback before the key has been established.
Validate container identities when listing projects
src/windows/wslcsession/WSLCSession.cpp:2490
Project listing only validates the project label and metadata version before adding a summary. It does not validate the required service/container-number identity (or reject duplicate identities), even though DiscoverComposeContainers rejects those same malformed resources. A managed v1 container with only project and metadata-version labels is therefore exposed by compose list as a valid project, but every lifecycle operation on that project fails with invalid metadata. Apply the same fail-closed identity validation and skip/log invalid projects (including duplicate service/number pairs) while building summaries.
After the initial up request has normalized the file, projectKey is available even if the operation is cancelled. The cancellation cleanup nevertheless starts the graceful stop by passing project, which makes RunOperation reread the Compose file. If the file is deleted or edited while up is running, cleanup can fail and leave the newly created/partially created containers running. Use the captured project key for the stop request whenever it is available, falling back to the path only when no key was produced yet.
The normalizer only supports one document, but capture accepts any nonzero DocumentCount and reserves/copies every entry before that validation runs. A caller can therefore force a very large allocation and copy at the COM boundary with a request that will inevitably be rejected. Reject counts other than one before reserve/the loop (or otherwise cap the capture) to fail closed without doing attacker-controlled work.
The reason will be displayed to describe this comment to others. Learn more.
Both of my findings are resolved at c270c61, so my changes-requested vote no longer reflects an open issue:
Service-key DNS alias: the reconciler now calls AddPrimaryNetworkAlias(definition.ServiceName) when the service key differs from the container name, and ComposeServiceNameHttpRequest checks that a peer reaches the service by its key.
Second Ctrl+C:CancellationMode (Graceful vs GracefulAndForce) means a command that only creates CancelEvent no longer swallows the second signal.
This is a 4.7k-line Compose change, so I would still like a WSLC owner to review at head, and CI to finish, before I approve, which will clear that vote.
The reason will be displayed to describe this comment to others. Learn more.
Can we also use the saved projectKey for the stop request when it’s available? The callback already uses it for ForceKill, but Execute still receives project, and that contains the Compose file path.
RunOperation uses that argument to construct CapturedComposeDocuments, which reopens the file before calling BeginComposeOperation. If the file was deleted or renamed while up was attached, the request fails before reaching the service. If it was edited into an invalid configuration, validation fails before stopping the containers.
The callback only runs when forceCancelEvent is signaled, so using the key there doesn’t remove the file dependency from the initial stop request. We already know which project was started, so stopping it shouldn’t depend on the file still being present and valid.
The reason will be displayed to describe this comment to others. Learn more.
BeginComposeOperation creates a WSLCExecutionContext, but that context is thread-local. Parsing now runs on a worker thread, and Run() never constructs an execution context on that thread, so THROW_HR_WITH_USER_ERROR skips SetErrorMessage there.
Run() then saves only the HRESULT from wil::ResultFromException. GetResult() returns that code without the message, and the CLI rethrows it with THROW_IF_FAILED(value.Result).
The message needs to be collected on the worker and carried back to the CLI when the operation completes.
The reason will be displayed to describe this comment to others. Learn more.
This is probably not a bug we want to resolve in this PR, but it’s worth calling out. There may be a race: during the second Ctrl+C, a container can stop after WSLC checks its state but before Docker processes the kill request. Docker returns HTTP 409, which the existing WSLC code converts to E_FAIL rather than WSLC_E_CONTAINER_NOT_RUNNING.
That makes ForceKill throw instead of skipping the stopped container. The caller’s scope guard also cancels the graceful Stop operation, so the remaining containers can be left running.
The reason will be displayed to describe this comment to others. Learn more.
CreateContainerImpl releases m_containersLock while waiting for OnContainerCreated to add the container to m_containers. During that time, the Docker container already exists, but its implementation is held in m_pendingCreate. We need to check m_pendingCreate as well.
The reason will be displayed to describe this comment to others. Learn more.
Docker Compose rejects duplicate keys in the services mapping. We should reject them before creating containers too. With different container_name values, both definitions can currently create containers, but they receive the same service label and container number. Subsequent discovery rejects them as duplicates, so stop and remove fail.
The reason will be displayed to describe this comment to others. Learn more.
If up is cancelled before creating any containers for a new project, this Stop call returns ERROR_NOT_FOUND and prints “Project containers may still be running.” The cancellation cleanup should treat that case as nothing to stop, rather than a failure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change evolves the initial
wslc composeproof of concept from a configuration-path-based session model into an identity-based, asynchronous project lifecycle model.The primary changes are:
IWSLCComposeSessionobjects with asynchronousIComposeOperationrequests.upcancellation.wslc compose list/lswith table, JSON, quiet, and all-project output.wslc compose removewithdeleteandrmaliases; its current project-wide behavior is equivalent todocker compose rm --force.start,attach,stop, andremoveto address a project by either Compose file path or listed project name.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:
PR Checklist
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:
Created containers receive Docker Compose identity labels:
com.docker.compose.projectcom.docker.compose.servicecom.docker.compose.container-numbercom.docker.compose.oneoffCreated networks receive the project and network labels. Containers and networks also receive WSLC ownership and schema labels:
com.microsoft.wslc.compose.managed=truecom.microsoft.wslc.compose.metadata-version=1This 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
IWSLCComposeSessioninterface has been replaced by:WSLCComposeOperationRequestcaptures:IComposeOperationprovides: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:
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 upfollows Docker's staged cancellation behavior:The CLI separates arbitrary attached container output from its cancellation message and reports:
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:
Examples:
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
ULONGCOM transport limit remains.The parser fails closed for unsupported fields rather than silently ignoring them. The accepted top-level fields are:
servicesversionfield, treated as a compatibility no-opThe currently accepted service fields are:
namecontainer_nameimageenvironmentworking_dircommandvolumesportsUnsupported external references are rejected before discovery or mutation, including:
includeenv_filelabel_fileextends.fileProfiles, explicit service selection, and dependency inclusion return
E_NOTIMPLconsistently 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
ComposeReconcilerboundary. A per-project lock serializes mutations while allowing unrelated projects to proceed independently, and lock acquisition remains cancellation-aware.The command behaviors are:
createcreates project containers without starting them. If the project already has managed containers, it leaves them unchanged without evaluating configuration drift.startstarts existing containers and preserves their container IDs.upapplies the supplied document by force-removing current containers, recreating the default network, creating replacement containers, starting them, and attaching output.attachattaches output from existing project containers without changing lifecycle state.stopstops all project containers using the requested timeout.remove/delete/rmis currently equivalent to project-widedocker compose rm --force: it removes stopped project containers without prompting, leaves running containers untouched, preserves project networks and volumes, and reportsNo stopped containerswhen nothing is eligible.Warning
The
createandupreconciliation lifecycle is intentionally incomplete in this iteration. Docker Compose reuses unchanged resources, starts existing stopped containers duringup, and selectively recreates resources when their configuration or image changes. The currentcreatepath is an existing-project no-op without drift evaluation. The currentuppath 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
removebehavior covers the non-interactive, project-widedocker compose rm --forcecase only. Follow-up PRs are required for confirmation prompts, service selection,--force,--stop,--volumes, and the related command structure.createanduprequire a Compose file.start,attach,stop, andremoveaccept either an existing Compose file or an exact project name returned bycompose 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:
Supported options are:
--allto include projects without running containers.--format jsonfor machine-readable output.--quietfor 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), andexited(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:
attachcreatelist/lsremove/delete/rmstartstopupThis 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
The non-fast test run redeployed the package and test distro. Result: 22 passed, 0 failed.
Coverage includes:
upandattachbehavior.remove,delete, andrmcommand forms.docker compose rm --forcebehavior, includingNo stopped containerswhen all project containers are running.