Skip to content
24 changes: 23 additions & 1 deletion localization/strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,10 @@ Usage:
<value>{} exited with: {}</value>
<comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcCreatedSession" xml:space="preserve">
<value>Created session: '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="MessageWslcHeaderId" xml:space="preserve">
<value>ID</value>
</data>
Expand Down Expand Up @@ -2114,10 +2118,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<value>Failed to open '{}': {}</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name = "MessageWslcBuildFileNotFound" xml:space = "preserve" >
<data name = "MessageWslcBuildFileNotFound" xml:space = "preserve" >
<value>No Containerfile or Dockerfile found in '{}'</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name = "MessageWslcSessionStorageNotFound" xml:space = "preserve" >
<value>No WSLC session found in '{}'</value>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

MessageWslcSessionStorageNotFound currently says "No WSLC session found in '{}'", but it’s used when the storage path doesn’t exist / can’t be opened. This reads like a lookup failure by name rather than a filesystem/storage issue. Consider rewording to reference the storage path (e.g., "No WSLC session storage found at '{}'" or similar) to make the error actionable.

Suggested change
<value>No WSLC session found in '{}'</value>
<value>No WSLC session storage found at '{}'</value>

Copilot uses AI. Check for mistakes.
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
</data>
<data name="WSLCCLI_RootCommandDesc" xml:space="preserve">
<value>WSLC is the Windows Subsystem for Linux Container CLI tool.</value>
<comment>{Locked="WSLC"}Product names should not be translated</comment>
Expand Down Expand Up @@ -2277,6 +2285,17 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<data name="WSLCCLI_SessionTerminateLongDesc" xml:space="preserve">
<value>Terminates an active session. If no session is specified, the default session will be terminated.</value>
</data>
<data name="WSLCCLI_SessionEnterDesc" xml:space="preserve">
<value>Enter a temporary session.</value>
</data>
<data name="WSLCCLI_SessionEnterLongDesc" xml:space="preserve">
<value>Creates a non-persistent session with the given storage path and opens a shell into it. The session is deleted when the shell exits. If no name is provided, a GUID is generated and printed to stderr.</value>
<comment>{Locked="GUID"}{Locked="stderr"}Technical terms should not be translated</comment>
</data>
<data name="WSLCCLI_SessionEnterNameArgDescription" xml:space="preserve">
<value>Name for the session. If not provided, a GUID is generated.</value>
<comment>{Locked="GUID"}Technical terms should not be translated</comment>
</data>
<data name="WSLCCLI_SettingsCommandDesc" xml:space="preserve">
<value>Open the settings file in the default editor.</value>
</data>
Expand Down Expand Up @@ -2371,6 +2390,9 @@ On first run, creates the file with all settings commented out at their defaults
<data name="WSLCCLI_SessionIdPositionalArgDescription" xml:space="preserve">
<value>Session ID</value>
</data>
<data name="WSLCCLI_SessionStoragePositionalArgDescription" xml:space="preserve">
<value>Session storage path</value>
</data>
<data name="WSLCCLI_SignalArgDescription" xml:space="preserve">
<value>Signal to send (default: {})</value>
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
Expand Down
192 changes: 0 additions & 192 deletions src/windows/common/WslClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ Module Name:
#include "Distribution.h"
#include "CommandLine.h"
#include <conio.h>
#include "wslc.h"
#include "WSLCProcessLauncher.h"
#include "WslCoreFilesystem.h"

#define BASH_PATH L"/bin/bash"
Expand All @@ -30,7 +28,6 @@ using winrt::Windows::Management::Deployment::DeploymentOptions;
using wsl::shared::Localization;
using wsl::windows::common::ClientExecutionContext;
using wsl::windows::common::Context;
using wsl::windows::common::WSLCProcessLauncher;
using namespace wsl::windows::common;
using namespace wsl::shared;
using namespace wsl::windows::common::distribution;
Expand Down Expand Up @@ -1521,191 +1518,6 @@ int RunDebugShell()
THROW_HR(HCS_E_CONNECTION_CLOSED);
}

DEFINE_ENUM_FLAG_OPERATORS(WSLCFeatureFlags);

// Temporary debugging tool for WSLC
int WslcShell(_In_ std::wstring_view commandLine)
{
WSLCSessionSettings sessionSettings{};
sessionSettings.DisplayName = L"WSLCShell";
sessionSettings.CpuCount = 4;
sessionSettings.MemoryMb = 4096;
sessionSettings.NetworkingMode = WSLCNetworkingModeVirtioProxy;
sessionSettings.BootTimeoutMs = 30 * 1000;
sessionSettings.MaximumStorageSizeMb = 4096;

std::string shell = "/bin/bash";
std::string cmd;

bool help = false;
bool noTty = false;
std::wstring debugShell;

std::wstring storagePath;
std::wstring rootVhdOverride;
std::string rootVhdTypeOverride;
ArgumentParser parser(std::wstring{commandLine}, WSL_BINARY_NAME);
parser.AddArgument(rootVhdOverride, L"--vhd");
parser.AddArgument(Utf8String(shell), L"--shell");
parser.AddArgument(SetFlag<WslcFeatureFlagsDnsTunneling>(sessionSettings.FeatureFlags), L"--dns-tunneling");
parser.AddArgument(SetFlag<WslcFeatureFlagsVirtioFs>(sessionSettings.FeatureFlags), L"--virtiofs");
parser.AddArgument(Integer(sessionSettings.MemoryMb), L"--memory");
parser.AddArgument(Integer(sessionSettings.CpuCount), L"--cpu");
parser.AddArgument(Utf8String(rootVhdTypeOverride), L"--fstype");
parser.AddArgument(storagePath, L"--storage");
parser.AddArgument(Integer(reinterpret_cast<int&>(sessionSettings.NetworkingMode)), L"--networking-mode");
parser.AddArgument(debugShell, L"--debug-shell");
parser.AddArgument(noTty, L"--no-tty");
parser.AddArgument(help, L"--help");
parser.Parse();

if (help)
{
const auto usage = std::format(
LR"({} --wslc [--vhd </path/to/vhd>] [--shell </path/to/shell>] [--memory <memory-mb>] [--cpu <cpus>] [--dns-tunneling] [--virtiofs] [--networking-mode <mode>] [--fstype <fstype>] [--container-vhd </path/to/vhd>] [--help])",
WSL_BINARY_NAME);

wprintf(L"%ls\n", usage.c_str());
return 1;
}

switch (sessionSettings.NetworkingMode)
{
case WSLCNetworkingMode::WSLCNetworkingModeNone:
case WSLCNetworkingMode::WSLCNetworkingModeNAT:
case WSLCNetworkingMode::WSLCNetworkingModeVirtioProxy:
break;
default:
THROW_HR(E_INVALIDARG);
}

wil::com_ptr<IWSLCSessionManager> sessionManager;
THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());

wil::com_ptr<IWSLCSession> session;

if (!rootVhdOverride.empty())
{
if (rootVhdTypeOverride.empty())
{
wprintf(L"--fstype required when --vhd is passed\n");
return 1;
}

sessionSettings.RootVhdOverride = rootVhdOverride.c_str();
sessionSettings.RootVhdTypeOverride = rootVhdTypeOverride.c_str();
}

if (!storagePath.empty())
{
storagePath = std::filesystem::weakly_canonical(storagePath).wstring();
sessionSettings.StoragePath = storagePath.c_str();
}

if (!debugShell.empty())
{
THROW_IF_FAILED(sessionManager->OpenSessionByName(debugShell.c_str(), &session));
}
else
{
THROW_IF_FAILED(sessionManager->CreateSession(&sessionSettings, WSLCSessionFlagsNone, &session));
}

wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());

std::optional<wil::com_ptr<IWSLCContainer>> container;
std::optional<wsl::windows::common::ClientRunningWSLCProcess> process;
// Get the terminal size.
HANDLE Stdout = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE Stdin = GetStdHandle(STD_INPUT_HANDLE);

CONSOLE_SCREEN_BUFFER_INFOEX Info{};
Info.cbSize = sizeof(Info);
THROW_IF_WIN32_BOOL_FALSE(::GetConsoleScreenBufferInfoEx(Stdout, &Info));

wsl::windows::common::WSLCProcessLauncher launcher{shell, {shell, "--login"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty};
launcher.SetTtySize(Info.srWindow.Bottom - Info.srWindow.Top + 1, Info.srWindow.Right - Info.srWindow.Left + 1);

process = launcher.Launch(*session);

if (noTty)
{
using namespace wsl::windows::common::relay;
wsl::windows::common::relay::MultiHandleWait io;

// Create a thread to relay stdin to the pipe.

std::thread inputThread;
wil::unique_event exitEvent{wil::EventOptions::ManualReset};

auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
if (inputThread.joinable())
{
exitEvent.SetEvent();
inputThread.join();
}
});

// Required because ReadFile() blocks if stdin is a tty.
if (wsl::windows::common::wslutil::IsInteractiveConsole())
{
inputThread = std::thread{[&]() {
wsl::windows::common::relay::StandardInputRelay(Stdin, process->GetStdHandle(0).get(), []() {}, exitEvent.get());
}};
}
else
{
io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(GetStdHandle(STD_INPUT_HANDLE), process->GetStdHandle(0)));
}

io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(process->GetStdHandle(1), GetStdHandle(STD_OUTPUT_HANDLE)));
io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(process->GetStdHandle(2), GetStdHandle(STD_ERROR_HANDLE)));

io.Run({});
}
else
{
// Configure console for interactive usage.
wsl::windows::common::ConsoleState console;
auto exitEvent = wil::unique_event(wil::EventOptions::ManualReset);

std::vector<wil::unique_handle> handleStorage;
HANDLE ttyInput = nullptr;
HANDLE ttyOutput = nullptr;
auto& it = handleStorage.emplace_back(process->GetStdHandle(WSLCFDTty));
ttyInput = it.get();
ttyOutput = it.get();

{
// Create a thread to relay stdin to the pipe.
std::thread inputThread([&]() {
auto updateTerminal = [&console, &process]() {
const auto windowSize = console.GetWindowSize();
LOG_IF_FAILED(process->Get().ResizeTty(windowSize.Y, windowSize.X));
};

wsl::windows::common::relay::StandardInputRelay(Stdin, ttyInput, updateTerminal, exitEvent.get());
});

auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
exitEvent.SetEvent();
inputThread.join();
});

// Relay the contents of the pipe to stdout.
wsl::windows::common::relay::InterruptableRelay(ttyOutput, Stdout);
}
}

process->GetExitEvent().wait();

auto exitCode = process->GetExitCode();
wprintf(L"%hs exited with: %i", shell.c_str(), exitCode);

return exitCode;
}

int WslMain(_In_ std::wstring_view commandLine)
{
// Call the MSI package if we're in an MSIX context
Expand Down Expand Up @@ -1948,10 +1760,6 @@ int WslMain(_In_ std::wstring_view commandLine)

return Uninstall();
}
else if (argument == L"--wslc")
{
return WslcShell(commandLine);
}
else
{
if ((argument.size() > 0) && (argument[0] == L'-'))
Expand Down
6 changes: 6 additions & 0 deletions src/windows/service/exe/WSLCSessionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings,
// Ensure that the session display name is non-null and not too long.
THROW_HR_IF(E_INVALIDARG, Settings->DisplayName == nullptr);
THROW_HR_IF(E_INVALIDARG, wcslen(Settings->DisplayName) >= std::size(WSLCSessionInformation{}.DisplayName));
THROW_HR_IF_MSG(
E_INVALIDARG,
WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid),
"Invalid storage flags: %i",
Settings->StorageFlags);

auto tokenInfo = GetCallingProcessTokenInfo();

Expand Down Expand Up @@ -212,6 +217,7 @@ WSLCSessionInitSettings WSLCSessionManagerImpl::CreateSessionSettings(_In_ ULONG
sessionSettings.NetworkingMode = Settings->NetworkingMode;
sessionSettings.FeatureFlags = Settings->FeatureFlags;
sessionSettings.RootVhdTypeOverride = Settings->RootVhdTypeOverride;
sessionSettings.StorageFlags = Settings->StorageFlags;
return sessionSettings;
}

Expand Down
14 changes: 14 additions & 0 deletions src/windows/service/inc/wslc.idl
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,15 @@ interface IWSLCVirtualMachine : IUnknown
HRESULT RemoveShare([in] REFGUID ShareId);
}

typedef enum _WSLCSessionStorageFlags
{
WSLCSessionStorageFlagsNone = 0,
WSLCSessionStorageFlagsNoCreate = 1, // Open an existing storage path, but don't create a new one.
WSLCSessionStorageFlagsValid = WSLCSessionStorageFlagsNoCreate
} WSLCSessionStorageFlags;

cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionStorageFlags);")

// Settings for IWSLCSessionManager::CreateSession - full session configuration
typedef struct _WSLCSessionSettings {
LPCWSTR DisplayName;
Expand All @@ -427,6 +436,7 @@ typedef struct _WSLCSessionSettings {
[unique] ITerminationCallback* TerminationCallback;
WSLCFeatureFlags FeatureFlags;
WSLCHandle DmesgOutput;
WSLCSessionStorageFlags StorageFlags;

Comment on lines 427 to 440
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Adding StorageFlags to WSLCSessionSettings and WSLCSessionInitSettings changes the wire/layout contract used by IWSLCSessionManager::CreateSession. This is an ABI/RPC breaking change for any external SDK consumers built against the previous struct definition. To keep compatibility, this should be versioned (e.g., introduce a WSLCSessionSettingsV2/InitSettingsV2 with a new CreateSessionV2 entry point or a size/version field pattern) rather than modifying the existing structs in-place.

Copilot uses AI. Check for mistakes.
Comment on lines 436 to 440
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

WSLCSessionSettings is part of the COM/RPC contract for IWSLCSessionManager::CreateSession. Adding StorageFlags changes the struct layout and will break existing clients built against the previous header/IDL. This should be versioned (e.g., introduce a WSLCSessionSettingsV2 + CreateSession2, or another backward-compatible mechanism) rather than modifying the existing struct in-place.

Copilot uses AI. Check for mistakes.
// Below options are used for debugging purposes only.
[unique] LPCWSTR RootVhdOverride;
Expand Down Expand Up @@ -554,6 +564,7 @@ typedef struct _WSLCSessionInitSettings
ULONG CreatorPid;
LPCWSTR DisplayName;
LPCWSTR StoragePath;
WSLCSessionStorageFlags StorageFlags;
ULONGLONG MaximumStorageSizeMb;
Comment on lines 564 to 568
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

WSLCSessionInitSettings is also part of the cross-process initialization contract. Adding StorageFlags changes the marshaled struct layout and breaks compatibility with older components (service/user COM server) that still expect the previous struct definition. Consider versioning the init settings struct (or the Initialize call) instead of extending it in-place.

Copilot uses AI. Check for mistakes.
ULONG BootTimeoutMs;
WSLCNetworkingMode NetworkingMode;
Expand Down Expand Up @@ -681,6 +692,9 @@ typedef enum _WSLCSessionFlags
WSLCSessionFlagsOpenExisting = 2, // Open an existing session if the name is in use.
} WSLCSessionFlags;

cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionFlags);")


[
uuid(82A7ABC8-6B50-43FC-AB96-15FBBE7E8760),
pointer_default(unique),
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 @@ -72,6 +72,7 @@ _(Remove, "rm", NO_ALIAS, Kind::Flag, L
/*_(Scheme, "scheme", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SchemeArgDescription())*/ \
_(Session, "session", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SessionIdArgDescription()) \
_(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

ArgType::StoragePath is introduced with a hard-coded English description string. All other arguments in this table use Localization::WSLCCLI_* resources, and this PR also adds WSLCCLI_SessionStoragePositionalArgDescription in Resources.resw but it isn't used here. Please wire this argument description to a localized resource for consistency and localization support.

Suggested change
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \
_(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionStoragePositionalArgDescription()) \

Copilot uses AI. Check for mistakes.
_(Signal, "signal", L"s", Kind::Value, Localization::WSLCCLI_SignalArgDescription(L"SIGKILL")) \
_(Tag, "tag", L"t", Kind::Value, Localization::WSLCCLI_TagArgDescription()) \
_(Time, "time", L"t", Kind::Value, Localization::WSLCCLI_TimeArgDescription()) \
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/commands/SessionCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace wsl::windows::wslc {
std::vector<std::unique_ptr<Command>> SessionCommand::GetCommands() const
{
std::vector<std::unique_ptr<Command>> commands;
commands.push_back(std::make_unique<SessionEnterCommand>(FullName()));
commands.push_back(std::make_unique<SessionListCommand>(FullName()));
commands.push_back(std::make_unique<SessionShellCommand>(FullName()));
commands.push_back(std::make_unique<SessionTerminateCommand>(FullName()));
Expand Down
15 changes: 15 additions & 0 deletions src/windows/wslc/commands/SessionCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ struct SessionShellCommand final : public Command
void ExecuteInternal(CLIExecutionContext& context) const override;
};

// Enter Command
struct SessionEnterCommand final : public Command
{
constexpr static std::wstring_view CommandName = L"enter";
SessionEnterCommand(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;
};

// Terminate Command
struct SessionTerminateCommand final : public Command
{
Expand Down
Loading