master
cpp 775 lines 35.8 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EGlobalTests.cpp
8
9 Abstract:
10
11 This file contains end-to-end tests for WSLC.
12 --*/
13
14 #include "precomp.h"
15 #include "windows/Common.h"
16 #include "WSLCCLITestHelpers.h"
17 #include "WSLCExecutor.h"
18 #include "WSLCE2EHelpers.h"
19 #include "TestImageRegistry.h"
20 #include "WSLCSessionDefaults.h"
21 #include "WSLCUserSettings.h"
22 #include "Argument.h"
23
24 using namespace WEX::Logging;
25
26 namespace WSLCE2ETests {
27 using namespace wsl::shared;
28
29 namespace {
30
31 const std::wstring c_copyrightPrefix = L"Copyright (c) Microsoft Corporation.";
32
33 // Returns the expected default session name for the current user (e.g. "wslc-cli-admin-benhill").
34 std::wstring GetExpectedDefaultSessionName(bool elevated)
35 {
36 auto baseName = elevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName;
37
38 wchar_t username[256 + 1] = {};
39 DWORD usernameLen = ARRAYSIZE(username);
40 THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen));
41
42 return std::format(L"{}-{}", baseName, username);
43 }
44
45 // TableOutput sizes each column to its widest row, so a session leaving re-pads the survivors.
46 // Comparing leading ID tokens rather than whole rendered lines keeps comparisons padding-independent.
47 std::vector<std::wstring> GetLeadingTokens(const std::vector<std::wstring>& lines)
48 {
49 std::vector<std::wstring> tokens;
50 for (const auto& line : lines)
51 {
52 tokens.emplace_back(line.substr(0, line.find(L' ')));
53 }
54
55 return tokens;
56 }
57
58 } // namespace
59
60 class WSLCE2EGlobalTests
61 {
62 WSLC_TEST_CLASS(WSLCE2EGlobalTests)
63
64 wil::unique_couninitialize_call m_coinit = wil::CoInitializeEx();
65
66 TEST_CLASS_SETUP(TestClassSetup)
67 {
68 return true;
69 }
70
71 TEST_CLASS_CLEANUP(TestClassCleanup)
72 {
73 return true;
74 }
75
76 WSLC_TEST_METHOD(WSLCE2E_HelpCommand)
77 {
78 auto result = RunWslc(L"--help");
79 result.Verify({.Stderr = L"", .ExitCode = 0});
80 VERIFY_IS_FALSE(result.Stdout.value().empty());
81 }
82
83 WSLC_TEST_METHOD(WSLCE2E_InvalidCommand_DisplaysErrorMessage)
84 {
85 auto result = RunWslc(L"INVALID_CMD");
86 result.Verify({.Stdout = L"", .ExitCode = 1});
87 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
88 }
89
90 WSLC_TEST_METHOD(WSLCE2E_Help_RoutesToStdout)
91 {
92 auto result = RunWslc(L"--help");
93 result.Verify({.Stderr = L"", .ExitCode = 0});
94 VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"Usage: wslc"));
95 VERIFY_IS_TRUE(result.StdoutContainsSubstring(c_copyrightPrefix));
96 VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_RootCommandLongDesc()));
97 VERIFY_IS_TRUE(result.StdoutContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
98 VERIFY_IS_FALSE(result.StdoutContainsSubstring(Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc")));
99 }
100
101 WSLC_TEST_METHOD(WSLCE2E_Help_CommandErrorRoutesToStderr)
102 {
103 auto result = RunWslc(L"INVALID_CMD");
104 VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
105 VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
106 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Unrecognized command: 'INVALID_CMD'"));
107 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc"));
108 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingCommands()));
109 VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
110 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_RootCommandLongDesc()));
111 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
112 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc")));
113 }
114
115 WSLC_TEST_METHOD(WSLCE2E_Help_ArgumentErrorRoutesToStderr)
116 {
117 auto result = RunWslc(L"container create");
118 VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
119 VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
120 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Required argument not provided: 'image'"));
121 VERIFY_IS_TRUE(result.StderrContainsSubstring(L"Usage: wslc container create"));
122 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedArguments()));
123 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_ImageIdArgDescription()));
124 VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
125 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_ContainerCreateLongDesc()));
126 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingOptions()));
127 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_NameArgDescription()));
128 VERIFY_IS_TRUE(result.StderrContainsSubstring(
129 Localization::WSLCCLI_ImageIdArgDescription() + L"\r\n\r\n" +
130 Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc container create")));
131 }
132
133 WSLC_TEST_METHOD(WSLCE2E_Help_ArgumentConflictShowsRelevantArguments)
134 {
135 auto result = RunWslc(L"container list --last 1 --latest");
136 VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
137 VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
138 VERIFY_IS_TRUE(
139 result.StderrContainsSubstring(Localization::WSLCCLI_MultipleExclusiveArgumentsProvided(L"--last, --latest")));
140 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_LastArgDescription()));
141 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_LatestArgDescription()));
142 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedOptions()));
143 VERIFY_IS_FALSE(result.StderrContainsSubstring(c_copyrightPrefix));
144 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_AllArgDescription()));
145 VERIFY_IS_TRUE(result.StderrContainsSubstring(
146 Localization::WSLCCLI_LatestArgDescription() + L"\r\n\r\n" + Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc container list")));
147 }
148
149 WSLC_TEST_METHOD(WSLCE2E_Help_OptionErrorUsesOptionTerminology)
150 {
151 auto result = RunWslc(L"list -a=falseee");
152 VERIFY_ARE_NOT_EQUAL(0u, result.ExitCode.value_or(0));
153 VERIFY_IS_TRUE(result.Stdout.has_value() && result.Stdout->empty());
154 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_FlagInvalidBooleanError(L"-a=falseee")));
155 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedOptions()));
156 VERIFY_IS_TRUE(result.StderrContainsSubstring(Localization::WSLCCLI_AllArgDescription()));
157 VERIFY_IS_FALSE(result.StderrContainsSubstring(Localization::WSLCCLI_HeadingRelatedArguments()));
158 VERIFY_IS_TRUE(result.StderrContainsSubstring(
159 Localization::WSLCCLI_AllArgDescription() + L"\r\n\r\n" + Localization::WSLCCLI_RunHelpForMoreInformation(L"wslc list")));
160 }
161
162 WSLC_TEST_METHOD(WSLCE2E_Help_NoColorWhenRedirected)
163 {
164 // Captured via anonymous pipe; Terminal must suppress VT escape sequences.
165 auto result = RunWslc(L"--help");
166 result.Verify({.Stderr = L"", .ExitCode = 0});
167 VERIFY_ARE_EQUAL(std::wstring::npos, result.Stdout.value().find(L'\x1b'));
168 }
169
170 WSLC_TEST_METHOD(WSLCE2E_Help_ColorOnTerminal)
171 {
172 // Pseudo console reports VT support; Terminal should emit SGR sequences.
173 auto session = RunWslcInteractive(L"--help", ElevationType::Elevated, PseudoConsole{120, 30});
174 session.WaitForExit();
175 VERIFY_IS_TRUE(session.GetStdoutData().find('\x1b') != std::string::npos);
176 }
177
178 WSLC_TEST_METHOD(WSLCE2E_VersionCommand)
179 {
180 RunWslcAndVerify(L"version", {.Stdout = GetVersionMessage(), .Stderr = L"", .ExitCode = 0});
181 }
182
183 WSLC_TEST_METHOD(WSLCE2E_VersionFlag)
184 {
185 RunWslcAndVerify(L"--version", {.Stdout = GetVersionMessage(), .Stderr = L"", .ExitCode = 0});
186 }
187
188 WSLC_TEST_METHOD(WSLCE2E_VersionCommand_FormatJson)
189 {
190 auto result = RunWslc(L"version --format json");
191 result.Verify({.Stderr = L"", .ExitCode = 0});
192 VERIFY_IS_TRUE(result.Stdout.has_value());
193 // The version payload is emitted as one compact object.
194 const auto root = VerifyCompactJsonOutput(result);
195 VERIFY_ARE_EQUAL(std::string{WSL_PACKAGE_VERSION}, root["Client"]["Version"].get<std::string>());
196 }
197
198 WSLC_TEST_METHOD(WSLCE2E_VersionCommand_FormatTable)
199 {
200 // Explicit table format matches the default plain-text output.
201 RunWslcAndVerify(L"version --format table", {.Stdout = GetVersionMessage(), .Stderr = L"", .ExitCode = 0});
202 }
203
204 WSLC_TEST_METHOD(WSLCE2E_SystemInfoCommand)
205 {
206 auto result = RunWslc(L"system info");
207 result.Verify({.Stderr = L"", .ExitCode = 0});
208 VERIFY_IS_TRUE(result.Stdout.has_value());
209
210 const auto& output = result.Stdout.value();
211 VERIFY_IS_TRUE(output.find(Localization::WSLCCLI_SystemInfoClientHeader()) != std::wstring::npos);
212 VERIFY_IS_TRUE(output.find(std::format(L"{}", WSL_PACKAGE_VERSION)) != std::wstring::npos);
213 VERIFY_IS_TRUE(output.find(Localization::WSLCCLI_SystemInfoServerHeader()) != std::wstring::npos);
214 VERIFY_IS_TRUE(output.find(Localization::WSLCCLI_SystemInfoSessionManagerVersion(GetExpectedManagerVersion())) != std::wstring::npos);
215
216 const auto settingsFilePath = wsl::windows::wslc::settings::User().SettingsFilePath().wstring();
217 VERIFY_IS_TRUE(output.find(Localization::WSLCCLI_SystemInfoSettingsFile(settingsFilePath)) != std::wstring::npos);
218 }
219
220 WSLC_TEST_METHOD(WSLCE2E_SystemInfoCommand_InvalidFormatOption)
221 {
222 const auto result = RunWslc(L"system info --format invalid");
223 result.Verify({.Stdout = L"", .ExitCode = 1});
224 VERIFY_IS_TRUE(result.StderrContainsSubstring(
225 L"Invalid format value: invalid is not a recognized format type. Supported format types are: json, table."));
226 }
227
228 WSLC_TEST_METHOD(WSLCE2E_SystemInfoCommand_FormatJson)
229 {
230 auto result = RunWslc(L"system info --format json");
231 result.Verify({.Stderr = L"", .ExitCode = 0});
232
233 const auto root = VerifyCompactJsonOutput(result);
234
235 const auto& client = root.at("Client");
236 VERIFY_ARE_EQUAL(std::string{WSL_PACKAGE_VERSION}, client.at("Version").get<std::string>());
237 VERIFY_ARE_EQUAL(std::string{KERNEL_VERSION}, client.at("KernelVersion").get<std::string>());
238 VERIFY_ARE_EQUAL(std::string{DIRECT3D_VERSION}, client.at("Direct3DVersion").get<std::string>());
239 VERIFY_ARE_EQUAL(std::string{DXCORE_VERSION}, client.at("DxCoreVersion").get<std::string>());
240 VERIFY_IS_FALSE(client.at("WindowsVersion").get<std::string>().empty());
241 VERIFY_IS_FALSE(client.at("SettingsFile").get<std::string>().empty());
242
243 // WSLg and MSRDC aren't relevant to wslc.
244 VERIFY_IS_FALSE(client.contains("WslgVersion"));
245 VERIFY_IS_FALSE(client.contains("MsrdcVersion"));
246
247 const auto& server = root.at("Server");
248 VERIFY_ARE_EQUAL(
249 wsl::shared::string::WideToMultiByte(GetExpectedManagerVersion()), server.at("SessionManagerVersion").get<std::string>());
250
251 const auto& sessions = server.at("Sessions");
252 VERIFY_IS_TRUE(sessions.is_array());
253 for (const auto& session : sessions)
254 {
255 VERIFY_ARE_EQUAL(3u, session.size());
256 VERIFY_IS_TRUE(session.contains("ID"));
257 VERIFY_IS_FALSE(session.contains("Id"));
258 VERIFY_IS_TRUE(session.contains("CreatorPid"));
259 VERIFY_IS_TRUE(session.contains("Name"));
260 }
261 }
262
263 WSLC_TEST_METHOD(WSLCE2E_SystemInfoCommand_RootAlias)
264 {
265 auto systemResult = RunWslc(L"system info --format json");
266 systemResult.Verify({.Stderr = L"", .ExitCode = 0});
267
268 auto rootResult = RunWslc(L"info --format json");
269 rootResult.Verify({.Stderr = L"", .ExitCode = 0});
270
271 // The server section can change between invocations; the client section cannot.
272 VERIFY_ARE_EQUAL(
273 VerifyCompactJsonOutput(systemResult).at("Client").dump(), VerifyCompactJsonOutput(rootResult).at("Client").dump());
274 }
275
276 WSLC_TEST_METHOD(WSLCE2E_SystemInfoCommand_DoesNotCreateSession)
277 {
278 auto before = RunWslc(L"system session list");
279 before.Verify({.Stderr = L"", .ExitCode = 0});
280
281 RunWslcAndVerify(L"system info --format json", {.Stderr = L"", .ExitCode = 0});
282
283 auto after = RunWslc(L"system session list");
284 after.Verify({.Stderr = L"", .ExitCode = 0});
285
286 // An idle session can terminate between the two snapshots, so only assert that none appeared.
287 const auto beforeIds = GetLeadingTokens(before.GetStdoutLines());
288 for (const auto& id : GetLeadingTokens(after.GetStdoutLines()))
289 {
290 VERIFY_IS_TRUE(
291 std::find(beforeIds.begin(), beforeIds.end(), id) != beforeIds.end(),
292 std::format(L"'system info' must not create a session, but session '{}' appeared", id).c_str());
293 }
294 }
295
296 WSLC_TEST_METHOD(WSLCE2E_Session_DefaultElevated)
297 {
298 // Run container list to create the default elevated session
299 auto result = RunWslc(L"container list", ElevationType::Elevated);
300 result.Verify({.Stderr = L"", .ExitCode = 0});
301
302 // Verify session list shows the admin session name
303 result = RunWslc(L"system session list", ElevationType::Elevated);
304 result.Verify({.Stderr = L"", .ExitCode = 0});
305
306 VERIFY_IS_TRUE(result.Stdout.has_value());
307 auto adminName = GetExpectedDefaultSessionName(true);
308 VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
309 }
310
311 WSLC_TEST_METHOD(WSLCE2E_Session_DefaultNonElevated)
312 {
313 // Run container list non-elevated to create the default non-elevated session
314 auto result = RunWslc(L"container list", ElevationType::NonElevated);
315 result.Verify({.Stderr = L"", .ExitCode = 0});
316
317 // Verify session list shows the non-admin session name
318 result = RunWslc(L"system session list", ElevationType::NonElevated);
319 result.Verify({.Stderr = L"", .ExitCode = 0});
320
321 VERIFY_IS_TRUE(result.Stdout.has_value());
322
323 // The "\r\n" after session name is important to differentiate it from the admin session.
324 auto nonAdminName = GetExpectedDefaultSessionName(false);
325 VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
326 }
327
328 WSLC_TEST_METHOD(WSLCE2E_Session_NonElevatedCannotAccessAdminSession)
329 {
330 // First ensure admin session is created by running container list.
331 auto result = RunWslc(L"container list", ElevationType::Elevated);
332 result.Verify({.Stderr = L"", .ExitCode = 0});
333
334 // Try to explicitly target the admin session from non-elevated process
335 auto adminName = GetExpectedDefaultSessionName(true);
336 result = RunWslc(std::format(L"--session \"{}\" container list", adminName), ElevationType::NonElevated);
337
338 // Should fail with access denied.
339 result.Verify({.Stderr = FormatErrorMessage(L"The requested operation requires elevation. ", L"ERROR_ELEVATION_REQUIRED"), .ExitCode = 1});
340 }
341
342 WSLC_TEST_METHOD(WSLCE2E_Session_ElevatedCanAccessNonElevatedSession)
343 {
344 // First ensure non-elevated session is created by running container list.
345 auto result = RunWslc(L"container list", ElevationType::NonElevated);
346 result.Verify({.Stderr = L"", .ExitCode = 0});
347
348 // Elevated user should be able to explicitly target the non-admin session
349 auto nonAdminName = GetExpectedDefaultSessionName(false);
350 result = RunWslc(std::format(L"--session \"{}\" container list", nonAdminName), ElevationType::Elevated);
351
352 // This should work - elevated users can access non-elevated sessions
353 result.Verify({.Stderr = L"", .ExitCode = 0});
354 }
355
356 WSLC_TEST_METHOD(WSLCE2E_Session_CreateMixedElevation_Fails)
357 {
358 EnsureSessionIsTerminated(GetExpectedDefaultSessionName(false));
359 EnsureSessionIsTerminated(GetExpectedDefaultSessionName(true));
360
361 // Ensure elevated cannot create the non-elevated session.
362 auto nonAdminName = GetExpectedDefaultSessionName(false);
363 auto adminName = GetExpectedDefaultSessionName(true);
364 auto result = RunWslc(std::format(L"--session \"{}\" container list", nonAdminName), ElevationType::Elevated);
365 result.Verify(
366 {.Stderr = FormatErrorMessage(std::format(L"Session not found: '{}'", nonAdminName), L"WSLC_E_SESSION_NOT_FOUND"), .ExitCode = 1});
367
368 // Ensure non-elevated cannot create the elevated session.
369 result = RunWslc(std::format(L"--session \"{}\" container list", adminName), ElevationType::NonElevated);
370 result.Verify(
371 {.Stderr = FormatErrorMessage(std::format(L"Session not found: '{}'", adminName), L"WSLC_E_SESSION_NOT_FOUND"), .ExitCode = 1});
372 }
373
374 // Regression test for session name squatting vulnerability.
375 //
376 // Validates that a process cannot create a session with the reserved default
377 // session names ("wslc-cli" or "wslc-cli-admin") via the COM API. These names
378 // are assigned server-side when the client passes null Settings to CreateSession,
379 // preventing a malicious process from squatting on the name and blocking
380 // legitimate wslc.exe clients.
381 WSLC_TEST_METHOD(WSLCE2E_Session_NameSquatting_ElevatedCannotBlockNonElevated)
382 {
383 // Ensure no existing sessions with default names.
384 EnsureSessionIsTerminated(wsl::windows::wslc::DefaultSessionName);
385 EnsureSessionIsTerminated(wsl::windows::wslc::DefaultAdminSessionName);
386
387 // Attack: attempt to create a session with the reserved non-admin default
388 // name directly through the COM API from this elevated process.
389 // The service should reject this because reserved default session names
390 // cannot be explicitly created.
391 {
392 wil::com_ptr<IWSLCSessionManager> sessionManager;
393 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
394 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
395
396 WSLCSessionSettings settings{};
397 settings.DisplayName = wsl::windows::wslc::DefaultSessionName;
398 settings.StoragePath = L"C:\\dummy";
399 settings.CpuCount = 4;
400 settings.MemoryMb = 2048;
401 settings.BootTimeoutMs = 30000;
402 settings.MaximumStorageSizeMb = 4096;
403
404 wil::com_ptr<IWSLCSession> session;
405 HRESULT hr = sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session);
406 VERIFY_ARE_EQUAL(hr, WSLC_E_SESSION_RESERVED);
407 }
408
409 // Also verify that the admin reserved name is rejected.
410 {
411 wil::com_ptr<IWSLCSessionManager> sessionManager;
412 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
413 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
414
415 WSLCSessionSettings settings{};
416 settings.DisplayName = wsl::windows::wslc::DefaultAdminSessionName;
417 settings.StoragePath = L"C:\\dummy";
418 settings.CpuCount = 4;
419 settings.MemoryMb = 2048;
420 settings.BootTimeoutMs = 30000;
421 settings.MaximumStorageSizeMb = 4096;
422
423 wil::com_ptr<IWSLCSession> session;
424 HRESULT hr = sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session);
425 VERIFY_ARE_EQUAL(hr, WSLC_E_SESSION_RESERVED);
426 }
427
428 // Non-elevated wslc.exe should still be able to create and use its default
429 // session (which now passes null Settings, resolved entirely server-side).
430 auto result = RunWslc(L"container list", ElevationType::NonElevated);
431 result.Verify({.Stderr = L"", .ExitCode = S_OK});
432
433 // Verify that case variations of reserved names are also rejected,
434 // preventing bypass on case-insensitive filesystems (NTFS).
435 {
436 wil::com_ptr<IWSLCSessionManager> sessionManager;
437 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
438 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
439
440 WSLCSessionSettings settings{};
441 settings.DisplayName = L"WSLC-CLI";
442 settings.StoragePath = L"C:\\dummy";
443 settings.CpuCount = 4;
444 settings.MemoryMb = 2048;
445 settings.BootTimeoutMs = 30000;
446 settings.MaximumStorageSizeMb = 4096;
447
448 wil::com_ptr<IWSLCSession> session;
449 VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), WSLC_E_SESSION_RESERVED);
450
451 settings.DisplayName = L"Wslc-Cli-Admin";
452 VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session), WSLC_E_SESSION_RESERVED);
453 }
454 }
455
456 WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_Implicit)
457 {
458 auto adminName = GetExpectedDefaultSessionName(true);
459 auto nonAdminName = GetExpectedDefaultSessionName(false);
460
461 // Run container list to create the default session if it does not already exist
462 auto result = RunWslc(L"container list");
463 result.Verify({.Stderr = L"", .ExitCode = 0});
464
465 // Verify session list shows the admin session name
466 result = RunWslc(L"system session list");
467 result.Verify({.Stderr = L"", .ExitCode = 0});
468 VERIFY_IS_TRUE(result.Stdout.has_value());
469 VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
470
471 // Terminate the session
472 result = RunWslc(L"system session terminate");
473 result.Verify({.Stderr = L"", .ExitCode = 0});
474
475 // Verify session no longer shows up
476 result = RunWslc(L"system session list");
477 result.Verify({.Stderr = L"", .ExitCode = 0});
478 VERIFY_IS_TRUE(result.Stdout.has_value());
479 VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
480
481 // Repeat test for non-elevated session.
482
483 // Run container list to create the default session if it does not already exist
484 result = RunWslc(L"container list", ElevationType::NonElevated);
485 result.Verify({.Stderr = L"", .ExitCode = 0});
486
487 // Verify session list shows the non-elevated session name
488 result = RunWslc(L"system session list");
489 result.Verify({.Stderr = L"", .ExitCode = 0});
490 VERIFY_IS_TRUE(result.Stdout.has_value());
491 VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
492
493 // Terminate the session
494 result = RunWslc(L"system session terminate", ElevationType::NonElevated);
495 result.Verify({.Stderr = L"", .ExitCode = 0});
496
497 // Verify session no longer shows up
498 result = RunWslc(L"system session list");
499 result.Verify({.Stderr = L"", .ExitCode = 0});
500 VERIFY_IS_TRUE(result.Stdout.has_value());
501 VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
502 }
503
504 WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_Explicit)
505 {
506 auto adminName = GetExpectedDefaultSessionName(true);
507 auto nonAdminName = GetExpectedDefaultSessionName(false);
508
509 // Run container list to create the default session if it does not already exist
510 auto result = RunWslc(L"container list");
511 result.Verify({.Stderr = L"", .ExitCode = 0});
512
513 // Verify session list shows the admin session name
514 result = RunWslc(L"system session list");
515 result.Verify({.Stderr = L"", .ExitCode = 0});
516 VERIFY_IS_TRUE(result.Stdout.has_value());
517 VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
518
519 // Terminate the session
520 result = RunWslc(std::format(L"--session \"{}\" system session terminate", adminName));
521 result.Verify({.Stderr = L"", .ExitCode = 0});
522
523 // Verify session no longer shows up
524 result = RunWslc(L"system session list");
525 result.Verify({.Stderr = L"", .ExitCode = 0});
526 VERIFY_IS_TRUE(result.Stdout.has_value());
527 VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
528
529 // Repeat test for non-elevated session.
530
531 // Run container list to create the default session if it does not already exist
532 result = RunWslc(L"container list", ElevationType::NonElevated);
533 result.Verify({.Stderr = L"", .ExitCode = 0});
534
535 // Verify session list shows the non-elevated session name
536 result = RunWslc(L"system session list");
537 result.Verify({.Stderr = L"", .ExitCode = 0});
538 VERIFY_IS_TRUE(result.Stdout.has_value());
539 VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
540
541 // Terminate the session
542 result = RunWslc(std::format(L"--session \"{}\" system session terminate", nonAdminName), ElevationType::NonElevated);
543 result.Verify({.Stderr = L"", .ExitCode = 0});
544
545 // Verify session no longer shows up
546 result = RunWslc(L"system session list");
547 result.Verify({.Stderr = L"", .ExitCode = 0});
548 VERIFY_IS_TRUE(result.Stdout.has_value());
549 VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
550 }
551
552 WSLC_TEST_METHOD(WSLCE2E_Session_Terminate_MixedElevation)
553 {
554 auto adminName = GetExpectedDefaultSessionName(true);
555 auto nonAdminName = GetExpectedDefaultSessionName(false);
556
557 // Run container list to create the default sessions if they do not already exist.
558 auto result = RunWslc(L"container list", ElevationType::Elevated);
559 result.Verify({.Stderr = L"", .ExitCode = 0});
560 result = RunWslc(L"container list", ElevationType::NonElevated);
561 result.Verify({.Stderr = L"", .ExitCode = 0});
562
563 // Verify session list shows both sessions.
564 result = RunWslc(L"system session list");
565 result.Verify({.Stderr = L"", .ExitCode = 0});
566 VERIFY_IS_TRUE(result.Stdout.has_value());
567 VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
568 VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
569
570 // Attempt to terminate the admin session from the non-elevated process and fail.
571 result = RunWslc(std::format(L"--session \"{}\" system session terminate", adminName), ElevationType::NonElevated);
572 result.Verify({.Stderr = FormatErrorMessage(L"The requested operation requires elevation. ", L"ERROR_ELEVATION_REQUIRED"), .ExitCode = 1});
573
574 // Terminate the non-elevated session from the elevated process.
575 result = RunWslc(std::format(L"--session \"{}\" system session terminate", nonAdminName), ElevationType::Elevated);
576 result.Verify({.Stderr = L"", .ExitCode = 0});
577
578 // Verify non-elevated session no longer shows up
579 result = RunWslc(L"system session list");
580 result.Verify({.Stderr = L"", .ExitCode = 0});
581 VERIFY_IS_TRUE(result.Stdout.has_value());
582 VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
583 }
584
585 WSLC_TEST_METHOD(WSLCE2E_Session_Targeting)
586 {
587 // Generate a unique session name to avoid conflicts with previous runs or concurrent tests.
588 // Use only a short portion of the GUID to avoid MAX_PATH issues.
589 GUID sessionGuid;
590 VERIFY_SUCCEEDED(CoCreateGuid(&sessionGuid));
591 auto guidStr = wsl::shared::string::GuidToString<wchar_t>(sessionGuid, wsl::shared::string::GuidToStringFlags::None);
592 const auto sessionName = std::format(L"wslc-test-{}", guidStr.substr(0, 8));
593
594 auto session = TestSession::Create(sessionName);
595
596 // Load the Debian image into the test session to avoid hitting Docker Hub rate limits.
597 TestImageRegistry::Instance().EnsureLoaded(DebianTestImage(), session.Name());
598
599 // Verify targeting a non-existent session fails.
600 auto result = RunWslc(L"--session INVALID_SESSION_NAME container list");
601 result.Verify(
602 {.Stdout = L"",
603 .Stderr = FormatErrorMessage(L"Session not found: 'INVALID_SESSION_NAME'", L"WSLC_E_SESSION_NOT_FOUND"),
604 .ExitCode = 1});
605
606 // Verify session list
607 result = RunWslc(L"system session list");
608 result.Verify({.Stderr = L"", .ExitCode = 0});
609
610 // Verify there is a session with the name of the test session in the session list output.
611 VERIFY_IS_TRUE(result.Stdout.has_value());
612 auto findResult = result.Stdout->find(session.Name());
613 VERIFY_ARE_NOT_EQUAL(findResult, std::wstring::npos);
614
615 // Run container list in the test session, which should succeed if the session is valid.
616 result = RunWslc(std::format(L"--session \"{}\" container list", session.Name()));
617 result.Verify({.Stderr = L"", .ExitCode = 0});
618
619 // Add a container to the new session. The listing checks below match on substrings, so the
620 // name carries the session's unique suffix: "test-cont" on its own is a prefix of the
621 // "wslc-test-container" names other tests leave in the default session.
622 const auto containerName = std::format(L"wslc-session-cont-{}", guidStr.substr(0, 8));
623 result = RunWslc(std::format(
624 L"--session \"{}\" container create --name {} {}", session.Name(), containerName, DebianTestImage().NameAndTag()));
625 result.Dump(); // Dump so it is easier to find any potential issues with the pull in the test output.
626 result.Verify({.ExitCode = 0});
627
628 // Verify container exists in the custom session
629 VerifyContainerIsListed(containerName, L"created", session.Name());
630
631 // Verify container does not exist in the default CLI session.
632 VerifyContainerIsNotListed(containerName);
633 }
634
635 WSLC_TEST_METHOD(WSLCE2E_Session_Shell)
636 {
637 // Ensure sessions are created by running container list elevated and non-elevated.
638 auto result = RunWslc(L"container list", ElevationType::NonElevated);
639 result.Verify({.Stderr = L"", .ExitCode = 0});
640 result = RunWslc(L"container list", ElevationType::Elevated);
641 result.Verify({.Stderr = L"", .ExitCode = 0});
642
643 {
644 Log::Comment(L"Testing elevated interactive session");
645 // Session shell should attach to the correct default session.
646 // Test should be elevated, therefore this should be the admin session.
647 auto session = RunWslcInteractive(L"system session shell");
648 VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
649
650 session.ExpectStdout(VT::SESSION_PROMPT);
651
652 session.WriteLine("echo hello");
653 session.ExpectStdout(VT::RESET);
654 session.ExpectCommandEcho("echo hello");
655 session.ExpectStdout("hello\r\n");
656 session.ExpectStdout(VT::SESSION_PROMPT);
657
658 session.WriteLine("whoami");
659 session.ExpectStdout(VT::RESET);
660 session.ExpectCommandEcho("whoami");
661 session.ExpectStdout("root\r\n");
662 session.ExpectStdout(VT::SESSION_PROMPT);
663
664 session.ExitAndVerifyNoErrors();
665 auto exitCode = session.Wait();
666 VERIFY_ARE_EQUAL(0, exitCode);
667 }
668 {
669 Log::Comment(L"Testing non-elevated interactive session with explicit session name");
670 // Non-Elevated session shell should attach to the wslc by name also.
671 auto nonAdminName = GetExpectedDefaultSessionName(false);
672 auto session = RunWslcInteractive(std::format(L"--session \"{}\" system session shell", nonAdminName), ElevationType::NonElevated);
673 VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
674
675 session.ExpectStdout(VT::SESSION_PROMPT);
676
677 session.WriteLine("echo hello");
678 session.ExpectStdout(VT::RESET);
679 session.ExpectCommandEcho("echo hello");
680 session.ExpectStdout("hello\r\n");
681 session.ExpectStdout(VT::SESSION_PROMPT);
682
683 session.WriteLine("whoami");
684 session.ExpectStdout(VT::RESET);
685 session.ExpectCommandEcho("whoami");
686 session.ExpectStdout("root\r\n");
687 session.ExpectStdout(VT::SESSION_PROMPT);
688
689 session.ExitAndVerifyNoErrors();
690 auto exitCode = session.Wait();
691 VERIFY_ARE_EQUAL(0, exitCode);
692 }
693 {
694 Log::Comment(L"Testing elevated interactive session with explicit admin session name");
695 // Elevated session shell should attach to the wslc by name also.
696 auto adminName = GetExpectedDefaultSessionName(true);
697 auto session = RunWslcInteractive(std::format(L"--session \"{}\" system session shell", adminName), ElevationType::Elevated);
698 VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
699
700 session.ExpectStdout(VT::SESSION_PROMPT);
701
702 session.WriteLine("echo hello");
703 session.ExpectStdout(VT::RESET);
704 session.ExpectCommandEcho("echo hello");
705 session.ExpectStdout("hello\r\n");
706 session.ExpectStdout(VT::SESSION_PROMPT);
707
708 session.WriteLine("whoami");
709 session.ExpectStdout(VT::RESET);
710 session.ExpectCommandEcho("whoami");
711 session.ExpectStdout("root\r\n");
712 session.ExpectStdout(VT::SESSION_PROMPT);
713
714 session.ExitAndVerifyNoErrors();
715 auto exitCode = session.Wait();
716 VERIFY_ARE_EQUAL(0, exitCode);
717 }
718 }
719
720 WSLC_TEST_METHOD(WSLCE2E_Session_Run)
721 {
722 {
723 auto result = RunWslc(L"system session run echo OK");
724 result.Verify({.Stdout = L"OK\n", .Stderr = L"", .ExitCode = 0});
725 }
726
727 {
728 auto result = RunWslc(std::format(L"--session \"{}\" system session run echo OK", GetExpectedDefaultSessionName(true)));
729 result.Verify({.Stdout = L"OK\n", .Stderr = L"", .ExitCode = 0});
730 }
731
732 {
733 auto result = RunWslc(L"--session not-found system session run echo OK");
734 result.Verify({.Stderr = FormatErrorMessage(L"Session not found: 'not-found'", L"WSLC_E_SESSION_NOT_FOUND"), .ExitCode = 1});
735 }
736
737 {
738 auto result = RunWslc(L"system session run not-found");
739 result.Verify(
740 {.Stdout = L"", .Stderr = FormatErrorMessage(L"Failed to launch command not-found. Errno = 2", L"E_FAIL"), .ExitCode = 1});
741 }
742 }
743
744 WSLC_TEST_METHOD(WSLCE2E_Session_List_Verbose)
745 {
746 auto result = RunWslc(L"container list");
747 result.Verify({.Stderr = L"", .ExitCode = 0});
748
749 auto verboseResult = RunWslc(L"system session list --verbose");
750 verboseResult.Verify({.Stderr = L"", .ExitCode = 0});
751 VERIFY_IS_TRUE(verboseResult.Stdout.has_value());
752 VERIFY_IS_TRUE(
753 verboseResult.Stdout->find(L"[wslc] Found ") != std::wstring::npos, L"--verbose should print a session summary line");
754
755 auto plainResult = RunWslc(L"system session list");
756 plainResult.Verify({.Stderr = L"", .ExitCode = 0});
757 VERIFY_IS_TRUE(plainResult.Stdout.has_value());
758 VERIFY_IS_TRUE(
759 plainResult.Stdout->find(L"[wslc] Found ") == std::wstring::npos,
760 L"plain list should not print a session summary line");
761 }
762
763 private:
764 std::wstring GetVersionMessage() const
765 {
766 return std::format(L"wslc {}\r\n", WSL_PACKAGE_VERSION);
767 }
768
769 // The session manager reports only the first three version components.
770 std::wstring GetExpectedManagerVersion() const
771 {
772 return std::format(L"{}.{}.{}", WSL_PACKAGE_VERSION_MAJOR, WSL_PACKAGE_VERSION_MINOR, WSL_PACKAGE_VERSION_REVISION);
773 }
774 };
775 } // namespace WSLCE2ETests