@samitouri / QOSAMI-WSL / commits / 0375138f

Implement wslc system session run and create the default session if it doesn't exist (#40812)

* Format * Simplify tests * Implement command * Add test coverage * Format

Blue committed Jun 16, 2026 at 13:05 UTC 0375138f09e2c0e59f79b51b820a73b0de8df36c
12 files changed +189 -27
localization/strings/en-US/Resources.resw
+14 -3
@@ -2066,6 +2066,10 @@ Usage:
2066 <value>OpenSessionByName('{}') failed</value>
2067 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2068 </data>
2069 + <data name="MessageWslcFailedToLaunchCommand" xml:space="preserve">
2070 + <value>Failed to launch command {}. Errno = {}</value>
2071 + <comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2072 + </data>
2073 <data name="MessageWslcNoSessionsFound" xml:space="preserve">
2074 <value>No WSLC sessions found.</value>
2075 </data>
@@ -2080,9 +2084,6 @@ Usage:
2084 <data name="MessageWslcDefaultSessionNotFound" xml:space="preserve">
2085 <value>Default session not found</value>
2086 </data>
2083 - <data name="MessageWslcOpenDefaultSessionFailed" xml:space="preserve">
2084 - <value>Failed to open default session</value>
2085 - </data>
2087 <data name="MessageWslcTerminateDefaultSessionFailed" xml:space="preserve">
2088 <value>Default session termination failed</value>
2089 </data>
@@ -2684,6 +2685,16 @@ For privacy information about this product please visit https://aka.ms/privacy.<
2685 <value>Attaches to an active session. If no session ID is provided, the wslc default session will be used.</value>
2686 <comment>{Locked="wslc"}Command line arguments should not be translated</comment>
2687 </data>
2688 + <data name="WSLCCLI_SessionRunDesc" xml:space="preserve">
2689 + <value>Run a command in a session.</value>
2690 + </data>
2691 + <data name="WSLCCLI_SessionRunLongDesc" xml:space="preserve">
2692 + <value>Runs a command in an active session without a TTY. The command and its arguments are forwarded directly to the session. If no session is specified, the wslc default session will be used.</value>
2693 + <comment>{Locked="wslc"}{Locked="TTY"}Command line arguments and technical terms should not be translated</comment>
2694 + </data>
2695 + <data name="WSLCCLI_SessionRunForwardArgsDescription" xml:space="preserve">
2696 + <value>Arguments to pass to the command being run</value>
2697 + </data>
2698 <data name="WSLCCLI_SessionTerminateDesc" xml:space="preserve">
2699 <value>Terminate a session.</value>
2700 </data>
src/linux/init/WSLCInit.cpp
+1 -1
@@ -765,7 +765,7 @@ void HandleMessageImpl(
765 auto EnvironmentArray = wsl::shared::string::ArrayFromSpan(Buffer, Message.EnvironmentIndex);
766 auto EnvironmentPointers = wsl::shared::string::StringPointersFromArray(EnvironmentArray, true);
767
768 - execve(Executable, (char* const*)(ArgumentPointers.data()), (char* const*)(EnvironmentPointers.data()));
768 + execvpe(Executable, (char* const*)(ArgumentPointers.data()), (char* const*)(EnvironmentPointers.data()));
769
770 // Only reached if exec() fails
771 Transaction.SendResultMessage<int32_t>(errno);
src/windows/wslc/commands/SessionCommand.cpp
+1
@@ -25,6 +25,7 @@ std::vector<std::unique_ptr<Command>> SessionCommand::GetCommands() const
25 std::vector<std::unique_ptr<Command>> commands;
26 commands.push_back(std::make_unique<SessionEnterCommand>(FullName()));
27 commands.push_back(std::make_unique<SessionListCommand>(FullName()));
28 + commands.push_back(std::make_unique<SessionRunCommand>(FullName()));
29 commands.push_back(std::make_unique<SessionShellCommand>(FullName()));
30 commands.push_back(std::make_unique<SessionTerminateCommand>(FullName()));
31 return commands;
src/windows/wslc/commands/SessionCommand.h
+15
@@ -62,6 +62,21 @@ protected:
62 void ExecuteInternal(CLIExecutionContext& context) const override;
63 };
64
65 +// Run Command
66 +struct SessionRunCommand final : public Command
67 +{
68 + constexpr static std::wstring_view CommandName = L"run";
69 + SessionRunCommand(const std::wstring& parent) : Command(CommandName, parent)
70 + {
71 + }
72 + std::vector<Argument> GetArguments() const override;
73 + std::wstring ShortDescription() const override;
74 + std::wstring LongDescription() const override;
75 +
76 +protected:
77 + void ExecuteInternal(CLIExecutionContext& context) const override;
78 +};
79 +
80 // Enter Command
81 struct SessionEnterCommand final : public Command
82 {
src/windows/wslc/commands/SessionRunCommand.cpp new
+48
@@ -0,0 +1,48 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + SessionRunCommand.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of the session run command.
12 +
13 +--*/
14 +#include "CLIExecutionContext.h"
15 +#include "SessionCommand.h"
16 +#include "SessionTasks.h"
17 +#include "Task.h"
18 +
19 +using namespace wsl::windows::wslc::execution;
20 +using namespace wsl::windows::wslc::task;
21 +using namespace wsl::shared;
22 +
23 +namespace wsl::windows::wslc {
24 +// Session Run Command
25 +std::vector<Argument> SessionRunCommand::GetArguments() const
26 +{
27 + return {
28 + Argument::Create(ArgType::Command, true),
29 + Argument::Create(ArgType::ForwardArgs, std::nullopt, std::nullopt, Localization::WSLCCLI_SessionRunForwardArgsDescription()),
30 + Argument::Create(ArgType::Session),
31 + };
32 +}
33 +
34 +std::wstring SessionRunCommand::ShortDescription() const
35 +{
36 + return Localization::WSLCCLI_SessionRunDesc();
37 +}
38 +
39 +std::wstring SessionRunCommand::LongDescription() const
40 +{
41 + return Localization::WSLCCLI_SessionRunLongDesc();
42 +}
43 +
44 +void SessionRunCommand::ExecuteInternal(CLIExecutionContext& context) const
45 +{
46 + context << RunInSession;
47 +}
48 +} // namespace wsl::windows::wslc
src/windows/wslc/services/SessionService.cpp
+49 -23
@@ -24,36 +24,43 @@ using namespace wsl::shared;
24 using namespace wsl::windows::wslc::models;
25 namespace wslutil = wsl::windows::common::wslutil;
26
27 -int SessionService::Attach(const std::wstring& sessionName)
28 -{
29 - wil::com_ptr<IWSLCSessionManager> manager;
30 - THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&manager)));
31 - wsl::windows::common::security::ConfigureForCOMImpersonation(manager.get());
27 +namespace {
28
33 - wil::com_ptr<IWSLCSession> session;
34 - HRESULT hr = manager->OpenSessionByName(sessionName.empty() ? nullptr : sessionName.c_str(), &session);
35 - if (FAILED(hr))
29 + wil::com_ptr<IWSLCSession> OpenOrCreateSession(const std::wstring& sessionName)
30 {
37 - if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
31 + wil::com_ptr<IWSLCSessionManager> manager;
32 + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&manager)));
33 + wsl::windows::common::security::ConfigureForCOMImpersonation(manager.get());
34 +
35 + wil::com_ptr<IWSLCSession> session;
36 + if (sessionName.empty())
37 {
39 - wslutil::PrintMessage(
40 - sessionName.empty() ? Localization::MessageWslcDefaultSessionNotFound()
41 - : Localization::MessageWslcSessionNotFound(sessionName.c_str()),
42 - stderr);
43 - return 1;
38 + // Default session: open it if it exists, otherwise create it.
39 + auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
40 + THROW_IF_FAILED(manager->CreateSession(nullptr, WSLCSessionFlagsNone, warningCallback.Get(), &session));
41 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
42 + return session;
43 }
44
46 - auto errorString = wsl::windows::common::wslutil::ErrorCodeToString(hr);
47 - wslutil::PrintMessage(
48 - Localization::MessageErrorCode(
49 - sessionName.empty() ? Localization::MessageWslcOpenDefaultSessionFailed()
50 - : Localization::MessageWslcOpenSessionFailed(sessionName.c_str()),
51 - errorString),
52 - stderr);
53 - return 1;
45 + HRESULT hr = manager->OpenSessionByName(sessionName.c_str(), &session);
46 + if (FAILED(hr))
47 + {
48 + THROW_HR_WITH_USER_ERROR_IF(
49 + hr, Localization::MessageWslcSessionNotFound(sessionName.c_str()), hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
50 +
51 + THROW_HR_WITH_USER_ERROR(hr, Localization::MessageWslcOpenSessionFailed(sessionName.c_str()));
52 + }
53 +
54 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
55 +
56 + return session;
57 }
58
56 - wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
59 +} // namespace
60 +
61 +int SessionService::Attach(const std::wstring& sessionName)
62 +{
63 + auto session = OpenOrCreateSession(sessionName);
64
65 // Configure console for interactive usage.
66 wsl::windows::common::ConsoleState console{};
@@ -180,6 +187,25 @@ Session SessionService::OpenSession(const std::wstring& displayName)
187 return Session(std::move(session));
188 }
189
190 +int SessionService::Run(const std::wstring& sessionName, const std::vector<std::string>& arguments)
191 +{
192 + WI_ASSERT(!arguments.empty());
193 +
194 + auto session = OpenOrCreateSession(sessionName);
195 +
196 + // Pass a default $PATH environment for convenience.
197 + const std::vector<std::string> environment{"PATH=/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin"};
198 + wsl::windows::common::WSLCProcessLauncher launcher{arguments.front(), arguments, environment, WSLCProcessFlagsStdin};
199 +
200 + auto [result, process, error] = launcher.LaunchNoThrow(*session);
201 + THROW_HR_WITH_USER_ERROR_IF(result, Localization::MessageWslcFailedToLaunchCommand(arguments.front(), error), FAILED(result) && error != 0);
202 +
203 + THROW_IF_FAILED(result);
204 +
205 + wsl::windows::common::ConsoleState console{};
206 + return ConsoleService::AttachToCurrentConsole(console, std::move(process.value()));
207 +}
208 +
209 int SessionService::TerminateSession(const std::wstring& displayName)
210 {
211 wil::com_ptr<IWSLCSessionManager> sessionManager;
src/windows/wslc/services/SessionService.h
+2
@@ -32,6 +32,8 @@ struct SessionService
32 static int Enter(const std::wstring& storagePath, const std::wstring& displayName);
33 static std::vector<SessionInformation> List();
34 static wsl::windows::wslc::models::Session OpenSession(const std::wstring& displayName);
35 + // Runs the given command and arguments in a session without a TTY, resolving the executable from PATH.
36 + static int Run(const std::wstring& name, const std::vector<std::string>& arguments);
37 static int TerminateSession(const std::wstring& displayName);
38 };
39 } // namespace wsl::windows::wslc::services
src/windows/wslc/tasks/SessionTasks.cpp
+21
@@ -87,6 +87,27 @@ void TerminateSession(CLIExecutionContext& context)
87 context.ExitCode = SessionService::TerminateSession(sessionId);
88 }
89
90 +void RunInSession(CLIExecutionContext& context)
91 +{
92 + std::wstring sessionName;
93 + if (context.Args.Contains(ArgType::Session))
94 + {
95 + sessionName = context.Args.Get<ArgType::Session>();
96 + }
97 +
98 + std::vector<std::string> arguments;
99 + arguments.emplace_back(wsl::windows::common::string::WideToMultiByte(context.Args.Get<ArgType::Command>()));
100 + if (context.Args.Contains(ArgType::ForwardArgs))
101 + {
102 + for (const auto& arg : context.Args.Get<ArgType::ForwardArgs>())
103 + {
104 + arguments.emplace_back(wsl::windows::common::string::WideToMultiByte(arg));
105 + }
106 + }
107 +
108 + context.ExitCode = SessionService::Run(sessionName, arguments);
109 +}
110 +
111 void EnterSession(CLIExecutionContext& context)
112 {
113 auto storagePath = std::filesystem::absolute(context.Args.Get<ArgType::StoragePath>());
src/windows/wslc/tasks/SessionTasks.h
+1
@@ -21,5 +21,6 @@ void AttachToSession(CLIExecutionContext& context);
21 void CreateSession(CLIExecutionContext& context);
22 void EnterSession(CLIExecutionContext& context);
23 void ListSessions(CLIExecutionContext& context);
24 +void RunInSession(CLIExecutionContext& context);
25 void TerminateSession(CLIExecutionContext& context);
26 } // namespace wsl::windows::wslc::task
test/windows/wslc/CommandLineTestCases.h
+9
@@ -34,6 +34,15 @@ COMMAND_LINE_TEST_CASE(L"system session list --notanarg", L"list", false)
34 COMMAND_LINE_TEST_CASE(L"system session list extraarg", L"list", false)
35 COMMAND_LINE_TEST_CASE(L"system session shell session1", L"shell", true)
36 COMMAND_LINE_TEST_CASE(L"system session shell", L"shell", true)
37 +COMMAND_LINE_TEST_CASE(L"system session run ls", L"run", true)
38 +COMMAND_LINE_TEST_CASE(L"system session run echo foo", L"run", true) // Command with trailing arguments
39 +COMMAND_LINE_TEST_CASE(L"system session run ls -la", L"run", true) // Flags after the command are forwarded
40 +COMMAND_LINE_TEST_CASE(L"system session run --session session1 ls", L"run", true)
41 +COMMAND_LINE_TEST_CASE(L"system session run --session session1 echo foo", L"run", true)
42 +COMMAND_LINE_TEST_CASE(L"system session run \"ls -la /tmp\"", L"run", true)
43 +COMMAND_LINE_TEST_CASE(L"system session run", L"run", false) // Missing required command positional
44 +COMMAND_LINE_TEST_CASE(L"system session run --session session1", L"run", false) // Missing required command positional
45 +COMMAND_LINE_TEST_CASE(L"system session run --notanarg ls", L"run", false) // Invalid flag before command
46 COMMAND_LINE_TEST_CASE(L"system session terminate session1", L"terminate", true)
47 COMMAND_LINE_TEST_CASE(L"system session terminate", L"terminate", true)
48 COMMAND_LINE_TEST_CASE(L"system session enter C:\\storage", L"enter", true)
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+23
@@ -492,6 +492,29 @@ class WSLCE2EGlobalTests
492 }
493 }
494
495 + WSLC_TEST_METHOD(WSLCE2E_Session_Run)
496 + {
497 + {
498 + auto result = RunWslc(L"system session run echo OK");
499 + result.Verify({.Stdout = L"OK\n", .Stderr = L"", .ExitCode = 0});
500 + }
501 +
502 + {
503 + auto result = RunWslc(std::format(L"system session run --session {} echo OK", GetExpectedDefaultSessionName(true)));
504 + result.Verify({.Stdout = L"OK\n", .Stderr = L"", .ExitCode = 0});
505 + }
506 +
507 + {
508 + auto result = RunWslc(L"system session run --session not-found echo OK");
509 + result.Verify({.Stdout = L"", .Stderr = L"Session not found: 'not-found'\r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
510 + }
511 +
512 + {
513 + auto result = RunWslc(L"system session run not-found");
514 + result.Verify({.Stdout = L"", .Stderr = L"Failed to launch command not-found. Errno = 2\r\nError code: E_FAIL\r\n", .ExitCode = 1});
515 + }
516 + }
517 +
518 WSLC_TEST_METHOD(WSLCE2E_Session_List_Verbose)
519 {
520 auto result = RunWslc(L"container list");
test/windows/wslc/e2e/WSLCExecutor.cpp
+5
@@ -160,6 +160,11 @@ WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType eleva
160 process.SetToken(nonElevatedToken.get());
161 }
162
163 + auto nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ);
164 + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
165 +
166 + process.SetStdHandles(nul.get(), nullptr, nullptr);
167 +
168 const auto output = process.RunAndCaptureOutput();
169 return {.CommandLine = commandLine, .Stdout = output.Stdout, .Stderr = output.Stderr, .ExitCode = output.ExitCode};
170 }