master
cpp 3,080 lines 141 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WslcSdkTests.cpp
8
9 Abstract:
10
11 This file contains test cases for the WSLC SDK.
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 "WSLCProcessLauncher.h"
21 #include "wslc_schema.h"
22 #include "wslc/e2e/WSLCE2EHelpers.h"
23 #include <optional>
24
25 extern std::wstring g_testDataPath;
26 extern bool g_fastTestRun;
27
28 using namespace std::chrono_literals;
29
30 namespace {
31
32 //
33 // RAII guards for opaque WSLC handle types.
34 //
35
36 void CloseSession(WslcSession session)
37 {
38 if (session)
39 {
40 WslcTerminateSession(session);
41 WslcReleaseSession(session);
42 }
43 }
44
45 using UniqueSession = wil::unique_any<WslcSession, decltype(CloseSession), CloseSession>;
46
47 void CloseContainer(WslcContainer container)
48 {
49 if (container)
50 {
51 WslcStopContainer(container, WSLC_SIGNAL_SIGKILL, 0, nullptr);
52 WslcDeleteContainer(container, WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr);
53 WslcReleaseContainer(container);
54 }
55 }
56
57 using UniqueContainer = wil::unique_any<WslcContainer, decltype(CloseContainer), CloseContainer>;
58
59 // Release-only handle: does not stop or delete the container.
60 // Used when a second handle to the same container is needed (e.g., OpenContainer tests).
61 void ReleaseContainerHandle(WslcContainer container)
62 {
63 if (container)
64 {
65 THROW_IF_FAILED(WslcReleaseContainer(container));
66 }
67 }
68
69 using UniqueContainerReleaseOnly = wil::unique_any<WslcContainer, decltype(ReleaseContainerHandle), ReleaseContainerHandle>;
70
71 void CloseProcess(WslcProcess process)
72 {
73 if (process)
74 {
75 WslcReleaseProcess(process);
76 }
77 }
78
79 using UniqueProcess = wil::unique_any<WslcProcess, decltype(CloseProcess), CloseProcess>;
80
81 using UniqueCrashDumpSubscription =
82 wil::unique_any<WslcCrashDumpSubscription, decltype(&WslcReleaseCrashDumpSubscription), WslcReleaseCrashDumpSubscription>;
83
84 struct ProcessOutput
85 {
86 std::string stdoutOutput;
87 std::string stderrOutput;
88 };
89
90 ProcessOutput WaitForProcessOutput(WslcProcess process, std::chrono::milliseconds timeout = 2min)
91 {
92 // Borrow the exit-event handle (lifetime tied to the process object; do NOT close it).
93 HANDLE exitEvent = nullptr;
94 THROW_IF_FAILED(WslcGetProcessExitEvent(process, &exitEvent));
95
96 // Acquire stdout / stderr pipe handles (caller owns these).
97 wil::unique_handle ownedStdout;
98 THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
99
100 wil::unique_handle ownedStderr;
101 THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDERR, &ownedStderr));
102
103 // Read stdout / stderr concurrently so that full pipe buffers do not stall the process.
104 ProcessOutput output;
105 wsl::windows::common::io::MultiHandleWait io;
106
107 io.AddHandle(std::make_unique<wsl::windows::common::io::ReadHandle>(
108 std::move(ownedStdout), [&](const auto& buffer) { output.stdoutOutput.append(buffer.data(), buffer.size()); }));
109
110 io.AddHandle(std::make_unique<wsl::windows::common::io::ReadHandle>(
111 std::move(ownedStderr), [&](const auto& buffer) { output.stderrOutput.append(buffer.data(), buffer.size()); }));
112
113 auto timeoutTime = std::chrono::steady_clock::now() + timeout;
114 io.Run(timeout);
115
116 auto remaining = timeoutTime - std::chrono::steady_clock::now();
117 if (remaining < 0ns)
118 {
119 remaining = {};
120 }
121
122 // Check that the process exits within the timeout.
123 THROW_HR_IF(
124 HRESULT_FROM_WIN32(WAIT_TIMEOUT),
125 WaitForSingleObject(exitEvent, static_cast<DWORD>(std::chrono::duration_cast<std::chrono::milliseconds>(remaining).count())) != WAIT_OBJECT_0);
126
127 return output;
128 }
129
130 //
131 // Runs a container with the given argv, waits up to timeoutMs for it to exit,
132 // and returns the captured stdout / stderr output.
133 //
134 ProcessOutput RunContainerAndCapture(WslcSession session, const WslcContainerSettings& containerSettings, std::chrono::milliseconds timeout = 2min)
135 {
136 // Create and start the container.
137 UniqueContainer container;
138 THROW_IF_FAILED(WslcCreateContainer(session, &containerSettings, &container, nullptr));
139 THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
140
141 // Acquire the init process handle.
142 UniqueProcess process;
143 THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
144
145 return WaitForProcessOutput(process.get());
146 }
147
148 ProcessOutput RunContainerAndCapture(
149 WslcSession session,
150 const char* image,
151 const std::vector<const char*>& argv,
152 WslcContainerFlags flags = WSLC_CONTAINER_FLAG_NONE,
153 const char* name = nullptr,
154 std::chrono::milliseconds timeout = 2min,
155 std::optional<WslcContainerNetworkingMode> networkingMode = std::nullopt)
156 {
157 // Build process settings.
158 WslcProcessSettings procSettings;
159 THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
160 if (!argv.empty())
161 {
162 THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv.data(), argv.size()));
163 }
164
165 // Build container settings.
166 WslcContainerSettings containerSettings;
167 THROW_IF_FAILED(WslcInitContainerSettings(image, &containerSettings));
168 THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
169 THROW_IF_FAILED(WslcSetContainerSettingsFlags(&containerSettings, flags));
170 if (name)
171 {
172 THROW_IF_FAILED(WslcSetContainerSettingsName(&containerSettings, name));
173 }
174 if (networkingMode.has_value())
175 {
176 THROW_IF_FAILED(WslcSetContainerSettingsNetworkingMode(&containerSettings, *networkingMode));
177 }
178
179 return RunContainerAndCapture(session, containerSettings, timeout);
180 }
181
182 } // namespace
183
184 class WslcSdkTests
185 {
186 WSLC_TEST_CLASS(WslcSdkTests)
187
188 WSADATA m_wsadata;
189 std::filesystem::path m_storagePath;
190 WslcSession m_defaultSession = nullptr;
191 static inline auto c_testSessionName = L"wslc-test";
192
193 void LoadTestImage(std::string_view imageName)
194 {
195 std::filesystem::path imagePath = GetTestImagePath(imageName);
196 THROW_IF_FAILED(WslcLoadSessionImageFromFile(m_defaultSession, imagePath.c_str(), nullptr, nullptr));
197 }
198
199 TEST_CLASS_SETUP(TestClassSetup)
200 {
201 THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
202
203 // Use the same storage path as WSLC runtime tests to reduce pull overhead.
204 m_storagePath = std::filesystem::current_path() / "test-storage";
205
206 // Build session settings using the WSLC SDK.
207 WslcSessionSettings sessionSettings;
208 VERIFY_SUCCEEDED(WslcInitSessionSettings(c_testSessionName, m_storagePath.c_str(), &sessionSettings));
209 VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 4));
210 VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 2048));
211 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
212
213 WslcVhdRequirements vhdReqs{};
214 vhdReqs.sizeBytes = 4096ull * 1024 * 1024; // 4 GB
215 vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
216 VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
217
218 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &m_defaultSession, nullptr));
219
220 // Pull images required by the tests (no-op if already present).
221 for (const char* image : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"})
222 {
223 LoadTestImage(image);
224 }
225
226 return true;
227 }
228
229 TEST_CLASS_CLEANUP(TestClassCleanup)
230 {
231 if (m_defaultSession)
232 {
233 WslcTerminateSession(m_defaultSession);
234 WslcReleaseSession(m_defaultSession);
235 m_defaultSession = nullptr;
236 }
237
238 // Preserve the VHD in fast-run mode so subsequent runs skip image pulling.
239 if (!g_fastTestRun && !m_storagePath.empty())
240 {
241 std::error_code error;
242 std::filesystem::remove_all(m_storagePath, error);
243 if (error)
244 {
245 LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
246 }
247 }
248
249 return true;
250 }
251
252 // -----------------------------------------------------------------------
253 // Session tests
254 // -----------------------------------------------------------------------
255
256 WSLC_TEST_METHOD(CreateSession)
257 {
258 std::filesystem::path extraStorage = m_storagePath / "wslc-extra-session-storage";
259
260 WslcSessionSettings sessionSettings;
261 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-extra-session", extraStorage.c_str(), &sessionSettings));
262 VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 2));
263 VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 1024));
264 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
265
266 WslcVhdRequirements vhdReqs{};
267 vhdReqs.sizeBytes = 1024ull * 1024 * 1024; // 1 GB
268 vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
269 VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
270
271 UniqueSession session;
272 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
273 VERIFY_IS_NOT_NULL(session.get());
274
275 // Null output pointer must fail.
276 VERIFY_ARE_EQUAL(WslcCreateSession(&sessionSettings, nullptr, nullptr), E_POINTER);
277
278 // Null settings pointer must fail.
279 UniqueSession session2;
280 VERIFY_ARE_EQUAL(WslcCreateSession(nullptr, &session2, nullptr), E_POINTER);
281 }
282
283 WSLC_TEST_METHOD(TerminationEventViaTerminate)
284 {
285 std::filesystem::path extraStorage = m_storagePath / "wslc-termevt-term-storage";
286
287 WslcSessionSettings sessionSettings;
288 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termevt-term-test", extraStorage.c_str(), &sessionSettings));
289 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
290
291 UniqueSession session;
292 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
293
294 wil::unique_handle terminationEvent;
295 VERIFY_SUCCEEDED(WslcGetSessionTerminationEvent(session.get(), &terminationEvent));
296 VERIFY_IS_NOT_NULL(terminationEvent.get());
297
298 // Terminating the session should trigger a graceful shutdown and signal the event.
299 VERIFY_SUCCEEDED(WslcTerminateSession(session.get()));
300
301 VERIFY_ARE_EQUAL(WaitForSingleObject(terminationEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
302
303 WslcSessionTerminationReason reason = WSLC_SESSION_TERMINATION_REASON_UNKNOWN;
304 VERIFY_SUCCEEDED(WslcGetSessionTerminationReason(session.get(), &reason));
305 VERIFY_ARE_EQUAL(reason, WSLC_SESSION_TERMINATION_REASON_SHUTDOWN);
306 }
307
308 WSLC_TEST_METHOD(TerminationEventViaRelease)
309 {
310 std::filesystem::path extraStorage = m_storagePath / "wslc-termevt-release-storage";
311
312 WslcSessionSettings sessionSettings;
313 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termevt-release-test", extraStorage.c_str(), &sessionSettings));
314 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
315
316 UniqueSession session;
317 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
318
319 // The termination event is owned by the caller and stays valid even after the session is released.
320 wil::unique_handle terminationEvent;
321 VERIFY_SUCCEEDED(WslcGetSessionTerminationEvent(session.get(), &terminationEvent));
322 VERIFY_IS_NOT_NULL(terminationEvent.get());
323
324 // Releasing the session should trigger a graceful shutdown and signal the event.
325 VERIFY_SUCCEEDED(WslcReleaseSession(session.get()));
326 // Calling WslcReleaseSession will destroy the session.
327 session.release();
328
329 VERIFY_ARE_EQUAL(WaitForSingleObject(terminationEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
330 }
331
332 WSLC_TEST_METHOD(CrashDumpCallback)
333 {
334 struct Invocation
335 {
336 std::wstring DumpPath;
337 std::string ProcessName;
338 uint32_t Pid;
339 uint32_t Signal;
340 uint64_t Timestamp;
341 };
342
343 std::promise<Invocation> promise;
344
345 auto callback = [](const WslcSessionCrashDumpInfo* info, PVOID context) {
346 auto* p = static_cast<std::promise<Invocation>*>(context);
347 p->set_value(Invocation{
348 info->dumpPath ? std::wstring{info->dumpPath} : std::wstring{},
349 info->processName ? std::string{info->processName} : std::string{},
350 info->pid,
351 info->signal,
352 info->timestamp});
353 };
354
355 std::filesystem::path extraStorage = m_storagePath / "wslc-crash-callback-storage";
356 auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
357 std::error_code ec;
358 std::filesystem::remove_all(extraStorage, ec);
359 });
360
361 WslcSessionSettings sessionSettings;
362 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-crashcb-test", extraStorage.c_str(), &sessionSettings));
363 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
364
365 UniqueSession session;
366 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
367
368 UniqueCrashDumpSubscription subscription;
369 VERIFY_SUCCEEDED(WslcRegisterSessionCrashDumpCallback(session.get(), callback, &promise, &subscription, nullptr));
370
371 auto comSession = reinterpret_cast<WslcSessionImpl*>(session.get())->session.query<IWSLCSession>();
372
373 wsl::windows::common::WSLCProcessLauncher launcher{"/bin/sh", {"/bin/sh", "-c", "kill -SEGV $$"}};
374 auto process = launcher.Launch(*comSession);
375 auto result = process.WaitAndCaptureOutput();
376 VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGSEGV);
377
378 auto future = promise.get_future();
379 VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(60)), std::future_status::ready);
380
381 auto invocation = future.get();
382 VERIFY_IS_FALSE(invocation.DumpPath.empty());
383 VERIFY_IS_TRUE(std::filesystem::exists(invocation.DumpPath));
384 VERIFY_IS_GREATER_THAN(std::filesystem::file_size(invocation.DumpPath), 0ull);
385 VERIFY_IS_TRUE(invocation.ProcessName.find("sh") != std::string::npos);
386 VERIFY_ARE_EQUAL(invocation.Signal, static_cast<uint32_t>(WSLCSignalSIGSEGV));
387 VERIFY_IS_GREATER_THAN(invocation.Pid, 0u);
388 VERIFY_IS_GREATER_THAN(invocation.Timestamp, 0ull);
389 }
390
391 // -----------------------------------------------------------------------
392 // Image tests
393 // -----------------------------------------------------------------------
394
395 WSLC_TEST_METHOD(ImageList)
396 {
397 // Positive: session has images pre-loaded — list must return at least one entry.
398 {
399 WslcImageInfo* images = nullptr;
400 uint32_t count = 0;
401 VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, &images, &count));
402 auto cleanupImages = wil::scope_exit([images]() { CoTaskMemFree(images); });
403 VERIFY_IS_TRUE(count >= 1);
404 VERIFY_IS_NOT_NULL(images);
405 // At least one image must have a non-empty name.
406 bool foundNonEmpty = false;
407 for (uint32_t i = 0; i < count; ++i)
408 {
409 if (images[i].name[0] != '\0' && (images[i].sha256[0] != 0 || images[i].sha256[31] != 0) &&
410 images[i].sizeBytes != 0 && images[i].createdUnixTime != 0)
411 {
412 foundNonEmpty = true;
413 break;
414 }
415 }
416 VERIFY_IS_TRUE(foundNonEmpty);
417 }
418
419 // Negative: null images pointer must fail.
420 {
421 uint32_t count = 0;
422 VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, nullptr, &count), E_POINTER);
423 }
424
425 // Negative: null count pointer must fail.
426 {
427 WslcImageInfo* images = nullptr;
428 VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, &images, nullptr), E_POINTER);
429 }
430 }
431
432 WSLC_TEST_METHOD(LoadImage)
433 {
434 // Positive: load a saved image tar and verify the image can be run.
435 {
436 // Remove the image first (ignore failure if it wasn't present).
437 WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr);
438
439 std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
440 wil::unique_handle imageTarFileHandle{
441 CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
442 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
443
444 LARGE_INTEGER fileSize{};
445 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
446
447 VERIFY_SUCCEEDED(WslcLoadSessionImage(
448 m_defaultSession, imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr));
449
450 // Verify the loaded image is usable.
451 auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {});
452 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
453 }
454
455 // Positive: load a saved image tar and verify the image can be run.
456 {
457 // Remove the image first (ignore failure if it wasn't present).
458 WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr);
459
460 std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
461
462 VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(m_defaultSession, imageTar.c_str(), nullptr, nullptr));
463
464 // Verify the loaded image is usable.
465 auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {});
466 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
467 }
468
469 WslcLoadImageOptions opts{};
470
471 // Negative: null ImageHandle must fail.
472 VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, nullptr, 1, &opts, nullptr), E_INVALIDARG);
473
474 // Negative: INVALID_HANDLE_VALUE must fail.
475 VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, INVALID_HANDLE_VALUE, 1, &opts, nullptr), E_INVALIDARG);
476
477 // Negative: zero ContentLength must fail.
478 VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG);
479
480 // Negative: null path must fail.
481 VERIFY_ARE_EQUAL(WslcLoadSessionImageFromFile(m_defaultSession, nullptr, &opts, nullptr), E_POINTER);
482 }
483
484 WSLC_TEST_METHOD(ImportImage)
485 {
486 const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
487 constexpr auto c_handleImportedImageName = "my-hello-world-handle:test";
488 constexpr auto c_pathImportedImageName = "my-hello-world-path:test";
489
490 // Positive: import an exported image tar via handle+length and verify the image can be run.
491 {
492 WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr);
493
494 auto cleanup = wil::scope_exit(
495 [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr)); });
496
497 wil::unique_handle imageTarFileHandle{CreateFileW(
498 exportedImageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
499 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
500
501 LARGE_INTEGER fileSize{};
502 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
503
504 VERIFY_SUCCEEDED(WslcImportSessionImage(
505 m_defaultSession, c_handleImportedImageName, imageTarFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, nullptr));
506
507 auto output = RunContainerAndCapture(m_defaultSession, c_handleImportedImageName, {"/hello"});
508 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
509 }
510
511 // Positive: import an exported image tar via path and verify the image can be run.
512 {
513
514 WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr);
515
516 auto cleanup = wil::scope_exit(
517 [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr)); });
518
519 VERIFY_SUCCEEDED(WslcImportSessionImageFromFile(m_defaultSession, c_pathImportedImageName, exportedImageTar.c_str(), nullptr, nullptr));
520
521 auto output = RunContainerAndCapture(m_defaultSession, c_pathImportedImageName, {"/hello"});
522 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
523 }
524
525 WslcImportImageOptions opts{};
526
527 // Negative: null image name must fail.
528 VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, nullptr, exportedImageTar.c_str(), &opts, nullptr), E_POINTER);
529
530 // Negative: missing file input must fail.
531 VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, "missing-file-input:test", nullptr, &opts, nullptr), E_POINTER);
532
533 // Negative: zero ContentLength must fail.
534 VERIFY_ARE_EQUAL(WslcImportSessionImage(m_defaultSession, "zero-length:test", GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG);
535 }
536
537 WSLC_TEST_METHOD(LoadImageNonTar)
538 {
539 // The load should fail but it just silently ignores the load currently.
540 SKIP_TEST_NOT_IMPL();
541
542 // Negative: attempt to load a non-tar file.
543 {
544 std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
545 wil::unique_handle selfFileHandle{
546 CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
547 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get());
548
549 LARGE_INTEGER fileSize{};
550 VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize));
551
552 wil::unique_cotaskmem_string errorMsg;
553 VERIFY_ARE_EQUAL(
554 WslcLoadSessionImage(m_defaultSession, selfFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, &errorMsg), E_FAIL);
555 VERIFY_IS_NOT_NULL(errorMsg.get());
556 }
557 }
558
559 WSLC_TEST_METHOD(ImportImageNonTar)
560 {
561 // Negative: attempt to load a non-tar file.
562 {
563 std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW<std::wstring>(GetCurrentProcess());
564 wil::unique_handle selfFileHandle{
565 CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
566 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get());
567
568 LARGE_INTEGER fileSize{};
569 VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize));
570
571 wil::unique_cotaskmem_string errorMsg;
572 VERIFY_ARE_EQUAL(
573 WslcImportSessionImage(
574 m_defaultSession, "import-self:test", selfFileHandle.get(), static_cast<uint64_t>(fileSize.QuadPart), nullptr, &errorMsg),
575 E_FAIL);
576 VERIFY_IS_NOT_NULL(errorMsg.get());
577 LogInfo("Import error: %ws", errorMsg.get());
578 }
579 }
580
581 WSLC_TEST_METHOD(ImageDelete)
582 {
583 VERIFY_IS_TRUE(HasImage("hello-world:latest"));
584
585 // Positive: delete an existing image.
586 wil::unique_cotaskmem_string errorMsg;
587 VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", &errorMsg));
588
589 // Verify the image is no longer present in the list.
590 VERIFY_IS_FALSE(HasImage("hello-world:latest"));
591
592 // Reload the image for subsequent tests.
593 LoadTestImage("hello-world:latest");
594
595 // Negative: null name must fail.
596 VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
597 }
598
599 // -----------------------------------------------------------------------
600 // Container lifecycle tests
601 // -----------------------------------------------------------------------
602
603 WSLC_TEST_METHOD(CreateContainer)
604 {
605 // Simple echo — verify stdout is captured correctly.
606 {
607 auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/echo", "OK"});
608 VERIFY_ARE_EQUAL(output.stdoutOutput, "OK\n");
609 VERIFY_ARE_EQUAL(output.stderrOutput, "");
610 }
611
612 // Verify stdout and stderr are routed independently.
613 {
614 auto output =
615 RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/sh", "-c", "echo stdout && echo stderr >&2"});
616 VERIFY_ARE_EQUAL(output.stdoutOutput, "stdout\n");
617 VERIFY_ARE_EQUAL(output.stderrOutput, "stderr\n");
618 }
619
620 // Verify that creating a container with a non-existent image fails.
621 {
622 WslcContainerSettings containerSettings;
623 VERIFY_SUCCEEDED(WslcInitContainerSettings("invalid-image:notfound", &containerSettings));
624
625 WslcContainer container = nullptr;
626 wil::unique_cotaskmem_string errorMsg;
627 VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, &errorMsg), WSLC_E_IMAGE_NOT_FOUND);
628 VERIFY_IS_NULL(container);
629 }
630
631 // Verify that a null image name is rejected.
632 {
633 WslcContainerSettings containerSettings;
634 VERIFY_ARE_EQUAL(WslcInitContainerSettings(nullptr, &containerSettings), E_POINTER);
635 }
636
637 // Verify that a null settings pointer is rejected.
638 {
639 VERIFY_ARE_EQUAL(WslcInitContainerSettings("debian:latest", nullptr), E_POINTER);
640 }
641
642 // Verify that a null container output pointer is rejected.
643 {
644 WslcContainerSettings containerSettings;
645 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
646 VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, nullptr, nullptr), E_POINTER);
647 }
648 }
649
650 WSLC_TEST_METHOD(ContainerGetID)
651 {
652 UniqueContainer container;
653 WslcContainerSettings containerSettings;
654 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
655 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
656
657 // Positive: ID is returned and is the expected length of hex characters.
658 CHAR id[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
659 VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), id));
660 VERIFY_ARE_EQUAL(strnlen(id, WSLC_CONTAINER_ID_BUFFER_SIZE), static_cast<size_t>(WSLC_CONTAINER_ID_BUFFER_SIZE - 1));
661
662 // Negative: null ID buffer must fail.
663 VERIFY_ARE_EQUAL(WslcGetContainerID(container.get(), nullptr), E_POINTER);
664
665 VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
666 }
667
668 WSLC_TEST_METHOD(ContainerGetState)
669 {
670 WslcProcessSettings procSettings;
671 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
672 const char* argv[] = {"/bin/sleep", "99"};
673 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
674
675 WslcContainerSettings containerSettings;
676 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
677 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
678
679 UniqueContainer container;
680 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
681
682 // State after creation: CREATED.
683 {
684 WslcContainerState state{};
685 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
686 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_CREATED);
687 }
688
689 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
690
691 // State while running: RUNNING.
692 {
693 WslcContainerState state{};
694 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
695 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
696 }
697
698 VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr));
699
700 // State after stop: EXITED.
701 {
702 WslcContainerState state{};
703 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
704 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_EXITED);
705 }
706
707 // Negative: null state pointer must fail.
708 VERIFY_ARE_EQUAL(WslcGetContainerState(container.get(), nullptr), E_POINTER);
709
710 VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
711 }
712
713 WSLC_TEST_METHOD(ContainerStopAndDelete)
714 {
715 // Build a long-running container.
716 WslcProcessSettings procSettings;
717 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
718 const char* argv[] = {"/bin/sleep", "999"};
719 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
720
721 WslcContainerSettings containerSettings;
722 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
723 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
724 VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "wslc-stop-delete-test"));
725
726 UniqueContainer container;
727 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
728 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
729
730 // Acquire and release the init process handle — we won't read its I/O.
731 {
732 UniqueProcess process;
733 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
734 }
735
736 // Stop the container gracefully (after the timeout).
737 VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr));
738
739 // Delete the stopped container.
740 VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr));
741 }
742
743 WSLC_TEST_METHOD(ProcessIOHandles)
744 {
745 // Verify that stdout and stderr can each be read, and are independent streams.
746 WslcProcessSettings procSettings;
747 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
748 const char* argv[] = {"/bin/sh", "-c", "printf 'stdout-line\n' ; printf 'stderr-line\n' >&2"};
749 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
750
751 WslcContainerSettings containerSettings;
752 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
753 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
754 VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_NONE));
755
756 UniqueContainer container;
757 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
758 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
759
760 UniqueProcess process;
761 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
762
763 HANDLE exitEvent = nullptr;
764 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
765
766 HANDLE rawStdout = nullptr;
767 VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &rawStdout));
768 wil::unique_handle ownedStdout(rawStdout);
769
770 HANDLE rawStderr = nullptr;
771 VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &rawStderr));
772 wil::unique_handle ownedStderr(rawStderr);
773
774 // Verify that each handle can only be acquired once.
775 {
776 HANDLE duplicate = nullptr;
777 VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &duplicate), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
778 }
779
780 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), WAIT_OBJECT_0);
781 }
782
783 WSLC_TEST_METHOD(ContainerNetworkingMode)
784 {
785 // BRIDGED: container should have an eth0 interface in sysfs.
786 {
787 auto output = RunContainerAndCapture(
788 m_defaultSession,
789 "debian:latest",
790 {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
791 WSLC_CONTAINER_FLAG_NONE,
792 nullptr,
793 60s,
794 WSLC_CONTAINER_NETWORKING_MODE_BRIDGED);
795 VERIFY_ARE_EQUAL(output.stdoutOutput, "HAS_ETH0\n");
796 }
797
798 // NONE: container should not have an eth0 interface.
799 {
800 auto output = RunContainerAndCapture(
801 m_defaultSession,
802 "debian:latest",
803 {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"},
804 WSLC_CONTAINER_FLAG_NONE,
805 nullptr,
806 60s,
807 WSLC_CONTAINER_NETWORKING_MODE_NONE);
808 VERIFY_ARE_EQUAL(output.stdoutOutput, "NO_ETH0\n");
809 }
810
811 // Invalid networking mode must fail.
812 {
813 WslcContainerSettings containerSettings;
814 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
815 VERIFY_ARE_EQUAL(WslcSetContainerSettingsNetworkingMode(&containerSettings, static_cast<WslcContainerNetworkingMode>(99)), E_INVALIDARG);
816 }
817 }
818
819 WSLC_TEST_METHOD(SessionHostLoopbackDisabled)
820 {
821 constexpr uint16_t c_hostLoopbackTestPort = 1237;
822 const auto endpoint = std::format(L"http://127.0.0.1:{}/", c_hostLoopbackTestPort);
823 UniqueWebServer server(endpoint.c_str(), L"sdk-loopback-enabled");
824 ExpectHttpResponse(endpoint.c_str(), HTTP_STATUS_OK, true);
825
826 const auto command = std::format(
827 "if python3 -c \"import http.client,socket;"
828 "a=socket.getaddrinfo('host.wslc.internal',{},socket.AF_INET,socket.SOCK_STREAM)[0][4];"
829 "c=http.client.HTTPConnection(*a,timeout=5);"
830 "c.request('GET','/');"
831 "c.getresponse().read()\";"
832 "then echo enabled; else echo disabled; fi",
833 c_hostLoopbackTestPort);
834 auto output = RunContainerAndCapture(m_defaultSession, "python:3.12-alpine", {"/bin/sh", "-c", command.c_str()});
835
836 VERIFY_ARE_EQUAL("disabled\n", output.stdoutOutput);
837 }
838
839 WSLC_TEST_METHOD(ContainerPortMapping)
840 {
841 // Negative: null mappings with nonzero count must fail.
842 {
843 WslcContainerSettings containerSettings;
844 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
845 VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 1), E_INVALIDARG);
846 }
847
848 // Negative: non-null pointer with zero count must fail.
849 {
850 WslcContainerSettings containerSettings;
851 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
852 WslcContainerPortMapping portMappings[1] = {};
853 VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, portMappings, 0), E_INVALIDARG);
854 }
855
856 // Positive: null mappings with zero count must succeed (clears the mapping).
857 {
858 WslcContainerSettings containerSettings;
859 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
860 VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 0));
861 }
862
863 // Negative: port mappings with NONE networking must fail at container creation.
864 {
865 WslcContainerSettings containerSettings1;
866 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings1));
867 VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings1, WSLC_CONTAINER_NETWORKING_MODE_NONE));
868
869 WslcContainerPortMapping mapping{};
870 mapping.windowsPort = 12342;
871 mapping.containerPort = 8000;
872 mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
873 VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings1, &mapping, 1));
874
875 WslcContainer rawContainer = nullptr;
876 VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings1, &rawContainer, nullptr), E_INVALIDARG);
877 VERIFY_IS_NULL(rawContainer);
878 }
879
880 // Functional: create a container with BRIDGED networking and a port mapping;
881 // verify that a TCP connection from the host reaches the container.
882 {
883 WslcProcessSettings procSettings;
884 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
885 const char* argv[] = {"python3", "-m", "http.server", "8000"};
886 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
887 const char* env[] = {"PYTHONUNBUFFERED=1"};
888 VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
889
890 WslcContainerSettings containerSettings2;
891 VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings2));
892 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings2, &procSettings));
893 VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings2, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
894
895 WslcContainerPortMapping mapping{};
896 mapping.windowsPort = 12341;
897 mapping.containerPort = 8000;
898 mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
899 VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings2, &mapping, 1));
900
901 UniqueContainer container;
902 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings2, &container, nullptr));
903 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
904
905 UniqueProcess process;
906 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
907
908 wil::unique_handle ownedStdout;
909 VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
910
911 WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
912
913 ExpectHttpResponse(L"http://127.0.0.1:12341", 200);
914 }
915
916 // Functional: port mapping with explicit IPv4 windowsAddress (127.0.0.1).
917 {
918 WslcProcessSettings procSettings;
919 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
920 const char* argv[] = {"python3", "-m", "http.server", "8000"};
921 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
922 const char* env[] = {"PYTHONUNBUFFERED=1"};
923 VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
924
925 WslcContainerSettings containerSettings3;
926 VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings3));
927 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings3, &procSettings));
928 VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings3, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
929
930 sockaddr_storage addr4{};
931 auto* sin4 = reinterpret_cast<sockaddr_in*>(&addr4);
932 sin4->sin_family = AF_INET;
933 VERIFY_ARE_EQUAL(inet_pton(AF_INET, "127.0.0.1", &sin4->sin_addr), 1);
934
935 WslcContainerPortMapping mapping{};
936 mapping.windowsPort = 12343;
937 mapping.containerPort = 8000;
938 mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
939 mapping.windowsAddress = &addr4;
940 VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings3, &mapping, 1));
941
942 UniqueContainer container;
943 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings3, &container, nullptr));
944 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
945
946 UniqueProcess process;
947 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
948
949 wil::unique_handle ownedStdout;
950 VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
951
952 WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
953
954 ExpectHttpResponse(L"http://127.0.0.1:12343", 200);
955 }
956
957 // Functional: port mapping with explicit IPv6 windowsAddress (::1).
958 {
959 WslcProcessSettings procSettings;
960 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
961 const char* argv[] = {"python3", "-m", "http.server", "8000", "--bind", "::"};
962 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
963 const char* env[] = {"PYTHONUNBUFFERED=1"};
964 VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env)));
965
966 WslcContainerSettings containerSettings4;
967 VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings4));
968 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings4, &procSettings));
969 VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings4, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
970
971 sockaddr_storage addr6{};
972 auto* sin6 = reinterpret_cast<sockaddr_in6*>(&addr6);
973 sin6->sin6_family = AF_INET6;
974 VERIFY_ARE_EQUAL(inet_pton(AF_INET6, "::1", &sin6->sin6_addr), 1);
975
976 WslcContainerPortMapping mapping{};
977 mapping.windowsPort = 12344;
978 mapping.containerPort = 8000;
979 mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
980 mapping.windowsAddress = &addr6;
981 VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings4, &mapping, 1));
982
983 UniqueContainer container;
984 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings4, &container, nullptr));
985 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
986
987 UniqueProcess process;
988 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
989
990 wil::unique_handle ownedStdout;
991 VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout));
992
993 WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s);
994
995 ExpectHttpResponse(L"http://[::1]:12344", 200);
996 }
997
998 // Negative: unsupported address family must fail when setting container portmapping values.
999 {
1000 WslcContainerSettings containerSettings5;
1001 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings5));
1002 VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings5, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED));
1003
1004 sockaddr_storage badAddr{};
1005 badAddr.ss_family = AF_UNIX; // unsupported for port mapping
1006
1007 WslcContainerPortMapping mapping{};
1008 mapping.windowsPort = 12345;
1009 mapping.containerPort = 8000;
1010 mapping.protocol = WSLC_PORT_PROTOCOL_TCP;
1011 mapping.windowsAddress = &badAddr;
1012 VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings5, &mapping, 1), E_INVALIDARG);
1013 }
1014 }
1015
1016 WSLC_TEST_METHOD(ContainerVolumeUnit)
1017 {
1018 // Negative: null volumes with nonzero count must fail.
1019 {
1020 WslcContainerSettings containerSettings;
1021 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1022 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 1), E_INVALIDARG);
1023 }
1024
1025 // Negative: non-null pointer with zero count must fail.
1026 {
1027 WslcContainerSettings containerSettings;
1028 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1029 WslcContainerVolume containerVolumes[1] = {};
1030 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, 0), E_INVALIDARG);
1031 }
1032
1033 // Negative: null paths must fail.
1034 {
1035 WslcContainerSettings containerSettings;
1036 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1037 WslcContainerVolume containerVolumes[1] = {nullptr, "/mnt/path"};
1038 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
1039 }
1040
1041 {
1042 WslcContainerSettings containerSettings;
1043 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1044 auto currentDirectory = std::filesystem::current_path();
1045 WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), nullptr};
1046 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
1047 }
1048
1049 // Relative paths must fail.
1050 {
1051 WslcContainerSettings containerSettings;
1052 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1053 WslcContainerVolume containerVolumes[1] = {L"relative", "/mnt/path"};
1054 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
1055 }
1056
1057 {
1058 WslcContainerSettings containerSettings;
1059 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1060 auto currentDirectory = std::filesystem::current_path();
1061 WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "./mnt/path"};
1062 VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG);
1063 }
1064
1065 // Positive: null volumes with zero count must succeed (clears volumes).
1066 {
1067 WslcContainerSettings containerSettings;
1068 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1069 VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 0));
1070 }
1071
1072 // Absolute paths should succeed
1073 {
1074 WslcContainerSettings containerSettings;
1075 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1076 auto currentDirectory = std::filesystem::current_path();
1077 WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "/mnt/path"};
1078 VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)));
1079 }
1080 }
1081
1082 WSLC_TEST_METHOD(ContainerVolumeFunctional)
1083 {
1084 // Functional: mount a read-write and a read-only directory into the container.
1085 {
1086 auto hostRwDir = std::filesystem::current_path() / "wslc-test-vol-rw";
1087 auto hostRoDir = std::filesystem::current_path() / "wslc-test-vol-ro";
1088 std::filesystem::create_directories(hostRwDir);
1089 std::filesystem::create_directories(hostRoDir);
1090
1091 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1092 std::error_code ec;
1093 std::filesystem::remove_all(hostRwDir, ec);
1094 std::filesystem::remove_all(hostRoDir, ec);
1095 });
1096
1097 // Write sentinel files into both host directories.
1098 {
1099 std::ofstream rwSentinel(hostRwDir / "hello.txt");
1100 rwSentinel << "hello-rw";
1101 }
1102 {
1103 std::ofstream roSentinel(hostRoDir / "hello.txt");
1104 roSentinel << "hello-ro";
1105 }
1106
1107 WslcContainerVolume volumes[2]{};
1108 volumes[0].windowsPath = hostRwDir.c_str();
1109 volumes[0].containerPath = "/mnt/rw";
1110 volumes[0].readOnly = FALSE;
1111 volumes[1].windowsPath = hostRoDir.c_str();
1112 volumes[1].containerPath = "/mnt/ro";
1113 volumes[1].readOnly = TRUE;
1114
1115 // Container script:
1116 // 1. Read from the rw mount.
1117 // 2. Read from the ro mount.
1118 // 3. Write a file to the rw mount; print WRITE_OK on success.
1119 // 4. Try to write to the ro mount; print RO_WRITE_BLOCKED if correctly rejected.
1120 const char* script =
1121 "cat /mnt/rw/hello.txt && "
1122 "cat /mnt/ro/hello.txt && "
1123 "echo 'container-write' > /mnt/rw/written.txt && echo 'WRITE_OK' && "
1124 "if touch /mnt/ro/probe 2>/dev/null; then echo 'RO_WRITE_ALLOWED'; else echo 'RO_WRITE_BLOCKED'; fi";
1125
1126 WslcProcessSettings procSettings;
1127 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1128 const char* argv[] = {"/bin/sh", "-c", script};
1129 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1130
1131 WslcContainerSettings containerSettings;
1132 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1133 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1134 VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, volumes, 2));
1135
1136 ProcessOutput output = RunContainerAndCapture(m_defaultSession, containerSettings);
1137
1138 // Verify all four outcomes.
1139 VERIFY_IS_TRUE(output.stdoutOutput.find("hello-rw") != std::string::npos);
1140 VERIFY_IS_TRUE(output.stdoutOutput.find("hello-ro") != std::string::npos);
1141 VERIFY_IS_TRUE(output.stdoutOutput.find("WRITE_OK") != std::string::npos);
1142 VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_BLOCKED") != std::string::npos);
1143 VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_ALLOWED") == std::string::npos);
1144
1145 // Verify the file written by the container is visible on the host.
1146 std::ifstream written(hostRwDir / "written.txt");
1147 VERIFY_IS_TRUE(written.is_open());
1148 std::string writtenContent((std::istreambuf_iterator<char>(written)), std::istreambuf_iterator<char>());
1149 VERIFY_ARE_EQUAL(writtenContent, "container-write\n");
1150 }
1151 }
1152
1153 WSLC_TEST_METHOD(ContainerInspect)
1154 {
1155 UniqueContainer container;
1156 WslcContainerSettings containerSettings;
1157 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1158 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1159
1160 wil::unique_cotaskmem_ansistring inspectData;
1161 VERIFY_SUCCEEDED(WslcInspectContainer(container.get(), &inspectData));
1162
1163 VERIFY_IS_NOT_NULL(inspectData);
1164
1165 auto inspectObject = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectContainer>(inspectData.get());
1166
1167 CHAR containerId[WSLC_CONTAINER_ID_BUFFER_SIZE];
1168 VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), containerId));
1169
1170 VERIFY_ARE_EQUAL(containerId, inspectObject.Id);
1171 }
1172
1173 WSLC_TEST_METHOD(ContainerExec)
1174 {
1175 // Start a long-running container so we can exec into it.
1176 WslcProcessSettings initProcSettings;
1177 VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1178 const char* initArgv[] = {"/bin/sleep", "99"};
1179 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1180
1181 WslcContainerSettings containerSettings;
1182 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1183 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1184
1185 UniqueContainer container;
1186 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1187 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1188
1189 // Positive: exec an echo command and verify its output.
1190 {
1191 WslcProcessSettings execProcSettings;
1192 VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1193 const char* execArgv[] = {"/bin/echo", "exec-hello"};
1194 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1195
1196 UniqueProcess execProcess;
1197 VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1198
1199 auto output = WaitForProcessOutput(execProcess.get());
1200 VERIFY_ARE_EQUAL(output.stdoutOutput, "exec-hello\n");
1201 }
1202
1203 // Negative: process settings with no command line must fail.
1204 {
1205 WslcProcessSettings emptyProcSettings;
1206 VERIFY_SUCCEEDED(WslcInitProcessSettings(&emptyProcSettings));
1207 WslcProcess newProcess = nullptr;
1208 VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &emptyProcSettings, &newProcess, nullptr), E_INVALIDARG);
1209 VERIFY_IS_NULL(newProcess);
1210 }
1211
1212 // Negative: null newProcess output pointer must fail.
1213 {
1214 WslcProcessSettings execProcSettings;
1215 VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1216 const char* execArgv[] = {"/bin/echo", "x"};
1217 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1218 VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execProcSettings, nullptr, nullptr), E_POINTER);
1219 }
1220 }
1221
1222 WSLC_TEST_METHOD(ContainerHostName)
1223 {
1224 // Unit: setting a hostname succeeds.
1225 {
1226 WslcContainerSettings containerSettings;
1227 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1228 VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "test-host"));
1229 }
1230
1231 // Functional: container process should see the configured hostname.
1232 {
1233 WslcProcessSettings procSettings;
1234 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1235 const char* argv[] = {"/bin/hostname"};
1236 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1237
1238 WslcContainerSettings containerSettings;
1239 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1240 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1241 VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "my-test-host"));
1242
1243 auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1244 VERIFY_ARE_EQUAL(output.stdoutOutput, "my-test-host\n");
1245 }
1246 }
1247
1248 WSLC_TEST_METHOD(ContainerDomainName)
1249 {
1250 // Unit: setting a domain name succeeds.
1251 {
1252 WslcContainerSettings containerSettings;
1253 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1254 VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "my.domain"));
1255 }
1256
1257 // Functional: container should see the configured domain name.
1258 {
1259 WslcProcessSettings procSettings;
1260 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1261 const char* argv[] = {"/bin/sh", "-c", "echo $(domainname)"};
1262 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1263
1264 WslcContainerSettings containerSettings;
1265 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1266 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1267 VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "test.local"));
1268
1269 auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1270 VERIFY_ARE_EQUAL(output.stdoutOutput, "test.local\n");
1271 }
1272 }
1273
1274 WSLC_TEST_METHOD(ProcessEnvVariables)
1275 {
1276 // Negative: null pointer with nonzero count must fail.
1277 {
1278 WslcProcessSettings procSettings;
1279 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1280 VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 1), E_INVALIDARG);
1281 }
1282
1283 // Negative: non-null pointer with zero count must fail.
1284 {
1285 WslcProcessSettings procSettings;
1286 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1287 const char* envVars[] = {"FOO=bar"};
1288 VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, 0), E_INVALIDARG);
1289 }
1290
1291 // Positive: null pointer with zero count must succeed (clears env vars).
1292 {
1293 WslcProcessSettings procSettings;
1294 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1295 VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 0));
1296 }
1297
1298 // Functional: set an env var and verify it is visible inside the container.
1299 {
1300 WslcProcessSettings procSettings;
1301 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1302 const char* argv[] = {"/bin/sh", "-c", "echo $MY_TEST_VAR"};
1303 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1304 const char* envVars[] = {"MY_TEST_VAR=hello-from-test"};
1305 VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, ARRAYSIZE(envVars)));
1306
1307 WslcContainerSettings containerSettings;
1308 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1309 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1310
1311 auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1312 VERIFY_ARE_EQUAL(output.stdoutOutput, "hello-from-test\n");
1313 }
1314 }
1315
1316 WSLC_TEST_METHOD(ProcessSignal)
1317 {
1318 WslcProcessSettings procSettings;
1319 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1320 const char* argv[] = {"/bin/sleep", "99"};
1321 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1322
1323 WslcContainerSettings containerSettings;
1324 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1325 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1326
1327 UniqueContainer container;
1328 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1329 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1330
1331 UniqueProcess process;
1332 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1333
1334 HANDLE exitEvent = nullptr;
1335 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1336
1337 // Positive: SIGKILL the running process.
1338 VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL));
1339
1340 // The process exit event should fire after the signal.
1341 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1342
1343 // Negative: null process handle must return an error.
1344 VERIFY_ARE_EQUAL(WslcSignalProcess(nullptr, WSLC_SIGNAL_SIGKILL), E_POINTER);
1345 }
1346
1347 WSLC_TEST_METHOD(ProcessGetPid)
1348 {
1349 WslcProcessSettings procSettings;
1350 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1351 const char* argv[] = {"/bin/sleep", "99"};
1352 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1353
1354 WslcContainerSettings containerSettings;
1355 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1356 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1357
1358 UniqueContainer container;
1359 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1360 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1361
1362 UniqueProcess process;
1363 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1364
1365 // Positive: PID of a running process must be non-zero.
1366 uint32_t pid = 0;
1367 VERIFY_SUCCEEDED(WslcGetProcessPid(process.get(), &pid));
1368 VERIFY_IS_TRUE(pid > 0);
1369
1370 // Negative: null pid pointer must fail.
1371 VERIFY_ARE_EQUAL(WslcGetProcessPid(process.get(), nullptr), E_POINTER);
1372
1373 // Negative: null process handle must return an error.
1374 WslcProcess nullProcess = nullptr;
1375 VERIFY_ARE_EQUAL(WslcGetProcessPid(nullProcess, &pid), E_POINTER);
1376 }
1377
1378 WSLC_TEST_METHOD(ProcessGetExitCode)
1379 {
1380 auto RunAndGetProcess = [&](int exitCodeArg) -> UniqueProcess {
1381 std::string script = "exit " + std::to_string(exitCodeArg);
1382 const char* argv[] = {"/bin/sh", "-c", script.c_str()};
1383
1384 WslcProcessSettings procSettings;
1385 THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
1386 THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1387
1388 WslcContainerSettings containerSettings;
1389 THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings));
1390 THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1391
1392 UniqueContainer container;
1393 THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1394 THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1395
1396 UniqueProcess process;
1397 THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
1398
1399 HANDLE exitEvent = nullptr;
1400 THROW_IF_FAILED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1401 THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(exitEvent, 30 * 1000) != WAIT_OBJECT_0);
1402
1403 return process;
1404 };
1405
1406 auto RunAndGetExitCode = [&](int exitCodeArg) -> INT32 {
1407 UniqueProcess process = RunAndGetProcess(exitCodeArg);
1408
1409 INT32 code = -1;
1410 THROW_IF_FAILED(WslcGetProcessExitCode(process.get(), &code));
1411 return code;
1412 };
1413
1414 // Positive: verify exit 0 and exit 42 are reported correctly.
1415 VERIFY_ARE_EQUAL(RunAndGetExitCode(0), 0);
1416 VERIFY_ARE_EQUAL(RunAndGetExitCode(42), 42);
1417
1418 // Negative: null exit code pointer must fail.
1419 {
1420 auto process = RunAndGetProcess(0);
1421 VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), nullptr), E_POINTER);
1422 }
1423
1424 // Negative: null process handle must return an error.
1425 {
1426 WslcProcess nullProcess = nullptr;
1427 INT32 code = 0;
1428 VERIFY_ARE_EQUAL(WslcGetProcessExitCode(nullProcess, &code), E_POINTER);
1429 }
1430 }
1431
1432 WSLC_TEST_METHOD(ProcessGetState)
1433 {
1434 WslcProcessSettings procSettings;
1435 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1436 const char* argv[] = {"/bin/sleep", "99"};
1437 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1438
1439 WslcContainerSettings containerSettings;
1440 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1441 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1442
1443 UniqueContainer container;
1444 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1445 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1446
1447 UniqueProcess process;
1448 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1449
1450 HANDLE exitEvent = nullptr;
1451 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1452
1453 // State while running: RUNNING.
1454 {
1455 WslcProcessState state{};
1456 VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state));
1457 VERIFY_ARE_EQUAL(state, WSLC_PROCESS_STATE_RUNNING);
1458 }
1459
1460 // Bonus test for exit code while running
1461 {
1462 INT32 exitCode{};
1463 VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), &exitCode), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1464 VERIFY_ARE_EQUAL(exitCode, -1);
1465 }
1466
1467 // Kill the process and wait for the exit event.
1468 VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL));
1469 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1470
1471 // State after kill: SIGNALLED or EXITED.
1472 {
1473 WslcProcessState state{};
1474 VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state));
1475 VERIFY_IS_TRUE(state == WSLC_PROCESS_STATE_SIGNALLED || state == WSLC_PROCESS_STATE_EXITED);
1476 }
1477
1478 // Negative: null state pointer must fail.
1479 VERIFY_ARE_EQUAL(WslcGetProcessState(process.get(), nullptr), E_POINTER);
1480
1481 // Negative: null process handle must return an error.
1482 {
1483 WslcProcess nullProcess = nullptr;
1484 WslcProcessState state{};
1485 VERIFY_ARE_EQUAL(WslcGetProcessState(nullProcess, &state), E_POINTER);
1486 }
1487 }
1488
1489 WSLC_TEST_METHOD(ProcessWorkingDirectory)
1490 {
1491 // Unit: setting a working directory returns S_OK.
1492 {
1493 WslcProcessSettings procSettings;
1494 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1495 VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp"));
1496 }
1497
1498 // Negative: null processSettings must fail.
1499 VERIFY_ARE_EQUAL(WslcSetProcessSettingsWorkingDirectory(nullptr, "/tmp"), E_POINTER);
1500
1501 // Functional: verify pwd reports the configured working directory.
1502 {
1503 auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/pwd"});
1504 // Default working directory baseline — just verify pwd succeeds.
1505 VERIFY_IS_FALSE(output.stdoutOutput.empty());
1506 }
1507
1508 // Functional: set working directory to /tmp and verify pwd output.
1509 {
1510 WslcProcessSettings procSettings;
1511 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1512 const char* argv[] = {"/bin/pwd"};
1513 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1514 VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp"));
1515
1516 WslcContainerSettings containerSettings;
1517 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1518 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1519
1520 auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
1521 VERIFY_ARE_EQUAL(output.stdoutOutput, "/tmp\n");
1522 }
1523 }
1524
1525 WSLC_TEST_METHOD(GetVersion)
1526 {
1527 // Positive: returns S_OK and fills in a non-zero version.
1528 {
1529 WslcVersion version{};
1530 VERIFY_SUCCEEDED(WslcGetVersion(&version));
1531 VERIFY_IS_TRUE(version.major > 0 || version.minor > 0 || version.revision > 0);
1532 }
1533
1534 // Negative: null pointer must fail.
1535 VERIFY_ARE_EQUAL(WslcGetVersion(nullptr), E_POINTER);
1536 }
1537
1538 WSLC_TEST_METHOD(GetMissingComponents)
1539 {
1540 WslcComponentFlags missing{};
1541 VERIFY_SUCCEEDED(WslcGetMissingComponents(&missing));
1542
1543 // Presumably anywhere that we run the tests we should get these results.
1544 // The levels of OS state modification required to test beyond this are beyond the scope of these tests.
1545 VERIFY_ARE_EQUAL(missing, WSLC_COMPONENT_FLAG_NONE);
1546 }
1547
1548 WSLC_TEST_METHOD(InstallWithDependencies_NoComponents_Succeeds)
1549 {
1550 // Passing WSLC_COMPONENT_FLAG_NONE must return S_OK immediately without requiring elevation.
1551 VERIFY_SUCCEEDED(WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_NONE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
1552 }
1553
1554 WSLC_TEST_METHOD(InstallWithDependencies_SdkNeedsUpdate_ReturnsError)
1555 {
1556 // Passing SDK_NEEDS_UPDATE must always return WSLC_E_SDK_UPDATE_NEEDED — the caller must update their SDK.
1557 VERIFY_ARE_EQUAL(
1558 WSLC_E_SDK_UPDATE_NEEDED,
1559 WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_SDK_NEEDS_UPDATE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
1560 }
1561
1562 WSLC_TEST_METHOD(InstallWithDependencies_WslPackage_GhFallback404)
1563 {
1564 // Without repair semantics, DCAT uses EnsureProductRegistration so it should find no update
1565 // (the product is already registered at the current version). The code then falls back to the
1566 // GitHub release endpoint. This test intercepts that fallback: the fake API server returns a
1567 // release whose asset URL points at a second local server that always responds with HTTP 404,
1568 // so the download fails and WslcInstallWithDependencies surfaces an error HRESULT.
1569 constexpr auto apiEndpoint = L"http://127.0.0.1:12345/";
1570 constexpr auto assetEndpoint = L"http://127.0.0.1:12346/";
1571
1572 RegistryKeyChange<std::wstring> urlOverride(
1573 HKEY_LOCAL_MACHINE,
1574 L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss",
1575 wsl::windows::common::wslutil::c_githubUrlOverrideRegistryValue,
1576 apiEndpoint);
1577
1578 // Version 1.0.0 is below the current package version, so without repair=true the version
1579 // check in UpdatePackageImpl would short-circuit and return early. Using a sub-2.0 version
1580 // here confirms that the repair flag (always set in the GH fallback) is what drives the
1581 // download attempt rather than the version being newer than the installed one.
1582 constexpr auto GitHubApiResponse =
1583 LR"([{
1584 \"name\": \"1.0.0\",
1585 \"created_at\": \"2023-06-14T16:56:30Z\",
1586 \"assets\": [
1587 {
1588 \"url\": \"http://127.0.0.1:12346/fake.msixbundle\",
1589 \"id\": 1,
1590 \"name\": \"Microsoft.WSL_1.0.0.0_x64_ARM64.msixbundle\"
1591 }
1592 ]
1593 }])";
1594
1595 UniqueWebServer apiServer(apiEndpoint, GitHubApiResponse);
1596 UniqueWebServer assetServer(assetEndpoint, L"", 404u);
1597
1598 VERIFY_ARE_EQUAL(
1599 HTTP_E_STATUS_NOT_FOUND, WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_WSL_PACKAGE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
1600 }
1601
1602 // -----------------------------------------------------------------------
1603 // WslcSetProcessSettingsCallbacks tests
1604 // -----------------------------------------------------------------------
1605
1606 WSLC_TEST_METHOD(ProcessIoCallbackUnit)
1607 {
1608 auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {};
1609 auto noopExitCb = [](INT32, PVOID) {};
1610
1611 // Negative: null processSettings must fail.
1612 {
1613 WslcProcessCallbacks callbacks{};
1614 callbacks.onStdOut = noopIoCb;
1615 VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(nullptr, &callbacks, nullptr), E_POINTER);
1616 }
1617
1618 // Negative: null callbacks pointer with non-null context must fail.
1619 {
1620 int ctx = 0;
1621 WslcProcessSettings procSettings;
1622 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1623 VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, &ctx), E_INVALIDARG);
1624 }
1625
1626 // Positive: null callbacks pointer with null context clears all callbacks.
1627 {
1628 WslcProcessSettings procSettings;
1629 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1630 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, nullptr));
1631 }
1632
1633 // Positive: set onStdOut only.
1634 {
1635 WslcProcessSettings procSettings;
1636 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1637 WslcProcessCallbacks callbacks{};
1638 callbacks.onStdOut = noopIoCb;
1639 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1640 }
1641
1642 // Positive: set onStdErr only.
1643 {
1644 WslcProcessSettings procSettings;
1645 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1646 WslcProcessCallbacks callbacks{};
1647 callbacks.onStdErr = noopIoCb;
1648 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1649 }
1650
1651 // Positive: set onExit only.
1652 {
1653 WslcProcessSettings procSettings;
1654 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1655 WslcProcessCallbacks callbacks{};
1656 callbacks.onExit = noopExitCb;
1657 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1658 }
1659
1660 // Positive: set all three callbacks with a context.
1661 {
1662 int ctx = 0;
1663 WslcProcessSettings procSettings;
1664 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1665 WslcProcessCallbacks callbacks{};
1666 callbacks.onStdOut = noopIoCb;
1667 callbacks.onStdErr = noopIoCb;
1668 callbacks.onExit = noopExitCb;
1669 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx));
1670 }
1671
1672 // Negative: StartContainer without ATTACH fails when callbacks are registered.
1673 // The ATTACH flag is required so the IOCallback can claim the init process pipe handles.
1674 {
1675 WslcProcessSettings procSettings;
1676 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1677 const char* argv[] = {"/bin/sleep", "1"};
1678 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1679 WslcProcessCallbacks callbacks{};
1680 callbacks.onStdOut = noopIoCb;
1681 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1682
1683 WslcContainerSettings containerSettings;
1684 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1685 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1686
1687 UniqueContainer container;
1688 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1689 VERIFY_ARE_EQUAL(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr), E_INVALIDARG);
1690 }
1691 }
1692
1693 WSLC_TEST_METHOD(ProcessIoCallbackInitProcess)
1694 {
1695 struct IOContext
1696 {
1697 std::string stdoutData;
1698 std::string stderrData;
1699 } ioContext;
1700
1701 // Both streams share one callback; ioHandle distinguishes which accumulator to use.
1702 auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1703 auto* c = static_cast<IOContext*>(ctx);
1704 auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
1705 target.append(reinterpret_cast<const char*>(data), size);
1706 };
1707
1708 WslcProcessSettings procSettings;
1709 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1710 const char* argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"};
1711 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1712
1713 WslcProcessCallbacks callbacks{};
1714 callbacks.onStdOut = ioCb;
1715 callbacks.onStdErr = ioCb;
1716 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ioContext));
1717
1718 WslcContainerSettings containerSettings;
1719 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1720 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1721
1722 UniqueContainer container;
1723 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1724 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1725
1726 UniqueProcess process;
1727 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1728
1729 HANDLE exitEvent = nullptr;
1730 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
1731 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1732
1733 // Release the process handle first, then the container handle.
1734 // Releasing the container destroys the WslcContainerImpl which joins the IOCallback
1735 // thread, guaranteeing all bytes have been delivered before the assertions below.
1736 process.reset();
1737 container.reset();
1738
1739 VERIFY_ARE_EQUAL(ioContext.stdoutData, "STDOUT\n");
1740 VERIFY_ARE_EQUAL(ioContext.stderrData, "STDERR\n");
1741 }
1742
1743 WSLC_TEST_METHOD(ProcessIoCallbackExecProcess)
1744 {
1745 // Start a long-running container so we can exec into it.
1746 WslcProcessSettings initProcSettings;
1747 VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1748 const char* initArgv[] = {"/bin/sleep", "99"};
1749 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1750
1751 WslcContainerSettings containerSettings;
1752 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1753 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1754
1755 UniqueContainer container;
1756 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1757 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1758
1759 struct IOContext
1760 {
1761 std::string stdoutData;
1762 std::string stderrData;
1763 } ioContext;
1764
1765 auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1766 auto* c = static_cast<IOContext*>(ctx);
1767 auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
1768 target.append(reinterpret_cast<const char*>(data), size);
1769 };
1770
1771 WslcProcessSettings execProcSettings;
1772 VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1773 const char* execArgv[] = {"/bin/sh", "-c", "echo EXEC_OUT && echo EXEC_ERR >&2"};
1774 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1775
1776 WslcProcessCallbacks callbacks{};
1777 callbacks.onStdOut = ioCb;
1778 callbacks.onStdErr = ioCb;
1779 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ioContext));
1780
1781 UniqueProcess execProcess;
1782 VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1783
1784 HANDLE exitEvent = nullptr;
1785 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(execProcess.get(), &exitEvent));
1786 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
1787
1788 // Releasing the exec process handle destroys WslcProcessImpl and joins its IOCallback
1789 // thread, ensuring all bytes are delivered before assertions.
1790 execProcess.reset();
1791
1792 VERIFY_ARE_EQUAL(ioContext.stdoutData, "EXEC_OUT\n");
1793 VERIFY_ARE_EQUAL(ioContext.stderrData, "EXEC_ERR\n");
1794 }
1795
1796 WSLC_TEST_METHOD(ProcessIoCallbackHandleExclusion)
1797 {
1798 // Register a stdout callback only. IOCallback always acquires ALL pipe handles
1799 // (draining uncallbacked streams to prevent deadlock), so both stdout and stderr
1800 // handles are consumed and neither can be obtained via WslcGetProcessIOHandle.
1801 auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {};
1802
1803 WslcProcessSettings procSettings;
1804 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1805 const char* argv[] = {"/bin/sleep", "99"};
1806 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1807
1808 WslcProcessCallbacks callbacks{};
1809 callbacks.onStdOut = noopIoCb;
1810 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr));
1811
1812 WslcContainerSettings containerSettings;
1813 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1814 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1815
1816 UniqueContainer container;
1817 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1818 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1819
1820 UniqueProcess process;
1821 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
1822
1823 // stdout handle was consumed by the IOCallback — must not be obtainable.
1824 {
1825 HANDLE h = nullptr;
1826 VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1827 }
1828
1829 // stderr handle was also consumed in order to drain it despite not being given a callback.
1830 {
1831 HANDLE h = nullptr;
1832 VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
1833 }
1834 }
1835
1836 WSLC_TEST_METHOD(ProcessIoCallbackExitCallback)
1837 {
1838 // Verify the onExit callback fires with the correct exit code after IO has been flushed.
1839 // We test both exit 0 and a non-zero exit code.
1840 auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair<INT32, std::string> {
1841 std::string stdoutData;
1842 std::atomic<INT32> capturedExitCode{-999};
1843
1844 struct Context
1845 {
1846 std::string* stdoutData;
1847 std::atomic<INT32>* capturedExitCode;
1848 wil::unique_event exitEvent{wil::EventOptions::ManualReset};
1849 } ctx{&stdoutData, &capturedExitCode};
1850
1851 auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID c) {
1852 static_cast<Context*>(c)->stdoutData->append(reinterpret_cast<const char*>(data), size);
1853 };
1854 auto exitCb = [](INT32 code, PVOID c) {
1855 static_cast<Context*>(c)->capturedExitCode->store(code);
1856 static_cast<Context*>(c)->exitEvent.SetEvent();
1857 };
1858
1859 std::string script = "echo HELLO && exit " + std::to_string(exitCodeArg);
1860 const char* argv[] = {"/bin/sh", "-c", script.c_str()};
1861
1862 WslcProcessSettings procSettings;
1863 THROW_IF_FAILED(WslcInitProcessSettings(&procSettings));
1864 THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1865
1866 WslcProcessCallbacks callbacks{};
1867 callbacks.onStdOut = ioCb;
1868 callbacks.onExit = exitCb;
1869 THROW_IF_FAILED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx));
1870
1871 WslcContainerSettings containerSettings;
1872 THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings));
1873 THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1874
1875 UniqueContainer container;
1876 THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1877 THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
1878
1879 UniqueProcess process;
1880 THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process));
1881
1882 THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(ctx.exitEvent.get(), 60 * 1000) != WAIT_OBJECT_0);
1883
1884 return {capturedExitCode.load(), stdoutData};
1885 };
1886
1887 // Exit 0: onExit must fire with code 0; IO must have been delivered first.
1888 {
1889 auto [exitCode, output] = RunAndCaptureExit(0);
1890 VERIFY_ARE_EQUAL(exitCode, 0);
1891 VERIFY_ARE_EQUAL(output, "HELLO\n");
1892 }
1893
1894 // Non-zero exit: onExit must report the correct code.
1895 {
1896 auto [exitCode, output] = RunAndCaptureExit(42);
1897 VERIFY_ARE_EQUAL(exitCode, 42);
1898 VERIFY_ARE_EQUAL(output, "HELLO\n");
1899 }
1900 }
1901
1902 WSLC_TEST_METHOD(ProcessIoCallbackCancelOnRelease)
1903 {
1904 // Verify that releasing the process handle while an exec'd process is still running
1905 // and writing IO cancels the IOCallback pump:
1906 // - No IO callbacks arrive after the handle is released.
1907 // - onExit is never invoked (cancellation returns runResult=false, suppressing it).
1908 //
1909 // A secondary (exec'd) process is used so that the long-lived init process keeps the
1910 // container alive, allowing UniqueContainer to clean up normally at scope exit.
1911
1912 // Start a long-running init process to keep the container alive.
1913 WslcProcessSettings initProcSettings;
1914 VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings));
1915 const char* initArgv[] = {"/bin/sleep", "999"};
1916 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv)));
1917
1918 WslcContainerSettings containerSettings;
1919 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1920 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings));
1921
1922 UniqueContainer container;
1923 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
1924 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
1925
1926 struct Context
1927 {
1928 std::atomic<int> callbackCount{0};
1929 std::atomic<bool> exitFired{false};
1930 } ctx;
1931
1932 auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) {
1933 static_cast<Context*>(c)->callbackCount.fetch_add(1);
1934 };
1935 auto exitCb = [](INT32, PVOID c) { static_cast<Context*>(c)->exitFired.store(true); };
1936
1937 // Continuous writer: emits one line every 50 ms indefinitely.
1938 WslcProcessSettings execProcSettings;
1939 VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings));
1940 const char* execArgv[] = {"/bin/sh", "-c", "while true; do echo LINE; sleep 0.05; done"};
1941 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv)));
1942
1943 WslcProcessCallbacks callbacks{};
1944 callbacks.onStdOut = ioCb;
1945 callbacks.onExit = exitCb;
1946 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ctx));
1947
1948 UniqueProcess execProcess;
1949 VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
1950
1951 // Wait long enough for at least several callbacks to arrive.
1952 Sleep(500);
1953 VERIFY_IS_TRUE(ctx.callbackCount.load() > 0);
1954
1955 // Release the exec process handle while the process is still running and writing.
1956 // This destructs WslcProcessImpl → cancels the IOCallback → joins its thread.
1957 // By the time execProcess.reset() returns, the pump thread has exited.
1958 execProcess.reset();
1959
1960 // Snapshot the count now that the thread is confirmed stopped.
1961 int countAtRelease = ctx.callbackCount.load();
1962
1963 // onExit must not have fired: cancellation sets runResult=false, suppressing the call.
1964 VERIFY_IS_FALSE(ctx.exitFired.load());
1965
1966 // Wait another interval — no further callbacks can arrive after the thread has joined.
1967 Sleep(200);
1968 VERIFY_ARE_EQUAL(ctx.callbackCount.load(), countAtRelease);
1969 VERIFY_IS_FALSE(ctx.exitFired.load());
1970 }
1971
1972 WSLC_TEST_METHOD(ProcessIoCallbackLargeOutput)
1973 {
1974 // Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64
1975 // 1,048,576 zero bytes → base64 output is 1,398,104 bytes (ceil(1048576/3)*4).
1976 static constexpr size_t c_expectedBytes = 1'398'104;
1977
1978 std::string stdoutData;
1979 stdoutData.reserve(c_expectedBytes + 4096);
1980
1981 auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID ctx) {
1982 static_cast<std::string*>(ctx)->append(reinterpret_cast<const char*>(data), size);
1983 };
1984
1985 WslcProcessSettings procSettings;
1986 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
1987 const char* argv[] = {"/bin/sh", "-c", "dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"};
1988 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
1989
1990 WslcProcessCallbacks callbacks{};
1991 callbacks.onStdOut = ioCb;
1992 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &stdoutData));
1993
1994 WslcContainerSettings containerSettings;
1995 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
1996 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
1997
1998 UniqueContainer container;
1999 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2000 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
2001
2002 UniqueProcess process;
2003 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
2004
2005 HANDLE exitEvent = nullptr;
2006 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent));
2007 VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
2008
2009 // Join the IOCallback thread before inspecting the accumulator.
2010 process.reset();
2011 container.reset();
2012
2013 VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes);
2014 }
2015
2016 WSLC_TEST_METHOD(ReleaseFromIOCallbackFails)
2017 {
2018 struct Context
2019 {
2020 std::atomic<WslcProcess> process{nullptr};
2021 std::atomic<WslcContainer> container{nullptr};
2022 std::atomic<HRESULT> releaseProcessHr{S_OK};
2023 std::atomic<HRESULT> releaseContainerHr{S_OK};
2024 std::atomic<bool> captured{false};
2025 wil::unique_event done{wil::EventOptions::ManualReset};
2026 } ctx;
2027
2028 auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) {
2029 auto* cx = static_cast<Context*>(c);
2030
2031 // Wait until the test thread has published both handles before sampling.
2032 auto process = cx->process.load(std::memory_order_acquire);
2033 auto container = cx->container.load(std::memory_order_acquire);
2034 if (!process || !container)
2035 {
2036 return;
2037 }
2038
2039 // Only capture on the first eligible callback; later callbacks no-op.
2040 bool expected = false;
2041 if (!cx->captured.compare_exchange_strong(expected, true))
2042 {
2043 return;
2044 }
2045
2046 // Both calls should fail with ERROR_INVALID_HANDLE_STATE without consuming the handles.
2047 cx->releaseProcessHr.store(WslcReleaseProcess(process));
2048 cx->releaseContainerHr.store(WslcReleaseContainer(container));
2049 cx->done.SetEvent();
2050 };
2051
2052 // Continuous writer for the init process so onStdOut fires repeatedly.
2053 WslcProcessSettings procSettings;
2054 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2055 const char* argv[] = {"/bin/sh", "-c", "while true; do echo LINE; sleep 0.05; done"};
2056 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2057
2058 WslcProcessCallbacks callbacks{};
2059 callbacks.onStdOut = ioCb;
2060 VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx));
2061
2062 WslcContainerSettings containerSettings;
2063 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2064 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2065
2066 UniqueContainer container;
2067 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2068 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
2069
2070 UniqueProcess process;
2071 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
2072
2073 // Publish handles to the callback now that both are valid.
2074 ctx.container.store(container.get(), std::memory_order_release);
2075 ctx.process.store(process.get(), std::memory_order_release);
2076
2077 VERIFY_ARE_EQUAL(WaitForSingleObject(ctx.done.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
2078
2079 VERIFY_ARE_EQUAL(ctx.releaseProcessHr.load(), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE_STATE));
2080 VERIFY_ARE_EQUAL(ctx.releaseContainerHr.load(), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE_STATE));
2081 }
2082
2083 // -----------------------------------------------------------------------
2084 // Storage tests
2085 // -----------------------------------------------------------------------
2086
2087 WSLC_TEST_METHOD(SessionCreateVhd)
2088 {
2089 constexpr auto c_volumeName = "wslc-test-data-vol";
2090 constexpr auto c_vhdSizeBytes = _1GB;
2091
2092 std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-vhd-test-storage";
2093 auto removeStorage = wil::scope_exit([&]() {
2094 std::error_code error;
2095 std::filesystem::remove_all(vhdSessionStorage, error);
2096 if (error)
2097 {
2098 LogError("Failed to remove VHD test storage %ws: %hs", vhdSessionStorage.c_str(), error.message().c_str());
2099 }
2100 });
2101
2102 // Create a dedicated session so that volume creation does not affect the shared default session.
2103 WslcSessionSettings sessionSettings;
2104 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-vhd-test", vhdSessionStorage.c_str(), &sessionSettings));
2105
2106 WslcVhdRequirements sessionVhd{};
2107 sessionVhd.sizeBytes = 4 * _1GB;
2108 sessionVhd.type = WSLC_VHD_TYPE_DYNAMIC;
2109 VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &sessionVhd));
2110
2111 UniqueSession session;
2112 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
2113
2114 // Load debian so we have a container image to work with.
2115 std::filesystem::path debianTar = GetTestImagePath("debian:latest");
2116 VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(session.get(), debianTar.c_str(), nullptr, nullptr));
2117
2118 // Positive: create a named VHD volume in the session.
2119 {
2120 WslcVhdRequirements vhd{};
2121 vhd.name = c_volumeName;
2122 vhd.sizeBytes = c_vhdSizeBytes;
2123 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2124 wil::unique_cotaskmem_string errorMsg;
2125 VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2126
2127 // The backing VHD file must exist on disk.
2128 std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx");
2129 VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
2130 }
2131
2132 // Positive: write a marker via a container that mounts the named volume.
2133 {
2134 WslcProcessSettings procSettings;
2135 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2136 const char* argv[] = {"/bin/sh", "-c", "echo wslc-vhd-test > /data/marker.txt"};
2137 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2138
2139 WslcContainerSettings containerSettings;
2140 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2141 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2142
2143 WslcContainerNamedVolume namedVol{};
2144 namedVol.name = c_volumeName;
2145 namedVol.containerPath = "/data";
2146 namedVol.readOnly = FALSE;
2147 VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
2148
2149 auto output = RunContainerAndCapture(session.get(), containerSettings);
2150 VERIFY_IS_TRUE(output.stderrOutput.empty());
2151 }
2152
2153 // Positive: read back the marker in a second container (read-only mount).
2154 {
2155 WslcProcessSettings procSettings;
2156 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2157 const char* argv[] = {"/bin/sh", "-c", "cat /data/marker.txt"};
2158 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2159
2160 WslcContainerSettings containerSettings;
2161 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2162 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2163
2164 WslcContainerNamedVolume namedVol{};
2165 namedVol.name = c_volumeName;
2166 namedVol.containerPath = "/data";
2167 namedVol.readOnly = TRUE;
2168 VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
2169
2170 auto output = RunContainerAndCapture(session.get(), containerSettings);
2171 VERIFY_ARE_EQUAL(output.stdoutOutput, "wslc-vhd-test\n");
2172 }
2173
2174 // Positive: delete the volume.
2175 {
2176 wil::unique_cotaskmem_string errorMsg;
2177 VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_volumeName, &errorMsg));
2178
2179 // The backing VHD file must not exist on disk.
2180 std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx");
2181 VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath));
2182 }
2183
2184 // Negative: null options pointer must fail.
2185 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), nullptr, nullptr), E_POINTER);
2186
2187 // Negative: null name must fail.
2188 {
2189 WslcVhdRequirements vhd{};
2190 vhd.name = nullptr;
2191 vhd.sizeBytes = c_vhdSizeBytes;
2192 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2193 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2194 }
2195
2196 // Negative: zero sizeInBytes must fail.
2197 {
2198 WslcVhdRequirements vhd{};
2199 vhd.name = c_volumeName;
2200 vhd.sizeBytes = 0;
2201 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2202 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2203 }
2204
2205 // Negative: invalid VHD type must fail.
2206 {
2207 WslcVhdRequirements vhd{};
2208 vhd.name = c_volumeName;
2209 vhd.sizeBytes = c_vhdSizeBytes;
2210 vhd.type = static_cast<WslcVhdType>(42);
2211 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2212 }
2213
2214 // Positive: fixed-allocation VHD; on-disk file size must be >= SizeBytes.
2215 {
2216 constexpr auto c_fixedVolumeName = "wslc-sdk-vhd-fixed";
2217 constexpr auto c_fixedSizeBytes = 64ull * _1MB;
2218 WslcVhdRequirements vhd{};
2219 vhd.name = c_fixedVolumeName;
2220 vhd.sizeBytes = c_fixedSizeBytes;
2221 vhd.type = WSLC_VHD_TYPE_FIXED;
2222 wil::unique_cotaskmem_string errorMsg;
2223 VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2224
2225 auto deleteVolume =
2226 wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_fixedVolumeName, nullptr)); });
2227
2228 std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_fixedVolumeName) + ".vhdx");
2229 VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath));
2230 VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes);
2231 }
2232
2233 // Positive: owner flags are honored — uid/gid baked into the volume root
2234 // inode at mkfs time. Verify by stat-ing the mount inside a container.
2235 {
2236 constexpr auto c_ownedVolumeName = "wslc-sdk-vhd-owned";
2237 WslcVhdRequirements vhd{};
2238 vhd.name = c_ownedVolumeName;
2239 vhd.sizeBytes = c_vhdSizeBytes;
2240 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2241 vhd.flags = WSLC_VHD_REQ_FLAG_OWNER;
2242 vhd.uid = 65534; // nobody
2243 vhd.gid = 65534; // nogroup
2244 wil::unique_cotaskmem_string errorMsg;
2245 VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2246
2247 auto deleteVolume =
2248 wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_ownedVolumeName, nullptr)); });
2249
2250 WslcProcessSettings procSettings;
2251 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2252 const char* argv[] = {"/usr/bin/stat", "-c", "%u %g", "/data"};
2253 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2254
2255 WslcContainerSettings containerSettings;
2256 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2257 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2258
2259 WslcContainerNamedVolume namedVol{};
2260 namedVol.name = c_ownedVolumeName;
2261 namedVol.containerPath = "/data";
2262 namedVol.readOnly = FALSE;
2263 VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1));
2264
2265 auto output = RunContainerAndCapture(session.get(), containerSettings);
2266 VERIFY_ARE_EQUAL(output.stdoutOutput, "65534 65534\n");
2267 }
2268
2269 // Negative: unknown flag bits are rejected.
2270 {
2271 WslcVhdRequirements vhd{};
2272 vhd.name = c_volumeName;
2273 vhd.sizeBytes = c_vhdSizeBytes;
2274 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2275 vhd.flags = static_cast<WslcVhdRequirementsFlags>(0x80000000);
2276 VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG);
2277 }
2278
2279 // Positive: flags=NONE silently ignores uid/gid (volume defaults to root:root).
2280 {
2281 constexpr auto c_unflaggedVolumeName = "wslc-sdk-vhd-unflagged";
2282 WslcVhdRequirements vhd{};
2283 vhd.name = c_unflaggedVolumeName;
2284 vhd.sizeBytes = c_vhdSizeBytes;
2285 vhd.type = WSLC_VHD_TYPE_DYNAMIC;
2286 vhd.flags = WSLC_VHD_REQ_FLAG_NONE;
2287 vhd.uid = 1000;
2288 vhd.gid = 1000;
2289 wil::unique_cotaskmem_string errorMsg;
2290 VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg));
2291
2292 wil::unique_cotaskmem_string deleteErr;
2293 VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_unflaggedVolumeName, &deleteErr));
2294 }
2295 }
2296
2297 // -----------------------------------------------------------------------
2298 // Authentication helpers
2299 // -----------------------------------------------------------------------
2300
2301 // Starts a local registry container with host-mode networking and returns [container, registryAddress].
2302 // Uses the COM API (via GetInternalType) with WSLCContainerLauncher to get host-mode networking,
2303 // which the SDK doesn't expose. Host networking shares the VM's network namespace, so the registry
2304 // is reachable at 127.0.0.1:<port> from both dockerd (inside the VM) and the host.
2305 std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
2306 const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000)
2307 {
2308 // Get the IWSLCSession COM object from the SDK session handle and delegate to the shared helper.
2309 auto session = reinterpret_cast<WslcSessionImpl*>(m_defaultSession)->session.query<IWSLCSession>();
2310 return WSLCE2ETests::StartLocalRegistry(*session, username, password, port);
2311 }
2312
2313 // Tags and pushes an image to a local registry via the SDK APIs.
2314 void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth)
2315 {
2316 auto imageName = std::format("{}:{}", repo, tag);
2317 auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag);
2318 auto registryRepo = std::format("{}/{}", registryAddress, repo);
2319
2320 VERIFY_IS_TRUE(HasImage(imageName));
2321
2322 // Tag the image with the registry address so it can be pushed.
2323 WslcTagImageOptions tagOptions{};
2324 tagOptions.image = imageName.c_str();
2325 tagOptions.repo = registryRepo.c_str();
2326 tagOptions.tag = tag.c_str();
2327 VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr));
2328
2329 // Ensures the registry-prefixed tag is removed after the push.
2330 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2331 LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
2332 });
2333
2334 WslcPushImageOptions pushOptions{};
2335 pushOptions.image = registryImage.c_str();
2336 pushOptions.registryAuth = registryAuth.c_str();
2337 VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &pushOptions, nullptr));
2338 }
2339
2340 bool HasImage(const std::string& imageName)
2341 {
2342 wil::unique_cotaskmem_array_ptr<WslcImageInfo> images;
2343 VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, images.addressof(), images.size_address<uint32_t>()));
2344
2345 for (const auto& image : images)
2346 {
2347 if (image.name == imageName)
2348 {
2349 return true;
2350 }
2351 }
2352 return false;
2353 }
2354
2355 // -----------------------------------------------------------------------
2356 // Authentication tests
2357 // -----------------------------------------------------------------------
2358
2359 WSLC_TEST_METHOD(AuthenticateTests)
2360 {
2361 constexpr auto c_username = "wslctest";
2362 constexpr auto c_password = "password";
2363
2364 auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
2365
2366 // Negative: wrong password must fail.
2367 {
2368 wil::unique_cotaskmem_ansistring token;
2369 wil::unique_cotaskmem_string errorMsg;
2370 WslcIdentityTokenType tokenType{};
2371 VERIFY_ARE_EQUAL(
2372 WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &tokenType, &errorMsg),
2373 E_FAIL);
2374 VERIFY_IS_NOT_NULL(errorMsg.get());
2375 VERIFY_ARE_EQUAL(tokenType, WSLC_IDENTITY_TOKEN_TYPE_UNKNOWN);
2376 }
2377
2378 // Positive: correct credentials must succeed and return a non-null, registry-auth-ready token.
2379 {
2380 wil::unique_cotaskmem_ansistring token;
2381 wil::unique_cotaskmem_string errorMsg;
2382 WslcIdentityTokenType tokenType{};
2383 VERIFY_SUCCEEDED(WslcSessionAuthenticate(
2384 m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &tokenType, &errorMsg));
2385 VERIFY_IS_NOT_NULL(token.get());
2386 // The local test registry does not return an identity token, so credentials are embedded.
2387 VERIFY_ARE_EQUAL(tokenType, WSLC_IDENTITY_TOKEN_TYPE_CREDENTIALS);
2388 }
2389
2390 // The local registry requires auth; push the test image for the pull tests below.
2391 auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password);
2392 PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
2393
2394 auto image = std::format("{}/hello-world:latest", registryAddress);
2395
2396 auto imageCleanup = wil::scope_exit_log(
2397 WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr)); });
2398
2399 // Positive: pulling with the identityToken from WslcSessionAuthenticate directly should succeed,
2400 // demonstrating that the output can be passed as registryAuth without any transformation.
2401 {
2402 wil::unique_cotaskmem_ansistring authToken;
2403 VERIFY_SUCCEEDED(WslcSessionAuthenticate(
2404 m_defaultSession, registryAddress.c_str(), c_username, c_password, &authToken, nullptr, nullptr));
2405
2406 WslcPullImageOptions opts{};
2407 opts.uri = image.c_str();
2408 opts.registryAuth = authToken.get();
2409 VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
2410 VERIFY_IS_TRUE(HasImage(image));
2411 }
2412
2413 // Negative: Pulling without credentials should fail.
2414 {
2415 WslcPullImageOptions opts{};
2416 opts.uri = image.c_str();
2417
2418 wil::unique_cotaskmem_string errorMsg;
2419 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2420 VERIFY_IS_NOT_NULL(errorMsg.get());
2421 }
2422
2423 // Negative: Pulling with bad credentials should fail.
2424 {
2425 auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong");
2426
2427 WslcPullImageOptions opts{};
2428 opts.uri = image.c_str();
2429 opts.registryAuth = badAuth.c_str();
2430
2431 wil::unique_cotaskmem_string errorMsg;
2432 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2433 VERIFY_IS_NOT_NULL(errorMsg.get());
2434 }
2435
2436 // Negative: null parameters must fail; tokenType is optional and may be null.
2437 {
2438 wil::unique_cotaskmem_ansistring token;
2439 VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, nullptr, c_username, c_password, &token, nullptr, nullptr), E_POINTER);
2440 VERIFY_ARE_EQUAL(
2441 WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), nullptr, c_password, &token, nullptr, nullptr), E_POINTER);
2442 VERIFY_ARE_EQUAL(
2443 WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, nullptr, &token, nullptr, nullptr), E_POINTER);
2444 VERIFY_ARE_EQUAL(
2445 WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, nullptr, nullptr, nullptr), E_POINTER);
2446 }
2447 }
2448
2449 WSLC_TEST_METHOD(PullImage)
2450 {
2451 // Start a local registry without auth to avoid Docker Hub rate limits.
2452 auto [registryContainer, registryAddress] = StartLocalRegistry();
2453 auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
2454
2455 {
2456 // Push hello-world:latest to the local registry.
2457 PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth);
2458
2459 auto image = std::format("{}/hello-world:latest", registryAddress);
2460
2461 auto imageCleanup = wil::scope_exit_log(
2462 WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr)); });
2463
2464 // Delete the image locally so the pull is a real pull.
2465 WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr);
2466
2467 // Pull from the local registry.
2468 {
2469 WslcPullImageOptions opts{};
2470 opts.uri = image.c_str();
2471 VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
2472 }
2473
2474 // Verify the pulled image is in the image list.
2475 VERIFY_IS_TRUE(HasImage(image));
2476
2477 // Verify the image is usable by running a container from it.
2478 auto output = RunContainerAndCapture(m_defaultSession, image.c_str(), {});
2479 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
2480 }
2481
2482 // Negative: pull an image that does not exist.
2483 {
2484 auto image = std::format("{}/does-not-exist", registryAddress);
2485
2486 WslcPullImageOptions opts{};
2487 opts.uri = image.c_str();
2488 opts.registryAuth = xRegistryAuth.c_str();
2489
2490 wil::unique_cotaskmem_string errorMsg;
2491 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND);
2492 }
2493
2494 // Negative: null URI inside options must fail.
2495 {
2496 WslcPullImageOptions opts{};
2497 opts.uri = nullptr;
2498
2499 wil::unique_cotaskmem_string errorMsg;
2500 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_INVALIDARG);
2501 }
2502 }
2503
2504 WSLC_TEST_METHOD(PushImage)
2505 {
2506 auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
2507
2508 // Negative: pushing a non-existent image must fail.
2509 {
2510 WslcPushImageOptions opts{};
2511 opts.image = "does-not-exist";
2512 opts.registryAuth = emptyRegistryAuth.c_str();
2513
2514 wil::unique_cotaskmem_string errorMsg;
2515 VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL);
2516 VERIFY_IS_NOT_NULL(errorMsg.get());
2517 }
2518
2519 // Negative: null options must fail.
2520 VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
2521
2522 // Negative: null image inside options must fail.
2523 {
2524 WslcPushImageOptions opts{};
2525 opts.image = nullptr;
2526 opts.registryAuth = emptyRegistryAuth.c_str();
2527
2528 VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2529 }
2530 }
2531
2532 WSLC_TEST_METHOD(TagImage)
2533 {
2534 // Positive: tag an existing image.
2535 {
2536 WslcTagImageOptions opts{};
2537 opts.image = "debian:latest";
2538 opts.repo = "debian";
2539 opts.tag = "sdk-test-tag";
2540 VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &opts, nullptr));
2541
2542 // Verify the tag is present.
2543 VERIFY_IS_TRUE(HasImage("debian:sdk-test-tag"));
2544
2545 // Cleanup: delete the tag.
2546 WslcDeleteSessionImage(m_defaultSession, "debian:sdk-test-tag", nullptr);
2547 }
2548
2549 // Negative: null options must fail.
2550 VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER);
2551
2552 // Negative: null fields must fail.
2553 {
2554 WslcTagImageOptions opts{};
2555 opts.image = nullptr;
2556 opts.repo = "debian";
2557 opts.tag = "test";
2558 VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2559 }
2560 {
2561 WslcTagImageOptions opts{};
2562 opts.image = "debian:latest";
2563 opts.repo = nullptr;
2564 opts.tag = "test";
2565 VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2566 }
2567 {
2568 WslcTagImageOptions opts{};
2569 opts.image = "debian:latest";
2570 opts.repo = "debian";
2571 opts.tag = nullptr;
2572 VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG);
2573 }
2574 }
2575
2576 // Negative tests: handle lifecycle and invalid state transitions
2577
2578 WSLC_TEST_METHOD(ReleaseNullSessionHandle)
2579 {
2580 VERIFY_ARE_EQUAL(WslcReleaseSession(nullptr), E_POINTER);
2581 }
2582
2583 WSLC_TEST_METHOD(TerminateNullSessionHandle)
2584 {
2585 VERIFY_ARE_EQUAL(WslcTerminateSession(nullptr), E_POINTER);
2586 }
2587
2588 WSLC_TEST_METHOD(ReleaseNullContainerHandle)
2589 {
2590 VERIFY_ARE_EQUAL(WslcReleaseContainer(nullptr), E_POINTER);
2591 }
2592
2593 WSLC_TEST_METHOD(ReleaseNullProcessHandle)
2594 {
2595 VERIFY_ARE_EQUAL(WslcReleaseProcess(nullptr), E_POINTER);
2596 }
2597
2598 WSLC_TEST_METHOD(CreateContainerWithNullSession)
2599 {
2600 WslcContainerSettings containerSettings;
2601 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2602
2603 WslcContainer container = nullptr;
2604 VERIFY_ARE_EQUAL(WslcCreateContainer(nullptr, &containerSettings, &container, nullptr), E_POINTER);
2605 }
2606
2607 WSLC_TEST_METHOD(StopContainerWithInvalidSignal)
2608 {
2609 UniqueContainer container;
2610 WslcContainerSettings containerSettings;
2611 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2612
2613 WslcProcessSettings procSettings;
2614 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2615 PCSTR argv[] = {"/bin/sleep", "10"};
2616 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2617 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2618
2619 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2620 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
2621
2622 // Wait for the short-lived init process to exit
2623 UniqueProcess initProcess;
2624 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &initProcess));
2625 HANDLE exitEvent = nullptr;
2626 VERIFY_SUCCEEDED(WslcGetProcessExitEvent(initProcess.get(), &exitEvent));
2627 VERIFY_ARE_EQUAL(WAIT_OBJECT_0, WaitForSingleObject(exitEvent, 30000));
2628
2629 // Attempting to exec on a stopped container should fail
2630 WslcProcessSettings execSettings;
2631 VERIFY_SUCCEEDED(WslcInitProcessSettings(&execSettings));
2632 PCSTR execArgv[] = {"/bin/echo", "should-fail"};
2633 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execSettings, execArgv, ARRAYSIZE(execArgv)));
2634
2635 UniqueProcess execProcess;
2636 VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execSettings, &execProcess, nullptr), WSLC_E_CONTAINER_NOT_RUNNING);
2637 }
2638
2639 WSLC_TEST_METHOD(DuplicateContainerName)
2640 {
2641 WslcContainerSettings containerSettings;
2642 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2643
2644 WslcProcessSettings procSettings;
2645 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2646 PCSTR argv[] = {"/bin/sleep", "10"};
2647 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2648 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2649 VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "duplicate-name-test"));
2650
2651 UniqueContainer container1;
2652 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container1, nullptr));
2653 VERIFY_SUCCEEDED(WslcStartContainer(container1.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2654
2655 // Creating a second container with the same name should fail
2656 UniqueContainer container2;
2657 VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container2, nullptr), static_cast<HRESULT>(0x800700b7)); // ERROR_ALREADY_EXISTS
2658 }
2659
2660 WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce)
2661 {
2662 UniqueContainer container;
2663 WslcContainerSettings containerSettings;
2664 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2665
2666 WslcProcessSettings procSettings;
2667 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2668 PCSTR argv[] = {"/bin/sleep", "10"};
2669 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2670 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2671
2672 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2673 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2674
2675 // Deleting a running container without force flag should fail
2676 VERIFY_ARE_EQUAL(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr), WSLC_E_CONTAINER_IS_RUNNING);
2677 }
2678
2679 WSLC_TEST_METHOD(DeleteNonExistentImage)
2680 {
2681 VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, "nonexistent-image:this-tag-does-not-exist", nullptr), WSLC_E_IMAGE_NOT_FOUND);
2682 }
2683
2684 WSLC_TEST_METHOD(PullInvalidImageUri)
2685 {
2686 WslcPullImageOptions pullOptions = {};
2687 pullOptions.uri = "///invalid-registry-url///";
2688 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &pullOptions, nullptr), E_INVALIDARG);
2689 }
2690
2691 WSLC_TEST_METHOD(ContainerGpu)
2692 {
2693 // Validate that creating a GPU container on a session without GPU support fails.
2694 {
2695 WslcContainerSettings containerSettings;
2696 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2697 VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_ENABLE_GPU));
2698
2699 UniqueContainer container;
2700 VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
2701 }
2702
2703 // Create a GPU-enabled session.
2704 std::filesystem::path gpuStorage = m_storagePath / "wslc-gpu-session-storage";
2705 auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
2706 std::error_code error;
2707 std::filesystem::remove_all(gpuStorage, error);
2708 });
2709
2710 WslcSessionSettings sessionSettings;
2711 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-gpu-test", gpuStorage.c_str(), &sessionSettings));
2712 VERIFY_SUCCEEDED(WslcSetSessionSettingsFeatureFlags(&sessionSettings, WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU));
2713
2714 WslcVhdRequirements vhdReqs{};
2715 vhdReqs.sizeBytes = 4096ull * 1024 * 1024;
2716 vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
2717 VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
2718
2719 UniqueSession gpuSession;
2720 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &gpuSession, nullptr));
2721 THROW_IF_FAILED(WslcLoadSessionImageFromFile(gpuSession.get(), GetTestImagePath("debian:latest").c_str(), nullptr, nullptr));
2722
2723 // Validate /dev/dxg is available and the dynamic linker is configured to resolve the WSL
2724 // GPU libraries.
2725 {
2726 const char* initArgv[] = {
2727 "/bin/sh", "-c", "test -c /dev/dxg && test -r /dev/dxg && test -w /dev/dxg && cat /etc/ld.so.conf.d/ld.wsl.conf"};
2728
2729 auto output = RunContainerAndCapture(
2730 gpuSession.get(), "debian:latest", {initArgv[0], initArgv[1], initArgv[2]}, WSLC_CONTAINER_FLAG_ENABLE_GPU);
2731
2732 VERIFY_ARE_EQUAL(output.stdoutOutput, "/usr/lib/wsl/lib\n");
2733 }
2734 }
2735
2736 WSLC_TEST_METHOD(ContainerAutoRemove)
2737 {
2738 WslcProcessSettings procSettings;
2739 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2740 const char* argv[] = {"/bin/sh", "-c", "echo wslc-auto-remove"};
2741 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2742
2743 WslcContainerSettings containerSettings;
2744 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2745 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2746 VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_AUTO_REMOVE));
2747
2748 UniqueContainer container;
2749 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2750 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
2751
2752 UniqueProcess process;
2753 VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
2754 auto output = WaitForProcessOutput(process.get());
2755 VERIFY_ARE_EQUAL(output.stdoutOutput, "wslc-auto-remove\n");
2756
2757 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
2758 [&]() {
2759 WslcContainerState state{};
2760 THROW_IF_FAILED(WslcGetContainerState(container.get(), &state));
2761 THROW_WIN32_IF(ERROR_RETRY, state != WSLC_CONTAINER_STATE_DELETED);
2762 },
2763 std::chrono::milliseconds{100},
2764 std::chrono::seconds{30}));
2765 }
2766
2767 WSLC_TEST_METHOD(DeleteRunningContainerWithForce)
2768 {
2769 WslcProcessSettings procSettings;
2770 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2771 const char* argv[] = {"/bin/sleep", "99"};
2772 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2773
2774 WslcContainerSettings containerSettings;
2775 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2776 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2777
2778 UniqueContainer container;
2779 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2780 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2781
2782 {
2783 WslcContainerState state{};
2784 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
2785 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
2786 }
2787
2788 VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_FORCE, nullptr));
2789
2790 {
2791 WslcContainerState state{};
2792 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
2793 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_DELETED);
2794 }
2795 }
2796
2797 WSLC_TEST_METHOD(ImageProgressCallback)
2798 {
2799 auto [registryContainer, registryAddress] = StartLocalRegistry();
2800 auto registryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
2801
2802 struct ProgressContext
2803 {
2804 bool invoked = false;
2805 bool sawKnownStatus = false;
2806 };
2807
2808 auto progressCb = [](const WslcImageProgressMessage* progress, PVOID context) -> HRESULT {
2809 auto* ctx = static_cast<ProgressContext*>(context);
2810 ctx->invoked = true;
2811 if (progress != nullptr && progress->status != WSLC_IMAGE_PROGRESS_STATUS_UNKNOWN)
2812 {
2813 ctx->sawKnownStatus = true;
2814 }
2815 return S_OK;
2816 };
2817
2818 const auto registryImage = std::format("{}/hello-world:latest", registryAddress);
2819 const auto registryRepo = std::format("{}/hello-world", registryAddress);
2820
2821 WslcTagImageOptions tagOptions{};
2822 tagOptions.image = "hello-world:latest";
2823 tagOptions.repo = registryRepo.c_str();
2824 tagOptions.tag = "latest";
2825 VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr));
2826
2827 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2828 LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
2829 LOG_IF_FAILED(WslcLoadSessionImageFromFile(m_defaultSession, GetTestImagePath("hello-world:latest").c_str(), nullptr, nullptr));
2830 });
2831
2832 {
2833 ProgressContext ctx;
2834 WslcPushImageOptions opts{};
2835 opts.image = registryImage.c_str();
2836 opts.registryAuth = registryAuth.c_str();
2837 opts.progressCallback = progressCb;
2838 opts.progressCallbackContext = &ctx;
2839
2840 VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &opts, nullptr));
2841 VERIFY_IS_TRUE(ctx.invoked);
2842 }
2843
2844 VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
2845 VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr));
2846
2847 {
2848 ProgressContext ctx;
2849 WslcPullImageOptions opts{};
2850 opts.uri = registryImage.c_str();
2851 opts.registryAuth = registryAuth.c_str();
2852 opts.progressCallback = progressCb;
2853 opts.progressCallbackContext = &ctx;
2854
2855 VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
2856 VERIFY_IS_TRUE(HasImage(registryImage));
2857 VERIFY_IS_TRUE(ctx.sawKnownStatus);
2858 }
2859 }
2860
2861 WSLC_TEST_METHOD(ResourceReuseAfterCleanup)
2862 {
2863 // Each iteration exercises the complete lifecycle: create session → load image → run container
2864 // → terminate session → release session. Running 10 times on the same storage path verifies
2865 // that all resources (VHD locks, image store, container overlays, process handles, VM) are
2866 // fully freed by the time WslcReleaseSession returns, so the next iteration can reuse them
2867 // without errors.
2868 std::filesystem::path sessionStorage = m_storagePath / "wslc-resource-reuse-storage";
2869
2870 auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2871 std::error_code ec;
2872 std::filesystem::remove_all(sessionStorage, ec);
2873 });
2874
2875 WslcSessionSettings sessionSettings;
2876 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-resource-reuse", sessionStorage.c_str(), &sessionSettings));
2877 VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 2));
2878 VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 1024));
2879 VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
2880
2881 WslcVhdRequirements vhdReqs{};
2882 vhdReqs.sizeBytes = 2048ull * 1024 * 1024; // 2 GB
2883 vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
2884 VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
2885
2886 constexpr auto c_imageName = "hello-world:latest";
2887 constexpr int c_iterationCount = 10;
2888
2889 for (int i = 0; i < c_iterationCount; ++i)
2890 {
2891 LogInfo("ResourceReuseAfterCleanup: iteration %d / %d", i + 1, c_iterationCount);
2892
2893 // Create the session, reusing the same storage path every iteration.
2894 UniqueSession session;
2895 VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
2896 VERIFY_IS_NOT_NULL(session.get());
2897
2898 // Load the test image into the session.
2899 VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(session.get(), GetTestImagePath(c_imageName).c_str(), nullptr, nullptr));
2900
2901 // Run a container using the image's default entrypoint and capture its output.
2902 auto output = RunContainerAndCapture(session.get(), c_imageName, {});
2903 VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
2904
2905 // Terminate the session, then release it. WslcReleaseSession must return only after all
2906 // resources are freed so that the next iteration can reopen the same storage without error.
2907 VERIFY_SUCCEEDED(WslcTerminateSession(session.get()));
2908 VERIFY_SUCCEEDED(WslcReleaseSession(session.get()));
2909 session.release(); // explicit calls above
2910 }
2911 }
2912
2913 WSLC_TEST_METHOD(StopContainerTimeout)
2914 {
2915 WslcProcessSettings procSettings;
2916 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
2917 const char* argv[] = {"/bin/sh", "-c", "trap '' TERM; sleep 60"};
2918 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
2919
2920 WslcContainerSettings containerSettings;
2921 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2922 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
2923
2924 UniqueContainer container;
2925 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
2926 VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr));
2927
2928 {
2929 WslcContainerState state{};
2930 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
2931 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
2932 }
2933
2934 constexpr uint32_t c_timeoutSeconds = 1;
2935 const auto start = std::chrono::steady_clock::now();
2936 VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGTERM, c_timeoutSeconds, nullptr));
2937 const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
2938
2939 VERIFY_IS_GREATER_THAN_OR_EQUAL(elapsedMs, 1000LL);
2940 // Ensure it's not because of the sleep running out.
2941 VERIFY_IS_LESS_THAN(elapsedMs, 30000LL);
2942
2943 {
2944 WslcContainerState state{};
2945 VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
2946 VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_EXITED);
2947 }
2948 }
2949
2950 WSLC_TEST_METHOD(OpenContainer)
2951 {
2952 constexpr auto c_containerName = "wslc-sdk-open-test";
2953
2954 WslcContainerSettings containerSettings;
2955 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2956 VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, c_containerName));
2957
2958 UniqueContainer created;
2959 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &created, nullptr));
2960
2961 CHAR createdId[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
2962 VERIFY_SUCCEEDED(WslcGetContainerID(created.get(), createdId));
2963
2964 // Positive: open by name — IDs must match.
2965 {
2966 UniqueContainerReleaseOnly opened;
2967 VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, c_containerName, &opened, nullptr));
2968 VERIFY_IS_NOT_NULL(opened.get());
2969
2970 CHAR openedId[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
2971 VERIFY_SUCCEEDED(WslcGetContainerID(opened.get(), openedId));
2972
2973 VERIFY_ARE_EQUAL(strcmp(createdId, openedId), 0);
2974 }
2975
2976 // Positive: open by full 64-character ID.
2977 {
2978 UniqueContainerReleaseOnly opened;
2979 VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, createdId, &opened, nullptr));
2980 VERIFY_IS_NOT_NULL(opened.get());
2981 }
2982
2983 // Positive: open by partial ID prefix (12 hex chars — standard short ID).
2984 {
2985 createdId[12] = '\0';
2986
2987 UniqueContainerReleaseOnly opened;
2988 VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, createdId, &opened, nullptr));
2989 VERIFY_IS_NOT_NULL(opened.get());
2990 }
2991
2992 // Negative: non-existent name must fail with WSLC_E_CONTAINER_NOT_FOUND.
2993 {
2994 UniqueContainerReleaseOnly opened;
2995 VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, "no-such-container", &opened, nullptr), WSLC_E_CONTAINER_NOT_FOUND);
2996 VERIFY_IS_NULL(opened.get());
2997 }
2998
2999 // Negative: null session must fail with E_POINTER.
3000 {
3001 UniqueContainerReleaseOnly opened;
3002 VERIFY_ARE_EQUAL(WslcOpenContainer(nullptr, c_containerName, &opened, nullptr), E_POINTER);
3003 }
3004
3005 // Negative: null nameOrId must fail with E_POINTER.
3006 {
3007 UniqueContainerReleaseOnly opened;
3008 VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, nullptr, &opened, nullptr), E_POINTER);
3009 }
3010
3011 // Negative: null container output pointer must fail with E_POINTER.
3012 VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, c_containerName, nullptr, nullptr), E_POINTER);
3013 }
3014
3015 WSLC_TEST_METHOD(OpenContainerIOCallbacks)
3016 {
3017 // Verify that IO callbacks installed via WslcSetContainerInitProcessIOCallbacks fire when
3018 // the opened container is started with WSLC_CONTAINER_START_FLAG_ATTACH.
3019 constexpr auto c_containerName = "wslc-sdk-opencb-test";
3020
3021 WslcProcessSettings procSettings;
3022 VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
3023 PCSTR argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"};
3024 VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
3025
3026 WslcContainerSettings containerSettings;
3027 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
3028 VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, c_containerName));
3029 VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
3030
3031 UniqueContainer created;
3032 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &created, nullptr));
3033
3034 UniqueContainerReleaseOnly opened;
3035 VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, c_containerName, &opened, nullptr));
3036
3037 struct Context
3038 {
3039 std::string stdoutData;
3040 std::string stderrData;
3041 wil::unique_event exitEvent{wil::EventOptions::ManualReset};
3042 } ctx;
3043
3044 auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID c) {
3045 auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? static_cast<Context*>(c)->stdoutData
3046 : static_cast<Context*>(c)->stderrData;
3047 target.append(reinterpret_cast<const char*>(data), size);
3048 };
3049
3050 auto exitCb = [](INT32, PVOID c) { static_cast<Context*>(c)->exitEvent.SetEvent(); };
3051
3052 WslcProcessCallbacks callbacks{};
3053 callbacks.onStdOut = ioCb;
3054 callbacks.onStdErr = ioCb;
3055 callbacks.onExit = exitCb;
3056 VERIFY_SUCCEEDED(WslcSetContainerInitProcessIOCallbacks(opened.get(), &callbacks, &ctx));
3057
3058 VERIFY_SUCCEEDED(WslcStartContainer(opened.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
3059
3060 VERIFY_ARE_EQUAL(WaitForSingleObject(ctx.exitEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
3061
3062 // Release the opened handle first to flush all pending IO callbacks before asserting output.
3063 opened.reset();
3064
3065 VERIFY_ARE_EQUAL(ctx.stdoutData, "STDOUT\n");
3066 VERIFY_ARE_EQUAL(ctx.stderrData, "STDERR\n");
3067 }
3068
3069 WSLC_TEST_METHOD(SetContainerInitProcessIOCallbacksNullCallbacks)
3070 {
3071 WslcContainerSettings containerSettings;
3072 VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
3073
3074 UniqueContainer container;
3075 VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
3076
3077 // Null callbacks pointer must fail with E_POINTER.
3078 VERIFY_ARE_EQUAL(WslcSetContainerInitProcessIOCallbacks(container.get(), nullptr, nullptr), E_POINTER);
3079 }
3080 };