diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index ff843e40e..33500a863 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -1939,6 +1939,10 @@ Usage:
{} exited with: {}
{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Created session: '{}'
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
ID
@@ -2114,10 +2118,14 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Failed to open '{}': {}
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
-
+
No Containerfile or Dockerfile found in '{}'
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ No WSLC session found in '{}'
+ {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
WSLC is the Windows Subsystem for Linux Container CLI tool.
{Locked="WSLC"}Product names should not be translated
@@ -2277,6 +2285,17 @@ For privacy information about this product please visit https://aka.ms/privacy.<
Terminates an active session. If no session is specified, the default session will be terminated.
+
+ Enter a temporary session.
+
+
+ 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.
+ {Locked="GUID"}{Locked="stderr"}Technical terms should not be translated
+
+
+ Name for the session. If not provided, a GUID is generated.
+ {Locked="GUID"}Technical terms should not be translated
+
Open the settings file in the default editor.
@@ -2371,6 +2390,9 @@ On first run, creates the file with all settings commented out at their defaults
Session ID
+
+ Session storage path
+
Signal to send (default: {})
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp
index 746639f00..b47585500 100644
--- a/src/windows/common/WslClient.cpp
+++ b/src/windows/common/WslClient.cpp
@@ -19,8 +19,6 @@ Module Name:
#include "Distribution.h"
#include "CommandLine.h"
#include
-#include "wslc.h"
-#include "WSLCProcessLauncher.h"
#include "WslCoreFilesystem.h"
#define BASH_PATH L"/bin/bash"
@@ -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;
@@ -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(sessionSettings.FeatureFlags), L"--dns-tunneling");
- parser.AddArgument(SetFlag(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(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 ] [--shell ] [--memory ] [--cpu ] [--dns-tunneling] [--virtiofs] [--networking-mode ] [--fstype ] [--container-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 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 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> container;
- std::optional 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>(GetStdHandle(STD_INPUT_HANDLE), process->GetStdHandle(0)));
- }
-
- io.AddHandle(std::make_unique>(process->GetStdHandle(1), GetStdHandle(STD_OUTPUT_HANDLE)));
- io.AddHandle(std::make_unique>(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 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
@@ -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'-'))
diff --git a/src/windows/service/exe/WSLCSessionManager.cpp b/src/windows/service/exe/WSLCSessionManager.cpp
index 1b4333a2a..251cb7e8f 100644
--- a/src/windows/service/exe/WSLCSessionManager.cpp
+++ b/src/windows/service/exe/WSLCSessionManager.cpp
@@ -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();
@@ -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;
}
diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl
index 520fafa6a..8cbcbf0f7 100644
--- a/src/windows/service/inc/wslc.idl
+++ b/src/windows/service/inc/wslc.idl
@@ -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;
@@ -427,6 +436,7 @@ typedef struct _WSLCSessionSettings {
[unique] ITerminationCallback* TerminationCallback;
WSLCFeatureFlags FeatureFlags;
WSLCHandle DmesgOutput;
+ WSLCSessionStorageFlags StorageFlags;
// Below options are used for debugging purposes only.
[unique] LPCWSTR RootVhdOverride;
@@ -554,6 +564,7 @@ typedef struct _WSLCSessionInitSettings
ULONG CreatorPid;
LPCWSTR DisplayName;
LPCWSTR StoragePath;
+ WSLCSessionStorageFlags StorageFlags;
ULONGLONG MaximumStorageSizeMb;
ULONG BootTimeoutMs;
WSLCNetworkingMode NetworkingMode;
@@ -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),
diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h
index bc700ca70..ceddce9fa 100644
--- a/src/windows/wslc/arguments/ArgumentDefinitions.h
+++ b/src/windows/wslc/arguments/ArgumentDefinitions.h
@@ -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") \
_(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()) \
diff --git a/src/windows/wslc/commands/SessionCommand.cpp b/src/windows/wslc/commands/SessionCommand.cpp
index c439f0ea9..0774f824f 100644
--- a/src/windows/wslc/commands/SessionCommand.cpp
+++ b/src/windows/wslc/commands/SessionCommand.cpp
@@ -23,6 +23,7 @@ namespace wsl::windows::wslc {
std::vector> SessionCommand::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()));
diff --git a/src/windows/wslc/commands/SessionCommand.h b/src/windows/wslc/commands/SessionCommand.h
index 30f1865a9..19292aa73 100644
--- a/src/windows/wslc/commands/SessionCommand.h
+++ b/src/windows/wslc/commands/SessionCommand.h
@@ -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 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
{
diff --git a/src/windows/wslc/commands/SessionEnterCommand.cpp b/src/windows/wslc/commands/SessionEnterCommand.cpp
new file mode 100644
index 000000000..6cd2b9cf5
--- /dev/null
+++ b/src/windows/wslc/commands/SessionEnterCommand.cpp
@@ -0,0 +1,48 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ SessionEnterCommand.cpp
+
+Abstract:
+
+ Implementation of the session enter command.
+
+--*/
+
+#include "CLIExecutionContext.h"
+#include "SessionCommand.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 {
+
+std::vector SessionEnterCommand::GetArguments() const
+{
+ return {
+ Argument::Create(ArgType::StoragePath, true),
+ Argument::Create(ArgType::Name, std::nullopt, std::nullopt, Localization::WSLCCLI_SessionEnterNameArgDescription()),
+ };
+}
+
+std::wstring SessionEnterCommand::ShortDescription() const
+{
+ return Localization::WSLCCLI_SessionEnterDesc();
+}
+
+std::wstring SessionEnterCommand::LongDescription() const
+{
+ return Localization::WSLCCLI_SessionEnterLongDesc();
+}
+
+void SessionEnterCommand::ExecuteInternal(CLIExecutionContext& context) const
+{
+ context << EnterSession;
+}
+} // namespace wsl::windows::wslc
diff --git a/src/windows/wslc/services/SessionModel.h b/src/windows/wslc/services/SessionModel.h
index 12bbf1e9b..fe610d5fb 100644
--- a/src/windows/wslc/services/SessionModel.h
+++ b/src/windows/wslc/services/SessionModel.h
@@ -42,11 +42,18 @@ class SessionOptions
SessionOptions();
+ static const std::filesystem::path& GetStoragePath();
+
const WSLCSessionSettings* Get() const
{
return &m_sessionSettings;
}
+ WSLCSessionSettings* Get()
+ {
+ return &m_sessionSettings;
+ }
+
private:
static constexpr const wchar_t s_defaultSessionName[] = L"wslc-cli";
static constexpr const wchar_t s_defaultAdminSessionName[] = L"wslc-cli-admin";
@@ -54,7 +61,6 @@ class SessionOptions
static constexpr uint32_t s_defaultBootTimeoutMs = 30 * 1000;
static bool IsElevated();
- static const std::filesystem::path& GetStoragePath();
WSLCSessionSettings m_sessionSettings{};
};
diff --git a/src/windows/wslc/services/SessionService.cpp b/src/windows/wslc/services/SessionService.cpp
index f63c91f93..d58ade9b0 100644
--- a/src/windows/wslc/services/SessionService.cpp
+++ b/src/windows/wslc/services/SessionService.cpp
@@ -11,8 +11,10 @@ Module Name:
This file contains the SessionService implementation
--*/
+
#include "precomp.h"
#include "SessionService.h"
+#include "ConsoleService.h"
#include
#include
@@ -20,7 +22,6 @@ namespace wsl::windows::wslc::services {
using namespace wsl::shared;
using namespace wsl::windows::wslc::models;
namespace wslutil = wsl::windows::common::wslutil;
-DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionFlags);
int SessionService::Attach(const std::wstring& sessionName)
{
@@ -99,7 +100,7 @@ int SessionService::Attach(const std::wstring& sessionName)
return static_cast(exitCode);
}
-Session SessionService::CreateSession(const SessionOptions& options)
+Session SessionService::CreateSession(const SessionOptions& options, WSLCSessionFlags Flags)
{
const WSLCSessionSettings* settings = options.Get();
wil::com_ptr sessionManager;
@@ -107,11 +108,36 @@ Session SessionService::CreateSession(const SessionOptions& options)
wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
wil::com_ptr session;
- THROW_IF_FAILED(sessionManager->CreateSession(settings, WSLCSessionFlagsPersistent | WSLCSessionFlagsOpenExisting, &session));
+ THROW_IF_FAILED(sessionManager->CreateSession(settings, Flags, &session));
wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
return Session(std::move(session));
}
+int SessionService::Enter(const std::wstring& storagePath, const std::wstring& displayName)
+{
+ THROW_HR_IF(E_INVALIDARG, storagePath.empty());
+ THROW_HR_IF(E_INVALIDARG, displayName.empty());
+
+ // Build session settings from the user configuration, overriding storage path and display name.
+ SessionOptions options;
+ options.Get()->DisplayName = displayName.c_str();
+ options.Get()->StoragePath = storagePath.c_str();
+ options.Get()->StorageFlags = WSLCSessionStorageFlagsNoCreate; // Don't create storage if it doesn't exist.
+
+ // Create a non-persistent session: lifetime is tied to our COM reference.
+ auto session = SessionService::CreateSession(options, WSLCSessionFlagsNone);
+ wsl::windows::common::wslutil::PrintMessage(Localization::MessageWslcCreatedSession(displayName), stderr);
+
+ const std::string shell = "/bin/sh";
+ wsl::windows::common::WSLCProcessLauncher launcher{shell, {shell, "--login"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin};
+
+ wsl::windows::common::ConsoleState console;
+ const auto windowSize = console.GetWindowSize();
+ launcher.SetTtySize(windowSize.Y, windowSize.X);
+
+ return ConsoleService::AttachToCurrentConsole(launcher.Launch(*session.Get()));
+}
+
std::vector SessionService::List()
{
std::vector result;
diff --git a/src/windows/wslc/services/SessionService.h b/src/windows/wslc/services/SessionService.h
index 42013aa22..90fbcc2b9 100644
--- a/src/windows/wslc/services/SessionService.h
+++ b/src/windows/wslc/services/SessionService.h
@@ -27,7 +27,10 @@ struct SessionInformation
struct SessionService
{
static int Attach(const std::wstring& name);
- static wsl::windows::wslc::models::Session CreateSession(const wsl::windows::wslc::models::SessionOptions& options);
+ static wsl::windows::wslc::models::Session CreateSession(
+ const wsl::windows::wslc::models::SessionOptions& options,
+ WSLCSessionFlags Flags = WSLCSessionFlagsOpenExisting | WSLCSessionFlagsPersistent);
+ static int Enter(const std::wstring& storagePath, const std::wstring& displayName);
static std::vector List();
static wsl::windows::wslc::models::Session OpenSession(const std::wstring& displayName);
static int TerminateSession(const std::wstring& displayName);
diff --git a/src/windows/wslc/tasks/SessionTasks.cpp b/src/windows/wslc/tasks/SessionTasks.cpp
index ca2ec216e..8524504a7 100644
--- a/src/windows/wslc/tasks/SessionTasks.cpp
+++ b/src/windows/wslc/tasks/SessionTasks.cpp
@@ -106,4 +106,23 @@ void TerminateSession(CLIExecutionContext& context)
context.ExitCode = SessionService::TerminateSession(sessionId);
}
+void EnterSession(CLIExecutionContext& context)
+{
+ auto storagePath = std::filesystem::absolute(context.Args.Get());
+
+ std::wstring sessionName;
+ if (context.Args.Contains(ArgType::Name))
+ {
+ sessionName = context.Args.Get();
+ }
+ else
+ {
+ GUID guid{};
+ THROW_IF_FAILED(CoCreateGuid(&guid));
+ sessionName = wsl::shared::string::GuidToString(guid, wsl::shared::string::GuidToStringFlags::None);
+ }
+
+ context.ExitCode = SessionService::Enter(storagePath.wstring(), sessionName);
+}
+
} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslc/tasks/SessionTasks.h b/src/windows/wslc/tasks/SessionTasks.h
index f16c18f67..29ca1c01c 100644
--- a/src/windows/wslc/tasks/SessionTasks.h
+++ b/src/windows/wslc/tasks/SessionTasks.h
@@ -19,6 +19,7 @@ using wsl::windows::wslc::execution::CLIExecutionContext;
namespace wsl::windows::wslc::task {
void AttachToSession(CLIExecutionContext& context);
void CreateSession(CLIExecutionContext& context);
+void EnterSession(CLIExecutionContext& context);
void ListSessions(CLIExecutionContext& context);
void TerminateSession(CLIExecutionContext& context);
} // namespace wsl::windows::wslc::task
diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp
index b4834c422..7f781ca85 100644
--- a/src/windows/wslcsession/WSLCSession.cpp
+++ b/src/windows/wslcsession/WSLCSession.cpp
@@ -373,6 +373,11 @@ void WSLCSession::ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID
"Failed to attach vhd: %ls",
m_storageVhdPath.c_str());
+ THROW_HR_WITH_USER_ERROR_IF(
+ HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
+ Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath),
+ WI_IsFlagSet(Settings.StorageFlags, WSLCSessionStorageFlagsNoCreate));
+
// If the VHD wasn't found, create it.
WSL_LOG("CreateStorageVhd", TraceLoggingValue(m_storageVhdPath.c_str(), "StorageVhdPath"));
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index 00bbba7d8..bb3302524 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -355,6 +355,23 @@ class WSLCTests
wil::com_ptr session;
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
}
+
+ // Validate that creating a session on a non-existing storage fails if WSLCSessionStorageFlagsNoCreate is set.
+ {
+ auto settings = GetDefaultSessionSettings(L"storage-not-found");
+ settings.StoragePath = L"C:\\does-not-exist";
+ settings.StorageFlags = WSLCSessionStorageFlagsNoCreate;
+ wil::com_ptr session;
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
+ }
+
+ // Reject invalid storage flags.
+ {
+ auto settings = GetDefaultSessionSettings(L"invalid-storage-flags");
+ settings.StorageFlags = static_cast(0x2);
+ wil::com_ptr session;
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
+ }
}
void ExpectImagePresent(IWSLCSession& Session, const char* Image, bool Present = true)
diff --git a/test/windows/wslc/CommandLineTestCases.h b/test/windows/wslc/CommandLineTestCases.h
index ed1025fba..9d58f8e73 100644
--- a/test/windows/wslc/CommandLineTestCases.h
+++ b/test/windows/wslc/CommandLineTestCases.h
@@ -35,6 +35,12 @@ COMMAND_LINE_TEST_CASE(L"session shell session1", L"shell", true)
COMMAND_LINE_TEST_CASE(L"session shell", L"shell", true)
COMMAND_LINE_TEST_CASE(L"session terminate session1", L"terminate", true)
COMMAND_LINE_TEST_CASE(L"session terminate", L"terminate", true)
+COMMAND_LINE_TEST_CASE(L"session enter C:\\storage", L"enter", true)
+COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --name my-session", L"enter", true)
+COMMAND_LINE_TEST_CASE(L"session enter --name my-session C:\\storage", L"enter", true)
+COMMAND_LINE_TEST_CASE(L"session enter", L"enter", false) // Missing required storage-path
+COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --notanarg", L"enter", false) // Invalid argument
+COMMAND_LINE_TEST_CASE(L"session enter --name my-session", L"enter", false) // Missing required positional before flag
// Container command tests
COMMAND_LINE_TEST_CASE(L"container list", L"list", true)
diff --git a/test/windows/wslc/WSLCCLICommandUnitTests.cpp b/test/windows/wslc/WSLCCLICommandUnitTests.cpp
index f72c63df3..f251952b2 100644
--- a/test/windows/wslc/WSLCCLICommandUnitTests.cpp
+++ b/test/windows/wslc/WSLCCLICommandUnitTests.cpp
@@ -79,6 +79,37 @@ class WSLCCLICommandUnitTests
}
}
+ // Test: Verify SessionEnterCommand has the expected arguments
+ TEST_METHOD(SessionEnterCommand_HasExpectedArguments)
+ {
+ auto cmd = SessionEnterCommand(L"session");
+ auto args = cmd.GetArguments();
+
+ // Should have 2 arguments: storage-path (positional, required) and name (value, optional)
+ VERIFY_ARE_EQUAL(2u, args.size());
+
+ // Verify storage-path argument
+ auto& storagePath = args[0];
+ VERIFY_ARE_EQUAL(ArgType::StoragePath, storagePath.Type());
+ VERIFY_ARE_EQUAL(Kind::Positional, storagePath.Kind());
+ VERIFY_IS_TRUE(storagePath.Required());
+
+ // Verify name argument
+ auto& name = args[1];
+ VERIFY_ARE_EQUAL(ArgType::Name, name.Type());
+ VERIFY_ARE_EQUAL(Kind::Value, name.Kind());
+ VERIFY_IS_FALSE(name.Required());
+ }
+
+ // Test: Verify SessionEnterCommand descriptions are not empty
+ TEST_METHOD(SessionEnterCommand_HasDescriptions)
+ {
+ auto cmd = SessionEnterCommand(L"session");
+
+ VERIFY_IS_FALSE(cmd.ShortDescription().empty());
+ VERIFY_IS_FALSE(cmd.LongDescription().empty());
+ }
+
// Test: Verify ContainerCommand has subcommands
TEST_METHOD(ContainerCommand_HasSubcommands)
{
diff --git a/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp
new file mode 100644
index 000000000..7eb69a649
--- /dev/null
+++ b/test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp
@@ -0,0 +1,100 @@
+/*++
+
+Copyright (c) Microsoft. All rights reserved.
+
+Module Name:
+
+ WSLCE2ESessionEnterTests.cpp
+
+Abstract:
+
+ This file contains end-to-end tests for the wslc session enter command.
+--*/
+
+#include "precomp.h"
+#include "windows/Common.h"
+#include "WSLCCLITestHelpers.h"
+#include "WSLCExecutor.h"
+#include "WSLCE2EHelpers.h"
+#include "SessionModel.h"
+
+using namespace WEX::Logging;
+
+using wsl::windows::wslc::models::SessionOptions;
+
+namespace WSLCE2ETests {
+
+class WSLCE2ESessionEnterTests
+{
+ WSLC_TEST_CLASS(WSLCE2ESessionEnterTests)
+
+ TEST_CLASS_SETUP(TestClassSetup)
+ {
+ // Ensure that the wslc cli session storage is created.
+ RunWslc(L"image ls").Verify({.ExitCode = 0});
+
+ // Terminate the wslc session since we use its storage path in this test class.
+ RunWslc(L"session terminate");
+ return true;
+ }
+
+ TEST_METHOD(WSLCE2E_SessionEnter_WithName)
+ {
+ WSL2_TEST_ONLY();
+
+ constexpr auto sessionName = L"test-wslc-session-enter";
+
+ // Run an interactive session enter with an explicit name.
+ auto session = RunWslcInteractive(std::format(L"session enter \"{}\" --name {}", SessionOptions::GetStoragePath(), sessionName));
+ VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
+
+ session.ExpectStdout(VT::SESSION_PROMPT);
+
+ // Validate that the shell is running as root.
+ session.WriteLine("whoami");
+ session.ExpectStdout(VT::RESET);
+ session.ExpectCommandEcho("whoami");
+ session.ExpectStdout("root\r\n");
+ session.ExpectStdout(VT::SESSION_PROMPT);
+
+ // Verify the session appears in session list.
+ auto listResult = RunWslc(L"session list");
+ listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
+ VERIFY_IS_TRUE(listResult.Stdout.has_value());
+ VERIFY_IS_TRUE(listResult.Stdout->find(sessionName) != std::wstring::npos);
+
+ // Exit the shell.
+ VERIFY_ARE_EQUAL(session.Exit(), 0);
+
+ // Verify the session is no longer in the session list after exiting.
+ listResult = RunWslc(L"session list");
+ listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
+ VERIFY_IS_TRUE(listResult.Stdout.has_value());
+ VERIFY_IS_FALSE(listResult.Stdout->find(sessionName) != std::wstring::npos);
+ }
+
+ TEST_METHOD(WSLCE2E_SessionEnter_WithoutName_GeneratesGuid)
+ {
+ WSL2_TEST_ONLY();
+
+ auto session = RunWslcInteractive(std::format(L"session enter \"{}\"", SessionOptions::GetStoragePath()));
+ VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
+
+ session.ExpectStderr("Created session: ");
+ session.ExpectStdout(VT::SESSION_PROMPT);
+
+ VERIFY_ARE_EQUAL(session.Exit(), 0);
+ }
+
+ TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound)
+ {
+ WSL2_TEST_ONLY();
+
+ auto result = RunWslc(L"session enter does-not-exist");
+ result.Verify({
+ .Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n",
+ .ExitCode = 1,
+ });
+ }
+};
+} // namespace WSLCE2ETests