Implement wslc session enter#40088
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new wslc session enter command intended for ephemeral/debugging sessions by creating a session with a user-specified storage path, attaching an interactive shell, and adding test coverage around the new command.
Changes:
- Introduces
session enteras a new subcommand undersession, including CLI argument definitions (storage-path, optional--name). - Implements the execution path for entering a session (task + service), launching an interactive shell attached to the current console.
- Adds unit tests, command-line parsing test cases, and new E2E tests for
session enter.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLICommandUnitTests.cpp | Adds unit tests validating SessionEnterCommand argument shape and non-empty descriptions. |
| test/windows/wslc/e2e/WSLCE2ESessionEnterTests.cpp | Adds E2E coverage for interactive session enter flows (with/without name, deletion expectations, missing args). |
| test/windows/wslc/CommandLineTestCases.h | Adds command-line parsing cases for session enter. |
| src/windows/wslc/tasks/SessionTasks.h | Declares the new EnterSession task entrypoint. |
| src/windows/wslc/tasks/SessionTasks.cpp | Implements EnterSession argument handling (storage path + optional name or generated GUID). |
| src/windows/wslc/services/SessionService.h | Declares SessionService::Enter(...). |
| src/windows/wslc/services/SessionService.cpp | Implements Enter(...) to create a session and launch/attach an interactive shell. |
| src/windows/wslc/services/SessionModel.h | Adds non-const SessionOptions::Get() to allow modifying session settings. |
| src/windows/wslc/services/ConsoleService.h | Adjusts AttachToCurrentConsole to be callable statically. |
| src/windows/wslc/commands/SessionEnterCommand.cpp | Adds the new command implementation (enter) and wires it to EnterSession. |
| src/windows/wslc/commands/SessionCommand.h | Declares SessionEnterCommand. |
| src/windows/wslc/commands/SessionCommand.cpp | Registers SessionEnterCommand as a session subcommand. |
| src/windows/wslc/arguments/ArgumentDefinitions.h | Adds the new positional argument type StoragePath (storage-path). |
| options.Get()->DisplayName = displayName.c_str(); | ||
| options.Get()->StoragePath = storagePath.c_str(); | ||
|
|
||
| // Create a non-persistent session: lifetime is tied to our COM reference. | ||
| auto session = SessionService::CreateSession(options); |
There was a problem hiding this comment.
SessionService::Enter currently calls SessionService::CreateSession(options), but CreateSession hard-codes WSLCSessionFlagsPersistent | WSLCSessionFlagsOpenExisting. That contradicts the “non-persistent” semantics described here and in SessionEnterCommand (and will cause the entered session to remain active/listed after the shell exits, or to unexpectedly reuse an existing session when the display name collides). Consider either adding a CreateSession overload that takes WSLCSessionFlags or creating the session directly in Enter() with WSLCSessionFlagsNone (and likely without OpenExisting) so the session is actually deleted when the COM reference is released.
| m_storageVhdPath.c_str()); | ||
|
|
||
| THROW_HR_WITH_USER_ERROR_IF( | ||
| HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), |
There was a problem hiding this comment.
In the AttachDisk failure path, this always throws HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) when WSLCSessionStorageFlagsNoCreate is set, even if the actual failure was ERROR_FILE_NOT_FOUND (missing storage.vhdx in an existing directory). This can surface an incorrect HRESULT/error code to callers and make diagnostics harder. Consider throwing with the actual result HRESULT (and/or mapping both ERROR_PATH_NOT_FOUND and ERROR_FILE_NOT_FOUND appropriately) when NoCreate is set, rather than hard-coding ERROR_PATH_NOT_FOUND.
| HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), | |
| result, |
| .Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n", | ||
| .ExitCode = 1, | ||
| }); |
There was a problem hiding this comment.
This test expects the system error string for a missing storage path, but the implementation throws a user-facing localized message (MessageWslcSessionStorageNotFound) when StorageFlags=NoCreate, so stderr will likely be the localized message + "Error code: ..." instead. Update the expected stderr (or adjust the implementation to intentionally use the system message) so the test matches the intended UX.
| .Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n", | |
| .ExitCode = 1, | |
| }); | |
| .ExitCode = 1, | |
| }); | |
| VERIFY_IS_TRUE(result.Stderr.has_value()); | |
| VERIFY_IS_TRUE(result.Stderr->find(L"Error code: ERROR_PATH_NOT_FOUND\r\n") != std::wstring::npos); |
| @@ -427,6 +435,7 @@ typedef struct _WSLCSessionSettings { | |||
| [unique] ITerminationCallback* TerminationCallback; | |||
| WSLCFeatureFlags FeatureFlags; | |||
| WSLCHandle DmesgOutput; | |||
| WSLCSessionStorageFlags StorageFlags; | |||
|
|
|||
There was a problem hiding this comment.
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.
| THROW_HR_IF_MSG( | ||
| E_INVALIDARG, | ||
| WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid), | ||
| "Invalid storage flags flags: %i", |
There was a problem hiding this comment.
nit: duplicate "flags" in error message
dkbennett
left a comment
There was a problem hiding this comment.
Looks like an exemplary CLI command implementation! Few comments that are not blocking.
| 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) |
There was a problem hiding this comment.
"session enter --name foo c:\storage" should also be valid, but not necessary, we have lots of coverage of this pattern for commands.
There was a problem hiding this comment.
I'm going to push another iteration to fix something else, will add
| { | ||
| WSL2_TEST_ONLY(); | ||
|
|
||
| auto session = RunWslcInteractive(std::format(L"session enter \"{}\"", SessionOptions::GetStoragePath())); |
There was a problem hiding this comment.
Is this pattern of using the default storage location for session enter a common way of doing it, or just for convenient testing? If it's common then perhaps we can make the storage path not required and use GetStoragePath internally if no path is provided to use the default session's storage.
There was a problem hiding this comment.
I just used that to make testing convenient (and faster), since we're in the CLI tests we need that the CLI session exists.
Right now I don't think we'd want wslc session enter to use the CLI session storage by default, because that would create a non persistent session that will use the CLI's storage but a different name, which would lead to confusing errors if the users tried to use the CLI session in the "regular" way in parallel
| [unique] ITerminationCallback* TerminationCallback; | ||
| WSLCFeatureFlags FeatureFlags; | ||
| WSLCHandle DmesgOutput; | ||
| WSLCSessionStorageFlags StorageFlags; | ||
|
|
There was a problem hiding this comment.
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.
| ULONG CreatorPid; | ||
| LPCWSTR DisplayName; | ||
| LPCWSTR StoragePath; | ||
| WSLCSessionStorageFlags StorageFlags; | ||
| ULONGLONG MaximumStorageSizeMb; |
There was a problem hiding this comment.
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.
| WSLCSessionFlagsNone = 0, | ||
| WSLCSessionFlagsPersistent = 1, // Session remains active after its COM reference is released. | ||
| WSLCSessionFlagsOpenExisting = 2, // Open an existing session if the name is in use. | ||
| WSLCSessionFlagsOpenExistingStorage = 4, // Only open an existing storage folder, don't create one if it doesn't already exist. |
There was a problem hiding this comment.
WSLCSessionFlagsOpenExistingStorage is introduced in the IDL, but there is no implementation that checks this bit (only WSLCSessionFlagsOpenExisting/Persistent are used in WSLCSessionManager). Either implement its semantics in the service or remove it to avoid a misleading/unused public flag.
| WSLCSessionFlagsOpenExistingStorage = 4, // Only open an existing storage folder, don't create one if it doesn't already exist. |
| // Terminate the wslc session since we use its storage path in this test class. | ||
| RunWslc(L"session terminate"); |
There was a problem hiding this comment.
TestClassSetup calls RunWslc(L"session terminate") without asserting success or explicitly tolerating expected failures (e.g., session not present). Consider using the existing EnsureSessionIsTerminated() helper (or at least verifying the result / tolerating ERROR_NOT_FOUND) so setup failures don’t silently mask issues or leave state behind.
| // Terminate the wslc session since we use its storage path in this test class. | |
| RunWslc(L"session terminate"); | |
| // Terminate any existing wslc session since we use its storage path in this test class. | |
| EnsureSessionIsTerminated(); |
There was a problem hiding this comment.
That's because that session isn't necessarily running
| #include "precomp.h" | ||
| #include "SessionService.h" | ||
| #include "ConsoleService.h" | ||
| #include "UserSettings.h" |
There was a problem hiding this comment.
UserSettings.h appears unused in this translation unit (no symbols referenced from it). Consider dropping the include to reduce build dependencies and avoid unused-include drift.
| #include "UserSettings.h" |
…oneblue/session-enter
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
src/windows/service/inc/wslc.idl:444
- Adding
StorageFlagstoWSLCSessionSettings(and the related init/settings structs) changes the COM/RPC contract shape generated from this IDL. If there are any external clients (SDK/nuget) built against the previous IDL, this is a breaking change and typically requires versioning (new struct/interface/IID) rather than in-place modification.
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;
LPCWSTR StoragePath;
ULONGLONG MaximumStorageSizeMb;
ULONG CpuCount;
ULONG MemoryMb;
ULONG BootTimeoutMs;
WSLCNetworkingMode NetworkingMode;
[unique] ITerminationCallback* TerminationCallback;
WSLCFeatureFlags FeatureFlags;
WSLCHandle DmesgOutput;
WSLCSessionStorageFlags StorageFlags;
// Below options are used for debugging purposes only.
[unique] LPCWSTR RootVhdOverride;
[unique] LPCSTR RootVhdTypeOverride;
} WSLCSessionSettings;
src/windows/common/WslClient.cpp:1766
wsl.exe --wslcsupport is removed here (the argument is no longer handled). Since the PR description frameswslc session enteras a long-term replacement, consider either keeping--wslcas a deprecated alias that forwards to the new command (or printing a clear upgrade message) to avoid a silent breaking change for existing workflows.
else if (argument == WSL_UNINSTALL_ARG)
{
commandLine = wsl::windows::common::helpers::ConsumeArgument(commandLine, argument);
argument = wsl::windows::common::helpers::ParseArgument(commandLine);
if (!argument.empty())
{
wsl::windows::common::wslutil::PrintMessage(
Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG), stdout);
return exitCode;
}
return Uninstall();
}
else
{
if ((argument.size() > 0) && (argument[0] == L'-'))
{
| <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve"> | ||
| <value>Requested load but no input provided.</value> | ||
| </data> | ||
| >>>>>>> origin/feature/wsl-for-apps |
There was a problem hiding this comment.
There is a leftover merge-conflict marker (">>>>>>> origin/feature/wsl-for-apps") in this .resw file, which will break localization parsing/build. Remove the conflict marker and ensure the XML is well-formed before merging.
| >>>>>>> origin/feature/wsl-for-apps |
| typedef enum _WSLCSessionFlags | ||
| { | ||
| WSLCSessionFlagsNone = 0, | ||
| WSLCSessionFlagsPersistent = 1, // Session remains active after its COM reference is released. | ||
| WSLCSessionFlagsOpenExisting = 2, // Open an existing session if the name is in use. | ||
| WSLCSessionFlagsOpenExistingStorage = 4, // Only open an existing storage folder, don't create one if it doesn't already exist. | ||
| } WSLCSessionFlags; | ||
|
|
||
| cpp_quote("DEFINE_ENUM_FLAG_OPERATORS(WSLCSessionFlags);") | ||
|
|
There was a problem hiding this comment.
WSLCSessionFlagsOpenExistingStorage is added to the public flags enum, but WSLCSessionManagerImpl::CreateSession currently only consults WSLCSessionFlagsOpenExisting and WSLCSessionFlagsPersistent when deciding behavior. As-is, this new flag appears to have no effect; either plumb it through (e.g., map it to StorageFlagsNoCreate) or remove it to avoid misleading API surface.
| /*_(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") \ |
There was a problem hiding this comment.
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.
| _(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \ | |
| _(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionStoragePositionalArgDescription()) \ |
| ArgType::Name, std::nullopt, std::nullopt, L"Name for the session. If not provided, a GUID is generated."), | ||
| }; | ||
| } | ||
|
|
||
| std::wstring SessionEnterCommand::ShortDescription() const | ||
| { | ||
| return {L"Enter a temporary session."}; | ||
| } | ||
|
|
||
| std::wstring SessionEnterCommand::LongDescription() const | ||
| { | ||
| return { | ||
| L"Creates a non-persistent session with the given storage path and opens a shell into it. " | ||
| L"The session is deleted when the shell exits. If no name is provided, a GUID is generated and printed to stderr."}; |
There was a problem hiding this comment.
This command introduces multiple user-facing help/description strings as raw literals (Name for the session..., Enter a temporary session., and the long description). Other commands use Localization::WSLCCLI_* resources for these strings; please add corresponding localized resources and use them here so wslc --help output can be localized consistently.
| ArgType::Name, std::nullopt, std::nullopt, L"Name for the session. If not provided, a GUID is generated."), | |
| }; | |
| } | |
| std::wstring SessionEnterCommand::ShortDescription() const | |
| { | |
| return {L"Enter a temporary session."}; | |
| } | |
| std::wstring SessionEnterCommand::LongDescription() const | |
| { | |
| return { | |
| L"Creates a non-persistent session with the given storage path and opens a shell into it. " | |
| L"The session is deleted when the shell exits. If no name is provided, a GUID is generated and printed to stderr."}; | |
| ArgType::Name, std::nullopt, std::nullopt, Localization::WSLCCLI_SESSIONENTER_NAME_DESCRIPTION), | |
| }; | |
| } | |
| std::wstring SessionEnterCommand::ShortDescription() const | |
| { | |
| return {Localization::WSLCCLI_SESSIONENTER_DESCRIPTION}; | |
| } | |
| std::wstring SessionEnterCommand::LongDescription() const | |
| { | |
| return {Localization::WSLCCLI_SESSIONENTER_LONG_DESCRIPTION}; |
| // TODO: Consider adding a 'create' verb to do that. | ||
| auto session = SessionService::CreateSession(options, WSLCSessionFlagsOpenExistingStorage); | ||
| wsl::windows::common::wslutil::PrintMessage(std::format(L"Created session: {}", displayName), stderr); | ||
|
|
There was a problem hiding this comment.
The status output Created session: ... is user-facing and currently hard-coded via std::format. Please route this through the localization system (e.g., a Localization::... resource) so the interactive command output can be localized consistently with the rest of the CLI.
| WSLCNetworkingMode NetworkingMode; | ||
| [unique] ITerminationCallback* TerminationCallback; | ||
| WSLCFeatureFlags FeatureFlags; | ||
| WSLCHandle DmesgOutput; | ||
| WSLCSessionStorageFlags StorageFlags; | ||
|
|
There was a problem hiding this comment.
Adding StorageFlags to WSLCSessionSettings changes the MIDL struct layout used by IWSLCSessionManager::CreateSession. This is an ABI/RPC contract break for any existing clients (including external SDK consumers) built against the previous struct definition, and can lead to proxy/stub mismatch or the service reading past the caller’s buffer. Please version this contract (e.g., introduce a new WSLCSessionSettings2 + CreateSession2 on a new IWSLCSessionManager IID) or use an explicit size/version field so older clients remain compatible.
| auto session = SessionService::CreateSession(options, WSLCSessionFlagsNone); | ||
| wsl::windows::common::wslutil::PrintMessage(Localization::MessageWslcCreatedSession(displayName), stderr); | ||
|
|
||
| const std::string shell = "/bin/sh"; |
There was a problem hiding this comment.
/bin/sh is being launched with --login, which is not a portable/standard option for sh (it’s typically a bash-ism). On many distros /bin/sh is dash/busybox and this will fail immediately, breaking wslc session enter. Consider switching to /bin/bash --login (as the previous debug shell did) or dropping the --login argument when using /bin/sh.
| const std::string shell = "/bin/sh"; | |
| const std::string shell = "/bin/bash"; |
| HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), | ||
| Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath), |
There was a problem hiding this comment.
When AttachDisk fails with ERROR_FILE_NOT_FOUND (missing storage.vhdx in an existing directory), this path still throws ERROR_PATH_NOT_FOUND if WSLCSessionStorageFlagsNoCreate is set. That changes the error code and can produce misleading diagnostics. Consider preserving the actual result (ERROR_FILE_NOT_FOUND vs ERROR_PATH_NOT_FOUND) when NoCreate is set, and tailor the user message accordingly.
| HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), | |
| Localization::MessageWslcSessionStorageNotFound(Settings.StoragePath), | |
| result, | |
| Localization::MessageWslcSessionStorageNotFound( | |
| result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ? m_storageVhdPath.c_str() : Settings.StoragePath), |
| <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> |
There was a problem hiding this comment.
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.
| <value>No WSLC session found in '{}'</value> | |
| <value>No WSLC session storage found at '{}'</value> |
Summary of the Pull Request
This change creates a new wslc command: `wslc sesssion enter [--name ].
This command is design to be a long term replacement to
wsl --wslcto be used for ephemeral debugging sessions (for instance when debugging the tests)PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed