@samitouri / QOSAMI-WSL / commits / 77382a3a

Add wslc container cp command for tar archive upload (#40835)

* Add wslc container cp command for tar archive upload 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> * Add unit and e2e tests for container cp command - 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> * Address code review comments - 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> * Remove accidentally committed test VHD files * Fix clang format errors * Add -a/--archive flag to container cp command - 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> * Support boolean values for flag arguments (-a=true/false, --archive=true/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> * Address code review feedback: validation, consistency, robustness - 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> * Fix clang format errors * Implement bidirectional container cp (local<->container) Extend wslc container cp to support all copy directions: - Local filesystem to container (file or directory) - Container to local filesystem (file or directory) - Stdin to container (existing behavior) Implementation details: - Add GetArchive to DockerHTTPClient (GET /containers/{id}/archive) - Add DownloadArchive to IWSLCContainer IDL interface - Implement DownloadArchive in WSLCContainerImpl with adaptive handling of chunked vs non-chunked transfer encoding - Add CopyFromContainer to ContainerService - Refactor CopyToContainer task to detect direction via CONTAINER:PATH pattern and dispatch to appropriate code path - Use Windows tar.exe for archive creation/extraction on local paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix trailing backslash tar bug and add container cp e2e tests Fix: Strip trailing path separators before embedding paths in tar.exe command lines. Windows CRT parses a trailing backslash-quote as an escaped quote character, causing tar.exe to receive a mangled directory path and fail with 'could not chdir'. Affects both upload and download. New e2e tests: - Local file to container (auto-tar upload) - Local file not found error - Container to local (download + extract) - Container to local with trailing backslash (regression) - Nonexistent path in container error - Copy from stopped container - Invalid direction (local to local) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix DownloadArchive/Export hang on HTTP error responses For non-200 responses, read the error body synchronously with a receive timeout instead of using the async io.Run() path. The ReadHandle-based approach hangs because HTTP/1.1 keep-alive holds the socket open indefinitely after the error body is sent. Also adds e2e tests for nonexistent file, nonexistent directory, and nonexistent container download scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments for container cp - Fix CreateProcessW error handling: only report 'tar.exe not found' for ERROR_FILE_NOT_FOUND/ERROR_PATH_NOT_FOUND, surface real error otherwise (Copilot feedback) - Add WSLCCLI_CpInvalidSourceError for source path validation instead of reusing the destination error string (Copilot feedback) - Update --archive/-a description to clarify it's accepted for Docker CLI compatibility only (Copilot feedback) - Use std::error_code overloads for filesystem::exists and create_directories to avoid uncontrolled exceptions (Copilot feedback) - Add error message validation to failure tests (OneBlue feedback) - Replace manual tar byte construction with tar.exe in test helper (OneBlue feedback) - Refactor RunWslc to accept optional stdin HANDLE parameter, simplify RunWslcWithStdinFile to delegate to RunWslc (OneBlue feedback) - Fix test comment for SourceNotStdin (now tests source-not-found) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix clang format errors * Ensure custom stdin handle is inheritable in RunWslc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Log setsockopt SO_RCVTIMEO failures instead of ignoring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add try/catch for JSON parsing in Export error path Mirrors DownloadArchive's pattern: if the error body is empty or truncated (e.g. timeout), fall back to surfacing the raw body text instead of letting FromJson throw an unhelpful exception. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix trailing separator stripping for root paths in container→local cp Use size() > 1 guard to preserve root paths like C:\ (matching the upload path logic), preventing tar.exe from receiving invalid 'C:'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename task function CopyToContainer -> ContainerCp The task handles both directions (local→container and container→local), so the old name was misleading. ContainerService::CopyToContainer (upload-only) keeps its name since it genuinely only copies to. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix clang format issues * Fix file-destination semantics for container-to-local copy When the target path does not end with a separator and is not an existing directory, treat it as a file destination instead of a directory. This matches docker cp behavior where 'cp CONTAINER:/file.txt C:\local\out.txt' creates out.txt as a file rather than extracting into a directory named out.txt. Implementation: extract to temp dir, then rename/copy the extracted file to the target path. Falls back to copy+delete if rename fails across volumes. Also adds an e2e test verifying this behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * clang format fixes * fix clang format errors * Restore WaitForContainerOutput lost during merge conflict resolution 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> * Addressed code review comments * Localize hardcoded error string in container cp 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> * Fix comments: explain hvsocket overlapped EOF delay (not keep-alive) 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> * Fix end to end test error message * Propagate create_directories failure in container cp file target path 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> * Revert Export/DownloadArchive error paths to async ReadHandle The synchronous recv workaround for hvsocket EOF delay is no longer needed — the hang is not reproducible. Revert to the simpler async ReadHandle + io.Run() pattern used before this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor container cp to use SubProcess and TempFile Replace raw CreateProcessW + manual temp file management with the repo's SubProcess and filesystem::TempFile utilities: - Local→container: TempFile with DeleteOnClose + InheritHandle, tar writes to stdout redirected to the temp file handle, rewind and upload from the same handle. - Container→local: TempFile for the downloaded archive, SubProcess for tar extraction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix 404 error code in UploadArchive/DownloadArchive to ERROR_PATH_NOT_FOUND Since UploadArchive and DownloadArchive operate on an already-resolved container handle, a 404 from the Docker API means the path was not found inside the container, not that the container itself is missing. Use HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) instead of WSLC_E_CONTAINER_NOT_FOUND. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove redundant try/catch in UploadArchive/DownloadArchive error paths The try/catch(wil::ResultException)/catch(...) pattern was re-throwing WIL exceptions and wrapping JSON parse failures. Since the outer CATCH_RETURN() at the COM boundary handles all exceptions, the inner try/catch is unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate error messages in container cp failure tests Assert specific error substrings instead of just checking stderr is non-empty, so tests catch regressions in error reporting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use exact string equality for exec stdout verification in cp tests Replace substring finds with VERIFY_ARE_EQUAL for exec cat output, ensuring we validate the complete expected output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Replace Sleep(1000) with container exec for file creation in cp tests Instead of running a shell command that creates a file and then sleeping to wait for it, start the container with 'sleep infinity' and use a synchronous 'container exec' to create the test file. This eliminates the race condition and removes all Sleep calls from the tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use current directory for test temp files instead of system temp path Match the pattern used by other e2e tests (ImageBuild, Helpers) which use std::filesystem::current_path() for temporary test artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Eliminate temp directory from container-to-local copy path Replace temp directory approach with streamlined extraction: - Directory target: pipe download directly to tar stdin for extraction (no temp files at all) - File target: single download to temp file with exclusive write handle, validate with tar -t, then extract with tar -x -O to target file. The exclusive handle prevents tampering by other processes. This eliminates the temp directory and its associated ACL security concerns. The file case uses a temp file (safe with exclusive handle) to avoid double-downloading from the container. Add WSLCCLI_CpSourceIsDirectoryError for when a directory source is copied to a file destination. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix e2e test assertions: container exec returns LF not CRLF Container exec cat output uses Unix line endings (\n). The tests were incorrectly asserting \r\n which fails in the TAEF test runner that captures stdout in binary mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix localization comment: remove invalid Locked token The {Locked="wslc container cp"} token must appear in the string value for validation to pass. Remove it since the error message does not contain that text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Read tar -t output via pipe instead of buffering to memory Replace RunAndCaptureOutput() with pipe-based reading for the tar -tf validation step. This avoids buffering the entire listing in memory and enables early exit as soon as a directory entry or second file is detected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add e2e test for stdin copy from pipe (no content-length) Validates that 'wslc container cp - container:/path' works correctly when stdin is a pipe rather than a file, exercising the chunked transfer code path where GetFileSize is not available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix clang format errors * Kill tar -t process after reading instead of waiting for broken pipe Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Pooja Trivedi committed Jul 10, 2026 at 14:12 UTC 77382a3a0634ba214874c79eb737fd054a170d17
22 files changed +1242 -14
.gitignore
+7 -1
@@ -69,7 +69,13 @@ doc/site/
69 directory.build.targets
70 test-storage/
71 *.vhdx
72 +*.vhd
73 *.tar
74 *.etl
75 *.lscache
75 -__pycache__
\ No newline at end of file
76 +__pycache__
77 +deploy-log.txt
78 +test-output*.txt
79 +test-results.txt
80 +testfile.txt
81 +output/
\ No newline at end of file
localization/strings/en-US/Resources.resw
+60
@@ -2259,6 +2259,10 @@ Usage:
2259 <value>Flag argument cannot contain adjoined value: '{}'</value>
2260 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2261 </data>
2262 + <data name="WSLCCLI_FlagInvalidBooleanError" xml:space="preserve">
2263 + <value>Invalid boolean value for flag argument: '{}'. Expected true, false, 1, or 0.</value>
2264 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2265 + </data>
2266 <data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
2267 <value>Found a positional argument when none was expected: '{}'</value>
2268 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
@@ -2483,6 +2487,58 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2487 <value>Write to a file, instead of STDOUT</value>
2488 <comment>{Locked="STDOUT"}Command line arguments, file names and string inserts should not be translated</comment>
2489 </data>
2490 + <data name="WSLCCLI_ContainerCpDesc" xml:space="preserve">
2491 + <value>Copy files between a container and the local filesystem.</value>
2492 + </data>
2493 + <data name="WSLCCLI_ContainerCpLongDesc" xml:space="preserve">
2494 + <value>Copy files between a container and the local filesystem.
2495 +Usage: wslc container cp [OPTIONS] SOURCE DEST
2496 + Local to container: wslc container cp LOCAL_PATH CONTAINER:PATH
2497 + Container to local: wslc container cp CONTAINER:PATH LOCAL_PATH
2498 + Stdin to container: wslc container cp - CONTAINER:PATH</value>
2499 + <comment>{Locked="wslc"}{Locked="container cp"}{Locked="CONTAINER:PATH"}{Locked="LOCAL_PATH"}</comment>
2500 + </data>
2501 + <data name="WSLCCLI_CpSourceArgDescription" xml:space="preserve">
2502 + <value>Source: local path, CONTAINER:PATH, or '-' for stdin</value>
2503 + <comment>{Locked="-"}{Locked="CONTAINER:PATH"}</comment>
2504 + </data>
2505 + <data name="WSLCCLI_CpTargetArgDescription" xml:space="preserve">
2506 + <value>Destination: local path or CONTAINER:PATH</value>
2507 + <comment>{Locked="CONTAINER:PATH"}</comment>
2508 + </data>
2509 + <data name="WSLCCLI_CpInvalidTargetError" xml:space="preserve">
2510 + <value>Invalid destination format. Expected CONTAINER:PATH</value>
2511 + <comment>{Locked="CONTAINER:PATH"}</comment>
2512 + </data>
2513 + <data name="WSLCCLI_CpInvalidSourceError" xml:space="preserve">
2514 + <value>Invalid source format. Expected CONTAINER:PATH</value>
2515 + <comment>{Locked="CONTAINER:PATH"}</comment>
2516 + </data>
2517 + <data name="WSLCCLI_CpSourceNotFoundError" xml:space="preserve">
2518 + <value>Source path not found: {0}</value>
2519 + <comment>{0} is the source path</comment>
2520 + </data>
2521 + <data name="WSLCCLI_CpTarNotFoundError" xml:space="preserve">
2522 + <value>tar.exe not found. Windows tar is required for local file copy.</value>
2523 + <comment>{Locked="tar.exe"}</comment>
2524 + </data>
2525 + <data name="WSLCCLI_CpInvalidDirectionError" xml:space="preserve">
2526 + <value>Invalid copy direction. Use CONTAINER:PATH as either source or destination.</value>
2527 + <comment>{Locked="CONTAINER:PATH"}</comment>
2528 + </data>
2529 + <data name="WSLCCLI_CpStdinIsTerminalError" xml:space="preserve">
2530 + <value>Cannot read tar data from terminal. Pipe a tar archive to stdin.
2531 +Example: tar -cf - files | wslc container cp - CONTAINER:/path</value>
2532 + <comment>{Locked="tar -cf -"}{Locked="wslc container cp"}{Locked="CONTAINER:/path"}</comment>
2533 + </data>
2534 + <data name="WSLCCLI_CpNoFileExtractedError" xml:space="preserve">
2535 + <value>No file extracted from container archive</value>
2536 + <comment>Shown when the container archive download produced no extractable files.</comment>
2537 + </data>
2538 + <data name="WSLCCLI_CpSourceIsDirectoryError" xml:space="preserve">
2539 + <value>Cannot copy a directory to a file path. Use a directory target (with trailing separator) instead.</value>
2540 + <comment>Shown when the user tries to copy a directory from the container to a file destination.</comment>
2541 + </data>
2542 <data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
2543 <value>Inspect a container.</value>
2544 </data>
@@ -2783,6 +2839,10 @@ On first run, creates the file with all settings commented out at their defaults
2839 <data name="WSLCCLI_AllArgDescription" xml:space="preserve">
2840 <value>Show all regardless of state.</value>
2841 </data>
2842 + <data name="WSLCCLI_ArchiveArgDescription" xml:space="preserve">
2843 + <value>Archive mode (accepted for Docker CLI compatibility)</value>
2844 + <comment>{Locked="Docker"}</comment>
2845 + </data>
2846 <data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
2847 <value>Set build-time variables (KEY=VALUE)</value>
2848 <comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
src/windows/service/inc/wslc.idl
+2
@@ -498,6 +498,8 @@ interface IWSLCContainer : IUnknown
498 HRESULT Stats([out] LPSTR* Output);
499 HRESULT ConnectToNetwork([in] const WSLCNetworkConnectionOptions* Options);
500 HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName);
501 + HRESULT UploadArchive([in] WSLCHandle TarHandle, [in, string] LPCSTR DestPath, [in] ULONGLONG ContentSize);
502 + HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle);
503 }
504
505 typedef struct _WSLCDeletedImageInformation
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -34,6 +34,7 @@ Abstract:
34 // clang-format off
35 #define WSLC_ARGUMENTS(_) \
36 _(All, "all", L"a", Kind::Flag, Localization::WSLCCLI_AllArgDescription()) \
37 +_(Archive, "archive", L"a", Kind::Flag, Localization::WSLCCLI_ArchiveArgDescription()) \
38 _(Attach, "attach", L"a", Kind::Flag, Localization::WSLCCLI_AttachArgDescription()) \
39 _(BuildArg, "build-arg", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildArgDescription()) \
40 _(BuildPull, "pull", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_BuildPullArgDescription()) \
src/windows/wslc/arguments/ArgumentParser.cpp
+48 -7
@@ -17,6 +17,7 @@ Abstract:
17 using namespace wsl::shared;
18
19 namespace wsl::windows::wslc {
20 +
21 ParseArgumentsStateMachine::ParseArgumentsStateMachine(
22 Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) :
23 m_invocation(inv),
@@ -124,8 +125,6 @@ void ParseArgumentsStateMachine::AddFlag(ArgType type)
125 if (!ConsumeOverrideIfPresent(type) && m_executionArgs.Contains(type))
126 {
127 // Repeating the same flag on the CLI is a no-op, matching docker.
127 - // TODO: revisit when --flag=value (explicit bool) lands so a mismatch
128 - // between env-preload and CLI-explicit can warn or error.
128 return;
129 }
130
@@ -343,9 +342,24 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
342 return {};
343 }
344
346 - // Boolean flag - add it and process any adjoined flags. Once we have added a
347 - // flag to m_executionArgs for this token, stopOnUnknown no longer applies for
348 - // mid-chain unknowns; the token has already been claimed.
345 + // Boolean flag - check for adjoined boolean value (e.g., -a=true or -a=false).
346 + if (currentPos < currArg.length() && currArg[currentPos] == WSLC_CLI_ARG_SPLIT_CHAR)
347 + {
348 + auto boolVal = string::ParseBool(std::wstring(currArg.substr(currentPos + 1)).c_str());
349 + if (!boolVal.has_value())
350 + {
351 + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
352 + }
353 +
354 + if (boolVal.value())
355 + {
356 + AddFlag(firstArg->Type());
357 + }
358 +
359 + return {};
360 + }
361 +
362 + // No adjoined value — add the flag as true.
363 AddFlag(firstArg->Type());
364
365 // Process remaining adjoined flags
@@ -381,6 +395,23 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume
395 return {};
396 }
397
398 + // Boolean flag in chain — check for adjoined boolean value.
399 + if (nextPos < currArg.length() && currArg[nextPos] == WSLC_CLI_ARG_SPLIT_CHAR)
400 + {
401 + auto boolVal = string::ParseBool(std::wstring(currArg.substr(nextPos + 1)).c_str());
402 + if (!boolVal.has_value())
403 + {
404 + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
405 + }
406 +
407 + if (boolVal.value())
408 + {
409 + AddFlag(nextArg->Type());
410 + }
411 +
412 + return {};
413 + }
414 +
415 AddFlag(nextArg->Type());
416 currentPos = nextPos;
417 }
@@ -430,10 +461,20 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume
461 // Found a match, process by kind.
462 if (arg.Kind() == Kind::Flag)
463 {
433 - // TODO: Consider supporting --flag and --flag=true or --flag=false for bool args.
464 if (hasAdjoinedValue)
465 {
436 - return ArgumentException(Localization::WSLCCLI_FlagContainAdjoinedError(currArg));
466 + auto boolVal = string::ParseBool(std::wstring(argValue).c_str());
467 + if (!boolVal.has_value())
468 + {
469 + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
470 + }
471 +
472 + if (boolVal.value())
473 + {
474 + AddFlag(arg.Type());
475 + }
476 +
477 + return {};
478 }
479
480 AddFlag(arg.Type());
src/windows/wslc/commands/ContainerCommand.cpp
+1
@@ -23,6 +23,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
23 {
24 std::vector<std::unique_ptr<Command>> commands;
25 commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
26 + commands.push_back(std::make_unique<ContainerCpCommand>(FullName()));
27 commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
28 commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
29 commands.push_back(std::make_unique<ContainerExportCommand>(FullName()));
src/windows/wslc/commands/ContainerCommand.h
+15
@@ -62,6 +62,21 @@ protected:
62 void ExecuteInternal(CLIExecutionContext& context) const override;
63 };
64
65 +// Cp Command
66 +struct ContainerCpCommand final : public Command
67 +{
68 + constexpr static std::wstring_view CommandName = L"cp";
69 + ContainerCpCommand(const std::wstring& parent) : Command(CommandName, parent)
70 + {
71 + }
72 + std::vector<Argument> GetArguments() const override;
73 + std::wstring ShortDescription() const override;
74 + std::wstring LongDescription() const override;
75 +
76 +protected:
77 + void ExecuteInternal(CLIExecutionContext& context) const override;
78 +};
79 +
80 // Exec Command
81 struct ContainerExecCommand final : public Command
82 {
src/windows/wslc/commands/ContainerCpCommand.cpp new
+40
@@ -0,0 +1,40 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "ContainerCommand.h"
4 +#include "CLIExecutionContext.h"
5 +#include "ContainerTasks.h"
6 +#include "SessionTasks.h"
7 +#include "Task.h"
8 +
9 +using namespace wsl::windows::wslc::execution;
10 +using namespace wsl::windows::wslc::task;
11 +using namespace wsl::shared;
12 +
13 +namespace wsl::windows::wslc {
14 +// Container Cp Command
15 +std::vector<Argument> ContainerCpCommand::GetArguments() const
16 +{
17 + return {
18 + Argument::Create(ArgType::Archive),
19 + Argument::Create(ArgType::Source, true, std::nullopt, Localization::WSLCCLI_CpSourceArgDescription()),
20 + Argument::Create(ArgType::Target, true, std::nullopt, Localization::WSLCCLI_CpTargetArgDescription()),
21 + };
22 +}
23 +
24 +std::wstring ContainerCpCommand::ShortDescription() const
25 +{
26 + return Localization::WSLCCLI_ContainerCpDesc();
27 +}
28 +
29 +std::wstring ContainerCpCommand::LongDescription() const
30 +{
31 + return Localization::WSLCCLI_ContainerCpLongDesc();
32 +}
33 +
34 +void ContainerCpCommand::ExecuteInternal(CLIExecutionContext& context) const
35 +{
36 + context //
37 + << ResolveSession //
38 + << ContainerCp;
39 +}
40 +} // namespace wsl::windows::wslc
src/windows/wslc/services/ContainerService.cpp
+16
@@ -643,6 +643,22 @@ void ContainerService::Export(Session& session, const std::string& id, HANDLE ou
643 THROW_IF_FAILED(container->Export(ToCOMInputHandle(outputHandle)));
644 }
645
646 +void ContainerService::CopyToContainer(Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize)
647 +{
648 + wil::com_ptr<IWSLCContainer> container;
649 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
650 +
651 + THROW_IF_FAILED(container->UploadArchive(ToCOMInputHandle(inputHandle), destPath.c_str(), contentSize));
652 +}
653 +
654 +void ContainerService::CopyFromContainer(Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle)
655 +{
656 + wil::com_ptr<IWSLCContainer> container;
657 + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
658 +
659 + THROW_IF_FAILED(container->DownloadArchive(srcPath.c_str(), ToCOMInputHandle(outputHandle)));
660 +}
661 +
662 void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail)
663 {
664 wil::com_ptr<IWSLCContainer> container;
src/windows/wslc/services/ContainerService.h
+2
@@ -36,6 +36,8 @@ struct ContainerService
36 static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options);
37 static void Export(models::Session& session, const std::string& id, const std::wstring& outputPath);
38 static void Export(models::Session& session, const std::string& id, HANDLE outputHandle);
39 + static void CopyToContainer(models::Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize);
40 + static void CopyFromContainer(models::Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle);
41 static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id);
42 static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail = 0);
43 static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id);
src/windows/wslc/tasks/ContainerTasks.cpp
+257
@@ -24,6 +24,7 @@ Abstract:
24 #include "TableOutput.h"
25 #include <wil/result_macros.h>
26 #include <wslc_schema.h>
27 +#include <filesystem>
28
29 using namespace wsl::shared;
30 using namespace wsl::windows::common;
@@ -277,6 +278,262 @@ void ExportContainer(CLIExecutionContext& context)
278 }
279 }
280
281 +void ContainerCp(CLIExecutionContext& context)
282 +{
283 + WI_ASSERT(context.Data.Contains(Data::Session));
284 + WI_ASSERT(context.Args.Contains(ArgType::Source));
285 + WI_ASSERT(context.Args.Contains(ArgType::Target));
286 +
287 + auto& session = context.Data.Get<Data::Session>();
288 + const auto& source = context.Args.Get<ArgType::Source>();
289 + const auto& target = context.Args.Get<ArgType::Target>();
290 +
291 + // Determine copy direction by looking for CONTAINER:PATH patterns.
292 + // A single letter before ':' is a Windows drive path (e.g. C:\path), not a container reference.
293 + auto isContainerPath = [](const std::wstring& path) -> bool {
294 + auto colonPos = path.find(L':');
295 + if (colonPos == std::wstring::npos || colonPos == 0)
296 + {
297 + return false;
298 + }
299 +
300 + // Single letter before colon is a Windows drive path
301 + if (colonPos == 1 && std::isalpha(static_cast<unsigned char>(path[0])))
302 + {
303 + return false;
304 + }
305 +
306 + return true;
307 + };
308 +
309 + auto parseContainerPath = [](const std::wstring& path) -> std::pair<std::string, std::string> {
310 + auto colonPos = path.find(L':');
311 + // Skip Windows drive letter if present
312 + if (colonPos == 1 && std::isalpha(static_cast<unsigned char>(path[0])))
313 + {
314 + colonPos = path.find(L':', 2);
315 + }
316 +
317 + auto container = WideToMultiByte(path.substr(0, colonPos));
318 + auto containerPath = WideToMultiByte(path.substr(colonPos + 1));
319 + return {container, containerPath};
320 + };
321 +
322 + bool sourceIsStdin = (source == L"-");
323 + bool sourceIsContainer = !sourceIsStdin && isContainerPath(source);
324 + bool targetIsContainer = isContainerPath(target);
325 +
326 + if ((sourceIsStdin || !sourceIsContainer) && targetIsContainer)
327 + {
328 + // stdin/local → container
329 + auto [containerId, destPath] = parseContainerPath(target);
330 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpInvalidTargetError(), containerId.empty() || destPath.empty());
331 +
332 + if (sourceIsStdin)
333 + {
334 + auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
335 + THROW_HR_WITH_USER_ERROR_IF(
336 + E_INVALIDARG, Localization::WSLCCLI_CpStdinIsTerminalError(), wsl::windows::common::wslutil::IsConsoleHandle(inputHandle));
337 +
338 + LARGE_INTEGER fileSize{};
339 + ULONGLONG contentSize = 0;
340 + if (GetFileSizeEx(inputHandle, &fileSize))
341 + {
342 + contentSize = static_cast<ULONGLONG>(fileSize.QuadPart);
343 + }
344 +
345 + // Note: The --archive/-a flag is accepted for CLI compatibility with docker cp, but is a
346 + // no-op here. Since the tar archive contains uid/gid ownership in its headers, and Docker's
347 + // PUT /archive extracts preserving that metadata.
348 + ContainerService::CopyToContainer(session, containerId, destPath, inputHandle, contentSize);
349 + }
350 + else
351 + {
352 + // Local path → container: create tar from local path using tar.exe
353 + std::error_code fsError;
354 + bool pathExists = std::filesystem::exists(source, fsError);
355 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpSourceNotFoundError(source), fsError || !pathExists);
356 +
357 + auto absPath = std::filesystem::absolute(source);
358 + auto parentDir = absPath.parent_path().wstring();
359 + auto fileName = absPath.filename().wstring();
360 +
361 + // Strip trailing separator to avoid the CRT parsing '\"' as an escaped quote
362 + while (parentDir.size() > 1 && (parentDir.back() == L'\\' || parentDir.back() == L'/'))
363 + {
364 + parentDir.pop_back();
365 + }
366 +
367 + // Create a temp file with DELETE_ON_CLOSE and InheritHandle so tar can write to it via stdout
368 + filesystem::TempFile tarFile(
369 + GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS, filesystem::TempFileFlags::DeleteOnClose | filesystem::TempFileFlags::InheritHandle);
370 +
371 + // Run tar.exe writing to stdout, redirected to our temp file handle
372 + auto tarCmd = std::format(L"tar.exe -cf - -C \"{}\" \"{}\"", parentDir, fileName);
373 + SubProcess process(nullptr, tarCmd.c_str());
374 + process.SetStdHandles(nullptr, tarFile.Handle.get(), nullptr);
375 + auto exitCode = process.Run();
376 + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode);
377 +
378 + // Rewind and get size for upload
379 + LARGE_INTEGER zero{};
380 + THROW_LAST_ERROR_IF(!SetFilePointerEx(tarFile.Handle.get(), zero, nullptr, FILE_BEGIN));
381 +
382 + LARGE_INTEGER fileSize{};
383 + THROW_LAST_ERROR_IF(!GetFileSizeEx(tarFile.Handle.get(), &fileSize));
384 +
385 + ContainerService::CopyToContainer(
386 + session, containerId, destPath, tarFile.Handle.get(), static_cast<ULONGLONG>(fileSize.QuadPart));
387 + }
388 + }
389 + else if (sourceIsContainer && !targetIsContainer)
390 + {
391 + // container → local
392 + auto [containerId, srcPath] = parseContainerPath(source);
393 + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpInvalidSourceError(), containerId.empty() || srcPath.empty());
394 +
395 + auto absTarget = std::filesystem::absolute(target);
396 +
397 + // Determine if target is a directory or a file destination.
398 + // Treat as directory if: ends with separator, or already exists as a directory.
399 + bool targetIsDir = (!target.empty() && (target.back() == L'\\' || target.back() == L'/')) || std::filesystem::is_directory(absTarget);
400 +
401 + if (targetIsDir)
402 + {
403 + // Extract directly into the target directory by piping the download to tar stdin.
404 + std::error_code dirError;
405 + std::filesystem::create_directories(absTarget, dirError);
406 + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(dirError.value()), !!dirError, "Failed to create directory: %ls", absTarget.c_str());
407 +
408 + // Strip trailing separator to avoid the CRT parsing a trailing '\"' as an escaped quote.
409 + auto targetDir = absTarget.wstring();
410 + while (targetDir.size() > 1 && (targetDir.back() == L'\\' || targetDir.back() == L'/'))
411 + {
412 + targetDir.pop_back();
413 + }
414 +
415 + auto [pipeRead, pipeWrite] = OpenAnonymousPipe(0, false, false);
416 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(pipeRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
417 +
418 + auto tarCmd = std::format(L"tar.exe -xf - -C \"{}\"", targetDir);
419 + SubProcess process(nullptr, tarCmd.c_str());
420 + process.SetStdHandles(pipeRead.get(), nullptr, nullptr);
421 + auto processHandle = process.Start();
422 + pipeRead.reset();
423 +
424 + ContainerService::CopyFromContainer(session, containerId, srcPath, pipeWrite.get());
425 + pipeWrite.reset();
426 +
427 + auto exitCode = SubProcess::GetExitCode(processHandle.get());
428 + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode);
429 + }
430 + else
431 + {
432 + // Target is a file path. Download archive once to a temp file (exclusive write handle),
433 + // validate it contains a single file with tar -t, then extract via tar -x -O.
434 +
435 + // Download archive to temp file. FILE_SHARE_READ allows tar to read it while we hold
436 + // the exclusive write handle, preventing other processes from tampering.
437 + filesystem::TempFile tarFile(GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS);
438 +
439 + ContainerService::CopyFromContainer(session, containerId, srcPath, tarFile.Handle.get());
440 +
441 + // Step 1: Pipe tar -t output and read just enough lines to classify the archive.
442 + auto [listStdoutRead, listStdoutWrite] = OpenAnonymousPipe(0, true, false);
443 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(listStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
444 +
445 + auto listCmd = std::format(L"tar.exe -tf \"{}\"", tarFile.Path.wstring());
446 + SubProcess listProcess(nullptr, listCmd.c_str());
447 + listProcess.SetStdHandles(nullptr, listStdoutWrite.get(), nullptr);
448 + auto listHandle = listProcess.Start();
449 + listStdoutWrite.reset();
450 +
451 + // Read lines from tar -t output. We only need to detect:
452 + // - zero entries (empty archive)
453 + // - exactly one non-directory entry (single file)
454 + // - anything else (directory or multi-file)
455 + size_t entryCount = 0;
456 + bool hasDirectory = false;
457 + std::string lineBuffer;
458 + char readBuf[4096];
459 + DWORD bytesRead = 0;
460 + bool done = false;
461 + while (!done && ReadFile(listStdoutRead.get(), readBuf, sizeof(readBuf), &bytesRead, nullptr) && bytesRead > 0)
462 + {
463 + for (DWORD i = 0; i < bytesRead && !done; i++)
464 + {
465 + if (readBuf[i] == '\n')
466 + {
467 + if (!lineBuffer.empty())
468 + {
469 + entryCount++;
470 + if (lineBuffer.back() == '/')
471 + {
472 + hasDirectory = true;
473 + }
474 +
475 + // We can stop early: directory entry or second entry means not a single file.
476 + if (hasDirectory || entryCount > 1)
477 + {
478 + done = true;
479 + }
480 +
481 + lineBuffer.clear();
482 + }
483 + }
484 + else if (readBuf[i] != '\r')
485 + {
486 + lineBuffer.append(1, readBuf[i]);
487 + }
488 + }
489 + }
490 +
491 + // Count trailing line without newline.
492 + if (!done && !lineBuffer.empty())
493 + {
494 + entryCount++;
495 + if (lineBuffer.back() == '/')
496 + {
497 + hasDirectory = true;
498 + }
499 + }
500 +
501 + listStdoutRead.reset();
502 +
503 + // Kill the tar -t process (it may still be writing lines we stopped reading) and wait for it to exit.
504 + TerminateProcess(listHandle.get(), 0);
505 + SubProcess::GetExitCode(listHandle.get());
506 +
507 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::WSLCCLI_CpSourceIsDirectoryError(), hasDirectory || entryCount > 1);
508 +
509 + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::WSLCCLI_CpNoFileExtractedError(), entryCount == 0);
510 +
511 + // Step 2: Extract the single file content directly to the target.
512 + std::error_code dirError;
513 + std::filesystem::create_directories(absTarget.parent_path(), dirError);
514 + THROW_HR_IF_MSG(
515 + HRESULT_FROM_WIN32(dirError.value()), !!dirError, "Failed to create directory: %ls", absTarget.parent_path().c_str());
516 +
517 + wil::unique_hfile targetFile(CreateFileW(absTarget.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
518 + THROW_LAST_ERROR_IF(!targetFile);
519 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(targetFile.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
520 +
521 + auto extractCmd = std::format(L"tar.exe -xf \"{}\" -O", tarFile.Path.wstring());
522 + SubProcess extractProcess(nullptr, extractCmd.c_str());
523 + extractProcess.SetStdHandles(nullptr, targetFile.get(), nullptr);
524 + auto extractHandle = extractProcess.Start();
525 + targetFile.reset();
526 +
527 + auto extractExitCode = SubProcess::GetExitCode(extractHandle.get());
528 + THROW_HR_IF_MSG(E_FAIL, extractExitCode != 0, "tar.exe -x -O exited with code %u", extractExitCode);
529 + }
530 + }
531 + else
532 + {
533 + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_CpInvalidDirectionError());
534 + }
535 +}
536 +
537 void ListContainers(CLIExecutionContext& context)
538 {
539 WI_ASSERT(context.Data.Contains(Data::Containers));
src/windows/wslc/tasks/ContainerTasks.h
+1
@@ -31,6 +31,7 @@ private:
31 };
32
33 void CreateContainer(CLIExecutionContext& context);
34 +void ContainerCp(CLIExecutionContext& context);
35 void ExecContainer(CLIExecutionContext& context);
36 void ExportContainer(CLIExecutionContext& context);
37 void GetContainers(CLIExecutionContext& context);
src/windows/wslcsession/DockerHTTPClient.cpp
+25
@@ -399,6 +399,31 @@ std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::ExportContainer(const
399 return {response.result_int(), std::move(socket)};
400 }
401
402 +std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PutArchive(
403 + const std::string& ContainerID, const std::string& Path, std::optional<uint64_t> ContentLength)
404 +{
405 + auto url = URL::Create("/containers/{}/archive", ContainerID);
406 + url.SetParameter("path", Path);
407 +
408 + std::map<std::string, std::string> headers = {{"Content-Type", "application/x-tar"}};
409 + if (ContentLength.has_value())
410 + {
411 + headers["Content-Length"] = std::to_string(ContentLength.value());
412 + }
413 +
414 + return SendRequestImpl(verb::put, url, {}, headers);
415 +}
416 +
417 +std::tuple<uint32_t, wil::unique_socket, bool> DockerHTTPClient::GetArchive(const std::string& ContainerID, const std::string& Path)
418 +{
419 + auto url = URL::Create("/containers/{}/archive", ContainerID);
420 + url.SetParameter("path", Path);
421 +
422 + auto [response, socket] = SendRequest(verb::get, url, {}, {});
423 +
424 + return {response.result_int(), std::move(socket), response.chunked()};
425 +}
426 +
427 docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::CreateVolume& Request)
428 {
429 return Transaction<docker_schema::CreateVolume>(verb::post, URL::Create("/volumes/create"), Request);
src/windows/wslcsession/DockerHTTPClient.h
+2
@@ -137,6 +137,8 @@ public:
137 void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
138 wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail);
139 std::pair<uint32_t, wil::unique_socket> ExportContainer(const std::string& ContainerID);
140 + std::unique_ptr<HTTPRequestContext> PutArchive(const std::string& ContainerID, const std::string& Path, std::optional<uint64_t> ContentLength);
141 + std::tuple<uint32_t, wil::unique_socket, bool> GetArchive(const std::string& ContainerID, const std::string& Path);
142 common::docker_schema::PruneContainerResult PruneContainers(const std::map<std::string, std::vector<std::string>>& filters = {});
143
144 // Volume management.
src/windows/wslcsession/WSLCContainer.cpp
+126
@@ -1139,6 +1139,114 @@ void WSLCContainerImpl::Export(WSLCHandle OutHandle) const
1139 }
1140 }
1141
1142 +void WSLCContainerImpl::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const
1143 +{
1144 + auto lock = m_lock.lock_shared();
1145 +
1146 + std::optional<uint64_t> contentLength;
1147 + if (ContentSize > 0)
1148 + {
1149 + contentLength = ContentSize;
1150 + }
1151 +
1152 + auto requestContext = m_dockerClient.PutArchive(m_id, DestPath, contentLength);
1153 +
1154 + auto userHandle = m_wslcSession.OpenUserHandle(TarHandle);
1155 +
1156 + auto io = m_wslcSession.CreateIOContext();
1157 +
1158 + std::optional<std::string> pendingErrorJson;
1159 + unsigned int httpStatusCode = 0;
1160 + auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
1161 + WSL_LOG("ContainerUploadArchiveHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
1162 +
1163 + httpStatusCode = response.result_int();
1164 + if (httpStatusCode != 200)
1165 + {
1166 + pendingErrorJson.emplace();
1167 + }
1168 + };
1169 +
1170 + auto onProgress = [&](const gsl::span<char>& buffer) {
1171 + if (pendingErrorJson.has_value())
1172 + {
1173 + pendingErrorJson->append(buffer.data(), buffer.size());
1174 + }
1175 + };
1176 +
1177 + // Shutdown the Docker stream's write side when the input is fully read.
1178 + auto onInputComplete = [socket = requestContext->stream.native_handle()]() {
1179 + LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR);
1180 + };
1181 +
1182 + io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(
1183 + HandleWrapper{userHandle.Get(), std::move(onInputComplete)}, HandleWrapper{requestContext->stream.native_handle()}));
1184 +
1185 + io.AddHandle(
1186 + std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(*requestContext, std::move(onHttpResponse), std::move(onProgress)),
1187 + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1188 +
1189 + // Release the lock so the container can still be interacted with while the upload is in progress.
1190 + lock.reset();
1191 +
1192 + io.Run({});
1193 +
1194 + if (pendingErrorJson.has_value())
1195 + {
1196 + auto error = wsl::shared::FromJson<ErrorResponse>(pendingErrorJson->c_str());
1197 +
1198 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), error.message, httpStatusCode == 404);
1199 + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message);
1200 + }
1201 +}
1202 +
1203 +void WSLCContainerImpl::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const
1204 +{
1205 + auto lock = m_lock.lock_shared();
1206 +
1207 + auto [statusCode, socket, isChunked] = m_dockerClient.GetArchive(m_id, SrcPath);
1208 +
1209 + auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
1210 +
1211 + wsl::windows::common::io::MultiHandleWait io = m_wslcSession.CreateIOContext();
1212 +
1213 + std::string errorJson;
1214 +
1215 + if (statusCode != 200)
1216 + {
1217 + io.AddHandle(std::make_unique<ReadHandle>(HandleWrapper{std::move(socket)}, [&](const gsl::span<char>& buffer) {
1218 + errorJson.append(buffer.data(), buffer.size());
1219 + }));
1220 + }
1221 + else
1222 + {
1223 + if (isChunked)
1224 + {
1225 + io.AddHandle(
1226 + std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(HandleWrapper{std::move(socket)}, userHandle.Get()),
1227 + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1228 + }
1229 + else
1230 + {
1231 + io.AddHandle(
1232 + std::make_unique<RelayHandle<ReadHandle>>(HandleWrapper{std::move(socket)}, userHandle.Get()),
1233 + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1234 + }
1235 + }
1236 +
1237 + lock.reset();
1238 +
1239 + io.Run({});
1240 +
1241 + if (statusCode != 200)
1242 + {
1243 + auto error = wsl::shared::FromJson<ErrorResponse>(errorJson.c_str());
1244 +
1245 + THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), error.message, statusCode == 404);
1246 + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message);
1247 + }
1248 +}
1249 +
1250 void WSLCContainerImpl::GetState(WSLCContainerState* Result)
1251 {
1252 auto lock = m_lock.lock_shared();
@@ -2436,6 +2544,24 @@ HRESULT WSLCContainer::Export(WSLCHandle TarHandle)
2544 return CallImpl(&WSLCContainerImpl::Export, TarHandle);
2545 }
2546
2547 +HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize)
2548 +{
2549 + WSLCExecutionContext context(&m_session);
2550 +
2551 + RETURN_HR_IF(E_POINTER, DestPath == nullptr);
2552 + RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0');
2553 + return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize);
2554 +}
2555 +
2556 +HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle)
2557 +{
2558 + WSLCExecutionContext context(&m_session);
2559 +
2560 + RETURN_HR_IF(E_POINTER, SrcPath == nullptr);
2561 + RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0');
2562 + return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle);
2563 +}
2564 +
2565 HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail)
2566 try
2567 {
src/windows/wslcsession/WSLCContainer.h
+4
@@ -101,6 +101,8 @@ public:
101 void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill);
102 void Delete(WSLCDeleteFlags Flags);
103 void Export(WSLCHandle TarHandle) const;
104 + void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const;
105 + void DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const;
106 void GetStateChangedAt(_Out_ ULONGLONG* StateChangedAt);
107 void GetCreatedAt(_Out_ ULONGLONG* CreatedAt);
108 void GetState(_Out_ WSLCContainerState* State);
@@ -237,6 +239,8 @@ public:
239 IFACEMETHOD(Kill)(_In_ WSLCSignal Signal) override;
240 IFACEMETHOD(Delete)(WSLCDeleteFlags Flags) override;
241 IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override;
242 + IFACEMETHOD(UploadArchive)(_In_ WSLCHandle TarHandle, _In_ LPCSTR DestPath, _In_ ULONGLONG ContentSize) override;
243 + IFACEMETHOD(DownloadArchive)(_In_ LPCSTR SrcPath, _In_ WSLCHandle OutHandle) override;
244 IFACEMETHOD(GetState)(_Out_ WSLCContainerState* State) override;
245 IFACEMETHOD(GetInitProcess)(_Out_ IWSLCProcess** process) override;
246 IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override;
test/windows/wslc/CommandLineTestCases.h
+22
@@ -203,6 +203,28 @@ COMMAND_LINE_TEST_CASE(L"container export -o foo cont1", L"export", true)
203 COMMAND_LINE_TEST_CASE(L"container export cont1 --output foo", L"export", true)
204 COMMAND_LINE_TEST_CASE(L"container export cont1 -o foo", L"export", true)
205
206 +// Cp command tests
207 +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/path", L"cp", true)
208 +COMMAND_LINE_TEST_CASE(L"container cp - mycontainer:/usr/local/etc", L"cp", true)
209 +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/", L"cp", true)
210 +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/path/to/deep/dir", L"cp", true)
211 +COMMAND_LINE_TEST_CASE(L"container cp somefile cont1:/path", L"cp", true)
212 +COMMAND_LINE_TEST_CASE(L"container cp -a - cont1:/path", L"cp", true)
213 +COMMAND_LINE_TEST_CASE(L"container cp --archive - cont1:/path", L"cp", true)
214 +COMMAND_LINE_TEST_CASE(L"container cp -a=true - cont1:/path", L"cp", true)
215 +COMMAND_LINE_TEST_CASE(L"container cp -a=false - cont1:/path", L"cp", true)
216 +COMMAND_LINE_TEST_CASE(L"container cp --archive=true - cont1:/path", L"cp", true)
217 +COMMAND_LINE_TEST_CASE(L"container cp --archive=false - cont1:/path", L"cp", true)
218 +COMMAND_LINE_TEST_CASE(L"container cp -a=1 - cont1:/path", L"cp", true)
219 +COMMAND_LINE_TEST_CASE(L"container cp -a=0 - cont1:/path", L"cp", true)
220 +COMMAND_LINE_TEST_CASE(L"container cp -a=invalid - cont1:/path", L"cp", false)
221 +COMMAND_LINE_TEST_CASE(L"container cp --archive=invalid - cont1:/path", L"cp", false)
222 +COMMAND_LINE_TEST_CASE(L"container cp", L"cp", false)
223 +COMMAND_LINE_TEST_CASE(L"container cp -", L"cp", false)
224 +COMMAND_LINE_TEST_CASE(L"container cp - ", L"cp", false)
225 +COMMAND_LINE_TEST_CASE(L"container cp --unknown - cont1:/path", L"cp", false)
226 +COMMAND_LINE_TEST_CASE(L"container cp --help", L"cp", true)
227 +
228 // Logs command
229 COMMAND_LINE_TEST_CASE(L"logs cont1", L"logs", true)
230 COMMAND_LINE_TEST_CASE(L"container logs cont1", L"logs", true)
test/windows/wslc/ParserTestCases.h
+2 -1
@@ -164,7 +164,8 @@ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg cont1)") \
164 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -i cont1 cont2)") \
165 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \
166 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \
167 -WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \
167 +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose=false cont2)") \
168 +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=invalid cont2)") \
169 WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)") \
170 \
171 /* Root-level globals: strict optionsOnly parsing. Stops cleanly at the first \
test/windows/wslc/e2e/WSLCE2EContainerCpTests.cpp new
+577
@@ -0,0 +1,577 @@
1 +// Copyright (C) Microsoft Corporation. All rights reserved.
2 +
3 +#include "precomp.h"
4 +#include "windows/Common.h"
5 +#include "WSLCExecutor.h"
6 +#include "WSLCE2EHelpers.h"
7 +
8 +namespace WSLCE2ETests {
9 +using namespace wsl::shared;
10 +
11 +class WSLCE2EContainerCpTests
12 +{
13 + WSLC_TEST_CLASS(WSLCE2EContainerCpTests)
14 +
15 + TEST_CLASS_SETUP(ClassSetup)
16 + {
17 + EnsureImageIsLoaded(DebianImage);
18 + return true;
19 + }
20 +
21 + TEST_CLASS_CLEANUP(ClassCleanup)
22 + {
23 + EnsureContainerDoesNotExist(WslcContainerName);
24 + EnsureImageIsDeleted(DebianImage);
25 + return true;
26 + }
27 +
28 + TEST_METHOD_SETUP(MethodSetup)
29 + {
30 + EnsureContainerDoesNotExist(WslcContainerName);
31 + TarPath = std::filesystem::current_path() / L"wslc-cp-test.tar";
32 + DeleteFileW(TarPath.c_str());
33 + return true;
34 + }
35 +
36 + TEST_METHOD_CLEANUP(MethodCleanup)
37 + {
38 + EnsureContainerDoesNotExist(WslcContainerName);
39 + DeleteFileW(TarPath.c_str());
40 + return true;
41 + }
42 +
43 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_HelpCommand)
44 + {
45 + auto result = RunWslc(L"container cp --help");
46 + VERIFY_IS_TRUE(result.ExitCode.has_value());
47 + VERIFY_ARE_EQUAL(0u, result.ExitCode.value());
48 + VERIFY_IS_TRUE(result.Stdout.has_value());
49 + VERIFY_IS_TRUE(result.Stdout->find(L"container cp") != std::wstring::npos);
50 + VERIFY_IS_TRUE(result.Stdout->find(L"source") != std::wstring::npos);
51 + VERIFY_IS_TRUE(result.Stdout->find(L"target") != std::wstring::npos);
52 + VERIFY_IS_TRUE(result.Stderr.has_value());
53 + VERIFY_ARE_EQUAL(L"", result.Stderr.value());
54 + }
55 +
56 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingBothArgs)
57 + {
58 + const auto result = RunWslc(L"container cp");
59 + VERIFY_IS_TRUE(result.ExitCode.has_value());
60 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
61 + VERIFY_IS_TRUE(result.Stderr.has_value());
62 + VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'source'") != std::wstring::npos);
63 + }
64 +
65 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingTarget)
66 + {
67 + const auto result = RunWslc(L"container cp -");
68 + VERIFY_IS_TRUE(result.ExitCode.has_value());
69 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
70 + VERIFY_IS_TRUE(result.Stderr.has_value());
71 + VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'target'") != std::wstring::npos);
72 + }
73 +
74 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinIsTerminal)
75 + {
76 + // Running without piped stdin should fail with a terminal error.
77 + // RunWslcAndRedirectToFile gives the child a real console stdout handle,
78 + // and since RunWslc pipes NUL to stdin, we use RunWslcAndRedirectToFile
79 + // with no output path to get a real console for the child.
80 + const auto result = RunWslcAndRedirectToFile(L"container cp - fakecontainer:/path");
81 + VERIFY_IS_TRUE(result.ExitCode.has_value());
82 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
83 + VERIFY_IS_TRUE(result.Stderr.has_value());
84 + VERIFY_IS_TRUE(result.Stderr->find(L"Cannot read tar data from terminal") != std::wstring::npos);
85 + }
86 +
87 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_SourceNotStdin)
88 + {
89 + // A local file that doesn't exist should fail with a "source not found" error.
90 + // Use RunWslc which pipes NUL to stdin (not a terminal).
91 + const auto result = RunWslc(L"container cp somefile.tar fakecontainer:/path");
92 + VERIFY_IS_TRUE(result.ExitCode.has_value());
93 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
94 + VERIFY_IS_TRUE(result.Stderr.has_value());
95 + VERIFY_IS_TRUE(result.Stderr->find(L"Source path not found: somefile.tar") != std::wstring::npos);
96 + }
97 +
98 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_NoColon)
99 + {
100 + // Target must be CONTAINER:PATH — missing colon should fail.
101 + const auto result = RunWslc(L"container cp - fakecontainer_nopath");
102 + VERIFY_IS_TRUE(result.ExitCode.has_value());
103 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
104 + VERIFY_IS_TRUE(result.Stderr.has_value());
105 + VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos);
106 + }
107 +
108 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyContainer)
109 + {
110 + // Target with empty container name (:path) should fail.
111 + const auto result = RunWslc(L"container cp - :/path");
112 + VERIFY_IS_TRUE(result.ExitCode.has_value());
113 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
114 + VERIFY_IS_TRUE(result.Stderr.has_value());
115 + VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos);
116 + }
117 +
118 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyPath)
119 + {
120 + // Target with empty path (container:) should fail.
121 + const auto result = RunWslc(L"container cp - fakecontainer:");
122 + VERIFY_IS_TRUE(result.ExitCode.has_value());
123 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
124 + VERIFY_IS_TRUE(result.Stderr.has_value());
125 + VERIFY_IS_TRUE(result.Stderr->find(L"Invalid destination format") != std::wstring::npos);
126 + }
127 +
128 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerNotFound)
129 + {
130 + // Create a valid tar file to pipe in, but target a nonexistent container.
131 + CreateTestTarFile();
132 +
133 + const auto result = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", InvalidContainerName), TarPath);
134 + VERIFY_IS_TRUE(result.ExitCode.has_value());
135 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
136 + VERIFY_IS_TRUE(result.Stderr.has_value());
137 + VERIFY_IS_TRUE(result.Stderr->find(L"not found") != std::wstring::npos);
138 + }
139 +
140 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Success)
141 + {
142 + // Create and start a container with sleep infinity to keep it running.
143 + auto runResult =
144 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
145 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
146 +
147 + // Create a test tar file with a known file inside.
148 + CreateTestTarFile();
149 +
150 + // Cp the tar into the running container.
151 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath);
152 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
153 +
154 + // Verify the file was copied by running a command inside the container.
155 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
156 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
157 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
158 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
159 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
160 + }
161 +
162 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinFromPipe)
163 + {
164 + // Validate that cp works when stdin is a pipe (no content-length available),
165 + // as opposed to a file where GetFileSize can determine the length upfront.
166 + auto runResult =
167 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
168 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
169 +
170 + CreateTestTarFile();
171 +
172 + // Open the tar file and relay its content into a pipe on a background thread.
173 + wil::unique_hfile tarFile(
174 + CreateFileW(TarPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
175 + THROW_LAST_ERROR_IF(!tarFile);
176 +
177 + auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, false);
178 +
179 + std::thread relayThread([&tarFile, &pipeWrite] {
180 + wsl::windows::common::relay::InterruptableRelay(tarFile.get(), pipeWrite.get());
181 + pipeWrite.reset();
182 + });
183 +
184 + // Pass the pipe read end as stdin — wslc cannot determine content-length from a pipe.
185 + const auto cpResult = RunWslc(std::format(L"container cp - {}:/tmp", WslcContainerName), ElevationType::Elevated, pipeRead.get());
186 + relayThread.join();
187 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
188 +
189 + // Verify the file was copied.
190 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
191 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
192 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
193 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
194 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
195 + }
196 +
197 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ToStoppedContainer)
198 + {
199 + // Create a stopped container (not started).
200 + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
201 + createResult.Verify({.Stderr = L"", .ExitCode = 0});
202 +
203 + // Create a test tar file.
204 + CreateTestTarFile();
205 +
206 + // Attempt to cp into the stopped container — Docker should accept this.
207 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath);
208 +
209 + // Docker's PUT /archive works on stopped containers too.
210 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
211 + }
212 +
213 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlag)
214 + {
215 + // Create and start a container.
216 + auto runResult =
217 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
218 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
219 +
220 + CreateTestTarFile();
221 +
222 + // Cp with -a flag (archive mode preserves uid/gid).
223 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a - {}:/tmp", WslcContainerName), TarPath);
224 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
225 +
226 + // Verify the file was copied.
227 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
228 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
229 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
230 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
231 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
232 + }
233 +
234 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagLongForm)
235 + {
236 + // Create and start a container.
237 + auto runResult =
238 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
239 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
240 +
241 + CreateTestTarFile();
242 +
243 + // Cp with --archive flag (long form).
244 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive - {}:/tmp", WslcContainerName), TarPath);
245 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
246 +
247 + // Verify the file was copied.
248 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
249 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
250 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
251 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
252 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
253 + }
254 +
255 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsTrue)
256 + {
257 + // Test -a=true syntax.
258 + auto runResult =
259 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
260 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
261 +
262 + CreateTestTarFile();
263 +
264 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=true - {}:/tmp", WslcContainerName), TarPath);
265 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
266 +
267 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
268 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
269 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
270 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
271 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
272 + }
273 +
274 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsFalse)
275 + {
276 + // Test -a=false syntax (no archive mode).
277 + auto runResult =
278 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
279 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
280 +
281 + CreateTestTarFile();
282 +
283 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=false - {}:/tmp", WslcContainerName), TarPath);
284 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
285 +
286 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
287 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
288 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
289 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
290 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
291 + }
292 +
293 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsTrue)
294 + {
295 + // Test --archive=true syntax.
296 + auto runResult =
297 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
298 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
299 +
300 + CreateTestTarFile();
301 +
302 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=true - {}:/tmp", WslcContainerName), TarPath);
303 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
304 +
305 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
306 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
307 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
308 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
309 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
310 + }
311 +
312 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsFalse)
313 + {
314 + // Test --archive=false syntax (no archive mode).
315 + auto runResult =
316 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
317 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
318 +
319 + CreateTestTarFile();
320 +
321 + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=false - {}:/tmp", WslcContainerName), TarPath);
322 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
323 +
324 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
325 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
326 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
327 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
328 + VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
329 + }
330 +
331 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagInvalidValue)
332 + {
333 + // Test -a=invalid should fail with an error.
334 + CreateTestTarFile();
335 +
336 + const auto result = RunWslcWithStdinFile(std::format(L"container cp -a=invalid - {}:/tmp", WslcContainerName), TarPath);
337 + VERIFY_IS_TRUE(result.ExitCode.has_value());
338 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
339 + VERIFY_IS_TRUE(result.Stderr.has_value());
340 + VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
341 + }
342 +
343 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileToContainer)
344 + {
345 + // Create and start a container.
346 + auto runResult =
347 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
348 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
349 +
350 + // Create a local file to copy.
351 + auto localFile = std::filesystem::current_path() / L"wslc-cp-local-test.txt";
352 + auto cleanupLocal = wil::scope_exit([&] { DeleteFileW(localFile.c_str()); });
353 +
354 + {
355 + wil::unique_hfile file(CreateFileW(localFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
356 + THROW_LAST_ERROR_IF(!file);
357 + const std::string content = "local-file-content\n";
358 + DWORD written = 0;
359 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
360 + }
361 +
362 + // Copy local file to container.
363 + const auto cpResult = RunWslc(std::format(L"container cp {} {}:/tmp/", localFile.wstring(), WslcContainerName));
364 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
365 +
366 + // Verify the file was copied.
367 + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/wslc-cp-local-test.txt", WslcContainerName));
368 + VERIFY_IS_TRUE(execResult.ExitCode.has_value());
369 + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
370 + VERIFY_IS_TRUE(execResult.Stdout.has_value());
371 + VERIFY_ARE_EQUAL(L"local-file-content\n", execResult.Stdout.value());
372 + }
373 +
374 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileNotFound)
375 + {
376 + // Copying a nonexistent local file should fail.
377 + const auto result = RunWslc(std::format(L"container cp C:\\nonexistent_wslc_test_file.txt {}:/tmp/", WslcContainerName));
378 + VERIFY_IS_TRUE(result.ExitCode.has_value());
379 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
380 + VERIFY_IS_TRUE(result.Stderr.has_value());
381 + VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
382 + }
383 +
384 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal)
385 + {
386 + // Create and start a container.
387 + auto runResult =
388 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
389 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
390 +
391 + // Create a file inside the container using exec.
392 + auto execResult =
393 + RunWslc(std::format(L"container exec {} sh -c \"echo container-content > /tmp/fromcontainer.txt\"", WslcContainerName));
394 + execResult.Verify({.ExitCode = 0});
395 +
396 + // Create a directory to download into.
397 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-download-test";
398 + std::filesystem::create_directories(downloadDir);
399 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
400 +
401 + // Copy from container to local.
402 + const auto cpResult =
403 + RunWslc(std::format(L"container cp {}:/tmp/fromcontainer.txt {}", WslcContainerName, downloadDir.wstring()));
404 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
405 +
406 + // Verify the file was extracted locally.
407 + auto extractedFile = downloadDir / L"fromcontainer.txt";
408 + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
409 + }
410 +
411 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_TrailingBackslash)
412 + {
413 + // Regression test: trailing backslash on local path should not break tar extraction.
414 + auto runResult =
415 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
416 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
417 +
418 + // Create the file inside the container using exec.
419 + auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo backslash-test > /tmp/bstest.txt\"", WslcContainerName));
420 + execResult.Verify({.ExitCode = 0});
421 +
422 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-backslash-test";
423 + std::filesystem::create_directories(downloadDir);
424 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
425 +
426 + // Copy with explicit trailing backslash in target path.
427 + auto targetWithBackslash = downloadDir.wstring() + L"\\";
428 + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/bstest.txt {}", WslcContainerName, targetWithBackslash));
429 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
430 +
431 + auto extractedFile = downloadDir / L"bstest.txt";
432 + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
433 + }
434 +
435 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_FileDestination)
436 + {
437 + // When the local target doesn't end with a separator and isn't an existing directory,
438 + // it should be treated as a file destination (matching docker cp semantics).
439 + auto runResult =
440 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
441 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
442 +
443 + // Create the file inside the container using exec.
444 + auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo file-dest-test > /tmp/srcfile.txt\"", WslcContainerName));
445 + execResult.Verify({.ExitCode = 0});
446 +
447 + auto targetFile = std::filesystem::current_path() / L"wslc-cp-file-dest-test" / L"renamed.txt";
448 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(targetFile.parent_path()); });
449 +
450 + // Copy from container to a specific file path (not a directory).
451 + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/srcfile.txt {}", WslcContainerName, targetFile.wstring()));
452 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
453 +
454 + // The file should exist at the exact target path, not inside a directory named "renamed.txt".
455 + VERIFY_IS_TRUE(std::filesystem::exists(targetFile));
456 + VERIFY_IS_TRUE(std::filesystem::is_regular_file(targetFile));
457 + }
458 +
459 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentPath)
460 + {
461 + // Regression test: DownloadArchive used to hang on 404 because the HTTP/1.1 keep-alive
462 + // socket never closed. The fix shuts down the socket so the read sees EOF immediately.
463 + auto runResult =
464 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
465 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
466 +
467 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-test";
468 + std::filesystem::create_directories(downloadDir);
469 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
470 +
471 + const auto cpResult = RunWslc(std::format(L"container cp {}:/nonexistent/file.txt {}", WslcContainerName, downloadDir.wstring()));
472 + VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
473 + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
474 + VERIFY_IS_TRUE(cpResult.Stderr.has_value());
475 + VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size());
476 + }
477 +
478 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentDir)
479 + {
480 + // Regression test: DownloadArchive 404 for a nonexistent directory path.
481 + auto runResult =
482 + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
483 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
484 +
485 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-dir-test";
486 + std::filesystem::create_directories(downloadDir);
487 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
488 +
489 + const auto cpResult = RunWslc(std::format(L"container cp {}:/no/such/directory/ {}", WslcContainerName, downloadDir.wstring()));
490 + VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
491 + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
492 + VERIFY_IS_TRUE(cpResult.Stderr.has_value());
493 + VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size());
494 + }
495 +
496 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Download_NonexistentContainer)
497 + {
498 + // Regression test: DownloadArchive error path when the container itself doesn't exist.
499 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-no-container-test";
500 + std::filesystem::create_directories(downloadDir);
501 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
502 +
503 + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/file.txt {}", InvalidContainerName, downloadDir.wstring()));
504 + VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
505 + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
506 + VERIFY_IS_TRUE(cpResult.Stderr.has_value());
507 + VERIFY_IS_TRUE(cpResult.Stderr->find(L"not found") != std::wstring::npos);
508 + }
509 +
510 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_FromStoppedContainer)
511 + {
512 + // Create a container, put a file in it, stop it, then copy out.
513 + auto runResult = RunWslc(std::format(
514 + L"container run --name {} {} sh -c \"echo stopped-content > /tmp/stopped.txt\"", WslcContainerName, DebianImage.NameAndTag()));
515 + runResult.Verify({.Stderr = L"", .ExitCode = 0});
516 +
517 + // Container has exited (ran a one-shot command). Copy from the stopped container.
518 + auto downloadDir = std::filesystem::current_path() / L"wslc-cp-stopped-test";
519 + std::filesystem::create_directories(downloadDir);
520 + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
521 +
522 + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/stopped.txt {}", WslcContainerName, downloadDir.wstring()));
523 + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
524 +
525 + auto extractedFile = downloadDir / L"stopped.txt";
526 + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
527 + }
528 +
529 + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidDirection_LocalToLocal)
530 + {
531 + // local → local is not a valid copy direction.
532 + const auto result = RunWslc(L"container cp C:\\temp\\somefile.txt C:\\temp\\dest\\");
533 + VERIFY_IS_TRUE(result.ExitCode.has_value());
534 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
535 + VERIFY_IS_TRUE(result.Stderr.has_value());
536 + VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
537 + }
538 +
539 +private:
540 + const std::wstring WslcContainerName = L"wslc-test-container-cp";
541 + const std::wstring InvalidContainerName = L"wslc-nonexistent-container-for-cp";
542 + const TestImage& DebianImage = DebianTestImage();
543 +
544 + std::filesystem::path TarPath{};
545 +
546 + // Creates a tar file containing a single text file using tar.exe.
547 + void CreateTestTarFile()
548 + {
549 + // Create a directory with a test file to archive.
550 + auto tarSrcDir = std::filesystem::current_path() / L"wslc-cp-tar-src";
551 + std::filesystem::create_directories(tarSrcDir);
552 + auto cleanupSrcDir = wil::scope_exit([&] { std::filesystem::remove_all(tarSrcDir); });
553 +
554 + auto testFile = tarSrcDir / L"testfile.txt";
555 + {
556 + wil::unique_hfile file(CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
557 + THROW_LAST_ERROR_IF(!file);
558 + const std::string content = "wslc-cp-test-content\n";
559 + DWORD written = 0;
560 + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
561 + }
562 +
563 + // Use tar.exe to create the archive.
564 + auto tarCmd = std::format(L"tar.exe -cf \"{}\" -C \"{}\" testfile.txt", TarPath.wstring(), tarSrcDir.wstring());
565 + STARTUPINFOW si{sizeof(si)};
566 + PROCESS_INFORMATION pi{};
567 + THROW_LAST_ERROR_IF(!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi));
568 + wil::unique_handle tarProcess(pi.hProcess);
569 + wil::unique_handle tarThread(pi.hThread);
570 + WaitForSingleObject(tarProcess.get(), INFINITE);
571 +
572 + DWORD exitCode = 0;
573 + GetExitCodeProcess(tarProcess.get(), &exitCode);
574 + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode);
575 + }
576 +};
577 +} // namespace WSLCE2ETests
test/windows/wslc/e2e/WSLCE2EContainerTests.cpp
+1
@@ -70,6 +70,7 @@ private:
70 {
71 std::vector<std::pair<std::wstring_view, std::wstring>> entries = {
72 {L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
73 + {L"cp", Localization::WSLCCLI_ContainerCpDesc()},
74 {L"create", Localization::WSLCCLI_ContainerCreateDesc()},
75 {L"exec", Localization::WSLCCLI_ContainerExecDesc()},
76 {L"export", Localization::WSLCCLI_ContainerExportDesc()},
test/windows/wslc/e2e/WSLCExecutor.cpp
+30 -4
@@ -147,7 +147,7 @@ bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring)
147 return Stdout.value().find(substring) != std::wstring::npos;
148 }
149
150 -WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType)
150 +WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType, HANDLE stdinHandle)
151 {
152 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
153 wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
@@ -160,10 +160,23 @@ WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType eleva
160 process.SetToken(nonElevatedToken.get());
161 }
162
163 - auto nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ);
164 - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
163 + wil::unique_hfile nul;
164 + wil::unique_hfile stdinDup;
165 + if (stdinHandle)
166 + {
167 + // Duplicate as inheritable to avoid mutating the caller's handle state.
168 + THROW_IF_WIN32_BOOL_FALSE(
169 + DuplicateHandle(GetCurrentProcess(), stdinHandle, GetCurrentProcess(), stdinDup.put(), 0, TRUE, DUPLICATE_SAME_ACCESS));
170 + stdinHandle = stdinDup.get();
171 + }
172 + else
173 + {
174 + nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ);
175 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
176 + stdinHandle = nul.get();
177 + }
178
166 - process.SetStdHandles(nul.get(), nullptr, nullptr);
179 + process.SetStdHandles(stdinHandle, nullptr, nullptr);
180
181 const auto output = process.RunAndCaptureOutput();
182 return {.CommandLine = commandLine, .Stdout = output.Stdout, .Stderr = output.Stderr, .ExitCode = output.ExitCode};
@@ -231,6 +244,19 @@ WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, st
244 return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode};
245 }
246
247 +WSLCExecutionResult RunWslcWithStdinFile(const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType)
248 +{
249 + SECURITY_ATTRIBUTES securityAttributes{};
250 + securityAttributes.nLength = sizeof(securityAttributes);
251 + securityAttributes.bInheritHandle = TRUE;
252 +
253 + wil::unique_hfile stdinFile(CreateFileW(
254 + stdinFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, &securityAttributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
255 + THROW_LAST_ERROR_IF(!stdinFile);
256 +
257 + return RunWslc(commandLine, elevationType, stdinFile.get());
258 +}
259 +
260 void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout)
261 {
262 auto cmd = std::format(L"\"{}\" container logs -f {}", GetWslcPath(), containerName);
test/windows/wslc/e2e/WSLCExecutor.h
+3 -1
@@ -126,11 +126,13 @@ private:
126 std::optional<std::string> m_ignoreSequence;
127 };
128
129 -WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated);
129 +WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, HANDLE stdinHandle = nullptr);
130 WSLCExecutionResult RunWslcAndRedirectToFile(
131 const std::wstring& commandLine,
132 std::optional<std::filesystem::path> outputPath = std::nullopt,
133 ElevationType elevationType = ElevationType::Elevated);
134 +WSLCExecutionResult RunWslcWithStdinFile(
135 + const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType = ElevationType::Elevated);
136 void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated);
137
138 std::wstring GetWslcHeader();