Add wslc container cp command for tar archive upload#40835
Conversation
Implements 'wslc container cp - CONTAINER:PATH' to copy a tar archive
from stdin into a running container via Docker's PUT /containers/{id}/archive API.
Usage: tar.exe -cf - files | wslc container cp - my_container:/dest
Changes across all layers:
- IDL: Added UploadArchive to IWSLCContainer
- DockerHTTPClient: Added PutArchive method (omits Content-Length for pipes)
- WSLCContainerImpl: Relay stdin to Docker socket with SD_SEND on EOF
- ContainerService: Added CopyToContainer static method
- ContainerTasks: Added CopyToContainer task with CONTAINER:PATH parsing
- ContainerCpCommand: New command registered under 'container cp'
- Localization: Added all user-facing strings
- Tests: Added CLI parsing test cases and updated e2e command list
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new wslc container cp subcommand that uploads a tar archive from stdin into a running container by relaying stdin to Docker’s PUT /containers/{id}/archive endpoint, with supporting wiring through the CLI task/service layers and new localized help/error strings.
Changes:
- Adds a new
container cpcommand and CLI task/service method to parseCONTAINER:PATHand stream stdin into the container. - Extends the Docker HTTP client with a
PutArchiverequest helper used by the session/container implementation. - Updates tests (CLI parsing + e2e container command list) and adds localized user-facing strings for the new command.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/e2e/WSLCE2EContainerTests.cpp | Adds cp to the container subcommand help list assertions. |
| test/windows/wslc/CommandLineTestCases.h | Adds command-line parsing coverage for container cp. |
| src/windows/wslcsession/WSLCContainer.h | Declares new UploadArchive APIs on the container implementation and COM wrapper. |
| src/windows/wslcsession/WSLCContainer.cpp | Implements archive upload by relaying stdin to Docker and handling HTTP responses/errors. |
| src/windows/wslcsession/DockerHTTPClient.h | Declares PutArchive for Docker archive upload. |
| src/windows/wslcsession/DockerHTTPClient.cpp | Implements PutArchive request creation (optionally omitting Content-Length). |
| src/windows/wslc/tasks/ContainerTasks.h | Declares CopyToContainer task entrypoint. |
| src/windows/wslc/tasks/ContainerTasks.cpp | Implements CONTAINER:PATH parsing and stdin validation/size detection. |
| src/windows/wslc/services/ContainerService.h | Adds CopyToContainer service API. |
| src/windows/wslc/services/ContainerService.cpp | Wires CLI service call to COM IWSLCContainer::UploadArchive. |
| src/windows/wslc/commands/ContainerCpCommand.cpp | Adds the new container cp command and argument definitions. |
| src/windows/wslc/commands/ContainerCommand.h | Declares ContainerCpCommand. |
| src/windows/wslc/commands/ContainerCommand.cpp | Registers container cp under the container root command. |
| src/windows/service/inc/wslc.idl | Adds UploadArchive to IWSLCContainer. |
| localization/strings/en-US/Resources.resw | Adds help text and user-facing error messages for container cp. |
dkbennett
left a comment
There was a problem hiding this comment.
Implementation looks good, but I think this should really have at least 1 E2E test verifying the copy actually works as expected.
Can put it in with one of the other container tests so we dont create an entirely new file for it if necessary, but should have something validating we do not regress this command's functionality in the future and have confidence that it is working correctly.
|
Just adding a note that this PR will have a conflict with #40832 due to the change from CreateSession (which is poorly named since it does an open or a create default only) to ResolveSession, depending on which one goes in first. If this one goes in first I can fix it in 40832. |
- Add 5 new CLI parsing test cases for cp in CommandLineTestCases.h - Add RunWslcWithStdinFile helper to pipe file contents to wslc stdin - Add WSLCE2EContainerCpTests with 11 e2e test methods covering: - Help output, missing arguments, invalid target formats - Stdin terminal detection, source validation - Container not found error handling - Successful tar upload to running container with exec verification - Copy to stopped container (Docker PUT /archive filesystem operation) - CreateTestTarFile builds minimal POSIX tar at runtime for tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix ContainerCpDesc to accurately describe stdin-only tar upload (was misleadingly implying bidirectional file copy) - Change PutArchive ContentLength parameter to std::optional<uint64_t> to distinguish 'unknown size' (pipe) from 'known zero size' (empty file) - WSLCContainerImpl::UploadArchive passes std::nullopt when ContentSize is 0 Note: Kept UploadArchive on IWSLCContainer (not a separate IWSLCContainer2) because wslc interfaces are internal — client and server are always deployed together from the same build, so there is no ABI compatibility concern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed code review feedback:
|
- Add Archive argument type in ArgumentDefinitions.h - Add --archive/-a flag to ContainerCpCommand arguments - Add WSLCCLI_ArchiveArgDescription localization string - Add 2 CLI parsing test cases for -a and --archive - Add 2 e2e tests: ArchiveFlag (-a) and ArchiveFlagLongForm (--archive) The flag is accepted for docker cp compatibility. Since the tar stream is relayed directly to Docker's PUT /archive API, uid/gid information from the source tar is always preserved (equivalent to -a behavior). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rue/false) Add ParseBoolValue() helper to ArgumentParser that accepts true/false/1/0 (case-insensitive). Modify ProcessNamedArgument() and ProcessAliasArgument() to parse adjoined boolean values for flag-type arguments. - --flag=true and -f=true set the flag (equivalent to --flag / -f) - --flag=false and -f=false leave the flag unset - Invalid values produce FlagInvalidBooleanError - Add 8 CLI parsing unit test cases for boolean flag values - Add 5 e2e tests for -a/--archive boolean value variants - All 143 unit tests and 18 e2e tests pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Validate DestPath is non-empty in UploadArchive (E_INVALIDARG) - Unify CONTAINER:DEST_PATH to CONTAINER:PATH in long description - Guard FromJson with try-catch for non-JSON error responses - Fix RunWslcWithStdinFile declaration line length (>130 col) - Add comment explaining archive flag is intentional no-op for stdin tar Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinIsTerminal) | ||
| { | ||
| // Running without piped stdin should fail with a terminal error. | ||
| // RunWslcAndRedirectToFile gives the child a real console stdout handle, | ||
| // and since RunWslc pipes NUL to stdin, we use RunWslcAndRedirectToFile | ||
| // with no output path to get a real console for the child. | ||
| const auto result = RunWslcAndRedirectToFile(L"container cp - fakecontainer:/path"); | ||
| VERIFY_IS_TRUE(result.ExitCode.has_value()); | ||
| VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); | ||
| VERIFY_IS_TRUE(result.Stderr.has_value()); | ||
| VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size()); | ||
| } |
| } | ||
| else | ||
| { | ||
| if (isChunked) |
There was a problem hiding this comment.
I recommend using DockerHttpResponseHandle, which already handles this
| { | ||
| auto error = wsl::shared::FromJson<ErrorResponse>(errorJson.c_str()); | ||
|
|
||
| THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, statusCode == 404); |
There was a problem hiding this comment.
Same comments as above: 404 shouldn't be possible here, and the below catch block can be removed
There was a problem hiding this comment.
Same comments as above: 404 shouldn't be possible here, and the below catch block can be removed
Are you saying because container would already be there? /containers/{}/archive could return 404 when the path is not found. The returned error code definitely needs to be changed from WSLC_E_CONTAINER_NOT_FOUND to WSLC_E_CONTAINER_CP_PATH_NOT_FOUND or something
There was a problem hiding this comment.
https://docs.docker.com/reference/api/engine/version/v1.47/#tag/Container/operation/ContainerArchive
("container or path does not exist")
| auto tarCmd = std::format(L"tar.exe -cf \"{}\" -C \"{}\" testfile.txt", TarPath.wstring(), tarSrcDir.wstring()); | ||
| STARTUPINFOW si{sizeof(si)}; | ||
| PROCESS_INFORMATION pi{}; | ||
| THROW_LAST_ERROR_IF(!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)); |
There was a problem hiding this comment.
I recommend using the subprocess helper here as well
|
It would be great to have this finished. This is needed for Aspire/DCP to run on WSL C. |
The function was accidentally deleted during merge conflict resolution. Restores the original implementation from master which launches 'container logs -f' as a subprocess and uses WaitForOutput to poll until the expected string appears in stdout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace hardcoded "No file extracted from container archive" with Localization::WSLCCLI_CpNoFileExtractedError() and add the corresponding entry to Resources.resw. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The synchronous recv workaround exists because overlapped ReadFile on hvsockets has delayed EOF detection (~5s stall), not because of HTTP/1.1 keep-alive. Connection: close is already set and the server closes promptly — synchronous recv sees EOF immediately while overlapped ReadFile takes ~5s to signal completion on hvsockets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| auto colonPos = path.find(L':'); | ||
| if (colonPos == std::wstring::npos || colonPos == 0) | ||
| { | ||
| return false; | ||
| } |
|
|
||
| // Create tar archive — strip trailing separator to avoid the CRT parsing '\"' as an escaped quote | ||
| auto parentDirStr = parentDir; | ||
| while (parentDirStr.size() > 1 && (parentDirStr.back() == L'\\' || parentDirStr.back() == L'/')) |
|
|
||
| // Strip trailing separator to avoid the CRT parsing a trailing '\"' as an escaped quote | ||
| auto targetDir = absTarget.wstring(); | ||
| while (targetDir.size() > 1 && (targetDir.back() == L'\\' || targetDir.back() == L'/')) |
| auto cleanupExtract = wil::scope_exit([&] { std::filesystem::remove_all(extractDir); }); | ||
|
|
||
| auto extractDirStr = extractDir.wstring(); | ||
| while (extractDirStr.size() > 1 && (extractDirStr.back() == L'\\' || extractDirStr.back() == L'/')) |
| // Ensure parent directory of target exists. | ||
| std::error_code dirError; | ||
| std::filesystem::create_directories(absTarget.parent_path(), dirError); | ||
|
|
| // Copy local file to container. | ||
| const auto cpResult = RunWslc(std::format(L"container cp {} {}:/tmp/", localFile.wstring(), WslcContainerName)); | ||
| cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| // Copy from container to local. | ||
| const auto cpResult = | ||
| RunWslc(std::format(L"container cp {}:/tmp/fromcontainer.txt {}", WslcContainerName, downloadDir.wstring())); | ||
| cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| // Copy with explicit trailing backslash in target path. | ||
| auto targetWithBackslash = downloadDir.wstring() + L"\\"; | ||
| const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/bstest.txt {}", WslcContainerName, targetWithBackslash)); | ||
| cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
| // Copy from container to a specific file path (not a directory). | ||
| const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/srcfile.txt {}", WslcContainerName, targetFile.wstring())); | ||
| cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); |
Check the error code from create_directories when ensuring the parent directory exists for a single-file container-to-local copy, matching the pattern used in the directory-target branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| if (boolVal.value()) | ||
| { | ||
| AddFlag(firstArg->Type()); | ||
| } | ||
|
|
||
| return {}; |
| if (boolVal.value()) | ||
| { | ||
| AddFlag(nextArg->Type()); | ||
| } | ||
|
|
||
| return {}; | ||
| } |
| if (boolVal.value()) | ||
| { | ||
| AddFlag(arg.Type()); | ||
| } | ||
|
|
||
| return {}; |
| // Single letter before colon is a Windows drive path | ||
| if (colonPos == 1 && std::isalpha(static_cast<unsigned char>(path[0]))) | ||
| { | ||
| return false; | ||
| } |
| auto extractDir = std::filesystem::path(tempDir) / L"wslc-cp-extract"; | ||
| std::filesystem::create_directories(extractDir); | ||
| auto cleanupExtract = wil::scope_exit([&] { std::filesystem::remove_all(extractDir); }); |
Summary
Implements
wslc container cpfor bidirectional file copy between containers and the local filesystem, plus stdin tar upload into containers, via Docker's archive APIs.Usage
Implementation
Full stack implementation across all layers:
UploadArchiveandDownloadArchivetoIWSLCContainerPutArchiveandGetArchivemethodsSD_SENDon EOF. Download: stream tar from Docker, extract with tar.exeCopyToContainerandCopyFromContainerstatic methodsCopyToContainertask with bidirectional CONTAINER:PATH parsingcontainer cpKey Design Decisions
GetFileSizeExwhen stdin is a file redirect; omits Content-Length header for pipes (relies onSD_SENDshutdown as EOF signal)SO_RCVTIMEOtimeout to avoid HTTP/1.1 keep-alive hang; JSON parse failures fall back to raw body text\"escape bug; root paths preserved withsize() > 1guardTesting
container cp