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
}