diff --git a/.gitignore b/.gitignore index 5dd7146c5..8409e6f1a 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,13 @@ doc/site/ directory.build.targets test-storage/ *.vhdx +*.vhd *.tar *.etl *.lscache -__pycache__ \ No newline at end of file +__pycache__ +deploy-log.txt +test-output*.txt +test-results.txt +testfile.txt +output/ \ No newline at end of file diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 95932960e..a735f8962 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2254,6 +2254,10 @@ Usage: Flag argument cannot contain adjoined value: '{}' {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Invalid boolean value for flag argument: '{}'. Expected true, false, 1, or 0. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Found a positional argument when none was expected: '{}' {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated @@ -2466,6 +2470,50 @@ For privacy information about this product please visit https://aka.ms/privacy.< Write to a file, instead of STDOUT {Locked="STDOUT"}Command line arguments, file names and string inserts should not be translated + + Copy files between a container and the local filesystem. + + + Copy files between a container and the local filesystem. +Usage: wslc container cp [OPTIONS] SOURCE DEST + Local to container: wslc container cp LOCAL_PATH CONTAINER:PATH + Container to local: wslc container cp CONTAINER:PATH LOCAL_PATH + Stdin to container: wslc container cp - CONTAINER:PATH + {Locked="wslc"}{Locked="container cp"}{Locked="CONTAINER:PATH"}{Locked="LOCAL_PATH"} + + + Source: local path, CONTAINER:PATH, or '-' for stdin + {Locked="-"}{Locked="CONTAINER:PATH"} + + + Destination: local path or CONTAINER:PATH + {Locked="CONTAINER:PATH"} + + + Invalid destination format. Expected CONTAINER:PATH + {Locked="CONTAINER:PATH"} + + + Invalid source format. Expected CONTAINER:PATH + {Locked="CONTAINER:PATH"} + + + Source path not found: {0} + {0} is the source path + + + tar.exe not found. Windows tar is required for local file copy. + {Locked="tar.exe"} + + + Invalid copy direction. Use CONTAINER:PATH as either source or destination. + {Locked="CONTAINER:PATH"} + + + Cannot read tar data from terminal. Pipe a tar archive to stdin. +Example: tar -cf - files | wslc container cp - CONTAINER:/path + {Locked="tar -cf -"}{Locked="wslc container cp"}{Locked="CONTAINER:/path"} + Inspect a container. @@ -2766,6 +2814,10 @@ On first run, creates the file with all settings commented out at their defaults Show all regardless of state. + + Archive mode (accepted for Docker CLI compatibility) + {Locked="Docker"} + Set build-time variables (KEY=VALUE) {Locked="KEY=VALUE"}Command line arguments should not be translated diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 147b44ce4..265aee726 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -470,6 +470,8 @@ interface IWSLCContainer : IUnknown HRESULT Stats([out] LPSTR* Output); HRESULT ConnectToNetwork([in] const WSLCNetworkConnectionOptions* Options); HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName); + HRESULT UploadArchive([in] WSLCHandle TarHandle, [in, string] LPCSTR DestPath, [in] ULONGLONG ContentSize); + HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle); } typedef struct _WSLCDeletedImageInformation diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 200022bcb..7ecd796ed 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -34,6 +34,7 @@ Module Name: // clang-format off #define WSLC_ARGUMENTS(_) \ _(All, "all", L"a", Kind::Flag, Localization::WSLCCLI_AllArgDescription()) \ +_(Archive, "archive", L"a", Kind::Flag, Localization::WSLCCLI_ArchiveArgDescription()) \ _(Attach, "attach", L"a", Kind::Flag, Localization::WSLCCLI_AttachArgDescription()) \ _(BuildArg, "build-arg", NO_ALIAS, Kind::Value, Localization::WSLCCLI_BuildArgDescription()) \ _(BuildPull, "pull", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_BuildPullArgDescription()) \ diff --git a/src/windows/wslc/arguments/ArgumentParser.cpp b/src/windows/wslc/arguments/ArgumentParser.cpp index 93621bc6c..c4f35ae9a 100644 --- a/src/windows/wslc/arguments/ArgumentParser.cpp +++ b/src/windows/wslc/arguments/ArgumentParser.cpp @@ -17,6 +17,24 @@ Module Name: using namespace wsl::shared; namespace wsl::windows::wslc { + +// Parses a boolean string value (true/false/1/0, case-insensitive). +// Returns the parsed value, or std::nullopt if the string is not a valid boolean. +static std::optional ParseBoolValue(std::wstring_view value) +{ + if (string::IsEqual(value, L"true") || value == L"1") + { + return true; + } + + if (string::IsEqual(value, L"false") || value == L"0") + { + return false; + } + + return std::nullopt; +} + ParseArgumentsStateMachine::ParseArgumentsStateMachine( Invocation& inv, ArgMap& execArgs, std::vector arguments, bool optionsOnly, bool stopOnUnknown, const std::vector& overridableDefaults) : m_invocation(inv), @@ -124,8 +142,6 @@ void ParseArgumentsStateMachine::AddFlag(ArgType type) if (!ConsumeOverrideIfPresent(type) && m_executionArgs.Contains(type)) { // Repeating the same flag on the CLI is a no-op, matching docker. - // TODO: revisit when --flag=value (explicit bool) lands so a mismatch - // between env-preload and CLI-explicit can warn or error. return; } @@ -343,9 +359,24 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume return {}; } - // Boolean flag - add it and process any adjoined flags. Once we have added a - // flag to m_executionArgs for this token, stopOnUnknown no longer applies for - // mid-chain unknowns; the token has already been claimed. + // Boolean flag - check for adjoined boolean value (e.g., -a=true or -a=false). + if (currentPos < currArg.length() && currArg[currentPos] == WSLC_CLI_ARG_SPLIT_CHAR) + { + auto boolVal = ParseBoolValue(currArg.substr(currentPos + 1)); + if (!boolVal.has_value()) + { + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg)); + } + + if (boolVal.value()) + { + AddFlag(firstArg->Type()); + } + + return {}; + } + + // No adjoined value — add the flag as true. AddFlag(firstArg->Type()); // Process remaining adjoined flags @@ -381,6 +412,23 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessAliasArgume return {}; } + // Boolean flag in chain — check for adjoined boolean value. + if (nextPos < currArg.length() && currArg[nextPos] == WSLC_CLI_ARG_SPLIT_CHAR) + { + auto boolVal = ParseBoolValue(currArg.substr(nextPos + 1)); + if (!boolVal.has_value()) + { + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg)); + } + + if (boolVal.value()) + { + AddFlag(nextArg->Type()); + } + + return {}; + } + AddFlag(nextArg->Type()); currentPos = nextPos; } @@ -430,10 +478,20 @@ ParseArgumentsStateMachine::State ParseArgumentsStateMachine::ProcessNamedArgume // Found a match, process by kind. if (arg.Kind() == Kind::Flag) { - // TODO: Consider supporting --flag and --flag=true or --flag=false for bool args. if (hasAdjoinedValue) { - return ArgumentException(Localization::WSLCCLI_FlagContainAdjoinedError(currArg)); + auto boolVal = ParseBoolValue(argValue); + if (!boolVal.has_value()) + { + return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg)); + } + + if (boolVal.value()) + { + AddFlag(arg.Type()); + } + + return {}; } AddFlag(arg.Type()); diff --git a/src/windows/wslc/commands/ContainerCommand.cpp b/src/windows/wslc/commands/ContainerCommand.cpp index 9d5002f3f..8a33bed10 100644 --- a/src/windows/wslc/commands/ContainerCommand.cpp +++ b/src/windows/wslc/commands/ContainerCommand.cpp @@ -23,6 +23,7 @@ std::vector> ContainerCommand::GetCommands() const { std::vector> commands; commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/commands/ContainerCommand.h b/src/windows/wslc/commands/ContainerCommand.h index 74f6c5629..f742eb937 100644 --- a/src/windows/wslc/commands/ContainerCommand.h +++ b/src/windows/wslc/commands/ContainerCommand.h @@ -62,6 +62,21 @@ struct ContainerCreateCommand final : public Command void ExecuteInternal(CLIExecutionContext& context) const override; }; +// Cp Command +struct ContainerCpCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"cp"; + ContainerCpCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + // Exec Command struct ContainerExecCommand final : public Command { diff --git a/src/windows/wslc/commands/ContainerCpCommand.cpp b/src/windows/wslc/commands/ContainerCpCommand.cpp new file mode 100644 index 000000000..1f7a44ee5 --- /dev/null +++ b/src/windows/wslc/commands/ContainerCpCommand.cpp @@ -0,0 +1,40 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#include "ContainerCommand.h" +#include "CLIExecutionContext.h" +#include "ContainerTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Container Cp Command +std::vector ContainerCpCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Archive), + Argument::Create(ArgType::Source, true, std::nullopt, Localization::WSLCCLI_CpSourceArgDescription()), + Argument::Create(ArgType::Target, true, std::nullopt, Localization::WSLCCLI_CpTargetArgDescription()), + }; +} + +std::wstring ContainerCpCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ContainerCpDesc(); +} + +std::wstring ContainerCpCommand::LongDescription() const +{ + return Localization::WSLCCLI_ContainerCpLongDesc(); +} + +void ContainerCpCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << ContainerCp; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index 529c1039f..0a18eb9fb 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -608,6 +608,22 @@ void ContainerService::Export(Session& session, const std::string& id, HANDLE ou THROW_IF_FAILED(container->Export(ToCOMInputHandle(outputHandle))); } +void ContainerService::CopyToContainer(Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize) +{ + wil::com_ptr container; + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); + + THROW_IF_FAILED(container->UploadArchive(ToCOMInputHandle(inputHandle), destPath.c_str(), contentSize)); +} + +void ContainerService::CopyFromContainer(Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle) +{ + wil::com_ptr container; + THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container)); + + THROW_IF_FAILED(container->DownloadArchive(srcPath.c_str(), ToCOMInputHandle(outputHandle))); +} + void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail) { wil::com_ptr container; diff --git a/src/windows/wslc/services/ContainerService.h b/src/windows/wslc/services/ContainerService.h index fccdf2056..87b296601 100644 --- a/src/windows/wslc/services/ContainerService.h +++ b/src/windows/wslc/services/ContainerService.h @@ -36,6 +36,8 @@ struct ContainerService static int Exec(models::Session& session, const std::string& id, models::ContainerOptions options); static void Export(models::Session& session, const std::string& id, const std::wstring& outputPath); static void Export(models::Session& session, const std::string& id, HANDLE outputHandle); + static void CopyToContainer(models::Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize); + static void CopyFromContainer(models::Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle); static wsl::windows::common::wslc_schema::InspectContainer Inspect(models::Session& session, const std::string& id); static void Logs(models::Session& session, const std::string& id, bool follow, bool timestamps, ULONGLONG since, ULONGLONG until, ULONGLONG tail = 0); static wsl::windows::common::docker_schema::ContainerStats Stats(models::Session& session, const std::string& id); diff --git a/src/windows/wslc/tasks/ContainerTasks.cpp b/src/windows/wslc/tasks/ContainerTasks.cpp index bbd61025a..6ceaa2927 100644 --- a/src/windows/wslc/tasks/ContainerTasks.cpp +++ b/src/windows/wslc/tasks/ContainerTasks.cpp @@ -24,6 +24,7 @@ Module Name: #include "TableOutput.h" #include #include +#include using namespace wsl::shared; using namespace wsl::windows::common; @@ -277,6 +278,260 @@ void ExportContainer(CLIExecutionContext& context) } } +void ContainerCp(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::Source)); + WI_ASSERT(context.Args.Contains(ArgType::Target)); + + auto& session = context.Data.Get(); + auto source = WideToMultiByte(context.Args.Get()); + auto target = WideToMultiByte(context.Args.Get()); + + // Determine copy direction by looking for CONTAINER:PATH patterns. + // A single letter before ':' is a Windows drive path (e.g. C:\path), not a container reference. + auto isContainerPath = [](const std::string& path) -> bool { + auto colonPos = path.find(':'); + if (colonPos == std::string::npos || colonPos == 0) + { + return false; + } + + // Single letter before colon is a Windows drive path + if (colonPos == 1 && std::isalpha(static_cast(path[0]))) + { + return false; + } + + return true; + }; + + auto parseContainerPath = [](const std::string& path) -> std::pair { + auto colonPos = path.find(':'); + // Skip Windows drive letter if present + if (colonPos == 1 && std::isalpha(static_cast(path[0]))) + { + colonPos = path.find(':', 2); + } + + return {path.substr(0, colonPos), path.substr(colonPos + 1)}; + }; + + bool sourceIsStdin = (source == "-"); + bool sourceIsContainer = !sourceIsStdin && isContainerPath(source); + bool targetIsContainer = isContainerPath(target); + + if ((sourceIsStdin || !sourceIsContainer) && targetIsContainer) + { + // stdin/local → container + auto [containerId, destPath] = parseContainerPath(target); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpInvalidTargetError(), containerId.empty() || destPath.empty()); + + if (sourceIsStdin) + { + auto inputHandle = GetStdHandle(STD_INPUT_HANDLE); + THROW_HR_WITH_USER_ERROR_IF( + E_INVALIDARG, Localization::WSLCCLI_CpStdinIsTerminalError(), wsl::windows::common::wslutil::IsConsoleHandle(inputHandle)); + + LARGE_INTEGER fileSize{}; + ULONGLONG contentSize = 0; + if (GetFileSizeEx(inputHandle, &fileSize)) + { + contentSize = static_cast(fileSize.QuadPart); + } + + // Note: The --archive/-a flag is accepted for CLI compatibility with docker cp, but is a + // no-op here. Since the tar archive contains uid/gid ownership in its headers, and Docker's + // PUT /archive extracts preserving that metadata. + ContainerService::CopyToContainer(session, containerId, destPath, inputHandle, contentSize); + } + else + { + // Local path → container: create tar from local path using tar.exe + auto widePath = MultiByteToWide(source); + std::error_code fsError; + bool pathExists = std::filesystem::exists(widePath, fsError); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpSourceNotFoundError(widePath), fsError || !pathExists); + + auto absPath = std::filesystem::absolute(widePath); + auto parentDir = absPath.parent_path().wstring(); + auto fileName = absPath.filename().wstring(); + + // Create a temp tar file + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + wchar_t tempFile[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempFileNameW(tempDir, L"wslcp", 0, tempFile) == 0); + auto tempPath = std::wstring(tempFile); + auto cleanupTemp = wil::scope_exit([&] { DeleteFileW(tempPath.c_str()); }); + + // 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'/')) + { + parentDirStr.pop_back(); + } + + auto tarCmd = std::format(L"tar.exe -cf \"{}\" -C \"{}\" \"{}\"", tempPath, parentDirStr, fileName); + STARTUPINFOW si{sizeof(si)}; + PROCESS_INFORMATION pi{}; + if (!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) + { + auto lastError = GetLastError(); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), + Localization::WSLCCLI_CpTarNotFoundError(), + lastError == ERROR_FILE_NOT_FOUND || lastError == ERROR_PATH_NOT_FOUND); + THROW_WIN32(lastError); + } + wil::unique_handle tarProcess(pi.hProcess); + wil::unique_handle tarThread(pi.hThread); + + WaitForSingleObject(tarProcess.get(), INFINITE); + DWORD exitCode = 0; + GetExitCodeProcess(tarProcess.get(), &exitCode); + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode); + + // Open the tar file and upload + wil::unique_hfile tarFileHandle( + CreateFileW(tempPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!tarFileHandle); + + LARGE_INTEGER fileSize{}; + THROW_LAST_ERROR_IF(!GetFileSizeEx(tarFileHandle.get(), &fileSize)); + + ContainerService::CopyToContainer(session, containerId, destPath, tarFileHandle.get(), static_cast(fileSize.QuadPart)); + } + } + else if (sourceIsContainer && !targetIsContainer) + { + // container → local + auto [containerId, srcPath] = parseContainerPath(source); + THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::WSLCCLI_CpInvalidSourceError(), containerId.empty() || srcPath.empty()); + + auto wideTarget = MultiByteToWide(target); + auto absTarget = std::filesystem::absolute(wideTarget); + + // Determine if target is a directory or a file destination. + // Treat as directory if: ends with separator, or already exists as a directory. + bool targetIsDir = (!wideTarget.empty() && (wideTarget.back() == L'\\' || wideTarget.back() == L'/')) || + std::filesystem::is_directory(absTarget); + + // Download archive from container to a temp file + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + wchar_t tempFile[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempFileNameW(tempDir, L"wslcp", 0, tempFile) == 0); + auto tempPath = std::wstring(tempFile); + auto cleanupTemp = wil::scope_exit([&] { DeleteFileW(tempPath.c_str()); }); + + { + wil::unique_hfile tarFileHandle( + CreateFileW(tempPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!tarFileHandle); + + ContainerService::CopyFromContainer(session, containerId, srcPath, tarFileHandle.get()); + } + + if (targetIsDir) + { + // Extract directly into the target directory. + std::error_code dirError; + std::filesystem::create_directories(absTarget, dirError); + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(dirError.value()), !!dirError, "Failed to create directory: %ls", absTarget.c_str()); + + // 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'/')) + { + targetDir.pop_back(); + } + + auto tarCmd = std::format(L"tar.exe -xf \"{}\" -C \"{}\"", tempPath, targetDir); + STARTUPINFOW si{sizeof(si)}; + PROCESS_INFORMATION pi{}; + if (!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) + { + auto lastError = GetLastError(); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), + Localization::WSLCCLI_CpTarNotFoundError(), + lastError == ERROR_FILE_NOT_FOUND || lastError == ERROR_PATH_NOT_FOUND); + THROW_WIN32(lastError); + } + wil::unique_handle tarProcess(pi.hProcess); + wil::unique_handle tarThread(pi.hThread); + + WaitForSingleObject(tarProcess.get(), INFINITE); + DWORD exitCode = 0; + GetExitCodeProcess(tarProcess.get(), &exitCode); + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode); + } + else + { + // Target is a file path. Extract to a temp directory, then move to the target. + 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); }); + + auto extractDirStr = extractDir.wstring(); + while (extractDirStr.size() > 1 && (extractDirStr.back() == L'\\' || extractDirStr.back() == L'/')) + { + extractDirStr.pop_back(); + } + + auto tarCmd = std::format(L"tar.exe -xf \"{}\" -C \"{}\"", tempPath, extractDirStr); + STARTUPINFOW si{sizeof(si)}; + PROCESS_INFORMATION pi{}; + if (!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) + { + auto lastError = GetLastError(); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), + Localization::WSLCCLI_CpTarNotFoundError(), + lastError == ERROR_FILE_NOT_FOUND || lastError == ERROR_PATH_NOT_FOUND); + THROW_WIN32(lastError); + } + wil::unique_handle tarProcess(pi.hProcess); + wil::unique_handle tarThread(pi.hThread); + + WaitForSingleObject(tarProcess.get(), INFINITE); + DWORD exitCode = 0; + GetExitCodeProcess(tarProcess.get(), &exitCode); + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode); + + // Find the extracted file and move it to the target path. + // Docker's archive API returns a tar with the file at its basename. + std::filesystem::path extractedFile; + for (const auto& entry : std::filesystem::directory_iterator(extractDir)) + { + extractedFile = entry.path(); + break; + } + + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, "No file extracted from container archive", extractedFile.empty()); + + // Ensure parent directory of target exists. + std::error_code dirError; + std::filesystem::create_directories(absTarget.parent_path(), dirError); + + std::error_code moveError; + std::filesystem::rename(extractedFile, absTarget, moveError); + if (moveError) + { + // rename can fail across volumes; fall back to copy+delete. + std::filesystem::copy_file(extractedFile, absTarget, std::filesystem::copy_options::overwrite_existing, moveError); + THROW_HR_IF_MSG( + HRESULT_FROM_WIN32(moveError.value()), !!moveError, "Failed to copy file to target: %ls", absTarget.c_str()); + } + } + } + else + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::WSLCCLI_CpInvalidDirectionError()); + } +} + void ListContainers(CLIExecutionContext& context) { WI_ASSERT(context.Data.Contains(Data::Containers)); diff --git a/src/windows/wslc/tasks/ContainerTasks.h b/src/windows/wslc/tasks/ContainerTasks.h index 7055f5e37..dd137fee4 100644 --- a/src/windows/wslc/tasks/ContainerTasks.h +++ b/src/windows/wslc/tasks/ContainerTasks.h @@ -31,6 +31,7 @@ struct AttachContainer : public Task }; void CreateContainer(CLIExecutionContext& context); +void ContainerCp(CLIExecutionContext& context); void ExecContainer(CLIExecutionContext& context); void ExportContainer(CLIExecutionContext& context); void GetContainers(CLIExecutionContext& context); diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 9c855a6d6..11e718667 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -399,6 +399,31 @@ std::pair DockerHTTPClient::ExportContainer(const return {response.result_int(), std::move(socket)}; } +std::unique_ptr DockerHTTPClient::PutArchive( + const std::string& ContainerID, const std::string& Path, std::optional ContentLength) +{ + auto url = URL::Create("/containers/{}/archive", ContainerID); + url.SetParameter("path", Path); + + std::map headers = {{"Content-Type", "application/x-tar"}}; + if (ContentLength.has_value()) + { + headers["Content-Length"] = std::to_string(ContentLength.value()); + } + + return SendRequestImpl(verb::put, url, {}, headers); +} + +std::tuple DockerHTTPClient::GetArchive(const std::string& ContainerID, const std::string& Path) +{ + auto url = URL::Create("/containers/{}/archive", ContainerID); + url.SetParameter("path", Path); + + auto [response, socket] = SendRequest(verb::get, url, {}, {}); + + return {response.result_int(), std::move(socket), response.chunked()}; +} + docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::CreateVolume& Request) { return Transaction(verb::post, URL::Create("/volumes/create"), Request); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index fd24a490b..bcda6b506 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -136,6 +136,8 @@ class DockerHTTPClient void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns); wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail); std::pair ExportContainer(const std::string& ContainerID); + std::unique_ptr PutArchive(const std::string& ContainerID, const std::string& Path, std::optional ContentLength); + std::tuple GetArchive(const std::string& ContainerID, const std::string& Path); common::docker_schema::PruneContainerResult PruneContainers(const std::map>& filters = {}); // Volume management. diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 4da373ed4..4356ac35a 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1092,34 +1092,205 @@ void WSLCContainerImpl::Export(WSLCHandle OutHandle) const wsl::windows::common::io::MultiHandleWait io = m_wslcSession.CreateIOContext(); std::string errorJson; - auto accumulateError = [&](const gsl::span& buffer) { - // If the export failed, accumulate the error message. - errorJson.append(buffer.data(), buffer.size()); - }; if (SocketCodePair.first != 200) { - io.AddHandle(std::make_unique(HandleWrapper{std::move(SocketCodePair.second)}, std::move(accumulateError))); + // Read the error body synchronously with a timeout. HTTP/1.1 keep-alive would hang + // the async io.Run() path because the socket never closes. + lock.reset(); + + DWORD timeout = 2000; + LOG_LAST_ERROR_IF( + setsockopt(SocketCodePair.second.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)) == SOCKET_ERROR); + + std::array buf{}; + for (;;) + { + auto bytesRead = recv(SocketCodePair.second.get(), buf.data(), static_cast(buf.size()), 0); + if (bytesRead <= 0) + { + break; + } + errorJson.append(buf.data(), bytesRead); + } } else { - io.AddHandle(std::make_unique>( - HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get())); + io.AddHandle( + std::make_unique>(HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get()), + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted); + + // Release the lock so the container can still be interacted with while the export is in progress. + // Past this point, no member variables can be accessed. + lock.reset(); + + io.Run({}); + } + + if (SocketCodePair.first != 200) + { + // Export failed, parse the error message. + try + { + auto error = wsl::shared::FromJson(errorJson.c_str()); + + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, SocketCodePair.first == 404); + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message); + } + catch (const wil::ResultException&) + { + throw; + } + catch (...) + { + THROW_HR_WITH_USER_ERROR(E_FAIL, errorJson); + } + } +} + +void WSLCContainerImpl::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const +{ + auto lock = m_lock.lock_shared(); + + std::optional contentLength; + if (ContentSize > 0) + { + contentLength = ContentSize; } - // Release the lock so the container can still be interacted with while the export is in progress. - // Past this point, no member variables can be accessed. + auto requestContext = m_dockerClient.PutArchive(m_id, DestPath, contentLength); + + auto userHandle = m_wslcSession.OpenUserHandle(TarHandle); + + auto io = m_wslcSession.CreateIOContext(); + + std::optional pendingErrorJson; + unsigned int httpStatusCode = 0; + auto onHttpResponse = [&](const boost::beast::http::message& response) { + WSL_LOG("ContainerUploadArchiveHttpResponse", TraceLoggingValue(static_cast(response.result()), "StatusCode")); + + httpStatusCode = response.result_int(); + if (httpStatusCode != 200) + { + pendingErrorJson.emplace(); + } + }; + + auto onProgress = [&](const gsl::span& buffer) { + if (pendingErrorJson.has_value()) + { + pendingErrorJson->append(buffer.data(), buffer.size()); + } + }; + + // Shutdown the Docker stream's write side when the input is fully read. + auto onInputComplete = [socket = requestContext->stream.native_handle()]() { + LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR); + }; + + io.AddHandle(std::make_unique>( + HandleWrapper{userHandle.Get(), std::move(onInputComplete)}, HandleWrapper{requestContext->stream.native_handle()})); + + io.AddHandle( + std::make_unique(*requestContext, std::move(onHttpResponse), std::move(onProgress)), + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted); + + // Release the lock so the container can still be interacted with while the upload is in progress. lock.reset(); io.Run({}); - if (SocketCodePair.first != 200) + if (pendingErrorJson.has_value()) { - // Export failed, parse the error message. - auto error = wsl::shared::FromJson(errorJson.c_str()); + try + { + auto error = wsl::shared::FromJson(pendingErrorJson->c_str()); - THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, SocketCodePair.first == 404); - THROW_HR_WITH_USER_ERROR(E_FAIL, error.message); + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, httpStatusCode == 404); + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message); + } + catch (const wil::ResultException&) + { + throw; + } + catch (...) + { + THROW_HR_WITH_USER_ERROR(E_FAIL, *pendingErrorJson); + } + } +} + +void WSLCContainerImpl::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const +{ + auto lock = m_lock.lock_shared(); + + auto [statusCode, socket, isChunked] = m_dockerClient.GetArchive(m_id, SrcPath); + + auto userHandle = m_wslcSession.OpenUserHandle(OutHandle); + + wsl::windows::common::io::MultiHandleWait io = m_wslcSession.CreateIOContext(); + + std::string errorJson; + + if (statusCode != 200) + { + // Read the error body synchronously. Docker error responses are small JSON and already + // buffered in the socket. We cannot use the async io.Run() path with a raw ReadHandle + // because HTTP/1.1 keep-alive holds the connection open indefinitely, causing a hang. + // Use a short receive timeout so we don't block if Docker keeps the connection alive. + lock.reset(); + + DWORD timeout = 2000; // 2 seconds — more than enough for a buffered error body + LOG_LAST_ERROR_IF(setsockopt(socket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)) == SOCKET_ERROR); + + std::array buf{}; + for (;;) + { + auto bytesRead = recv(socket.get(), buf.data(), static_cast(buf.size()), 0); + if (bytesRead <= 0) + { + break; + } + errorJson.append(buf.data(), bytesRead); + } + } + else + { + if (isChunked) + { + io.AddHandle( + std::make_unique>(HandleWrapper{std::move(socket)}, userHandle.Get()), + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted); + } + else + { + io.AddHandle( + std::make_unique>(HandleWrapper{std::move(socket)}, userHandle.Get()), + wsl::windows::common::io::MultiHandleWait::CancelOnCompleted); + } + + lock.reset(); + + io.Run({}); + } + + if (statusCode != 200) + { + try + { + auto error = wsl::shared::FromJson(errorJson.c_str()); + + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, error.message, statusCode == 404); + THROW_HR_WITH_USER_ERROR(E_FAIL, error.message); + } + catch (const wil::ResultException&) + { + throw; + } + catch (...) + { + THROW_HR_WITH_USER_ERROR(E_FAIL, errorJson); + } } } @@ -2335,6 +2506,24 @@ HRESULT WSLCContainer::Export(WSLCHandle TarHandle) return CallImpl(&WSLCContainerImpl::Export, TarHandle); } +HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) +{ + WSLCExecutionContext context(&m_session); + + RETURN_HR_IF(E_POINTER, DestPath == nullptr); + RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0'); + return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize); +} + +HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) +{ + WSLCExecutionContext context(&m_session); + + RETURN_HR_IF(E_POINTER, SrcPath == nullptr); + RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0'); + return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle); +} + HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) try { diff --git a/src/windows/wslcsession/WSLCContainer.h b/src/windows/wslcsession/WSLCContainer.h index 10fd57fd3..90ddf5fd9 100644 --- a/src/windows/wslcsession/WSLCContainer.h +++ b/src/windows/wslcsession/WSLCContainer.h @@ -99,6 +99,8 @@ class WSLCContainerImpl void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill); void Delete(WSLCDeleteFlags Flags); void Export(WSLCHandle TarHandle) const; + void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const; + void DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const; void GetStateChangedAt(_Out_ ULONGLONG* StateChangedAt); void GetCreatedAt(_Out_ ULONGLONG* CreatedAt); void GetState(_Out_ WSLCContainerState* State); @@ -235,6 +237,8 @@ class DECLSPEC_UUID("B1F1C4E3-C225-4CAE-AD8A-34C004DE1AE4") WSLCContainer IFACEMETHOD(Kill)(_In_ WSLCSignal Signal) override; IFACEMETHOD(Delete)(WSLCDeleteFlags Flags) override; IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override; + IFACEMETHOD(UploadArchive)(_In_ WSLCHandle TarHandle, _In_ LPCSTR DestPath, _In_ ULONGLONG ContentSize) override; + IFACEMETHOD(DownloadArchive)(_In_ LPCSTR SrcPath, _In_ WSLCHandle OutHandle) override; IFACEMETHOD(GetState)(_Out_ WSLCContainerState* State) override; IFACEMETHOD(GetInitProcess)(_Out_ IWSLCProcess** process) override; IFACEMETHOD(Exec)(_In_ const WSLCProcessOptions* Options, _In_opt_ const WSLCProcessStartOptions* StartOptions, _Out_ IWSLCProcess** Process) override; diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h index 6ac8329e4..78414acc9 100644 --- a/test/windows/wslc/CommandLineTestCases.h +++ b/test/windows/wslc/CommandLineTestCases.h @@ -182,6 +182,28 @@ COMMAND_LINE_TEST_CASE(L"container export -o foo cont1", L"export", true) COMMAND_LINE_TEST_CASE(L"container export cont1 --output foo", L"export", true) COMMAND_LINE_TEST_CASE(L"container export cont1 -o foo", L"export", true) +// Cp command tests +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp - mycontainer:/usr/local/etc", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp - cont1:/path/to/deep/dir", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp somefile cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp --archive - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a=true - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a=false - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp --archive=true - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp --archive=false - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a=1 - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a=0 - cont1:/path", L"cp", true) +COMMAND_LINE_TEST_CASE(L"container cp -a=invalid - cont1:/path", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp --archive=invalid - cont1:/path", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp -", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp - ", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp --unknown - cont1:/path", L"cp", false) +COMMAND_LINE_TEST_CASE(L"container cp --help", L"cp", true) + // Logs command COMMAND_LINE_TEST_CASE(L"logs cont1", L"logs", true) COMMAND_LINE_TEST_CASE(L"container logs cont1", L"logs", true) diff --git a/test/windows/wslc/ParserTestCases.h b/test/windows/wslc/ParserTestCases.h index b8955a199..3679d8eeb 100644 --- a/test/windows/wslc/ParserTestCases.h +++ b/test/windows/wslc/ParserTestCases.h @@ -164,7 +164,8 @@ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc --invalidarg cont1)") \ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -i cont1 cont2)") \ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc -vp cont1)") \ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 -v cont2 -12)") \ -WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=false cont2)") \ +WSLC_PARSER_TEST_CASE(List, true, LR"(wslc cont1 --verbose=false cont2)") \ +WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 --verbose=invalid cont2)") \ WSLC_PARSER_TEST_CASE(List, false, LR"(wslc cont1 cont2 --invalidarg)") \ \ /* Root-level globals: strict optionsOnly parsing. Stops cleanly at the first \ diff --git a/test/windows/wslc/e2e/WSLCE2EContainerCpTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerCpTests.cpp new file mode 100644 index 000000000..61564b6a1 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EContainerCpTests.cpp @@ -0,0 +1,560 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EContainerCpTests +{ + WSLC_TEST_CLASS(WSLCE2EContainerCpTests) + + TEST_CLASS_SETUP(ClassSetup) + { + EnsureImageIsLoaded(DebianImage); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + EnsureContainerDoesNotExist(WslcContainerName); + EnsureImageIsDeleted(DebianImage); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + EnsureContainerDoesNotExist(WslcContainerName); + TarPath = wsl::windows::common::filesystem::GetTempFilename(); + DeleteFileW(TarPath.c_str()); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + EnsureContainerDoesNotExist(WslcContainerName); + DeleteFileW(TarPath.c_str()); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_HelpCommand) + { + auto result = RunWslc(L"container cp --help"); + VERIFY_IS_TRUE(result.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stdout.has_value()); + VERIFY_IS_TRUE(result.Stdout->find(L"container cp") != std::wstring::npos); + VERIFY_IS_TRUE(result.Stdout->find(L"source") != std::wstring::npos); + VERIFY_IS_TRUE(result.Stdout->find(L"target") != std::wstring::npos); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_ARE_EQUAL(L"", result.Stderr.value()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingBothArgs) + { + const auto result = RunWslc(L"container cp"); + VERIFY_IS_TRUE(result.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'source'") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingTarget) + { + const auto result = RunWslc(L"container cp -"); + VERIFY_IS_TRUE(result.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'target'") != std::wstring::npos); + } + + 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_IS_TRUE(result.Stderr->find(L"tar") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_SourceNotStdin) + { + // A local file that doesn't exist should fail with a "source not found" error. + // Use RunWslc which pipes NUL to stdin (not a terminal). + const auto result = RunWslc(L"container cp somefile.tar fakecontainer:/path"); + VERIFY_IS_TRUE(result.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value()); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"somefile.tar") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_NoColon) + { + // Target must be CONTAINER:PATH — missing colon should fail. + const auto result = RunWslc(L"container cp - fakecontainer_nopath"); + 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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyContainer) + { + // Target with empty container name (:path) should fail. + const auto result = RunWslc(L"container cp - :/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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyPath) + { + // Target with empty path (container:) should fail. + const auto result = RunWslc(L"container cp - fakecontainer:"); + 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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerNotFound) + { + // Create a valid tar file to pipe in, but target a nonexistent container. + CreateTestTarFile(); + + const auto result = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", InvalidContainerName), TarPath); + 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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Success) + { + // Create and start a container with sleep infinity to keep it running. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Create a test tar file with a known file inside. + CreateTestTarFile(); + + // Cp the tar into the running container. + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Verify the file was copied by running a command inside the container. + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ToStoppedContainer) + { + // Create a stopped container (not started). + auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag())); + createResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Create a test tar file. + CreateTestTarFile(); + + // Attempt to cp into the stopped container — Docker should accept this. + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath); + + // Docker's PUT /archive works on stopped containers too. + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlag) + { + // Create and start a container. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + // Cp with -a flag (archive mode preserves uid/gid). + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Verify the file was copied. + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagLongForm) + { + // Create and start a container. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + // Cp with --archive flag (long form). + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + // Verify the file was copied. + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsTrue) + { + // Test -a=true syntax. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=true - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsFalse) + { + // Test -a=false syntax (no archive mode). + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=false - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsTrue) + { + // Test --archive=true syntax. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=true - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsFalse) + { + // Test --archive=false syntax (no archive mode). + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + CreateTestTarFile(); + + const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=false - {}:/tmp", WslcContainerName), TarPath); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName)); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"wslc-cp-test-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagInvalidValue) + { + // Test -a=invalid should fail with an error. + CreateTestTarFile(); + + const auto result = RunWslcWithStdinFile(std::format(L"container cp -a=invalid - {}:/tmp", WslcContainerName), TarPath); + 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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileToContainer) + { + // Create and start a container. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Create a local file to copy. + auto localFile = wsl::windows::common::filesystem::GetTempFilename(); + auto cleanupLocal = wil::scope_exit([&] { DeleteFileW(localFile.c_str()); }); + + { + wil::unique_hfile file(CreateFileW(localFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!file); + const std::string content = "local-file-content\n"; + DWORD written = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast(content.size()), &written, nullptr)); + } + + // 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}); + + // Verify the file was copied. + auto fileName = localFile.filename().string(); + const auto execResult = + RunWslc(std::format(L"container exec {} cat /tmp/{}", WslcContainerName, wsl::shared::string::MultiByteToWide(fileName))); + VERIFY_IS_TRUE(execResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value()); + VERIFY_IS_TRUE(execResult.Stdout.has_value()); + VERIFY_IS_TRUE(execResult.Stdout->find(L"local-file-content") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileNotFound) + { + // Copying a nonexistent local file should fail. + const auto result = RunWslc(std::format(L"container cp C:\\nonexistent_wslc_test_file.txt {}:/tmp/", WslcContainerName)); + 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()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal) + { + // Create and start a container with a known file. + auto runResult = RunWslc(std::format( + L"container run -d --name {} {} sh -c \"echo container-content > /tmp/fromcontainer.txt && sleep infinity\"", + WslcContainerName, + DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Wait briefly for the file to be created inside the container. + Sleep(1000); + + // Create a temp directory to download into. + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-download-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + // 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}); + + // Verify the file was extracted locally. + auto extractedFile = downloadDir / L"fromcontainer.txt"; + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_TrailingBackslash) + { + // Regression test: trailing backslash on local path should not break tar extraction. + auto runResult = RunWslc(std::format( + L"container run -d --name {} {} sh -c \"echo backslash-test > /tmp/bstest.txt && sleep infinity\"", + WslcContainerName, + DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + Sleep(1000); + + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-backslash-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + // 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}); + + auto extractedFile = downloadDir / L"bstest.txt"; + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_FileDestination) + { + // When the local target doesn't end with a separator and isn't an existing directory, + // it should be treated as a file destination (matching docker cp semantics). + auto runResult = RunWslc(std::format( + L"container run -d --name {} {} sh -c \"echo file-dest-test > /tmp/srcfile.txt && sleep infinity\"", + WslcContainerName, + DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + Sleep(1000); + + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto targetFile = std::filesystem::path(tempDir) / L"wslc-cp-file-dest-test" / L"renamed.txt"; + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(targetFile.parent_path()); }); + + // 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}); + + // The file should exist at the exact target path, not inside a directory named "renamed.txt". + VERIFY_IS_TRUE(std::filesystem::exists(targetFile)); + VERIFY_IS_TRUE(std::filesystem::is_regular_file(targetFile)); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentPath) + { + // Regression test: DownloadArchive used to hang on 404 because the HTTP/1.1 keep-alive + // socket never closed. The fix shuts down the socket so the read sees EOF immediately. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-notfound-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + const auto cpResult = RunWslc(std::format(L"container cp {}:/nonexistent/file.txt {}", WslcContainerName, downloadDir.wstring())); + VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); + VERIFY_IS_TRUE(cpResult.Stderr.has_value()); + VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentDir) + { + // Regression test: DownloadArchive 404 for a nonexistent directory path. + auto runResult = + RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-notfound-dir-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + const auto cpResult = RunWslc(std::format(L"container cp {}:/no/such/directory/ {}", WslcContainerName, downloadDir.wstring())); + VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); + VERIFY_IS_TRUE(cpResult.Stderr.has_value()); + VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Download_NonexistentContainer) + { + // Regression test: DownloadArchive error path when the container itself doesn't exist. + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-no-container-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/file.txt {}", InvalidContainerName, downloadDir.wstring())); + VERIFY_IS_TRUE(cpResult.ExitCode.has_value()); + VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value()); + VERIFY_IS_TRUE(cpResult.Stderr.has_value()); + VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size()); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_FromStoppedContainer) + { + // Create a container, put a file in it, stop it, then copy out. + auto runResult = RunWslc(std::format( + L"container run --name {} {} sh -c \"echo stopped-content > /tmp/stopped.txt\"", WslcContainerName, DebianImage.NameAndTag())); + runResult.Verify({.Stderr = L"", .ExitCode = 0}); + + // Container has exited (ran a one-shot command). Copy from the stopped container. + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto downloadDir = std::filesystem::path(tempDir) / L"wslc-cp-stopped-test"; + std::filesystem::create_directories(downloadDir); + auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); }); + + const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/stopped.txt {}", WslcContainerName, downloadDir.wstring())); + cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0}); + + auto extractedFile = downloadDir / L"stopped.txt"; + VERIFY_IS_TRUE(std::filesystem::exists(extractedFile)); + } + + WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidDirection_LocalToLocal) + { + // local → local is not a valid copy direction. + const auto result = RunWslc(L"container cp C:\\temp\\somefile.txt C:\\temp\\dest\\"); + 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()); + } + +private: + const std::wstring WslcContainerName = L"wslc-test-container-cp"; + const std::wstring InvalidContainerName = L"wslc-nonexistent-container-for-cp"; + const TestImage& DebianImage = DebianTestImage(); + + std::filesystem::path TarPath{}; + + // Creates a tar file containing a single text file using tar.exe. + void CreateTestTarFile() + { + // Create a temp directory with a test file to archive. + wchar_t tempDir[MAX_PATH]{}; + THROW_LAST_ERROR_IF(GetTempPathW(MAX_PATH, tempDir) == 0); + auto tarSrcDir = std::filesystem::path(tempDir) / L"wslc-cp-tar-src"; + std::filesystem::create_directories(tarSrcDir); + auto cleanupSrcDir = wil::scope_exit([&] { std::filesystem::remove_all(tarSrcDir); }); + + auto testFile = tarSrcDir / L"testfile.txt"; + { + wil::unique_hfile file(CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!file); + const std::string content = "wslc-cp-test-content\n"; + DWORD written = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast(content.size()), &written, nullptr)); + } + + // Use tar.exe to create the archive. + 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)); + wil::unique_handle tarProcess(pi.hProcess); + wil::unique_handle tarThread(pi.hThread); + WaitForSingleObject(tarProcess.get(), INFINITE); + + DWORD exitCode = 0; + GetExitCodeProcess(tarProcess.get(), &exitCode); + THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp index 09caf623c..58fac90c1 100644 --- a/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EContainerTests.cpp @@ -70,6 +70,7 @@ class WSLCE2EContainerTests { std::vector> entries = { {L"attach", Localization::WSLCCLI_ContainerAttachDesc()}, + {L"cp", Localization::WSLCCLI_ContainerCpDesc()}, {L"create", Localization::WSLCCLI_ContainerCreateDesc()}, {L"exec", Localization::WSLCCLI_ContainerExecDesc()}, {L"export", Localization::WSLCCLI_ContainerExportDesc()}, diff --git a/test/windows/wslc/e2e/WSLCExecutor.cpp b/test/windows/wslc/e2e/WSLCExecutor.cpp index 1b7c7f348..6e622a5f3 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.cpp +++ b/test/windows/wslc/e2e/WSLCExecutor.cpp @@ -147,7 +147,7 @@ bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) return Stdout.value().find(substring) != std::wstring::npos; } -WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType) +WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType, std::optional stdinHandle) { auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine; wsl::windows::common::SubProcess process(nullptr, cmd.c_str()); @@ -160,10 +160,21 @@ WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType eleva process.SetToken(nonElevatedToken.get()); } - auto nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ); - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + wil::unique_hfile nul; + HANDLE effectiveStdin = nullptr; + if (stdinHandle.has_value()) + { + effectiveStdin = stdinHandle.value(); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(effectiveStdin, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + } + else + { + nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + effectiveStdin = nul.get(); + } - process.SetStdHandles(nul.get(), nullptr, nullptr); + process.SetStdHandles(effectiveStdin, nullptr, nullptr); const auto output = process.RunAndCaptureOutput(); return {.CommandLine = commandLine, .Stdout = output.Stdout, .Stderr = output.Stderr, .ExitCode = output.ExitCode}; @@ -231,25 +242,17 @@ WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, st return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode}; } -void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout) +WSLCExecutionResult RunWslcWithStdinFile(const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType) { - auto cmd = std::format(L"\"{}\" container logs -f {}", GetWslcPath(), containerName); - - auto [parentStdoutRead, childStdoutWrite] = wslutil::OpenAnonymousPipe(65536, true, false); - THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); - - SubProcess process(nullptr, cmd.c_str()); - process.SetStdHandles(nullptr, childStdoutWrite.get(), nullptr); - - wil::unique_handle processHandle = process.Start(); - childStdoutWrite.reset(); + SECURITY_ATTRIBUTES securityAttributes{}; + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.bInheritHandle = TRUE; - auto terminate = wil::scope_exit([&]() { - LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(processHandle.get(), 1)); - LOG_LAST_ERROR_IF(WaitForSingleObject(processHandle.get(), DefaultWaitTimeoutMs) != WAIT_OBJECT_0); - }); + wil::unique_hfile stdinFile(CreateFileW( + stdinFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, &securityAttributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!stdinFile); - WaitForOutput(wil::unique_handle{parentStdoutRead.release()}, expected, timeout); + return RunWslc(commandLine, elevationType, stdinFile.get()); } std::wstring GetWslcHeader() diff --git a/test/windows/wslc/e2e/WSLCExecutor.h b/test/windows/wslc/e2e/WSLCExecutor.h index 83e07bca5..5e6cf1e45 100644 --- a/test/windows/wslc/e2e/WSLCExecutor.h +++ b/test/windows/wslc/e2e/WSLCExecutor.h @@ -126,11 +126,14 @@ struct WSLCInteractiveSession std::optional m_ignoreSequence; }; -WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated); +WSLCExecutionResult RunWslc( + const std::wstring& commandLine, ElevationType elevationType = ElevationType::Elevated, std::optional stdinHandle = std::nullopt); WSLCExecutionResult RunWslcAndRedirectToFile( const std::wstring& commandLine, std::optional outputPath = std::nullopt, ElevationType elevationType = ElevationType::Elevated); +WSLCExecutionResult RunWslcWithStdinFile( + const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType = ElevationType::Elevated); void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType = ElevationType::Elevated); std::wstring GetWslcHeader();