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();