master
cpp 585 lines 20 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCExecutor.cpp
8
9 Abstract:
10
11 This file contains the implementation of the WSLCExecutor class, which is
12 responsible for executing wslc commands and verifying their results in
13 end-to-end tests.
14 --*/
15
16 #include "precomp.h"
17 #include "windows/Common.h"
18 #include "WSLCExecutor.h"
19 #include "WSLCE2EHelpers.h"
20
21 namespace WSLCE2ETests {
22
23 using namespace WEX::Logging;
24
25 namespace wslutil = wsl::windows::common::wslutil;
26 using wsl::windows::common::SubProcess;
27
28 namespace {
29 wil::unique_handle GetNonElevatedPrimaryToken()
30 {
31 // This method is necessary because GetNonElevatedToken(TokenPrimary) does
32 // not actually give a de-elevated token when called from an elevated process.
33 // By getting impersonation token first this de-elevates the token, and then
34 // converts it to a primary token.
35 auto impersonationToken = GetNonElevatedToken(TokenImpersonation);
36 wil::unique_handle primaryToken;
37 THROW_IF_WIN32_BOOL_FALSE(
38 DuplicateTokenEx(impersonationToken.get(), TOKEN_ALL_ACCESS, nullptr, SecurityImpersonation, TokenPrimary, &primaryToken));
39
40 VERIFY_IS_FALSE(wsl::windows::common::security::IsTokenElevated(primaryToken.get()));
41 return primaryToken;
42 }
43 } // namespace
44
45 void WSLCExecutionResult::Dump(bool escapeStrings) const
46 {
47 Log::Comment((L"Command Line: \"" + CommandLine + L"\"").c_str());
48 if (Stdout)
49 {
50 if (escapeStrings)
51 {
52 std::string stdoutStr = wsl::windows::common::string::WideToMultiByte(*Stdout);
53 std::string escapedStdout = EscapeString(stdoutStr);
54 Log::Comment(std::format(L"Stdout: \"{}\"", wsl::shared::string::MultiByteToWide(escapedStdout)).c_str());
55 }
56 else
57 {
58 Log::Comment((L"Stdout: \"" + *Stdout + L"\"").c_str());
59 }
60 }
61
62 if (Stderr)
63 {
64 if (escapeStrings)
65 {
66 std::string stderrStr = wsl::windows::common::string::WideToMultiByte(*Stderr);
67 std::string escapedStderr = EscapeString(stderrStr);
68 Log::Comment(std::format(L"Stderr (escaped): \"{}\"", wsl::shared::string::MultiByteToWide(escapedStderr)).c_str());
69 }
70 else
71 {
72 Log::Comment((L"Stderr: \"" + *Stderr + L"\"").c_str());
73 }
74 }
75
76 if (ExitCode)
77 {
78 Log::Comment((L"Exit Code: " + std::to_wstring(*ExitCode)).c_str());
79 }
80 }
81
82 void WSLCExecutionResult::Verify(const WSLCExecutionResult& expected) const
83 {
84 if (expected.Stdout)
85 {
86 VERIFY_ARE_EQUAL(*expected.Stdout, *Stdout);
87 }
88
89 if (expected.Stderr)
90 {
91 VERIFY_ARE_EQUAL(*expected.Stderr, *Stderr);
92 }
93
94 if (expected.ExitCode)
95 {
96 VERIFY_ARE_EQUAL(*expected.ExitCode, *ExitCode);
97 }
98 }
99
100 std::vector<std::wstring> WSLCExecutionResult::GetStdoutLines() const
101 {
102 std::vector<std::wstring> lines;
103 std::wstringstream ss(*Stdout);
104 std::wstring line;
105 while (std::getline(ss, line))
106 {
107 // Remove carriage return if present
108 if (!line.empty() && line.back() == L'\r')
109 {
110 line.pop_back();
111 }
112
113 lines.push_back(line);
114 }
115 return lines;
116 }
117
118 std::wstring WSLCExecutionResult::GetStdoutOneLine() const
119 {
120 auto stdoutLines = GetStdoutLines();
121
122 // Remove empty trailing lines (common when output ends with \n)
123 while (!stdoutLines.empty() && stdoutLines.back().empty())
124 {
125 stdoutLines.pop_back();
126 }
127
128 VERIFY_ARE_EQUAL(1u, stdoutLines.size());
129 return stdoutLines[0];
130 }
131
132 bool WSLCExecutionResult::StdoutContainsLine(const std::wstring& expectedLine) const
133 {
134 VERIFY_IS_TRUE(Stdout.has_value());
135 for (const auto& line : GetStdoutLines())
136 {
137 if (line == expectedLine)
138 {
139 return true;
140 }
141 }
142
143 return false;
144 }
145
146 bool WSLCExecutionResult::StdoutContainsSubstring(const std::wstring& substring) const
147 {
148 VERIFY_IS_TRUE(Stdout.has_value());
149 return Stdout.value().find(substring) != std::wstring::npos;
150 }
151
152 bool WSLCExecutionResult::StderrContainsSubstring(const std::wstring& substring) const
153 {
154 VERIFY_IS_TRUE(Stderr.has_value());
155 return Stderr.value().find(substring) != std::wstring::npos;
156 }
157
158 WSLCExecutionResult RunWslc(const std::wstring& commandLine, ElevationType elevationType, HANDLE stdinHandle)
159 {
160 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
161 wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
162
163 // If running non-elevated we need to keep the token alive until it completes.
164 wil::unique_handle nonElevatedToken;
165 if (elevationType == ElevationType::NonElevated)
166 {
167 nonElevatedToken = GetNonElevatedPrimaryToken();
168 process.SetToken(nonElevatedToken.get());
169 }
170
171 wil::unique_hfile nul;
172 wil::unique_hfile stdinDup;
173 if (stdinHandle)
174 {
175 // Duplicate as inheritable to avoid mutating the caller's handle state.
176 THROW_IF_WIN32_BOOL_FALSE(
177 DuplicateHandle(GetCurrentProcess(), stdinHandle, GetCurrentProcess(), stdinDup.put(), 0, TRUE, DUPLICATE_SAME_ACCESS));
178 stdinHandle = stdinDup.get();
179 }
180 else
181 {
182 nul = wsl::windows::common::filesystem::OpenNulDevice(GENERIC_READ);
183 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(nul.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
184 stdinHandle = nul.get();
185 }
186
187 process.SetStdHandles(stdinHandle, nullptr, nullptr);
188
189 const auto output = process.RunAndCaptureOutput();
190 return {.CommandLine = commandLine, .Stdout = output.Stdout, .Stderr = output.Stderr, .ExitCode = output.ExitCode};
191 }
192
193 void RunWslcAndVerify(const std::wstring& cmd, const WSLCExecutionResult& expected, ElevationType elevationType)
194 {
195 RunWslc(cmd, elevationType).Verify(expected);
196 }
197
198 WSLCExecutionResult RunWslcAndRedirectToFile(const std::wstring& commandLine, std::optional<std::filesystem::path> outputPath, ElevationType elevationType)
199 {
200 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
201 wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
202
203 // If running non-elevated we need to keep the token alive until it completes.
204 wil::unique_handle nonElevatedToken;
205 if (elevationType == ElevationType::NonElevated)
206 {
207 nonElevatedToken = GetNonElevatedPrimaryToken();
208 process.SetToken(nonElevatedToken.get());
209 }
210
211 auto [parentStderrRead, childStderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
212 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
213
214 wil::unique_hfile redirectedStdout;
215 HANDLE stdoutHandle = nullptr;
216
217 std::wstring effectiveCommandLine = commandLine;
218 if (outputPath.has_value())
219 {
220 SECURITY_ATTRIBUTES securityAttributes{};
221 securityAttributes.nLength = sizeof(securityAttributes);
222 securityAttributes.bInheritHandle = TRUE;
223 redirectedStdout.reset(CreateFileW(
224 outputPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ, &securityAttributes, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
225 THROW_LAST_ERROR_IF(!redirectedStdout);
226 stdoutHandle = redirectedStdout.get();
227 effectiveCommandLine = std::format(L"{} > \"{}\"", commandLine, outputPath->wstring());
228 }
229 else
230 {
231 // Open CONOUT$ so the child process receives a real console handle regardless of
232 // how the test runner has configured its own stdout (e.g. piped in CI). This
233 // makes IsConsoleHandle() return true inside wslc, which is the condition under
234 // test in WSLCE2E_Image_Save_ToTerminal_Fail.
235 SECURITY_ATTRIBUTES securityAttributes{};
236 securityAttributes.nLength = sizeof(securityAttributes);
237 securityAttributes.bInheritHandle = TRUE;
238 redirectedStdout.reset(
239 CreateFileW(L"CONOUT$", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &securityAttributes, OPEN_EXISTING, 0, nullptr));
240 THROW_LAST_ERROR_IF(!redirectedStdout);
241 stdoutHandle = redirectedStdout.get();
242 }
243
244 process.SetStdHandles(nullptr, stdoutHandle, childStderrWrite.get());
245
246 const auto processHandle = process.Start();
247 childStderrWrite.reset();
248
249 const auto exitCode = wsl::windows::common::SubProcess::GetExitCode(processHandle.get());
250 const auto stdErrOutput = wsl::shared::string::MultiByteToWide(ReadToString(parentStderrRead.get()));
251
252 return {.CommandLine = std::move(effectiveCommandLine), .Stdout = L"", .Stderr = stdErrOutput, .ExitCode = exitCode};
253 }
254
255 WSLCExecutionResult RunWslcWithStdinFile(const std::wstring& commandLine, const std::filesystem::path& stdinFilePath, ElevationType elevationType)
256 {
257 SECURITY_ATTRIBUTES securityAttributes{};
258 securityAttributes.nLength = sizeof(securityAttributes);
259 securityAttributes.bInheritHandle = TRUE;
260
261 wil::unique_hfile stdinFile(CreateFileW(
262 stdinFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ, &securityAttributes, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
263 THROW_LAST_ERROR_IF(!stdinFile);
264
265 return RunWslc(commandLine, elevationType, stdinFile.get());
266 }
267
268 void WaitForContainerOutput(const std::wstring& containerName, std::string_view expected, std::chrono::milliseconds timeout)
269 {
270 auto cmd = std::format(L"\"{}\" container logs -f {}", GetWslcPath(), containerName);
271
272 auto [parentStdoutRead, childStdoutWrite] = wslutil::OpenAnonymousPipe(65536, true, false);
273 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
274
275 SubProcess process(nullptr, cmd.c_str());
276 process.SetStdHandles(nullptr, childStdoutWrite.get(), nullptr);
277
278 wil::unique_handle processHandle = process.Start();
279 childStdoutWrite.reset();
280
281 auto terminate = wil::scope_exit([&]() {
282 LOG_IF_WIN32_BOOL_FALSE(TerminateProcess(processHandle.get(), 1));
283 LOG_LAST_ERROR_IF(WaitForSingleObject(processHandle.get(), DefaultWaitTimeoutMs) != WAIT_OBJECT_0);
284 });
285
286 WaitForOutput(wil::unique_handle{parentStdoutRead.release()}, expected, timeout);
287 }
288
289 WSLCInteractiveSession RunWslcInteractive(const std::wstring& commandLine, ElevationType elevationType, std::optional<PseudoConsole> pseudoConsole)
290 {
291 auto cmd = L"\"" + GetWslcPath() + L"\" " + commandLine;
292
293 wsl::windows::common::SubProcess process(nullptr, cmd.c_str());
294
295 wil::unique_hfile parentStdinWrite;
296 wil::unique_hfile parentStdoutRead;
297 wil::unique_hfile parentStderrRead;
298 wsl::windows::common::helpers::unique_pseudo_console console;
299
300 wil::unique_hfile childStdinRead;
301 wil::unique_hfile childStdoutWrite;
302 wil::unique_hfile childStderrWrite;
303
304 if (pseudoConsole.has_value())
305 {
306 process.SetPseudoConsole(pseudoConsole->Handle.get());
307 parentStdinWrite = std::move(pseudoConsole->InputWrite);
308 parentStdoutRead = std::move(pseudoConsole->OutputRead);
309 console = std::move(pseudoConsole->Handle);
310 }
311 else
312 {
313 std::tie(childStdinRead, parentStdinWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, false, true);
314 std::tie(parentStdoutRead, childStdoutWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
315 std::tie(parentStderrRead, childStderrWrite) = wsl::windows::common::wslutil::OpenAnonymousPipe(65536, true, false);
316
317 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdinRead.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
318 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStdoutWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
319 THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(childStderrWrite.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
320
321 process.SetStdHandles(childStdinRead.get(), childStdoutWrite.get(), childStderrWrite.get());
322 }
323
324 wil::unique_handle nonElevatedToken;
325 if (elevationType == ElevationType::NonElevated)
326 {
327 nonElevatedToken = GetNonElevatedPrimaryToken();
328 process.SetToken(nonElevatedToken.get());
329 }
330
331 wil::unique_handle processHandle = process.Start();
332
333 childStdinRead.reset();
334 childStdoutWrite.reset();
335 childStderrWrite.reset();
336
337 return WSLCInteractiveSession(
338 commandLine,
339 std::move(parentStdinWrite),
340 std::move(parentStdoutRead),
341 std::move(parentStderrRead),
342 std::move(processHandle),
343 std::move(nonElevatedToken), // Transfer token ownership to the session
344 std::move(console));
345 }
346
347 PseudoConsole::PseudoConsole(SHORT columns, SHORT rows)
348 {
349 auto [inputRead, inputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, true);
350
351 auto [outputRead, outputWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
352
353 HPCON rawPseudoConsole{};
354 THROW_IF_FAILED(::CreatePseudoConsole(COORD{columns, rows}, inputRead.get(), outputWrite.get(), 0, &rawPseudoConsole));
355 Handle.reset(rawPseudoConsole);
356
357 InputWrite = std::move(inputWrite);
358 OutputRead = std::move(outputRead);
359 }
360
361 // WSLCInteractiveSession implementation
362
363 WSLCInteractiveSession::WSLCInteractiveSession(
364 std::wstring commandLine,
365 wil::unique_hfile stdinWrite,
366 wil::unique_hfile stdoutRead,
367 wil::unique_hfile stderrRead,
368 wil::unique_handle processHandle,
369 wil::unique_handle nonElevatedToken,
370 wsl::windows::common::helpers::unique_pseudo_console pseudoConsole) :
371 CommandLine(std::move(commandLine)),
372 m_stdinWrite(std::move(stdinWrite)),
373 m_stdoutRead(std::move(stdoutRead)),
374 m_stderrRead(std::move(stderrRead)),
375 m_pseudoConsole(std::move(pseudoConsole)),
376 m_processHandle(std::move(processHandle)),
377 m_nonElevatedToken(std::move(nonElevatedToken))
378 {
379 m_stdoutReader = std::make_unique<PartialHandleRead>(m_stdoutRead.get());
380
381 // In pseudoconsole mode stderr is multiplexed onto the conpty output, so there is no
382 // separate stderr handle to read from.
383 if (m_stderrRead.is_valid())
384 {
385 m_stderrReader = std::make_unique<PartialHandleRead>(m_stderrRead.get());
386 }
387 }
388
389 WSLCInteractiveSession::~WSLCInteractiveSession()
390 {
391 // Best-effort cleanup to avoid orphaned wslc process if Exit()/Wait() were not called.
392 if (!m_processHandle.is_valid())
393 {
394 return;
395 }
396
397 CloseStdin();
398
399 DWORD waitResult = ::WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
400 if (waitResult == WAIT_TIMEOUT)
401 {
402 // Still running: terminate and wait again, but do not throw.
403 ::TerminateProcess(m_processHandle.get(), 1);
404 ::WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
405 }
406 }
407
408 void WSLCInteractiveSession::ExpectStdout(const std::string& expected)
409 {
410 if (m_ignoreSequence.has_value())
411 {
412 while (m_stdoutReader->ReadBytes(m_ignoreSequence->size()) == *m_ignoreSequence)
413 {
414 Log::Comment(std::format(L"Consuming ignored sequence: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(*m_ignoreSequence)))
415 .c_str());
416 m_stdoutReader->ConsumeBytes(m_ignoreSequence->size());
417 }
418 }
419
420 Log::Comment(std::format(L"Expecting stdout: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
421 m_stdoutReader->ExpectConsume(expected);
422 }
423
424 std::string WSLCInteractiveSession::GetStdoutData() const
425 {
426 return m_stdoutReader->GetData();
427 }
428
429 void WSLCInteractiveSession::ResizePseudoConsole(SHORT columns, SHORT rows)
430 {
431 VERIFY_IS_TRUE(static_cast<bool>(m_pseudoConsole), L"ResizePseudoConsole requires a pseudoconsole-backed session");
432 THROW_IF_FAILED(::ResizePseudoConsole(m_pseudoConsole.get(), COORD{columns, rows}));
433 }
434
435 void WSLCInteractiveSession::ExpectStderr(const std::string& expected)
436 {
437 WI_ASSERT(m_stderrReader.get() != nullptr);
438 Log::Comment(std::format(L"Expecting stderr: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(expected))).c_str());
439 m_stderrReader->ExpectConsume(expected);
440 }
441
442 void WSLCInteractiveSession::ExpectCommandEcho(const std::string& command)
443 {
444 // TTY mode: expect command echo, then B_END and carriage return
445 ExpectStdout(std::format("{}\r\n{}\r", command, wsl::shared::string::WideToMultiByte(std::wstring(VT::B_END.Get()))));
446 }
447
448 void WSLCInteractiveSession::IgnoreSequence(const std::string& sequence)
449 {
450 VERIFY_IS_FALSE(m_ignoreSequence.has_value());
451 m_ignoreSequence = sequence;
452 }
453
454 void WSLCInteractiveSession::Write(const std::string& data)
455 {
456 Log::Comment(std::format(L"Writing to stdin: \"{}\"", wsl::shared::string::MultiByteToWide(EscapeString(data))).c_str());
457
458 OVERLAPPED overlapped{};
459 wil::unique_event event(wil::EventOptions::ManualReset);
460 overlapped.hEvent = event.get();
461
462 DWORD written = 0;
463 if (!WriteFile(m_stdinWrite.get(), data.c_str(), static_cast<DWORD>(data.size()), &written, &overlapped))
464 {
465 DWORD error = GetLastError();
466 if (error == ERROR_IO_PENDING)
467 {
468 DWORD waitResult = WaitForSingleObject(event.get(), DefaultWaitTimeoutMs);
469 if (waitResult == WAIT_TIMEOUT)
470 {
471 THROW_HR(HRESULT_FROM_WIN32(ERROR_TIMEOUT));
472 }
473 else if (waitResult == WAIT_FAILED)
474 {
475 THROW_LAST_ERROR();
476 }
477 else if (waitResult != WAIT_OBJECT_0)
478 {
479 THROW_HR_MSG(E_UNEXPECTED, "WaitForSingleObject returned unexpected result: 0x%08lx", waitResult);
480 }
481
482 THROW_IF_WIN32_BOOL_FALSE(GetOverlappedResult(m_stdinWrite.get(), &overlapped, &written, FALSE));
483 }
484 else
485 {
486 THROW_WIN32(error);
487 }
488 }
489 }
490
491 void WSLCInteractiveSession::WriteLine(const std::string& line)
492 {
493 Write(line + "\n");
494 }
495
496 bool WSLCInteractiveSession::IsRunning() const
497 {
498 DWORD exitCode = 0;
499 return GetExitCodeProcess(m_processHandle.get(), &exitCode) && exitCode == STILL_ACTIVE;
500 }
501
502 void WSLCInteractiveSession::CloseStdin()
503 {
504 m_stdinWrite.reset();
505 }
506
507 std::optional<int> WSLCInteractiveSession::GetExitCode() const
508 {
509 DWORD exitCode = 0;
510 if (GetExitCodeProcess(m_processHandle.get(), &exitCode) && exitCode != STILL_ACTIVE)
511 {
512 return static_cast<int>(exitCode);
513 }
514
515 return std::nullopt;
516 }
517
518 void WSLCInteractiveSession::WaitForExit(DWORD timeoutMs)
519 {
520 auto result = WaitForSingleObject(m_processHandle.get(), timeoutMs);
521 if (result == WAIT_TIMEOUT)
522 {
523 DWORD processId = GetProcessId(m_processHandle.get());
524
525 Log::Warning(std::format(L"Process (PID: {}) did not exit within timeout of {}ms", processId, timeoutMs).c_str());
526 Log::Warning(L"Attempting to terminate process forcefully");
527 Terminate(999);
528 WaitForSingleObject(m_processHandle.get(), DefaultWaitTimeoutMs);
529
530 THROW_HR_MSG(E_FAIL, "Process did not exit within timeout of %lums and was forcefully terminated", timeoutMs);
531 }
532
533 if (result == WAIT_FAILED)
534 {
535 THROW_LAST_ERROR_MSG("WaitForSingleObject failed while waiting for process exit");
536 }
537
538 if (result != WAIT_OBJECT_0)
539 {
540 THROW_HR_MSG(E_UNEXPECTED, "WaitForSingleObject returned unexpected result: 0x%08lx", result);
541 }
542 }
543
544 int WSLCInteractiveSession::Wait(DWORD timeoutMs)
545 {
546 WaitForExit(timeoutMs);
547 DWORD exitCode = 0;
548 THROW_IF_WIN32_BOOL_FALSE(GetExitCodeProcess(m_processHandle.get(), &exitCode));
549 return static_cast<int>(exitCode);
550 }
551
552 bool WSLCInteractiveSession::Terminate(UINT exitCode)
553 {
554 return TerminateProcess(m_processHandle.get(), exitCode) != FALSE;
555 }
556
557 void WSLCInteractiveSession::VerifyNoErrors()
558 {
559 WI_ASSERT(m_stderrReader.get() != nullptr);
560 m_stderrReader->ExpectClosed(DefaultWaitTimeoutMs);
561
562 // Verify that stderr was actually empty - not just closed
563 const auto& stderrContent = m_stderrReader->GetData();
564 if (!stderrContent.empty())
565 {
566 VERIFY_FAIL(std::format(L"Expected no errors but stderr contained: {}", wsl::shared::string::MultiByteToWide(EscapeString(stderrContent)))
567 .c_str());
568 }
569 }
570
571 int WSLCInteractiveSession::Exit(DWORD timeoutMs)
572 {
573 WriteLine("exit");
574 CloseStdin();
575 return Wait(timeoutMs);
576 }
577
578 int WSLCInteractiveSession::ExitAndVerifyNoErrors(DWORD timeoutMs)
579 {
580 const auto exitCode = Exit(timeoutMs);
581 VerifyNoErrors();
582 return exitCode;
583 }
584
585 } // namespace WSLCE2ETests