Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2eddc9
Add wslc container cp command for tar archive upload
Jun 17, 2026
2c30479
Add unit and e2e tests for container cp command
Jun 17, 2026
960603f
Address code review comments
Jun 17, 2026
b148cca
Remove accidentally committed test VHD files
Jun 17, 2026
ff5aeb4
Fix clang format errors
Jun 17, 2026
dc3fa26
Add -a/--archive flag to container cp command
Jun 17, 2026
dabe0e0
Support boolean values for flag arguments (-a=true/false, --archive=t…
Jun 18, 2026
e64028c
Address code review feedback: validation, consistency, robustness
Jun 18, 2026
cdea6af
Fix clang format errors
Jun 18, 2026
dbe37e7
Implement bidirectional container cp (local<->container)
Jun 24, 2026
27286c3
Fix trailing backslash tar bug and add container cp e2e tests
Jun 24, 2026
fc3913e
Fix DownloadArchive/Export hang on HTTP error responses
Jun 24, 2026
9ce3d58
Address PR review comments for container cp
Jun 24, 2026
ebb5f66
Fix clang format errors
Jun 24, 2026
ce3ef82
Ensure custom stdin handle is inheritable in RunWslc
Jun 24, 2026
aadb355
Log setsockopt SO_RCVTIMEO failures instead of ignoring
Jun 24, 2026
d5210bb
Add try/catch for JSON parsing in Export error path
Jun 24, 2026
b5193a6
Fix trailing separator stripping for root paths in container→local cp
Jun 24, 2026
b9c2240
Rename task function CopyToContainer -> ContainerCp
Jun 24, 2026
565fca3
Fix clang format issues
Jun 24, 2026
e298698
Merge branch 'master' into feature/wslc-container-cp
ptrivedi Jun 24, 2026
4d8283e
Merge branch 'master' into feature/wslc-container-cp
ptrivedi Jun 24, 2026
0b2b16f
Fix file-destination semantics for container-to-local copy
Jun 24, 2026
739ec44
clang format fixes
Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ doc/site/
directory.build.targets
test-storage/
*.vhdx
*.vhd
*.tar
*.etl
*.lscache
__pycache__
__pycache__
deploy-log.txt
test-output*.txt
test-results.txt
testfile.txt
output/
52 changes: 52 additions & 0 deletions localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,10 @@ Usage:
<value>Flag argument cannot contain adjoined value: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_FlagInvalidBooleanError" xml:space="preserve">
<value>Invalid boolean value for flag argument: '{}'. Expected true, false, 1, or 0.</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ExtraPositionalError" xml:space="preserve">
<value>Found a positional argument when none was expected: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
Expand Down Expand Up @@ -2466,6 +2470,50 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<value>Write to a file, instead of STDOUT</value>
<comment>{Locked="STDOUT"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_ContainerCpDesc" xml:space="preserve">
<value>Copy files between a container and the local filesystem.</value>
</data>
<data name="WSLCCLI_ContainerCpLongDesc" xml:space="preserve">
<value>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</value>
<comment>{Locked="wslc"}{Locked="container cp"}{Locked="CONTAINER:PATH"}{Locked="LOCAL_PATH"}</comment>
</data>
<data name="WSLCCLI_CpSourceArgDescription" xml:space="preserve">
<value>Source: local path, CONTAINER:PATH, or '-' for stdin</value>
<comment>{Locked="-"}{Locked="CONTAINER:PATH"}</comment>
</data>
<data name="WSLCCLI_CpTargetArgDescription" xml:space="preserve">
<value>Destination: local path or CONTAINER:PATH</value>
<comment>{Locked="CONTAINER:PATH"}</comment>
</data>
<data name="WSLCCLI_CpInvalidTargetError" xml:space="preserve">
<value>Invalid destination format. Expected CONTAINER:PATH</value>
<comment>{Locked="CONTAINER:PATH"}</comment>
</data>
<data name="WSLCCLI_CpInvalidSourceError" xml:space="preserve">
<value>Invalid source format. Expected CONTAINER:PATH</value>
<comment>{Locked="CONTAINER:PATH"}</comment>
</data>
<data name="WSLCCLI_CpSourceNotFoundError" xml:space="preserve">
<value>Source path not found: {0}</value>
<comment>{0} is the source path</comment>
</data>
<data name="WSLCCLI_CpTarNotFoundError" xml:space="preserve">
<value>tar.exe not found. Windows tar is required for local file copy.</value>
<comment>{Locked="tar.exe"}</comment>
</data>
<data name="WSLCCLI_CpInvalidDirectionError" xml:space="preserve">
<value>Invalid copy direction. Use CONTAINER:PATH as either source or destination.</value>
<comment>{Locked="CONTAINER:PATH"}</comment>
</data>
<data name="WSLCCLI_CpStdinIsTerminalError" xml:space="preserve">
<value>Cannot read tar data from terminal. Pipe a tar archive to stdin.
Example: tar -cf - files | wslc container cp - CONTAINER:/path</value>
<comment>{Locked="tar -cf -"}{Locked="wslc container cp"}{Locked="CONTAINER:/path"}</comment>
</data>
<data name="WSLCCLI_ContainerInspectDesc" xml:space="preserve">
<value>Inspect a container.</value>
</data>
Expand Down Expand Up @@ -2766,6 +2814,10 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_AllArgDescription" xml:space="preserve">
<value>Show all regardless of state.</value>
</data>
<data name="WSLCCLI_ArchiveArgDescription" xml:space="preserve">
<value>Archive mode (accepted for Docker CLI compatibility)</value>
<comment>{Locked="Docker"}</comment>
</data>
<data name="WSLCCLI_BuildArgDescription" xml:space="preserve">
<value>Set build-time variables (KEY=VALUE)</value>
<comment>{Locked="KEY=VALUE"}Command line arguments should not be translated</comment>
Expand Down
2 changes: 2 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle);
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
}
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.

typedef struct _WSLCDeletedImageInformation
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/arguments/ArgumentDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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()) \
Expand Down
72 changes: 65 additions & 7 deletions src/windows/wslc/arguments/ArgumentParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> ParseBoolValue(std::wstring_view value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily a blocker, but in the future I think we should separate parser changes from new commands being introduced. This will make changes smaller and easier to review

{
if (string::IsEqual(value, L"true") || value == L"1")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using wsl::shared::string::ParseBool() instead of reimplementing it here

{
return true;
}

if (string::IsEqual(value, L"false") || value == L"0")
{
return false;
}

return std::nullopt;
}

ParseArgumentsStateMachine::ParseArgumentsStateMachine(
Invocation& inv, ArgMap& execArgs, std::vector<Argument> arguments, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) :
m_invocation(inv),
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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());
}
Comment thread
ptrivedi marked this conversation as resolved.

return {};
}

// No adjoined value — add the flag as true.
AddFlag(firstArg->Type());

// Process remaining adjoined flags
Expand Down Expand Up @@ -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());
}
Comment on lines +424 to +427

return {};
}

AddFlag(nextArg->Type());
currentPos = nextPos;
}
Expand Down Expand Up @@ -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())
{
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
Comment thread
ptrivedi marked this conversation as resolved.
return ArgumentException(Localization::WSLCCLI_FlagInvalidBooleanError(currArg));
}

if (boolVal.value())
{
AddFlag(arg.Type());
}
Comment thread
ptrivedi marked this conversation as resolved.

return {};
Comment thread
ptrivedi marked this conversation as resolved.
}
Comment thread
ptrivedi marked this conversation as resolved.

AddFlag(arg.Type());
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/ContainerCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ std::vector<std::unique_ptr<Command>> ContainerCommand::GetCommands() const
{
std::vector<std::unique_ptr<Command>> commands;
commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
commands.push_back(std::make_unique<ContainerCpCommand>(FullName()));
Comment thread
ptrivedi marked this conversation as resolved.
commands.push_back(std::make_unique<ContainerCreateCommand>(FullName()));
commands.push_back(std::make_unique<ContainerExecCommand>(FullName()));
commands.push_back(std::make_unique<ContainerExportCommand>(FullName()));
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/commands/ContainerCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Argument> 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
{
Expand Down
40 changes: 40 additions & 0 deletions src/windows/wslc/commands/ContainerCpCommand.cpp
Original file line number Diff line number Diff line change
@@ -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<Argument> 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
16 changes: 16 additions & 0 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<IWSLCContainer> 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<IWSLCContainer> 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<IWSLCContainer> container;
Expand Down
2 changes: 2 additions & 0 deletions src/windows/wslc/services/ContainerService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading