master
cpp 1,999 lines 86.1 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WslcSdkWinRtTests.cpp
8
9 Abstract:
10
11 This file contains test cases for the WSLC SDK WinRT projection.
12
13 --*/
14
15 #include "precomp.h"
16 #include "Common.h"
17 #include "wslcsdk.h"
18 #include "WslcsdkPrivate.h"
19 #include "WSLCContainerLauncher.h"
20 #include "wslutil.h"
21 #include "wslc/e2e/WSLCE2EHelpers.h"
22
23 #include "winrt/Session.h"
24 #include "winrt/Helpers.h"
25 #include "winrt/ProcessCrashInformation.h"
26
27 #include <winrt/Microsoft.WSL.Containers.h>
28 #include <winrt/Windows.Foundation.h>
29 #include <winrt/Windows.Foundation.Collections.h>
30 #include <winrt/Windows.Networking.h>
31 #include <winrt/Windows.Storage.Streams.h>
32
33 using namespace winrt::Windows::Foundation;
34 using namespace winrt::Windows::Foundation::Collections;
35 using namespace winrt::Windows::Storage::Streams;
36 using namespace std::chrono_literals;
37
38 namespace WSLCSDK = winrt::Microsoft::WSL::Containers;
39
40 extern std::wstring g_testDataPath;
41 extern bool g_fastTestRun;
42
43 #define VERIFY_THROWS_HR(operation, expectedHr) \
44 VERIFY_THROWS_SPECIFIC( \
45 operation, winrt::hresult_error, [&](winrt::hresult_error const& e) { return e.code().value == expectedHr; })
46
47 #define IGNORE_ERRORS(operation) \
48 try \
49 { \
50 operation; \
51 } \
52 CATCH_LOG()
53 #define SCOPE_CLEANUP(operation) wil::scope_exit([&]() { IGNORE_ERRORS(operation) })
54 #define DELETE_CONTAINER_ON_SCOPE_EXIT(container) SCOPE_CLEANUP(container.Delete(WSLCSDK::DeleteContainerOption::Force))
55 #define DELETE_IMAGE_ON_SCOPE_EXIT(imageName) SCOPE_CLEANUP(m_defaultSession.DeleteImage(imageName))
56
57 struct CapturedProcessOutput
58 {
59 uint32_t ExitCode;
60 std::wstring StandardOutput;
61 std::wstring StandardError;
62 };
63
64 std::wstring ReadStream(IInputStream const& stream)
65 {
66 std::wstring output;
67 DataReader reader{stream};
68 reader.UnicodeEncoding(winrt::Windows::Storage::Streams::UnicodeEncoding::Utf8);
69 uint32_t bytesRead;
70 do
71 {
72 bytesRead = reader.LoadAsync(1024).get();
73 output += reader.ReadString(bytesRead).c_str();
74 } while (bytesRead > 0);
75 return output;
76 }
77
78 class WslcSdkWinRtTests
79 {
80 WSLC_TEST_CLASS(WslcSdkWinRtTests)
81
82 std::filesystem::path m_storagePath;
83 WSLCSDK::Session m_defaultSession{nullptr};
84
85 static inline constexpr auto c_testSessionName = L"wslc-winrt-test";
86
87 // -----------------------------------------------------------------------
88 // Helpers
89 // -----------------------------------------------------------------------
90
91 void StartProcessAndWaitForExit(WSLCSDK::Process const& process, std::chrono::milliseconds timeout = 2min)
92 {
93 std::promise<void> promise;
94 auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
95 process.Start();
96 VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready);
97 }
98
99 void StartContainerAndWaitForInitProcessExit(WSLCSDK::Container const& container, std::chrono::milliseconds timeout = 2min)
100 {
101 auto initProcess = container.InitProcess();
102 std::promise<void> promise;
103 auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
104 container.Start();
105 VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready);
106 }
107
108 CapturedProcessOutput GetProcessOutput(WSLCSDK::Process const& process)
109 {
110 CapturedProcessOutput output;
111 output.ExitCode = process.ExitCode();
112 output.StandardOutput = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput));
113 output.StandardError = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError));
114
115 return output;
116 }
117
118 void WaitForProcessOutput(WSLCSDK::Process const& process, std::string_view marker, std::chrono::seconds timeout = 60s)
119 {
120 auto stream = process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput);
121 Buffer buffer{1024};
122
123 std::string accumulated;
124 const auto deadline = std::chrono::steady_clock::now() + timeout;
125 for (auto now = std::chrono::steady_clock::now(); now < deadline; now = std::chrono::steady_clock::now())
126 {
127 const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now);
128
129 // N.B. InputStreamOptions::Partial is not supported.
130 auto read = stream.ReadAsync(buffer, buffer.Capacity(), InputStreamOptions::None);
131 if (read.wait_for(remaining) != winrt::Windows::Foundation::AsyncStatus::Completed)
132 {
133 break;
134 }
135
136 const auto result = read.GetResults();
137 if (result.Length() == 0)
138 {
139 break;
140 }
141
142 accumulated.append(reinterpret_cast<const char*>(result.data()), result.Length());
143 if (accumulated.find(marker) != std::string::npos)
144 {
145 return;
146 }
147 }
148
149 LogError("Timed out waiting for process output marker: '%hs'. Output: '%hs'", std::string(marker).c_str(), accumulated.c_str());
150 VERIFY_FAIL();
151 }
152
153 struct RunContainerOptions
154 {
155 std::vector<winrt::hstring> commandLine = {};
156 bool enableGpu = false;
157 std::optional<winrt::hstring> name = std::nullopt;
158 std::chrono::milliseconds timeout = 2min;
159 std::optional<WSLCSDK::ContainerNetworkingMode> networkingMode = std::nullopt;
160 };
161
162 // Creates and starts a one-shot container, waits for the init process to
163 // exit, and returns the exit code.
164 CapturedProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {})
165 {
166 auto procSettings = WSLCSDK::ProcessSettings();
167 if (!options.commandLine.empty())
168 {
169 procSettings.CommandLine(winrt::single_threaded_vector(std::move(options.commandLine)));
170 }
171
172 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
173
174 auto containerSettings = WSLCSDK::ContainerSettings(imageName);
175 containerSettings.InitProcess(procSettings);
176 containerSettings.EnableGpu(options.enableGpu);
177
178 if (options.name)
179 {
180 containerSettings.Name(options.name.value());
181 }
182
183 if (options.networkingMode)
184 {
185 containerSettings.NetworkingMode(options.networkingMode.value());
186 }
187
188 auto container = m_defaultSession.CreateContainer(containerSettings);
189 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
190
191 StartContainerAndWaitForInitProcessExit(container, options.timeout);
192 auto output = GetProcessOutput(container.InitProcess());
193
194 IGNORE_ERRORS(container.Delete(WSLCSDK::DeleteContainerOption::Force));
195
196 return output;
197 }
198
199 bool HasImage(winrt::hstring const& imageName)
200 {
201 auto images = m_defaultSession.GetImages();
202 return std::any_of(images.begin(), images.end(), [&](auto const& img) { return img.Name() == imageName; });
203 }
204
205 // Starts a local wslc-registry container using host-mode networking.
206 // Host networking is not exposed by the WinRT projection, so this helper
207 // uses WSLCContainerLauncher with the raw session handle.
208 std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
209 const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000)
210 {
211 // Get the IWSLCSession COM object from the SDK session handle and delegate to the shared helper.
212 auto comSession =
213 reinterpret_cast<WslcSessionImpl*>(WSLCSDK::implementation::GetHandle(m_defaultSession))->session.query<IWSLCSession>();
214 return WSLCE2ETests::StartLocalRegistry(*comSession, username, password, port);
215 }
216
217 // Tags and pushes an image to a local registry via the SDK APIs.
218 void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth)
219 {
220 const auto imageName = winrt::to_hstring(std::format("{}:{}", repo, tag));
221 const auto registryRepo = winrt::to_hstring(std::format("{}/{}", registryAddress, repo));
222 const auto registryImage = winrt::to_hstring(std::format("{}/{}:{}", registryAddress, repo, tag));
223
224 VERIFY_IS_TRUE(HasImage(imageName));
225
226 m_defaultSession.TagImage(WSLCSDK::TagImageOptions(imageName, registryRepo, winrt::to_hstring(tag)));
227
228 // Ensures the registry-prefixed tag is removed after the push.
229 auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(registryImage);
230
231 m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(registryImage, winrt::to_hstring(registryAuth))).get();
232 }
233
234 TEST_CLASS_SETUP(TestClassSetup)
235 {
236 WSADATA wsaData;
237 THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &wsaData));
238
239 winrt::init_apartment();
240
241 // Use the same storage path as WSLC runtime tests to reduce pull overhead.
242 m_storagePath = std::filesystem::current_path() / "test-storage";
243
244 // Build session settings using the WinRT API
245 auto settings = WSLCSDK::SessionSettings(c_testSessionName, m_storagePath.wstring());
246 settings.CpuCount(4);
247 settings.MemorySizeInMB(2048);
248 settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
249 settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
250
251 m_defaultSession = WSLCSDK::Session(settings);
252 m_defaultSession.Start();
253
254 // Pull images required by the tests (no-op if already present).
255 for (const auto* imageName : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"})
256 {
257 const auto imagePath = GetTestImagePath(imageName);
258 m_defaultSession.LoadImageAsync(imagePath.wstring()).get();
259 }
260
261 return true;
262 }
263
264 TEST_CLASS_CLEANUP(TestClassCleanup)
265 {
266 if (m_defaultSession)
267 {
268 m_defaultSession.Terminate();
269 m_defaultSession = nullptr;
270 }
271
272 // Preserve the VHD in fast-run mode so subsequent runs skip image pulling.
273 if (!g_fastTestRun && !m_storagePath.empty())
274 {
275 std::error_code error;
276 std::filesystem::remove_all(m_storagePath, error);
277 if (error)
278 {
279 LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
280 }
281 }
282
283 return true;
284 }
285
286 // -----------------------------------------------------------------------
287 // Session tests
288 // -----------------------------------------------------------------------
289
290 WSLC_TEST_METHOD(CreateSession)
291 {
292 const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-extra-session-storage";
293
294 auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-extra-session", extraStorage.wstring());
295 settings.CpuCount(2);
296 settings.MemorySizeInMB(1024);
297 settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
298 settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 1024ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
299
300 // Positive: Creation must succeed with valid settings.
301 {
302 auto session = WSLCSDK::Session(settings);
303 VERIFY_IS_NOT_NULL(session);
304 }
305
306 // Negative: Must throw if used before Start()
307 {
308 auto session = WSLCSDK::Session(settings);
309 VERIFY_THROWS_HR(std::ignore = session.GetImages(), E_ILLEGAL_METHOD_CALL);
310 }
311
312 // Positive: Starting the session must succeed.
313 {
314 auto session = WSLCSDK::Session(settings);
315 VERIFY_NO_THROW(session.Start());
316 }
317
318 // Negative: Null settings must fail.
319 {
320 VERIFY_THROWS_HR(WSLCSDK::Session(WSLCSDK::SessionSettings{nullptr}), E_POINTER);
321 }
322 }
323
324 WSLC_TEST_METHOD(TerminationHandler)
325 {
326 // Positive: Terminating the session must trigger a graceful shutdown and fire the event
327 std::promise<WSLCSDK::SessionTerminationReason> promise;
328
329 const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-termh-storage";
330
331 auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-termh", extraStorage.wstring());
332 settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
333
334 auto session = WSLCSDK::Session(settings);
335 session.Terminated([&](WSLCSDK::SessionTerminationReason reason) { promise.set_value(reason); });
336
337 session.Start();
338
339 session.Terminate();
340
341 auto future = promise.get_future();
342 VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready);
343 VERIFY_ARE_EQUAL(future.get(), WSLCSDK::SessionTerminationReason::Shutdown);
344 }
345
346 WSLC_TEST_METHOD(ProcessCrashedEvent)
347 {
348 // Start a long-running container so we can exec non-PID-1 processes into it.
349 // The crashing process must NOT be the container's init process: Linux silently
350 // drops kill()-sent signals with default disposition when targeting PID 1 in a
351 // PID namespace, so no core dump would be generated if we crash the init process.
352 auto initProcSettings = WSLCSDK::ProcessSettings();
353 initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
354
355 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
356 containerSettings.InitProcess(initProcSettings);
357
358 auto container = m_defaultSession.CreateContainer(containerSettings);
359 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
360 container.Start();
361
362 // Positive: A crashing exec process must fire the ProcessCrashed event with correctly populated info.
363 {
364 std::promise<WSLCSDK::ProcessCrashInformation> promise;
365
366 auto revoker = m_defaultSession.ProcessCrashed(winrt::auto_revoke, [&](WSLCSDK::ProcessCrashInformation info) {
367 // Guard against multiple firings from concurrently running tests.
368 try
369 {
370 promise.set_value(info);
371 }
372 catch (...)
373 {
374 }
375 });
376
377 auto execSettings = WSLCSDK::ProcessSettings();
378 execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"kill -SEGV $$"}));
379
380 const auto beforeCrash = winrt::clock::now();
381 StartProcessAndWaitForExit(container.CreateProcess(execSettings), 30s);
382
383 auto future = promise.get_future();
384 VERIFY_ARE_EQUAL(future.wait_for(60s), std::future_status::ready);
385 const auto afterCrash = winrt::clock::now();
386
387 auto info = future.get();
388
389 VERIFY_IS_FALSE(info.DumpPath().empty());
390 VERIFY_IS_TRUE(std::filesystem::exists(info.DumpPath().c_str()));
391 VERIFY_IS_TRUE(std::wstring_view(info.ProcessName()).find(L"sh") != std::wstring_view::npos);
392 VERIFY_IS_GREATER_THAN(info.Pid(), 0u);
393 VERIFY_ARE_EQUAL(info.Signal(), 11u); // SIGSEGV = 11
394
395 // Crash timestamps are second-granularity; allow some slack around the measured window.
396 VERIFY_IS_TRUE(info.Timestamp() >= beforeCrash - 1s);
397 VERIFY_IS_TRUE(info.Timestamp() <= afterCrash + 1s);
398 }
399
400 // Negative: After revoking the subscription token, the handler must no longer fire.
401 {
402 std::atomic<int> callCount{0};
403 {
404 auto revoker =
405 m_defaultSession.ProcessCrashed(winrt::auto_revoke, [&](WSLCSDK::ProcessCrashInformation) { ++callCount; });
406 // revoker goes out of scope here — handler is unsubscribed before any crash is triggered.
407 }
408
409 auto execSettings = WSLCSDK::ProcessSettings();
410 execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"kill -SEGV $$"}));
411
412 StartProcessAndWaitForExit(container.CreateProcess(execSettings), 60s);
413
414 // Allow the event system time to dispatch any pending callbacks.
415 Sleep(1000);
416 VERIFY_ARE_EQUAL(callCount.load(), 0);
417 }
418 }
419
420 WSLC_TEST_METHOD(ProcessCrashInformationProperties)
421 {
422 // Verify that ProcessCrashInformation correctly maps all fields from WslcSessionCrashDumpInfo.
423 constexpr PCWSTR c_dumpPath = L"C:\\test\\dump.dmp";
424 constexpr PCSTR c_processName = "test-process";
425 constexpr uint32_t c_pid = 42;
426 constexpr uint32_t c_signal = 11; // SIGSEGV
427 constexpr uint64_t c_timestamp = 1700000000; // 2023-11-14 22:13:20 UTC
428
429 WslcSessionCrashDumpInfo info{};
430 info.dumpPath = c_dumpPath;
431 info.processName = c_processName;
432 info.pid = c_pid;
433 info.signal = c_signal;
434 info.timestamp = c_timestamp;
435
436 auto impl = winrt::make_self<WSLCSDK::implementation::ProcessCrashInformation>(&info);
437
438 VERIFY_ARE_EQUAL(impl->DumpPath(), winrt::hstring(c_dumpPath));
439 VERIFY_ARE_EQUAL(impl->ProcessName(), winrt::to_hstring(c_processName));
440 VERIFY_ARE_EQUAL(impl->Pid(), c_pid);
441 VERIFY_ARE_EQUAL(impl->Signal(), c_signal);
442 VERIFY_ARE_EQUAL(winrt::clock::to_time_t(impl->Timestamp()), static_cast<time_t>(c_timestamp));
443 }
444
445 // -----------------------------------------------------------------------
446 // Image tests
447 // -----------------------------------------------------------------------
448
449 WSLC_TEST_METHOD(ImageList)
450 {
451 // Session has images pre-loaded - list must return at least one entry.
452 const auto images = m_defaultSession.GetImages();
453 VERIFY_IS_TRUE(images.Size() >= 1);
454
455 // At least one image must be non-empty
456 bool foundNonEmpty = false;
457 for (auto const& img : images)
458 {
459 if (!img.Name().empty() && img.Size() != 0)
460 {
461 foundNonEmpty = true;
462 break;
463 }
464 }
465 VERIFY_IS_TRUE(foundNonEmpty);
466 }
467
468 WSLC_TEST_METHOD(LoadImage)
469 {
470 // Positive: load a saved image tar and verify the image can be run
471 {
472 // Remove the image if it already exists
473 IGNORE_ERRORS(m_defaultSession.DeleteImage(L"hello-world:latest"));
474
475 const auto imageTar = GetTestImagePath("hello-world:latest");
476
477 // Positive: load from file path.
478 VERIFY_NO_THROW(m_defaultSession.LoadImageAsync(imageTar.wstring()).get());
479
480 // Verify the loaded image is usable
481 VERIFY_IS_TRUE(HasImage(L"hello-world:latest"));
482 auto output = RunContainerAndWaitForExit(L"hello-world:latest", {});
483 VERIFY_ARE_EQUAL(output.ExitCode, 0);
484 VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos);
485 }
486
487 // Negative: empty path must fail
488 {
489 VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"").get(), E_INVALIDARG);
490 }
491
492 // Negative: non-existent path must fail.
493 {
494 VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"C:\\bogus\\image.tar").get(), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
495 }
496 }
497
498 WSLC_TEST_METHOD(ImportImage)
499 {
500 const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
501 constexpr auto c_importedImageName = L"my-hello-world-winrt:test";
502
503 IGNORE_ERRORS(m_defaultSession.DeleteImage(c_importedImageName));
504
505 // Positive: import an exported image tar via path.
506 {
507 auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(c_importedImageName);
508
509 VERIFY_NO_THROW(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), c_importedImageName).get());
510
511 VERIFY_IS_TRUE(HasImage(c_importedImageName));
512
513 auto output = RunContainerAndWaitForExit(c_importedImageName, {.commandLine = {L"/hello"}});
514 VERIFY_ARE_EQUAL(output.ExitCode, 0);
515 VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos);
516 }
517
518 // Negative: empty path must fail.
519 {
520 VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(L"", c_importedImageName).get(), E_INVALIDARG);
521 }
522
523 // Negative: empty image name must fail.
524 {
525 VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), L"").get(), E_INVALIDARG);
526 }
527
528 // Negative: non-tar file must fail.
529 {
530 std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
531 VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(pathToSelf.wstring(), L"import-self:test").get(), E_FAIL);
532 }
533 }
534
535 WSLC_TEST_METHOD(ImageDelete)
536 {
537 VERIFY_IS_TRUE(HasImage(L"hello-world:latest"));
538
539 // Positive: delete an existing image.
540 {
541 m_defaultSession.DeleteImage(L"hello-world:latest");
542 VERIFY_IS_FALSE(HasImage(L"hello-world:latest"));
543
544 // Reload for subsequent tests.
545 const auto imageTar = GetTestImagePath("hello-world:latest");
546 m_defaultSession.LoadImageAsync(imageTar.wstring()).get();
547 }
548
549 // Negative: non-existent image name must throw.
550 {
551 VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent:no-such-tag"), WSLC_E_IMAGE_NOT_FOUND);
552 }
553 }
554
555 // -----------------------------------------------------------------------
556 // Container lifecycle tests
557 // -----------------------------------------------------------------------
558
559 WSLC_TEST_METHOD(CreateContainer)
560 {
561 // Positive: stdout is captured correctly.
562 {
563 auto output = RunContainerAndWaitForExit(L"debian:latest", {.commandLine = {L"/bin/echo", L"OK"}});
564 VERIFY_ARE_EQUAL(output.ExitCode, 0);
565 VERIFY_ARE_EQUAL(output.StandardOutput, L"OK\n");
566 VERIFY_ARE_EQUAL(output.StandardError, L"");
567 }
568
569 // Positive: stdout and stderr are routed independently.
570 {
571 auto output = RunContainerAndWaitForExit(
572 L"debian:latest", {.commandLine = {L"/bin/sh", L"-c", L"echo stdout && echo stderr >&2"}});
573 VERIFY_ARE_EQUAL(output.ExitCode, 0);
574 VERIFY_ARE_EQUAL(output.StandardOutput, L"stdout\n");
575 VERIFY_ARE_EQUAL(output.StandardError, L"stderr\n");
576 }
577
578 // Negative: creating a container with a non-existent image fails at CreateContainer.
579 {
580 WSLCSDK::ContainerSettings containerSettings{L"invalid-image:notfound"};
581 VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), WSLC_E_IMAGE_NOT_FOUND);
582 }
583
584 // Negative: an empty image name is rejected.
585 {
586 VERIFY_THROWS_HR(WSLCSDK::ContainerSettings{L""}, E_INVALIDARG);
587 }
588
589 // Verify that a null settings pointer is rejected.
590 {
591 VERIFY_THROWS_HR(m_defaultSession.CreateContainer(nullptr), E_POINTER);
592 }
593 }
594
595 WSLC_TEST_METHOD(ContainerGetId)
596 {
597 auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
598 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
599
600 const auto id = container.Id();
601 VERIFY_IS_FALSE(id.empty());
602 // Container ID is a 64-character lowercase hex string.
603 VERIFY_ARE_EQUAL(id.size(), 64u);
604 }
605
606 WSLC_TEST_METHOD(ContainerGetState)
607 {
608 auto procSettings = WSLCSDK::ProcessSettings();
609 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
610
611 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
612 containerSettings.InitProcess(procSettings);
613
614 auto container = m_defaultSession.CreateContainer(containerSettings);
615 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
616
617 // State after creation: Created.
618 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Created);
619
620 container.Start();
621
622 // State while running: Running.
623 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running);
624
625 container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero());
626
627 // State after stop: Exited.
628 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited);
629 }
630
631 WSLC_TEST_METHOD(ContainerStopAndDelete)
632 {
633 auto procSettings = WSLCSDK::ProcessSettings();
634 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"999"}));
635
636 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
637 containerSettings.InitProcess(procSettings);
638
639 auto container = m_defaultSession.CreateContainer(containerSettings);
640 VERIFY_NO_THROW(container.Start());
641
642 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running);
643
644 VERIFY_NO_THROW(container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero()));
645 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited);
646
647 VERIFY_NO_THROW(container.Delete(WSLCSDK::DeleteContainerOption::None));
648 VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Deleted);
649 }
650
651 WSLC_TEST_METHOD(ProcessIOHandles)
652 {
653 auto procSettings = WSLCSDK::ProcessSettings();
654 procSettings.CommandLine(
655 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT_TOKEN; echo STDERR_TOKEN >&2"}));
656 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
657
658 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
659 containerSettings.InitProcess(procSettings);
660
661 auto container = m_defaultSession.CreateContainer(containerSettings);
662 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
663
664 auto initProcess = container.InitProcess();
665
666 std::promise<void> promise;
667 auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
668
669 container.Start();
670
671 auto stdoutStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput);
672 auto stderrStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError);
673
674 // Verify that each handle can only be acquired once.
675 {
676 VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
677 VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
678 }
679
680 VERIFY_ARE_EQUAL(promise.get_future().wait_for(1min), std::future_status::ready);
681
682 VERIFY_ARE_EQUAL(ReadStream(stdoutStream), L"STDOUT_TOKEN\n");
683 VERIFY_ARE_EQUAL(ReadStream(stderrStream), L"STDERR_TOKEN\n");
684 }
685
686 WSLC_TEST_METHOD(ContainerNetworkingMode)
687 {
688 // BRIDGED: eth0 interface must be present.
689 {
690 auto output = RunContainerAndWaitForExit(
691 L"debian:latest",
692 {.commandLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
693 .networkingMode = WSLCSDK::ContainerNetworkingMode::Bridged});
694
695 VERIFY_ARE_EQUAL(output.StandardOutput, L"HAS_ETH0\n");
696 }
697
698 // NONE: eth0 interface must not be present.
699 {
700 auto output = RunContainerAndWaitForExit(
701 L"debian:latest",
702 {.commandLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
703 .networkingMode = WSLCSDK::ContainerNetworkingMode::None});
704
705 VERIFY_ARE_EQUAL(output.StandardOutput, L"NO_ETH0\n");
706 }
707
708 // Invalid networking mode must fail.
709 {
710 WSLCSDK::ContainerSettings containerSettings{L"debian:latest"};
711 VERIFY_THROWS_HR(containerSettings.NetworkingMode(static_cast<WSLCSDK::ContainerNetworkingMode>(99)), E_INVALIDARG);
712 }
713 }
714
715 WSLC_TEST_METHOD(ContainerPortMapping)
716 {
717 // Negative: port mappings with None networking mode must fail at Start.
718 {
719 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
720 containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::None);
721 containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>(
722 {WSLCSDK::ContainerPortMapping(12342, 8000, WSLCSDK::PortProtocol::TCP)}));
723
724 VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), E_INVALIDARG);
725 }
726
727 // Functional: BRIDGED networking with port mapping; HTTP server must be reachable.
728 {
729 auto procSettings = WSLCSDK::ProcessSettings();
730 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
731 procSettings.EnvironmentVariables(
732 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
733 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
734
735 auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
736 containerSettings.InitProcess(procSettings);
737 containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
738 containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>(
739 {WSLCSDK::ContainerPortMapping(12341, 8000, WSLCSDK::PortProtocol::TCP)}));
740
741 auto container = m_defaultSession.CreateContainer(containerSettings);
742 container.Start();
743
744 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
745
746 // Wait for the in-container HTTP server to start listening before issuing a request.
747 WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
748
749 ExpectHttpResponse(L"http://127.0.0.1:12341", 200, true);
750 }
751
752 // Functional: port mapping with explicit IPv4 WindowsAddress.
753 {
754 auto procSettings = WSLCSDK::ProcessSettings();
755 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"8000"}));
756 procSettings.EnvironmentVariables(
757 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
758 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
759
760 auto portMapping = WSLCSDK::ContainerPortMapping(12343, 8000, WSLCSDK::PortProtocol::TCP);
761 portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"127.0.0.1"));
762
763 auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
764 containerSettings.InitProcess(procSettings);
765 containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
766 containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>({portMapping}));
767
768 auto container = m_defaultSession.CreateContainer(containerSettings);
769 container.Start();
770
771 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
772
773 // Wait for the in-container HTTP server to start listening before issuing a request.
774 WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
775
776 ExpectHttpResponse(L"http://127.0.0.1:12343", 200, true);
777 }
778
779 // Functional: port mapping with explicit IPv6 WindowsAddress.
780 {
781 auto procSettings = WSLCSDK::ProcessSettings();
782 procSettings.CommandLine(
783 winrt::single_threaded_vector<winrt::hstring>({L"python3", L"-m", L"http.server", L"--bind", L"::", L"8000"}));
784 procSettings.EnvironmentVariables(
785 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"PYTHONUNBUFFERED", L"1"}}));
786 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
787
788 auto portMapping = WSLCSDK::ContainerPortMapping(12344, 8000, WSLCSDK::PortProtocol::TCP);
789 portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"::1"));
790
791 auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine");
792 containerSettings.InitProcess(procSettings);
793 containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged);
794 containerSettings.PortMappings(winrt::single_threaded_vector<WSLCSDK::ContainerPortMapping>({portMapping}));
795
796 auto container = m_defaultSession.CreateContainer(containerSettings);
797 container.Start();
798
799 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
800
801 // Wait for the in-container HTTP server to start listening before issuing a request.
802 WaitForProcessOutput(container.InitProcess(), "Serving HTTP on");
803
804 ExpectHttpResponse(L"http://[::1]:12344", 200, true);
805 }
806 }
807
808 WSLC_TEST_METHOD(ContainerVolumeUnit)
809 {
810 const auto currentDirectory = std::filesystem::current_path().wstring();
811
812 // Negative: non-absolute Windows path must fail at CreateContainer.
813 VERIFY_THROWS_HR(
814 {
815 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
816 containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
817 {WSLCSDK::ContainerVolume(L"relative", L"/mnt/path", false)}));
818 m_defaultSession.CreateContainer(containerSettings);
819 },
820 E_INVALIDARG);
821
822 // Negative: non-absolute container path must fail at CreateContainer.
823 VERIFY_THROWS_HR(
824 {
825 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
826 containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
827 {WSLCSDK::ContainerVolume(currentDirectory, L"./mnt/path", false)}));
828 m_defaultSession.CreateContainer(containerSettings);
829 },
830 E_INVALIDARG);
831
832 // Positive: absolute paths must succeed.
833 {
834 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
835 containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>(
836 {WSLCSDK::ContainerVolume(currentDirectory, L"/mnt/path", false)}));
837 auto container = m_defaultSession.CreateContainer(containerSettings);
838 container.Delete(WSLCSDK::DeleteContainerOption::None);
839 }
840 }
841
842 WSLC_TEST_METHOD(ContainerVolumeFunctional)
843 {
844 const auto hostRwDir = std::filesystem::current_path() / "wslc-winrt-test-vol-rw";
845 const auto hostRoDir = std::filesystem::current_path() / "wslc-winrt-test-vol-ro";
846 std::filesystem::create_directories(hostRwDir);
847 std::filesystem::create_directories(hostRoDir);
848
849 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
850 std::error_code ec;
851 std::filesystem::remove_all(hostRwDir, ec);
852 std::filesystem::remove_all(hostRoDir, ec);
853 });
854
855 std::ofstream{hostRwDir / "hello.txt"} << "hello-rw";
856 std::ofstream{hostRoDir / "hello.txt"} << "hello-ro";
857
858 // Container script exits 0 if all checks pass:
859 // 1. RW mount is readable (hello-rw).
860 // 2. RO mount is readable (hello-ro).
861 // 3. Writing to RW mount succeeds.
862 // 4. Writing to RO mount fails (! touch).
863 constexpr auto c_script =
864 "test \"$(cat /mnt/rw/hello.txt)\" = hello-rw && "
865 "test \"$(cat /mnt/ro/hello.txt)\" = hello-ro && "
866 "echo container-write > /mnt/rw/written.txt && "
867 "! touch /mnt/ro/probe 2>/dev/null";
868
869 auto procSettings = WSLCSDK::ProcessSettings();
870 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", winrt::to_hstring(c_script)}));
871
872 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
873 containerSettings.InitProcess(procSettings);
874 containerSettings.Volumes(winrt::single_threaded_vector<WSLCSDK::ContainerVolume>({
875 WSLCSDK::ContainerVolume(hostRwDir.wstring(), L"/mnt/rw", false),
876 WSLCSDK::ContainerVolume(hostRoDir.wstring(), L"/mnt/ro", true),
877 }));
878
879 auto container = m_defaultSession.CreateContainer(containerSettings);
880 StartContainerAndWaitForInitProcessExit(container);
881
882 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
883 container.Delete(WSLCSDK::DeleteContainerOption::Force);
884
885 // Verify the file written by the container is visible on the host.
886 std::ifstream written(hostRwDir / "written.txt");
887 VERIFY_IS_TRUE(written.is_open());
888 std::string writtenContent((std::istreambuf_iterator<char>(written)), std::istreambuf_iterator<char>());
889 VERIFY_ARE_EQUAL(writtenContent, "container-write\n");
890 }
891
892 WSLC_TEST_METHOD(ContainerInspect)
893 {
894 auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
895 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
896
897 const auto inspectJson = container.Inspect();
898 VERIFY_IS_FALSE(inspectJson.empty());
899
900 const auto id = container.Id();
901 VERIFY_IS_FALSE(id.empty());
902
903 // The inspect JSON must contain the container ID.
904 VERIFY_IS_TRUE(winrt::to_string(inspectJson).find(winrt::to_string(id)) != std::string::npos);
905
906 container.Delete(WSLCSDK::DeleteContainerOption::None);
907 cleanup.release();
908 }
909
910 WSLC_TEST_METHOD(ContainerExec)
911 {
912 // Start a long-running container so we can exec into it.
913 auto initProcSettings = WSLCSDK::ProcessSettings();
914 initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
915
916 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
917 containerSettings.InitProcess(initProcSettings);
918
919 auto container = m_defaultSession.CreateContainer(containerSettings);
920 container.Start();
921
922 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
923
924 // Positive: exec a command that exits 0.
925 {
926 auto execSettings = WSLCSDK::ProcessSettings();
927 execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/true"}));
928
929 auto execProcess = container.CreateProcess(execSettings);
930 StartProcessAndWaitForExit(execProcess);
931 VERIFY_ARE_EQUAL(execProcess.ExitCode(), 0);
932 }
933
934 // Negative: no command line must fail.
935 VERIFY_THROWS_HR(container.CreateProcess(WSLCSDK::ProcessSettings()).Start(), E_INVALIDARG);
936 }
937
938 WSLC_TEST_METHOD(ContainerHostName)
939 {
940 // Unit: setting a hostname must succeed.
941 {
942 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
943 containerSettings.HostName(L"unit-test-host");
944 }
945
946 // Functional: container process should see the configured hostname.
947 {
948 auto procSettings = WSLCSDK::ProcessSettings();
949 procSettings.CommandLine(
950 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(hostname)\" = my-test-host"}));
951
952 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
953 containerSettings.InitProcess(procSettings);
954 containerSettings.HostName(L"my-test-host");
955
956 auto container = m_defaultSession.CreateContainer(containerSettings);
957 StartContainerAndWaitForInitProcessExit(container);
958 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
959 container.Delete(WSLCSDK::DeleteContainerOption::Force);
960 }
961 }
962
963 WSLC_TEST_METHOD(ContainerDomainName)
964 {
965 auto procSettings = WSLCSDK::ProcessSettings();
966 procSettings.CommandLine(
967 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(domainname)\" = test.local"}));
968
969 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
970 containerSettings.InitProcess(procSettings);
971 containerSettings.DomainName(L"test.local");
972
973 auto container = m_defaultSession.CreateContainer(containerSettings);
974 StartContainerAndWaitForInitProcessExit(container);
975 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
976 container.Delete(WSLCSDK::DeleteContainerOption::Force);
977 }
978
979 // -----------------------------------------------------------------------
980 // Process tests
981 // -----------------------------------------------------------------------
982
983 WSLC_TEST_METHOD(ProcessEnvVariables)
984 {
985 auto procSettings = WSLCSDK::ProcessSettings();
986 procSettings.CommandLine(
987 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$MY_TEST_VAR\" = hello-from-test"}));
988 procSettings.EnvironmentVariables(
989 winrt::single_threaded_map(std::map<winrt::hstring, winrt::hstring>{{L"MY_TEST_VAR", L"hello-from-test"}}));
990
991 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
992 containerSettings.InitProcess(procSettings);
993
994 auto container = m_defaultSession.CreateContainer(containerSettings);
995 StartContainerAndWaitForInitProcessExit(container);
996 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
997 container.Delete(WSLCSDK::DeleteContainerOption::Force);
998 }
999
1000 WSLC_TEST_METHOD(ProcessSignal)
1001 {
1002 // Negative: Signal() before Start() must throw.
1003 {
1004 auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
1005 VERIFY_THROWS_HR(container.InitProcess().Signal(WSLCSDK::Signal::SIGKILL), E_ILLEGAL_METHOD_CALL);
1006 }
1007
1008 auto procSettings = WSLCSDK::ProcessSettings();
1009 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1010
1011 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1012 containerSettings.InitProcess(procSettings);
1013
1014 auto container = m_defaultSession.CreateContainer(containerSettings);
1015 auto process = container.InitProcess();
1016
1017 std::promise<void> promise;
1018 auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); });
1019
1020 container.Start();
1021 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1022
1023 VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
1024
1025 process.Signal(WSLCSDK::Signal::SIGKILL);
1026
1027 VERIFY_ARE_EQUAL(promise.get_future().wait_for(2min), std::future_status::ready);
1028
1029 const auto state = process.State();
1030 VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited);
1031 }
1032
1033 WSLC_TEST_METHOD(ProcessGetPid)
1034 {
1035 // Negative: Pid() before Start() must throw.
1036 {
1037 auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest"));
1038 VERIFY_THROWS_HR(std::ignore = container.InitProcess().Pid(), E_ILLEGAL_METHOD_CALL);
1039 }
1040
1041 auto procSettings = WSLCSDK::ProcessSettings();
1042 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1043
1044 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1045 containerSettings.InitProcess(procSettings);
1046
1047 auto container = m_defaultSession.CreateContainer(containerSettings);
1048 container.Start();
1049
1050 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1051
1052 auto process = container.InitProcess();
1053 VERIFY_IS_TRUE(process.Pid() > 0);
1054 }
1055
1056 WSLC_TEST_METHOD(ProcessGetExitCode)
1057 {
1058 auto runAndGetExitCode = [&](int code) -> int32_t {
1059 auto procSettings = WSLCSDK::ProcessSettings();
1060 procSettings.CommandLine(
1061 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", winrt::to_hstring(std::format("exit {}", code))}));
1062
1063 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1064 containerSettings.InitProcess(procSettings);
1065
1066 auto container = m_defaultSession.CreateContainer(containerSettings);
1067 StartContainerAndWaitForInitProcessExit(container);
1068 auto exitCode = container.InitProcess().ExitCode();
1069
1070 container.Delete(WSLCSDK::DeleteContainerOption::Force);
1071 return exitCode;
1072 };
1073
1074 VERIFY_ARE_EQUAL(runAndGetExitCode(0), 0);
1075 VERIFY_ARE_EQUAL(runAndGetExitCode(42), 42);
1076
1077 // Negative: querying ExitCode while process is still running must throw.
1078 {
1079 auto procSettings = WSLCSDK::ProcessSettings();
1080 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1081
1082 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1083 containerSettings.InitProcess(procSettings);
1084
1085 auto container = m_defaultSession.CreateContainer(containerSettings);
1086 container.Start();
1087
1088 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1089
1090 auto process = container.InitProcess();
1091 VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
1092
1093 VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1094 }
1095 }
1096
1097 WSLC_TEST_METHOD(ProcessGetState)
1098 {
1099 auto procSettings = WSLCSDK::ProcessSettings();
1100 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1101
1102 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1103 containerSettings.InitProcess(procSettings);
1104
1105 auto container = m_defaultSession.CreateContainer(containerSettings);
1106 container.Start();
1107
1108 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1109
1110 auto process = container.InitProcess();
1111
1112 // State while running.
1113 VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running);
1114
1115 // Querying ExitCode while running must throw ERROR_INVALID_STATE.
1116 VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1117
1118 // Register the Exited event.
1119 std::promise<int32_t> exitPromise;
1120 auto token = process.Exited([&](int32_t code) { exitPromise.set_value(code); });
1121 auto cleanupToken = wil::scope_exit([&]() { process.Exited(token); });
1122
1123 process.Signal(WSLCSDK::Signal::SIGKILL);
1124
1125 // The Exited event must fire after the signal.
1126 auto future = exitPromise.get_future();
1127 VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready);
1128
1129 const auto state = process.State();
1130 VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited);
1131 }
1132
1133 WSLC_TEST_METHOD(ProcessWorkingDirectory)
1134 {
1135 // Functional: container should see the configured working directory.
1136 auto procSettings = WSLCSDK::ProcessSettings();
1137 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"test \"$(pwd)\" = /tmp"}));
1138 procSettings.WorkingDirectory(L"/tmp");
1139
1140 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1141 containerSettings.InitProcess(procSettings);
1142
1143 auto container = m_defaultSession.CreateContainer(containerSettings);
1144 StartContainerAndWaitForInitProcessExit(container);
1145 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
1146 container.Delete(WSLCSDK::DeleteContainerOption::Force);
1147 }
1148
1149 // -----------------------------------------------------------------------
1150 // Service tests
1151 // -----------------------------------------------------------------------
1152
1153 WSLC_TEST_METHOD(GetVersion)
1154 {
1155 auto version = WSLCSDK::WslcService::GetVersion();
1156 VERIFY_IS_TRUE(version.Major() > 0 || version.Minor() > 0 || version.Revision() > 0);
1157 }
1158
1159 WSLC_TEST_METHOD(GetMissingComponents)
1160 {
1161 const auto missing = WSLCSDK::WslcService::GetMissingComponents();
1162 VERIFY_ARE_EQUAL(missing.Size(), 0u);
1163 }
1164
1165 WSLC_TEST_METHOD(InstallWithDependencies)
1166 {
1167 // Pass null options to auto-detect and install any missing components (same behavior as the old no-arg call).
1168 WSLCSDK::WslcService::InstallWithDependenciesAsync(nullptr).get();
1169 VERIFY_ARE_EQUAL(WSLCSDK::WslcService::GetMissingComponents().Size(), 0u);
1170 }
1171
1172 WSLC_TEST_METHOD(InstallOptions_DefaultValues)
1173 {
1174 // Default-constructed InstallOptions must have null Components and Repair=false.
1175 auto options = WSLCSDK::InstallOptions();
1176 VERIFY_IS_NULL(options.Components());
1177 VERIFY_IS_FALSE(options.Repair());
1178 }
1179
1180 WSLC_TEST_METHOD(InstallOptions_SetRepair)
1181 {
1182 auto options = WSLCSDK::InstallOptions();
1183 options.Repair(true);
1184 VERIFY_IS_TRUE(options.Repair());
1185 options.Repair(false);
1186 VERIFY_IS_FALSE(options.Repair());
1187 }
1188
1189 WSLC_TEST_METHOD(InstallOptions_SetComponents)
1190 {
1191 auto options = WSLCSDK::InstallOptions();
1192 auto components = winrt::single_threaded_vector<WSLCSDK::Component>({WSLCSDK::Component::WslPackage});
1193 options.Components(components.GetView());
1194 VERIFY_ARE_EQUAL(1u, options.Components().Size());
1195 VERIFY_ARE_EQUAL(WSLCSDK::Component::WslPackage, options.Components().GetAt(0));
1196 }
1197
1198 WSLC_TEST_METHOD(InstallWithDependencies_SdkNeedsUpdate_Throws)
1199 {
1200 // Passing SdkNeedsUpdate in the component list must throw WSLC_E_SDK_UPDATE_NEEDED.
1201 auto options = WSLCSDK::InstallOptions();
1202 auto components = winrt::single_threaded_vector<WSLCSDK::Component>({WSLCSDK::Component::SdkNeedsUpdate});
1203 options.Components(components.GetView());
1204 VERIFY_THROWS_HR(WSLCSDK::WslcService::InstallWithDependenciesAsync(options).get(), WSLC_E_SDK_UPDATE_NEEDED);
1205 }
1206
1207 // -----------------------------------------------------------------------
1208 // Process IO event tests
1209 // -----------------------------------------------------------------------
1210
1211 WSLC_TEST_METHOD(ProcessIoEventsUnit)
1212 {
1213 // Negative: registering OutputReceived/ErrorReceived without OutputMode::Event must throw.
1214 {
1215 auto procSettings = WSLCSDK::ProcessSettings();
1216 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
1217
1218 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1219 containerSettings.InitProcess(procSettings);
1220
1221 auto container = m_defaultSession.CreateContainer(containerSettings);
1222 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1223
1224 auto process = container.InitProcess();
1225 VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
1226 VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
1227
1228 // GetOutputStream requires OutputMode::Stream — must throw with Discard mode (even after Start).
1229 container.Start();
1230 VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
1231 }
1232
1233 // Positive: with OutputMode::Event, registering and revoking event handlers must succeed.
1234 {
1235 auto procSettings = WSLCSDK::ProcessSettings();
1236 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
1237 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1238
1239 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1240 containerSettings.InitProcess(procSettings);
1241
1242 auto container = m_defaultSession.CreateContainer(containerSettings);
1243 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1244
1245 auto process = container.InitProcess();
1246
1247 auto stdoutToken = process.OutputReceived([](winrt::array_view<uint8_t const>) {});
1248 auto stderrToken = process.ErrorReceived([](winrt::array_view<uint8_t const>) {});
1249 auto exitToken = process.Exited([](int32_t) {});
1250
1251 process.OutputReceived(stdoutToken);
1252 process.ErrorReceived(stderrToken);
1253 process.Exited(exitToken);
1254
1255 // GetOutputStream throws when OutputMode is Event.
1256 container.Start();
1257 VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
1258 }
1259
1260 // Negative: OutputReceived/ErrorReceived with OutputMode::Stream must throw.
1261 {
1262 auto procSettings = WSLCSDK::ProcessSettings();
1263 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"1"}));
1264 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
1265
1266 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1267 containerSettings.InitProcess(procSettings);
1268
1269 auto container = m_defaultSession.CreateContainer(containerSettings);
1270 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1271
1272 auto process = container.InitProcess();
1273 VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
1274 VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view<uint8_t const>) {}), E_ILLEGAL_METHOD_CALL);
1275 }
1276 }
1277
1278 WSLC_TEST_METHOD(ProcessIoEventsInitProcess)
1279 {
1280 std::string stdoutData, stderrData;
1281
1282 auto procSettings = WSLCSDK::ProcessSettings();
1283 procSettings.CommandLine(
1284 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT && echo STDERR >&2"}));
1285 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1286
1287 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1288 containerSettings.InitProcess(procSettings);
1289
1290 auto container = m_defaultSession.CreateContainer(containerSettings);
1291 auto process = container.InitProcess();
1292
1293 process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
1294 stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
1295 });
1296 process.ErrorReceived([&](winrt::array_view<uint8_t const> data) {
1297 stderrData.append(reinterpret_cast<const char*>(data.data()), data.size());
1298 });
1299
1300 // Start: claims IO handles and starts the IOCallback pump thread.
1301 StartContainerAndWaitForInitProcessExit(container);
1302
1303 VERIFY_ARE_EQUAL(stdoutData, "STDOUT\n");
1304 VERIFY_ARE_EQUAL(stderrData, "STDERR\n");
1305 }
1306
1307 WSLC_TEST_METHOD(ProcessIoEventsExecProcess)
1308 {
1309 // Long-running init process to keep the container alive.
1310 auto initProcSettings = WSLCSDK::ProcessSettings();
1311 initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1312
1313 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1314 containerSettings.InitProcess(initProcSettings);
1315
1316 auto container = m_defaultSession.CreateContainer(containerSettings);
1317 container.Start();
1318
1319 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1320
1321 std::string stdoutData, stderrData;
1322
1323 auto execProcSettings = WSLCSDK::ProcessSettings();
1324 execProcSettings.CommandLine(
1325 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo EXEC_OUT && echo EXEC_ERR >&2"}));
1326 execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1327
1328 auto execProcess = container.CreateProcess(execProcSettings);
1329
1330 execProcess.OutputReceived([&](winrt::array_view<uint8_t const> data) {
1331 stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
1332 });
1333 execProcess.ErrorReceived([&](winrt::array_view<uint8_t const> data) {
1334 stderrData.append(reinterpret_cast<const char*>(data.data()), data.size());
1335 });
1336
1337 StartProcessAndWaitForExit(execProcess);
1338
1339 VERIFY_ARE_EQUAL(stdoutData, "EXEC_OUT\n");
1340 VERIFY_ARE_EQUAL(stderrData, "EXEC_ERR\n");
1341 }
1342
1343 WSLC_TEST_METHOD(ProcessIoEventsHandleExclusion)
1344 {
1345 // Register an OutputReceived handler only. The IOCallback acquires ALL pipe handles
1346 // (draining uncallbacked streams to prevent deadlock), so both stdout and stderr
1347 // handles are consumed and neither can be obtained via GetOutputStream.
1348 auto procSettings = WSLCSDK::ProcessSettings();
1349 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"99"}));
1350 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1351
1352 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1353 containerSettings.InitProcess(procSettings);
1354
1355 auto container = m_defaultSession.CreateContainer(containerSettings);
1356 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1357
1358 auto process = container.InitProcess();
1359 process.OutputReceived([](winrt::array_view<uint8_t const>) {});
1360
1361 container.Start();
1362
1363 // stdout handle was consumed by the OutputReceived handler — must not be obtainable.
1364 VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL);
1365
1366 // stderr handle was also consumed in order to drain it despite not having a handler.
1367 VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), E_ILLEGAL_METHOD_CALL);
1368 }
1369
1370 WSLC_TEST_METHOD(ProcessIoEventsExitCallback)
1371 {
1372 // Verify the Exited event fires with the correct exit code after IO has been flushed.
1373 auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair<int32_t, std::string> {
1374 std::string stdoutData;
1375 std::promise<int32_t> exitPromise;
1376
1377 auto procSettings = WSLCSDK::ProcessSettings();
1378 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
1379 {L"/bin/sh", L"-c", winrt::hstring(std::format(L"echo HELLO && exit {}", exitCodeArg))}));
1380 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1381
1382 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1383 containerSettings.InitProcess(procSettings);
1384
1385 auto container = m_defaultSession.CreateContainer(containerSettings);
1386 auto process = container.InitProcess();
1387
1388 process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
1389 stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
1390 });
1391 process.Exited([&](int32_t code) { exitPromise.set_value(code); });
1392
1393 container.Start();
1394
1395 auto future = exitPromise.get_future();
1396 VERIFY_ARE_EQUAL(future.wait_for(60s), std::future_status::ready);
1397
1398 return {future.get(), stdoutData};
1399 };
1400
1401 // Exit 0: Exited event must fire with code 0; IO must have been delivered first.
1402 {
1403 auto [exitCode, output] = RunAndCaptureExit(0);
1404 VERIFY_ARE_EQUAL(exitCode, 0);
1405 VERIFY_ARE_EQUAL(output, "HELLO\n");
1406 }
1407
1408 // Non-zero exit: Exited event must report the correct code.
1409 {
1410 auto [exitCode, output] = RunAndCaptureExit(42);
1411 VERIFY_ARE_EQUAL(exitCode, 42);
1412 VERIFY_ARE_EQUAL(output, "HELLO\n");
1413 }
1414 }
1415
1416 WSLC_TEST_METHOD(ProcessIoEventsCancelOnRelease)
1417 {
1418 // Verify that releasing the process handle while an exec'd process is still running
1419 // and writing IO cancels the event pump:
1420 // - No IO events arrive after the handle is released.
1421 // - Exited is never invoked (cancellation suppresses it).
1422
1423 // Long-running init process to keep the container alive.
1424 auto initProcSettings = WSLCSDK::ProcessSettings();
1425 initProcSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"999"}));
1426
1427 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1428 containerSettings.InitProcess(initProcSettings);
1429
1430 auto container = m_defaultSession.CreateContainer(containerSettings);
1431 container.Start();
1432
1433 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1434
1435 std::atomic<int> callbackCount{0};
1436 std::atomic<bool> exitFired{false};
1437
1438 auto execProcSettings = WSLCSDK::ProcessSettings();
1439 execProcSettings.CommandLine(
1440 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"while true; do echo LINE; sleep 0.05; done"}));
1441 execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1442
1443 auto execProcess = container.CreateProcess(execProcSettings);
1444
1445 execProcess.OutputReceived([&](winrt::array_view<uint8_t const>) { callbackCount.fetch_add(1); });
1446 execProcess.Exited([&](int32_t) { exitFired.store(true); });
1447
1448 execProcess.Start();
1449
1450 // Wait for events to start arriving.
1451 Sleep(500);
1452 VERIFY_IS_TRUE(callbackCount.load() > 0);
1453
1454 // Release the exec process handle while it is still running and writing.
1455 execProcess = nullptr;
1456
1457 const int countAtRelease = callbackCount.load();
1458
1459 // Exited must not have fired: cancellation suppresses it.
1460 VERIFY_IS_FALSE(exitFired.load());
1461
1462 // No further events after release.
1463 Sleep(200);
1464 VERIFY_ARE_EQUAL(callbackCount.load(), countAtRelease);
1465 VERIFY_IS_FALSE(exitFired.load());
1466 }
1467
1468 WSLC_TEST_METHOD(ProcessIoEventsLargeOutput)
1469 {
1470 // Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64
1471 // 1,048,576 zero bytes → base64 output is 1,398,104 bytes.
1472 static constexpr size_t c_expectedBytes = 1'398'104;
1473
1474 std::string stdoutData;
1475 stdoutData.reserve(c_expectedBytes + 4096);
1476
1477 auto procSettings = WSLCSDK::ProcessSettings();
1478 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
1479 {L"/bin/sh", L"-c", L"dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"}));
1480 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event);
1481
1482 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1483 containerSettings.InitProcess(procSettings);
1484
1485 auto container = m_defaultSession.CreateContainer(containerSettings);
1486 auto process = container.InitProcess();
1487
1488 process.OutputReceived([&](winrt::array_view<uint8_t const> data) {
1489 stdoutData.append(reinterpret_cast<const char*>(data.data()), data.size());
1490 });
1491
1492 StartContainerAndWaitForInitProcessExit(container);
1493
1494 VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes);
1495 }
1496
1497 // -----------------------------------------------------------------------
1498 // Storage tests
1499 // -----------------------------------------------------------------------
1500
1501 WSLC_TEST_METHOD(SessionCreateVhd)
1502 {
1503 constexpr auto c_volumeName = L"wslc-winrt-test-data-vol";
1504 constexpr uint64_t c_vhdSizeBytes = 1ull * 1024 * 1024 * 1024; // 1 GiB
1505
1506 const std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-winrt-vhd-test-storage";
1507 IGNORE_ERRORS(std::filesystem::remove_all(vhdSessionStorage));
1508 auto cleanup = SCOPE_CLEANUP(std::filesystem::remove_all(vhdSessionStorage));
1509
1510 // Create a dedicated session so that volume creation does not affect the shared default session.
1511 auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-vhd-test", vhdSessionStorage.wstring());
1512 settings.Timeout(std::chrono::duration_cast<TimeSpan>(30s));
1513 settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
1514
1515 auto session = WSLCSDK::Session(settings);
1516 session.Start();
1517
1518 // Load debian.
1519 const auto debianTar = GetTestImagePath("debian:latest");
1520 session.LoadImageAsync(debianTar.wstring()).get();
1521
1522 // Positive: create a named VHD volume.
1523 session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic));
1524
1525 // The backing VHD file must exist on disk.
1526 const auto expectedVhdPath = vhdSessionStorage / "volumes" / (std::wstring(c_volumeName) + L".vhdx");
1527 VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
1528
1529 // Positive: write a marker via a container that mounts the named volume.
1530 {
1531 auto procSettings = WSLCSDK::ProcessSettings();
1532 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
1533 {L"/bin/sh", L"-c", L"echo wslc-winrt-vhd-test > /data/marker.txt"}));
1534
1535 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1536 containerSettings.InitProcess(procSettings);
1537 containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
1538 {WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", false)}));
1539
1540 auto container = session.CreateContainer(containerSettings);
1541 StartContainerAndWaitForInitProcessExit(container);
1542 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
1543 container.Delete(WSLCSDK::DeleteContainerOption::Force);
1544 }
1545
1546 // Positive: read back the marker in a second container (read-only mount).
1547 {
1548 auto procSettings = WSLCSDK::ProcessSettings();
1549 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
1550 {L"/bin/sh", L"-c", L"test \"$(cat /data/marker.txt)\" = wslc-winrt-vhd-test"}));
1551
1552 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1553 containerSettings.InitProcess(procSettings);
1554 containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
1555 {WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", true)}));
1556
1557 auto container = session.CreateContainer(containerSettings);
1558 StartContainerAndWaitForInitProcessExit(container);
1559 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
1560 container.Delete(WSLCSDK::DeleteContainerOption::Force);
1561 }
1562
1563 // Positive: delete the volume.
1564 session.DeleteVhdVolume(c_volumeName);
1565 VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath));
1566
1567 // Negative: zero size must fail.
1568 VERIFY_THROWS_HR(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, 0, WSLCSDK::VhdType::Dynamic)), E_INVALIDARG);
1569
1570 // Positive: fixed-allocation VHD; on-disk file size must be >= Size.
1571 {
1572 constexpr auto c_fixedVolumeName = L"wslc-sdk-vhd-fixed";
1573 constexpr auto c_fixedSizeBytes = 64ull * _1MB;
1574 VERIFY_NO_THROW(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_fixedVolumeName, c_fixedSizeBytes, WSLCSDK::VhdType::Fixed)));
1575
1576 auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_fixedVolumeName));
1577
1578 std::filesystem::path expectedVhdPath = vhdSessionStorage / L"volumes" / (std::wstring(c_fixedVolumeName) + L".vhdx");
1579 VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
1580 VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes);
1581 }
1582
1583 // Positive: Owner() bakes uid/gid into the volume root inode at mkfs time.
1584 // Verify by stat-ing the mount inside a container.
1585 {
1586 constexpr auto c_ownedVolumeName = L"wslc-sdk-vhd-owned";
1587 auto vhdOptions = WSLCSDK::VhdOptions(c_ownedVolumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic);
1588 vhdOptions.Owner(WSLCSDK::VhdOwner{65534, 65534}); // nobody:nogroup
1589 session.CreateVhdVolume(vhdOptions);
1590
1591 auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_ownedVolumeName));
1592
1593 auto procSettings = WSLCSDK::ProcessSettings();
1594 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/usr/bin/stat", L"-c", L"%u %g", L"/data"}));
1595 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
1596
1597 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1598 containerSettings.InitProcess(procSettings);
1599 containerSettings.NamedVolumes(winrt::single_threaded_vector<WSLCSDK::ContainerNamedVolume>(
1600 {WSLCSDK::ContainerNamedVolume(c_ownedVolumeName, L"/data", false)}));
1601
1602 auto container = session.CreateContainer(containerSettings);
1603 StartContainerAndWaitForInitProcessExit(container);
1604 auto output = GetProcessOutput(container.InitProcess());
1605 VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0);
1606 VERIFY_ARE_EQUAL(output.StandardOutput, L"65534 65534\n");
1607 container.Delete(WSLCSDK::DeleteContainerOption::Force);
1608 }
1609 }
1610
1611 // -----------------------------------------------------------------------
1612 // Authentication / registry tests
1613 // -----------------------------------------------------------------------
1614
1615 WSLC_TEST_METHOD(AuthenticateTests)
1616 {
1617 constexpr auto c_username = "wslctest";
1618 constexpr auto c_password = "password";
1619
1620 auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
1621
1622 const auto serverUri = Uri(winrt::to_hstring(std::format("http://{}", registryAddress)));
1623
1624 // Negative: wrong password must fail.
1625 VERIFY_THROWS_HR(m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), L"wrong-password"), E_FAIL);
1626
1627 // Positive: correct credentials must return a non-null token of the expected type.
1628 {
1629 const auto result = m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password));
1630 VERIFY_IS_FALSE(result.IdentityToken().empty());
1631 // The local test registry does not return an identity token, so credentials are embedded.
1632 VERIFY_ARE_EQUAL(result.TokenType(), WSLCSDK::IdentityTokenType::Credentials);
1633 }
1634
1635 const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password);
1636 PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
1637
1638 const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress));
1639
1640 auto cleanup = SCOPE_CLEANUP(m_defaultSession.DeleteImage(image));
1641
1642 // Positive: the IdentityToken from Authenticate can be passed directly as RegistryAuth.
1643 {
1644 const auto authResult = m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password));
1645 auto opts = WSLCSDK::PullImageOptions(image);
1646 opts.RegistryAuth(authResult.IdentityToken());
1647 VERIFY_NO_THROW(m_defaultSession.PullImageAsync(opts).get());
1648 VERIFY_IS_TRUE(HasImage(image));
1649 }
1650
1651 // Negative: pulling without credentials must fail.
1652 {
1653 VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get(), E_FAIL);
1654 }
1655
1656 // Negative: pulling with bad credentials must fail.
1657 {
1658 auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong");
1659 auto opts = WSLCSDK::PullImageOptions(image);
1660 opts.RegistryAuth(winrt::to_hstring(badAuth));
1661 VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), E_FAIL);
1662 }
1663 }
1664
1665 WSLC_TEST_METHOD(PullImage)
1666 {
1667 auto [registryContainer, registryAddress] = StartLocalRegistry();
1668 const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
1669
1670 {
1671 PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
1672
1673 const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress));
1674
1675 auto imageCleanup = SCOPE_CLEANUP(m_defaultSession.DeleteImage(image));
1676
1677 // Delete the image locally so the pull is a real network pull.
1678 IGNORE_ERRORS(m_defaultSession.DeleteImage(image));
1679
1680 // Positive: pull from the local registry.
1681 m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get();
1682 VERIFY_IS_TRUE(HasImage(image));
1683
1684 // Verify the pulled image is runnable.
1685 auto output = RunContainerAndWaitForExit(image, {});
1686 VERIFY_ARE_EQUAL(output.ExitCode, 0);
1687 }
1688
1689 // Negative: image that does not exist in the registry.
1690 {
1691 const auto missing = winrt::to_hstring(std::format("{}/does-not-exist", registryAddress));
1692 auto opts = WSLCSDK::PullImageOptions(missing);
1693 opts.RegistryAuth(winrt::to_hstring(xRegistryAuth));
1694 VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), static_cast<HRESULT>(WSLC_E_IMAGE_NOT_FOUND));
1695 }
1696
1697 // Negative: empty URI must fail.
1698 VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"")).get(), E_INVALIDARG);
1699 }
1700
1701 WSLC_TEST_METHOD(PushImage)
1702 {
1703 auto [registryContainer, registryAddress] = StartLocalRegistry();
1704 const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
1705
1706 // Positive: push an existing image to the local registry.
1707 PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
1708
1709 // Negative: pushing a non-existent image must fail.
1710 VERIFY_THROWS_HR(
1711 m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"does-not-exist", winrt::to_hstring(xRegistryAuth))).get(), E_FAIL);
1712
1713 // Negative: empty image name must fail.
1714 VERIFY_THROWS_HR(m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"", winrt::to_hstring(xRegistryAuth))).get(), E_INVALIDARG);
1715 }
1716
1717 WSLC_TEST_METHOD(TagImage)
1718 {
1719 // Positive: tag an existing image.
1720 m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"winrt-sdk-test-tag"));
1721 VERIFY_IS_TRUE(HasImage(L"debian:winrt-sdk-test-tag"));
1722
1723 auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(L"debian:winrt-sdk-test-tag");
1724
1725 // Negative: empty image name must fail.
1726 VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"", L"debian", L"test")), E_INVALIDARG);
1727
1728 // Negative: empty repository must fail.
1729 VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"", L"test")), E_INVALIDARG);
1730
1731 // Negative: empty tag must fail.
1732 VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"")), E_INVALIDARG);
1733 }
1734
1735 // -----------------------------------------------------------------------
1736 // Negative / edge-case tests
1737 // -----------------------------------------------------------------------
1738
1739 WSLC_TEST_METHOD(ExecOnStoppedContainer)
1740 {
1741 auto procSettings = WSLCSDK::ProcessSettings();
1742 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
1743
1744 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1745 containerSettings.InitProcess(procSettings);
1746
1747 auto container = m_defaultSession.CreateContainer(containerSettings);
1748
1749 // Wait for the short-lived init process to exit
1750 StartContainerAndWaitForInitProcessExit(container);
1751
1752 // The init process has now exited. Attempting to exec on a stopped container must fail.
1753 auto execSettings = WSLCSDK::ProcessSettings();
1754 execSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/echo", L"should-fail"}));
1755
1756 VERIFY_THROWS_HR(container.CreateProcess(execSettings).Start(), static_cast<HRESULT>(WSLC_E_CONTAINER_NOT_RUNNING));
1757 }
1758
1759 WSLC_TEST_METHOD(DuplicateContainerName)
1760 {
1761 auto procSettings = WSLCSDK::ProcessSettings();
1762 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
1763
1764 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1765 containerSettings.InitProcess(procSettings);
1766 containerSettings.Name(L"duplicate-name-test-winrt");
1767
1768 auto container1 = m_defaultSession.CreateContainer(containerSettings);
1769 container1.Start();
1770
1771 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container1);
1772
1773 // Creating a second container with the same name must fail.
1774 VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
1775 }
1776
1777 WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce)
1778 {
1779 auto procSettings = WSLCSDK::ProcessSettings();
1780 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"10"}));
1781
1782 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1783 containerSettings.InitProcess(procSettings);
1784
1785 auto container = m_defaultSession.CreateContainer(containerSettings);
1786 container.Start();
1787
1788 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1789
1790 // Deleting a running container without Force must fail.
1791 VERIFY_THROWS_HR(container.Delete(WSLCSDK::DeleteContainerOption::None), static_cast<HRESULT>(WSLC_E_CONTAINER_IS_RUNNING));
1792 }
1793
1794 WSLC_TEST_METHOD(DeleteNonExistentImage)
1795 {
1796 VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent-image:this-tag-does-not-exist"), static_cast<HRESULT>(WSLC_E_IMAGE_NOT_FOUND));
1797 }
1798
1799 WSLC_TEST_METHOD(PullInvalidImageUri)
1800 {
1801 VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"///invalid-registry-url///")).get(), E_INVALIDARG);
1802 }
1803
1804 WSLC_TEST_METHOD(ContainerGpu)
1805 {
1806 // Negative: creating a GPU container on a session without GPU support must fail.
1807 {
1808 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1809 containerSettings.EnableGpu(true);
1810
1811 VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings).Start(), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
1812 }
1813
1814 // Create a GPU-enabled session.
1815 const std::filesystem::path gpuStorage = m_storagePath / "wslc-winrt-gpu-session-storage";
1816 auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
1817 std::error_code error;
1818 std::filesystem::remove_all(gpuStorage, error);
1819 });
1820
1821 auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-gpu-test", gpuStorage.wstring());
1822 settings.EnableGpu(true);
1823 settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic));
1824
1825 auto gpuSession = WSLCSDK::Session(settings);
1826 gpuSession.Start();
1827
1828 const auto debianTar = GetTestImagePath("debian:latest");
1829 gpuSession.LoadImageAsync(debianTar.wstring()).get();
1830
1831 // Positive: /dev/dxg must be available with read/write permissions, and the dynamic linker must be configured to resolve
1832 // the WSL GPU libraries inside a GPU container.
1833 {
1834 auto procSettings = WSLCSDK::ProcessSettings();
1835 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>(
1836 {L"/bin/sh",
1837 L"-c",
1838 L"test -c /dev/dxg && test -r /dev/dxg && test -w /dev/dxg && cat /etc/ld.so.conf.d/ld.wsl.conf"}));
1839 procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream);
1840
1841 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1842 containerSettings.InitProcess(procSettings);
1843 containerSettings.EnableGpu(true);
1844
1845 auto container = gpuSession.CreateContainer(containerSettings);
1846 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1847
1848 StartContainerAndWaitForInitProcessExit(container);
1849 auto output = GetProcessOutput(container.InitProcess());
1850
1851 VERIFY_ARE_EQUAL(output.StandardOutput, L"/usr/lib/wsl/lib\n");
1852 }
1853
1854 gpuSession.Terminate();
1855 }
1856
1857 WSLC_TEST_METHOD(OpenContainer)
1858 {
1859 constexpr auto c_containerName = L"wslc-winrt-open-test";
1860
1861 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1862 containerSettings.Name(c_containerName);
1863
1864 auto container = m_defaultSession.CreateContainer(containerSettings);
1865 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1866
1867 const auto createdId = container.Id();
1868
1869 // Positive: open by name — IDs must match.
1870 {
1871 auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Discard);
1872 VERIFY_ARE_EQUAL(opened.Id(), createdId);
1873 }
1874
1875 // Positive: open by full 64-character ID.
1876 {
1877 auto opened = m_defaultSession.OpenContainer(createdId, WSLCSDK::ProcessOutputMode::Discard);
1878 VERIFY_ARE_EQUAL(opened.Id(), createdId);
1879 }
1880
1881 // Positive: open by partial ID prefix (12 hex chars — standard short ID).
1882 {
1883 const auto shortId = winrt::hstring(std::wstring(createdId).substr(0, 12));
1884 auto opened = m_defaultSession.OpenContainer(shortId, WSLCSDK::ProcessOutputMode::Discard);
1885 VERIFY_ARE_EQUAL(opened.Id(), createdId);
1886 }
1887
1888 // Negative: non-existent name must throw WSLC_E_CONTAINER_NOT_FOUND.
1889 VERIFY_THROWS_HR(m_defaultSession.OpenContainer(L"no-such-container", WSLCSDK::ProcessOutputMode::Discard), WSLC_E_CONTAINER_NOT_FOUND);
1890
1891 // Negative: empty nameOrId must throw E_INVALIDARG.
1892 VERIFY_THROWS_HR(m_defaultSession.OpenContainer(L"", WSLCSDK::ProcessOutputMode::Discard), E_INVALIDARG);
1893 }
1894
1895 WSLC_TEST_METHOD(OpenContainerInitProcess)
1896 {
1897 // Verify InitProcess() behaviour for an opened container:
1898 // - Before Start(), InitProcess() succeeds but the underlying process handle is not yet
1899 // attached, so methods that require a running process (Pid, State) throw.
1900 // - After Start(), the handle is attached and the process is fully usable.
1901 constexpr auto c_containerName = L"wslc-winrt-open-initproc-test";
1902
1903 auto procSettings = WSLCSDK::ProcessSettings();
1904 procSettings.CommandLine(winrt::single_threaded_vector<winrt::hstring>({L"/bin/sleep", L"5"}));
1905
1906 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1907 containerSettings.InitProcess(procSettings);
1908 containerSettings.Name(c_containerName);
1909
1910 auto container = m_defaultSession.CreateContainer(containerSettings);
1911 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1912
1913 auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Stream);
1914
1915 // InitProcess() must succeed even before Start() (m_initProcess is always created for opened containers).
1916 auto initProcess = opened.InitProcess();
1917
1918 // The process handle has not been attached yet — Pid() must throw.
1919 VERIFY_THROWS_HR(std::ignore = initProcess.Pid(), E_ILLEGAL_METHOD_CALL);
1920
1921 // Start the opened container; the init process handle will be attached inside Start().
1922 opened.Start();
1923
1924 // Now the init process handle is attached — Pid() must succeed and return a non-zero PID.
1925 VERIFY_IS_GREATER_THAN(initProcess.Pid(), 0u);
1926
1927 opened.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero());
1928 }
1929
1930 WSLC_TEST_METHOD(OpenContainerAndStartStreamMode)
1931 {
1932 // Verify that opening a container with ProcessOutputMode::Stream allows reading output
1933 // from the init process via GetOutputStream after Start().
1934 constexpr auto c_containerName = L"wslc-winrt-open-stream-test";
1935
1936 auto procSettings = WSLCSDK::ProcessSettings();
1937 procSettings.CommandLine(
1938 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo STDOUT && echo STDERR >&2"}));
1939
1940 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1941 containerSettings.InitProcess(procSettings);
1942 containerSettings.Name(c_containerName);
1943
1944 auto container = m_defaultSession.CreateContainer(containerSettings);
1945 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1946
1947 auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Stream);
1948
1949 StartContainerAndWaitForInitProcessExit(opened);
1950 auto output = GetProcessOutput(opened.InitProcess());
1951
1952 VERIFY_ARE_EQUAL(output.ExitCode, 0u);
1953 VERIFY_ARE_EQUAL(output.StandardOutput, L"STDOUT\n");
1954 VERIFY_ARE_EQUAL(output.StandardError, L"STDERR\n");
1955 }
1956
1957 WSLC_TEST_METHOD(OpenContainerAndStartEventMode)
1958 {
1959 // Verify that opening a container with ProcessOutputMode::Event delivers output via
1960 // OutputReceived / ErrorReceived events, and that Exited fires with the correct exit code.
1961 constexpr auto c_containerName = L"wslc-winrt-open-event-test";
1962
1963 auto procSettings = WSLCSDK::ProcessSettings();
1964 procSettings.CommandLine(
1965 winrt::single_threaded_vector<winrt::hstring>({L"/bin/sh", L"-c", L"echo EVENT_OUT && echo EVENT_ERR >&2"}));
1966
1967 auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest");
1968 containerSettings.InitProcess(procSettings);
1969 containerSettings.Name(c_containerName);
1970
1971 auto container = m_defaultSession.CreateContainer(containerSettings);
1972 auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container);
1973
1974 auto opened = m_defaultSession.OpenContainer(c_containerName, WSLCSDK::ProcessOutputMode::Event);
1975 auto initProcess = opened.InitProcess();
1976
1977 std::string stdoutAccum, stderrAccum;
1978 std::promise<int32_t> exitPromise;
1979
1980 auto stdoutRevoker = initProcess.OutputReceived(winrt::auto_revoke, [&](winrt::array_view<const uint8_t> data) {
1981 stdoutAccum.append(reinterpret_cast<const char*>(data.data()), data.size());
1982 });
1983
1984 auto stderrRevoker = initProcess.ErrorReceived(winrt::auto_revoke, [&](winrt::array_view<const uint8_t> data) {
1985 stderrAccum.append(reinterpret_cast<const char*>(data.data()), data.size());
1986 });
1987
1988 auto exitRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t exitCode) { exitPromise.set_value(exitCode); });
1989
1990 opened.Start();
1991
1992 auto exitFuture = exitPromise.get_future();
1993 VERIFY_ARE_EQUAL(exitFuture.wait_for(30s), std::future_status::ready);
1994 VERIFY_ARE_EQUAL(exitFuture.get(), 0);
1995
1996 VERIFY_ARE_EQUAL(stdoutAccum, "EVENT_OUT\n");
1997 VERIFY_ARE_EQUAL(stderrAccum, "EVENT_ERR\n");
1998 }
1999 };