wslc: add --follow-link to cp - #41501
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
It adds new behavior without an e2e test validating -L actually follows symlinks, and it introduces at least one naming/doc inconsistency that should be addressed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds --follow-link / -L support to wslc container cp to match docker cp -L semantics by resolving symlinks in the source path (both local→container and container→local) before copying.
Changes:
- Adds a new CLI flag (
ArgType::FollowLink,--follow-link, alias-L) and wires it intocontainer cp. - Implements container-side symlink resolution by statting
/containers/{id}/archiveand decodingX-Docker-Container-Path-Stat, exposed via a newIWSLCContainer::ResolveArchiveSymlinkmethod. - Adds parser/unit/e2e coverage for argument presence and help output surfacing.
File summaries
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLICommandUnitTests.cpp | Adds a unit test to pin --follow-link name/alias/kind. |
| test/windows/wslc/e2e/WSLCE2EContainerCpTests.cpp | Adds an e2e test ensuring container cp --help lists --follow-link/-L. |
| test/windows/wslc/CommandLineTestCases.h | Adds command-line parsing cases for -L/--follow-link (incl. boolean forms and negatives). |
| src/windows/wslcsession/WSLCContainer.h | Adds ResolveArchiveSymlink to the container implementation and COM interface class. |
| src/windows/wslcsession/WSLCContainer.cpp | Implements ResolveArchiveSymlink and exposes it via COM. |
| src/windows/wslcsession/DockerHTTPClient.h | Declares StatArchivePath helper for reading X-Docker-Container-Path-Stat. |
| src/windows/wslcsession/DockerHTTPClient.cpp | Implements StatArchivePath (GET archive, read stat header, close socket). |
| src/windows/wslc/tasks/ContainerTasks.cpp | Adds followLink behavior for container cp in both copy directions. |
| src/windows/wslc/services/ContainerService.h | Adds ResolveContainerSymlink service helper declaration. |
| src/windows/wslc/services/ContainerService.cpp | Implements ResolveContainerSymlink by calling IWSLCContainer::ResolveArchiveSymlink. |
| src/windows/wslc/commands/ContainerCpCommand.cpp | Registers ArgType::FollowLink for the container cp command. |
| src/windows/wslc/arguments/ArgumentDefinitions.h | Defines the new argument type and localization hook. |
| src/windows/service/inc/wslc.idl | Extends IWSLCContainer with ResolveArchiveSymlink. |
| src/windows/inc/docker_schema.h | Adds ContainerPathStat schema for the archive stat header payload. |
| localization/strings/en-US/Resources.resw | Adds localized help text for --follow-link. |
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
DockerHTTPClient::StatArchivePath currently treats all non-200 responses (and even missing stat headers on 200) as “path not found,” which can silently mask real engine errors and cause --follow-link to behave incorrectly.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
src/windows/wslcsession/DockerHTTPClient.cpp:449
- StatArchivePath() currently returns nullopt for any non-200 response, but its contract says nullopt means the path does not exist. This can silently mask real engine errors (e.g., 500/401) and cause --follow-link to fall back to copying the symlink instead of failing fast.
This issue also appears on line 451 of the same file.
src/windows/wslcsession/DockerHTTPClient.cpp:455
- On a 200 response, a missing X-Docker-Container-Path-Stat header indicates an unexpected daemon/proxy behavior and should not be treated the same as "path does not exist"; otherwise --follow-link can silently do the wrong thing.
const auto header = response["X-Docker-Container-Path-Stat"];
if (header.empty())
{
return std::nullopt;
}
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
DockerHTTPClient::StatArchivePath currently treats all non-200 responses (and missing stat headers) as “not found,” which can mask real engine/protocol failures and should be tightened to surface errors correctly.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
src/windows/wslcsession/DockerHTTPClient.cpp:455
- If the response is 200 but missing the
X-Docker-Container-Path-Statheader, returningnulloptwill make the caller treat it as "not a symlink" and proceed, which can mask a protocol/engine mismatch. It should be treated as an unexpected error so callers can fail fast with a clear diagnostic.
const auto header = response["X-Docker-Container-Path-Stat"];
if (header.empty())
{
return std::nullopt;
}
src/windows/wslcsession/DockerHTTPClient.h:150
- The comment for
StatArchivePathsays it returnsnulloptwhen the path does not exist, but the implementation also treats other non-200 responses (and a missing stat header) asnullopt. Update the comment to reflect the actual contract (e.g., 404 =>nullopt, other errors => exception) to avoid misleading future callers.
// Reads the X-Docker-Container-Path-Stat header for a container path. Returns nullopt if the path does not exist.
std::optional<common::docker_schema::ContainerPathStat> StatArchivePath(const std::string& ContainerID, const std::string& Path);
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
There is a confirmed path-handling bug in the container cp local→container flow where stripping a trailing separator can turn a drive-root parent directory (e.g. C:\) into C:, altering tar.exe -C behavior and potentially breaking copies from drive roots.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/windows/wslc/tasks/ContainerTasks.cpp:387
- When SRC_PATH is at a drive root (e.g. "C:\file"), absPath.parent_path() becomes "C:\". The subsequent trailing-separator trimming in this block turns that into "C:", which changes tar.exe -C semantics ("C:" means the current directory on that drive) and can break copies from the drive root. Preserve root paths when stripping trailing separators.
src/windows/wslc/services/ContainerService.h:68 - This comment says ResolveContainerSymlink returns nullopt only when the path is not a symlink, but the underlying service method also returns nullopt when the path does not exist. Update the comment to match the actual behavior.
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The new container-side symlink resolution currently holds a shared lock across network I/O, and the new local→container follow-link behavior lacks corresponding automated coverage.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/windows/wslc/tasks/ContainerTasks.cpp:384
- The new --follow-link behavior for local → container relies on std::filesystem::canonical() to resolve the source path before archiving. There is E2E coverage for container → local symlink following, but no test coverage for this local → container path-resolution behavior, so regressions here would be easy to miss.
src/windows/wslcsession/WSLCContainer.cpp:2688 - ResolveArchiveSymlink holds the container shared lock while issuing a Docker API request (StatArchivePath). This can block other container operations longer than necessary; other archive operations (e.g., DownloadArchive/UploadArchive) release the lock before doing potentially long I/O.
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary of the Pull Request
Adds
--follow-link/-Ltowslc container cpWithout
-Lthe behavior is unchanged: the symlink itself is copied.PR Checklist
Detailed Description of the Pull Request / Additional comments
CLI
ArgumentDefinitions.hgainsArgType::FollowLink(--follow-link, aliasL,Kind::Flag).ContainerCpCommandregisters it alongside the existing--archive.Container → local: statting a path over the Docker Engine API
Docker resolves the link with
ContainerStatPath, i.e.HEAD /containers/{id}/archive?path=..., reading theX-Docker-Container-Path-Statresponse header.DockerHTTPClientcannot safely issue aHEADtoday:SendRequestdrives a Boost.Beast parser that would block waiting for a body that aHEADresponse never sends. Rather than reshape the parser,StatArchivePathtakes advantage of two existing properties of the client:GET /containers/{id}/archivereturns the sameX-Docker-Container-Path-Statheader asHEAD.SendRequestreturns as soon as the response header has been parsed, handing the still-open socket back to the caller.So
StatArchivePathissues theGET, reads the header, and drops the socket without ever reading the tar body. Each request gets its own freshly-connected socket viaSendRequestImpl, so abandoning it has no effect on subsequent calls.The header is base64-encoded JSON, decoded with
wslutil::Base64Decodeinto a newdocker_schema::ContainerPathStat. The symlink test uses Go'sos.ModeSymlinkbit (1 << 27), since themodefield is a marshalled Goos.FileMode.When the target is relative, it is joined against the parent directory of the source path — the same rebasing docker performs via
archive.SplitPathDirEntry.Interface changes
IWSLCContainer::ResolveArchiveSymlinkis new.IWSLCContainer(wslc.idl) is internal and non-stable — it ships in lockstep with its only clients — so appending a method is safe. The SDK-facingIWSLCCompatContainerinWSLCCompat.idlis untouched.Validation Steps Performed
Full
cmake --build .— clean.wslc container cp --helplists the new option:13 new parser cases in
CommandLineTestCases.hcovering-L,--follow-link,=true/=false, combination with-a, the root-levelwslc cpalias, and the negatives--followlink,-l(aliases are case-sensitive) and-L=invalid.ContainerCpCommand_HasFollowLinkArgumentWithDockerAliaspins the name, alias, kind and optionality.WSLCE2E_Container_Cp_HelpListsFollowLinkasserts the flag is surfaced in help output.