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