1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ WSLCTests.cpp
8
+
9
+Abstract:
10
+
11
+ This file contains test cases for the WSLC API.
12
+
13
+--*/
14
+
15
+#include "precomp.h"
16
+#include "Common.h"
17
+#include "wslc.h"
18
+#include "WSLCProcessLauncher.h"
19
+#include "WSLCContainerLauncher.h"
20
+#include "WslCoreFilesystem.h"
21
+#include <nlohmann/json.hpp>
22
+
23
+using namespace std::literals::chrono_literals;
24
+using namespace wsl::windows::common::registry;
25
+using wsl::windows::common::RunningWSLCContainer;
26
+using wsl::windows::common::RunningWSLCProcess;
27
+using wsl::windows::common::WSLCContainerLauncher;
28
+using wsl::windows::common::WSLCProcessLauncher;
29
+using wsl::windows::common::relay::OverlappedIOHandle;
30
+using wsl::windows::common::relay::WriteHandle;
31
+using namespace wsl::windows::common::wslutil;
32
+
33
+extern std::wstring g_testDataPath;
34
+extern bool g_fastTestRun;
35
+
36
+class WSLCTests
37
+{
38
+ WSLC_TEST_CLASS(WSLCTests)
39
+
40
+ WSADATA m_wsadata;
41
+ std::filesystem::path m_storagePath;
42
+ WSLCSessionSettings m_defaultSessionSettings{};
43
+ wil::com_ptr<IWSLCSession> m_defaultSession;
44
+ static inline auto c_testSessionName = L"wslc-test";
45
+
46
+ void LoadTestImage(std::string_view imageName, IWSLCSession* session = nullptr)
47
+ {
48
+ std::filesystem::path imagePath = GetTestImagePath(imageName);
49
+ wil::unique_hfile imageFile{
50
+ CreateFileW(imagePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
51
+ THROW_LAST_ERROR_IF(!imageFile);
52
+
53
+ LARGE_INTEGER fileSize{};
54
+ THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
55
+
56
+ THROW_IF_FAILED(
57
+ (session ? session : m_defaultSession.get())->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
58
+ }
59
+
60
+ TEST_CLASS_SETUP(TestClassSetup)
61
+ {
62
+ THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
63
+
64
+ // The WSLC SDK tests use this same storage to reduce pull overhead.
65
+ m_storagePath = std::filesystem::current_path() / "test-storage";
66
+ m_defaultSessionSettings = GetDefaultSessionSettings(c_testSessionName, true, WSLCNetworkingModeVirtioProxy);
67
+ m_defaultSession = CreateSession(m_defaultSessionSettings);
68
+
69
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
70
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, &images, images.size_address<ULONG>()));
71
+
72
+ auto hasImage = [&](const std::string& imageName) {
73
+ return std::ranges::any_of(
74
+ images.get(), images.get() + images.size(), [&](const auto& e) { return e.Image == imageName; });
75
+ };
76
+
77
+ if (!hasImage("debian:latest"))
78
+ {
79
+ LoadTestImage("debian:latest");
80
+ }
81
+
82
+ if (!hasImage("python:3.12-alpine"))
83
+ {
84
+ LoadTestImage("python:3.12-alpine");
85
+ }
86
+
87
+ if (!hasImage("hello-world:latest"))
88
+ {
89
+ LoadTestImage("hello-world:latest");
90
+ }
91
+
92
+ if (!hasImage("alpine:latest"))
93
+ {
94
+ LoadTestImage("alpine:latest");
95
+ }
96
+
97
+ if (!hasImage("wslc-registry:latest"))
98
+ {
99
+ LoadTestImage("wslc-registry:latest");
100
+ }
101
+
102
+ PruneResult result;
103
+ VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
104
+ if (result.result.ContainersCount > 0)
105
+ {
106
+ LogInfo("Pruned %lu containers", result.result.ContainersCount);
107
+ }
108
+
109
+ return true;
110
+ }
111
+
112
+ TEST_CLASS_CLEANUP(TestClassCleanup)
113
+ {
114
+ m_defaultSession.reset();
115
+
116
+ // Keep the VHD when running in -f mode, to speed up subsequent test runs.
117
+ if (!g_fastTestRun && !m_storagePath.empty())
118
+ {
119
+ std::error_code error;
120
+ std::filesystem::remove_all(m_storagePath, error);
121
+ if (error)
122
+ {
123
+ LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str());
124
+ }
125
+ }
126
+
127
+ return true;
128
+ }
129
+
130
+ WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR Name, bool enableStorage = false, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
131
+ {
132
+ WSLCSessionSettings settings{};
133
+ settings.DisplayName = Name;
134
+ settings.CpuCount = 4;
135
+ settings.MemoryMb = 2048;
136
+ settings.BootTimeoutMs = 30 * 1000;
137
+ settings.StoragePath = enableStorage ? m_storagePath.c_str() : nullptr;
138
+ settings.MaximumStorageSizeMb = 1024 * 20; // 20GB.
139
+ settings.NetworkingMode = networkingMode;
140
+
141
+ return settings;
142
+ }
143
+
144
+ auto ResetTestSession()
145
+ {
146
+ m_defaultSession.reset();
147
+
148
+ return wil::scope_exit([this]() { m_defaultSession = CreateSession(m_defaultSessionSettings); });
149
+ }
150
+
151
+ static wil::com_ptr<IWSLCSessionManager> OpenSessionManager()
152
+ {
153
+ wil::com_ptr<IWSLCSessionManager> sessionManager;
154
+ VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
155
+ wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
156
+
157
+ return sessionManager;
158
+ }
159
+
160
+ wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
161
+ {
162
+ const auto sessionManager = OpenSessionManager();
163
+
164
+ wil::com_ptr<IWSLCSession> session;
165
+
166
+ VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, &session));
167
+ wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
168
+
169
+ WSLCSessionState state{};
170
+ VERIFY_SUCCEEDED(session->GetState(&state));
171
+ VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
172
+
173
+ return session;
174
+ }
175
+
176
+ RunningWSLCContainer OpenContainer(IWSLCSession* session, const std::string& name)
177
+ {
178
+ wil::com_ptr<IWSLCContainer> rawContainer;
179
+ VERIFY_SUCCEEDED(session->OpenContainer(name.c_str(), &rawContainer));
180
+
181
+ return RunningWSLCContainer(std::move(rawContainer), {});
182
+ }
183
+
184
+ std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, USHORT port = 5000)
185
+ {
186
+ std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
187
+ if (!username.empty())
188
+ {
189
+ env.push_back(std::format("USERNAME={}", username));
190
+ env.push_back(std::format("PASSWORD={}", password));
191
+ }
192
+
193
+ WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
194
+ launcher.SetEntrypoint({"/entrypoint.sh"});
195
+ launcher.AddPort(port, port, AF_INET);
196
+
197
+ auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
198
+
199
+ auto registryAddress = std::format("127.0.0.1:{}", port);
200
+ auto registryUrl = std::format(L"http://{}", registryAddress);
201
+ ExpectHttpResponse(registryUrl.c_str(), 200, true);
202
+
203
+ return {std::move(container), std::move(registryAddress)};
204
+ }
205
+
206
+ std::string PushImageToRegistry(const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth)
207
+ {
208
+ auto [repo, tag] = ParseImage(imageName);
209
+ auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest"));
210
+ auto registryRepo = std::format("{}/{}", registryAddress, repo);
211
+ auto registryTag = tag.value_or("latest");
212
+
213
+ WSLCTagImageOptions tagOptions{};
214
+ tagOptions.Image = imageName.c_str();
215
+ tagOptions.Repo = registryRepo.c_str();
216
+ tagOptions.Tag = registryTag.c_str();
217
+
218
+ // Tag the image with the registry address so it can be pushed.
219
+ VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
220
+
221
+ // Ensures the tag is removed to allow tests to try to push or pull the same image again.
222
+ auto cleanup = wil::scope_exit_log(
223
+ WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsNone).first); });
224
+
225
+ VERIFY_SUCCEEDED(m_defaultSession->PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr));
226
+
227
+ return registryImage;
228
+ }
229
+
230
+ WSLC_TEST_METHOD(GetVersion)
231
+ {
232
+ wil::com_ptr<IWSLCSessionManager> sessionManager;
233
+ VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
234
+
235
+ WSLCVersion version{};
236
+
237
+ VERIFY_SUCCEEDED(sessionManager->GetVersion(&version));
238
+
239
+ VERIFY_ARE_EQUAL(version.Major, WSL_PACKAGE_VERSION_MAJOR);
240
+ VERIFY_ARE_EQUAL(version.Minor, WSL_PACKAGE_VERSION_MINOR);
241
+ VERIFY_ARE_EQUAL(version.Revision, WSL_PACKAGE_VERSION_REVISION);
242
+ }
243
+
244
+ static RunningWSLCProcess::ProcessResult RunCommand(IWSLCSession* session, const std::vector<std::string>& command, int timeout = 600000)
245
+ {
246
+ WSLCProcessLauncher process(command[0], command);
247
+
248
+ return process.Launch(*session).WaitAndCaptureOutput(timeout);
249
+ }
250
+
251
+ static RunningWSLCProcess::ProcessResult ExpectCommandResult(
252
+ IWSLCSession* session, const std::vector<std::string>& command, int expectResult, int timeout = 600000)
253
+ {
254
+ auto result = RunCommand(session, command, timeout);
255
+
256
+ if (result.Code != expectResult)
257
+ {
258
+ auto cmd = wsl::shared::string::Join(command, ' ');
259
+ LogError(
260
+ "Command: %hs didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
261
+ cmd.c_str(),
262
+ expectResult,
263
+ result.Code,
264
+ result.Output[1].c_str(),
265
+ result.Output[2].c_str());
266
+ }
267
+
268
+ return result;
269
+ }
270
+
271
+ void ValidateProcessOutput(RunningWSLCProcess& process, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD Timeout = INFINITE)
272
+ {
273
+ auto result = process.WaitAndCaptureOutput(Timeout);
274
+
275
+ if (result.Code != expectedResult)
276
+ {
277
+ LogError(
278
+ "Command didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
279
+ expectedResult,
280
+ result.Code,
281
+ EscapeString(result.Output[1]).c_str(),
282
+ EscapeString(result.Output[2]).c_str());
283
+
284
+ return;
285
+ }
286
+
287
+ for (const auto& [fd, expected] : expectedOutput)
288
+ {
289
+ auto it = result.Output.find(fd);
290
+ if (it == result.Output.end())
291
+ {
292
+ LogError("Expected output on fd %i, but none found.", fd);
293
+ return;
294
+ }
295
+
296
+ if (it->second != expected)
297
+ {
298
+ LogError(
299
+ "Unexpected output on fd %i. Expected: '%hs', Actual: '%hs'",
300
+ fd,
301
+ EscapeString(expected).c_str(),
302
+ EscapeString(it->second).c_str());
303
+
304
+ return;
305
+ }
306
+ }
307
+ }
308
+
309
+ void ValidateContainerOutput(RunningWSLCContainer& container, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD timeout = INFINITE)
310
+ {
311
+ auto initProcess = container.GetInitProcess();
312
+ ValidateProcessOutput(initProcess, expectedOutput, expectedResult, timeout);
313
+ }
314
+
315
+ void ValidateContainerOutput(WSLCContainerLauncher& launcher, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD timeout = INFINITE)
316
+ {
317
+ auto container = launcher.Launch(*m_defaultSession);
318
+ ValidateContainerOutput(container, expectedOutput, expectedResult, timeout);
319
+ }
320
+
321
+ void ExpectMount(IWSLCSession* session, const std::string& target, const std::optional<std::string>& options)
322
+ {
323
+ auto cmd = std::format("set -o pipefail ; findmnt '{}' | tail -n 1", target);
324
+ auto result = ExpectCommandResult(session, {"/bin/sh", "-c", cmd}, options.has_value() ? 0 : 1);
325
+
326
+ const auto& output = result.Output[1];
327
+ const auto& error = result.Output[2];
328
+
329
+ if (result.Code != (options.has_value() ? 0 : 1))
330
+ {
331
+ LogError("%hs failed. code=%i, output: %hs, error: %hs", cmd.c_str(), result.Code, output.c_str(), error.c_str());
332
+ VERIFY_FAIL();
333
+ }
334
+
335
+ if (options.has_value() && !PathMatchSpecA(output.c_str(), options->c_str()))
336
+ {
337
+ std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", output, options.value());
338
+ VERIFY_FAIL(message.c_str());
339
+ }
340
+ }
341
+
342
+ WSLC_TEST_METHOD(ListSessionsReturnsSessionWithDisplayName)
343
+ {
344
+ auto sessionManager = OpenSessionManager();
345
+
346
+ // Act: list sessions
347
+ {
348
+ wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
349
+ VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
350
+
351
+ // Assert
352
+ VERIFY_ARE_EQUAL(sessions.size(), 1u);
353
+ const auto& info = sessions[0];
354
+
355
+ // SessionId is implementation detail (starts at 1), so we only assert DisplayName here.
356
+ VERIFY_ARE_EQUAL(std::wstring(info.DisplayName), c_testSessionName);
357
+ }
358
+
359
+ // List multiple sessions.
360
+ {
361
+ auto session2 = CreateSession(GetDefaultSessionSettings(L"wslc-test-list-2"));
362
+
363
+ wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
364
+ VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
365
+
366
+ VERIFY_ARE_EQUAL(sessions.size(), 2);
367
+
368
+ std::vector<std::wstring> displayNames;
369
+ for (const auto& e : sessions)
370
+ {
371
+ displayNames.push_back(e.DisplayName);
372
+ }
373
+
374
+ std::ranges::sort(displayNames);
375
+
376
+ VERIFY_ARE_EQUAL(displayNames[0], c_testSessionName);
377
+ VERIFY_ARE_EQUAL(displayNames[1], L"wslc-test-list-2");
378
+ }
379
+ }
380
+
381
+ WSLC_TEST_METHOD(OpenSessionByNameFindsExistingSession)
382
+ {
383
+ auto sessionManager = OpenSessionManager();
384
+
385
+ // Act: open by the same display name
386
+ wil::com_ptr<IWSLCSession> opened;
387
+ VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(c_testSessionName, &opened));
388
+ VERIFY_IS_NOT_NULL(opened.get());
389
+
390
+ // And verify we get ERROR_NOT_FOUND for a nonexistent name
391
+ wil::com_ptr<IWSLCSession> notFound;
392
+ auto hr = sessionManager->OpenSessionByName(L"this-name-does-not-exist", ¬Found);
393
+ VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
394
+ }
395
+
396
+ WSLC_TEST_METHOD(CreateSessionValidation)
397
+ {
398
+ auto sessionManager = OpenSessionManager();
399
+
400
+ // Reject NULL DisplayName.
401
+ {
402
+ auto settings = GetDefaultSessionSettings(nullptr);
403
+ wil::com_ptr<IWSLCSession> session;
404
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
405
+ }
406
+
407
+ // Reject DisplayName at exact boundary (no room for null terminator).
408
+ {
409
+ std::wstring boundaryName(std::size(WSLCSessionInformation{}.DisplayName), L'x');
410
+ auto settings = GetDefaultSessionSettings(boundaryName.c_str());
411
+ wil::com_ptr<IWSLCSession> session;
412
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
413
+ }
414
+
415
+ // Reject too long DisplayName.
416
+ {
417
+ std::wstring longName(std::size(WSLCSessionInformation{}.DisplayName) + 1, L'x');
418
+ auto settings = GetDefaultSessionSettings(longName.c_str());
419
+ wil::com_ptr<IWSLCSession> session;
420
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
421
+ }
422
+
423
+ // Validate that creating a session on a non-existing storage fails if WSLCSessionStorageFlagsNoCreate is set.
424
+ {
425
+ auto settings = GetDefaultSessionSettings(L"storage-not-found");
426
+ settings.StoragePath = L"C:\\does-not-exist";
427
+ settings.StorageFlags = WSLCSessionStorageFlagsNoCreate;
428
+ wil::com_ptr<IWSLCSession> session;
429
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
430
+ }
431
+
432
+ // Reject invalid storage flags.
433
+ {
434
+ auto settings = GetDefaultSessionSettings(L"invalid-storage-flags");
435
+ settings.StorageFlags = static_cast<WSLCSessionStorageFlags>(0x2);
436
+ wil::com_ptr<IWSLCSession> session;
437
+ VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
438
+ }
439
+ }
440
+
441
+ struct VmInfo
442
+ {
443
+ std::wstring Id;
444
+ std::wstring Owner;
445
+ };
446
+
447
+ // Returns VM info (Id + Owner) for all running VMs via hcsdiag.
448
+ static std::vector<VmInfo> ListVms()
449
+ {
450
+ wsl::windows::common::SubProcess process(nullptr, L"hcsdiag list -raw");
451
+ auto output = process.RunAndCaptureOutput(10000);
452
+
453
+ std::vector<VmInfo> vms;
454
+ auto json = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(output.Stdout), nullptr, false);
455
+ if (!json.is_array())
456
+ {
457
+ return vms;
458
+ }
459
+
460
+ for (const auto& entry : json)
461
+ {
462
+ if (entry.contains("Owner") && entry["Owner"].is_string() && entry.contains("Id") && entry["Id"].is_string())
463
+ {
464
+ vms.push_back(
465
+ {wsl::shared::string::MultiByteToWide(entry["Id"].get<std::string>()),
466
+ wsl::shared::string::MultiByteToWide(entry["Owner"].get<std::string>())});
467
+ }
468
+ }
469
+
470
+ return vms;
471
+ }
472
+
473
+ WSLC_TEST_METHOD(VmOwnerMatchesSessionDisplayName)
474
+ {
475
+ // The default session (c_testSessionName) is already running from class setup.
476
+ // Verify its display name appears as a VM owner in hcsdiag output.
477
+ auto vms = ListVms();
478
+
479
+ auto found = std::ranges::find_if(vms, [](const auto& vm) { return vm.Owner == c_testSessionName; });
480
+ if (found == vms.end())
481
+ {
482
+ LogError("Expected VM owner '%ws' not found. Owners:", c_testSessionName);
483
+ for (const auto& vm : vms)
484
+ {
485
+ LogError(" '%ws'", vm.Owner.c_str());
486
+ }
487
+
488
+ VERIFY_FAIL();
489
+ }
490
+ }
491
+
492
+ void ExpectImagePresent(IWSLCSession& Session, const char* Image, bool Present = true)
493
+ {
494
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
495
+ THROW_IF_FAILED(Session.ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
496
+
497
+ std::vector<std::string> tags;
498
+ for (const auto& e : images)
499
+ {
500
+ tags.push_back(e.Image);
501
+ }
502
+
503
+ auto found = std::ranges::find(tags, Image) != tags.end();
504
+ if (Present != found)
505
+ {
506
+ LogError("Image presence check failed for image: %hs, images: %hs", Image, wsl::shared::string::Join(tags, ',').c_str());
507
+ VERIFY_FAIL();
508
+ }
509
+ }
510
+
511
+ std::pair<HRESULT, wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation>> DeleteImageNoThrow(const std::string& Image, DWORD Flags)
512
+ {
513
+ WSLCDeleteImageOptions options{};
514
+ options.Image = Image.c_str();
515
+ options.Flags = Flags;
516
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
517
+ auto hr = m_defaultSession->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>());
518
+ return {hr, std::move(deletedImages)};
519
+ }
520
+
521
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> DeleteImage(const std::string& Image, DWORD Flags)
522
+ {
523
+ auto [hr, deletedImages] = DeleteImageNoThrow(Image, Flags);
524
+ VERIFY_SUCCEEDED(hr);
525
+
526
+ return std::move(deletedImages);
527
+ }
528
+
529
+ WSLC_TEST_METHOD(PullImage)
530
+ {
531
+ {
532
+ // Start a local registry without auth and push hello-world:latest to it.
533
+ auto [registryContainer, registryAddress] = StartLocalRegistry();
534
+
535
+ auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", ""));
536
+ ExpectImagePresent(*m_defaultSession, image.c_str(), false);
537
+
538
+ VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr));
539
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(image, WSLCDeleteImageFlagsForce).first); });
540
+
541
+ // Verify that the image is in the list of images.
542
+ ExpectImagePresent(*m_defaultSession, image.c_str());
543
+ WSLCContainerLauncher launcher(image, "wslc-pull-image-container");
544
+
545
+ auto container = launcher.Launch(*m_defaultSession);
546
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
547
+
548
+ VERIFY_ARE_EQUAL(0, result.Code);
549
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
550
+ }
551
+
552
+ {
553
+ std::wstring expectedError =
554
+ L"pull access denied for does-not, repository does not exist or may require 'docker login': denied: requested "
555
+ L"access to the resource is denied";
556
+
557
+ VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND);
558
+ ValidateCOMErrorMessage(expectedError.c_str());
559
+ }
560
+
561
+ // Validate that PullImage() returns the appropriate error if the session is terminated.
562
+ {
563
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
564
+
565
+ auto cleanup = wil::scope_exit([&]() {
566
+ ResetTestSession(); // Reopen the test session since the session was terminated.
567
+ });
568
+
569
+ VERIFY_ARE_EQUAL(m_defaultSession->PullImage("hello-world:linux", nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
570
+ }
571
+ }
572
+
573
+ WSLC_TEST_METHOD(PullImageAdvanced)
574
+ {
575
+ // Start a local registry without auth to avoid Docker Hub rate limits.
576
+ auto [registryContainer, registryAddress] = StartLocalRegistry();
577
+ auto auth = BuildRegistryAuthHeader("", "");
578
+
579
+ auto validatePull = [&](const std::string& sourceImage) {
580
+ // Push the source image to the local registry.
581
+ auto registryImage = PushImageToRegistry(sourceImage, registryAddress, auth);
582
+ ExpectImagePresent(*m_defaultSession, registryImage.c_str(), false);
583
+
584
+ VERIFY_SUCCEEDED(m_defaultSession->PullImage(registryImage.c_str(), nullptr, nullptr));
585
+
586
+ auto cleanup =
587
+ wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsForce).first); });
588
+
589
+ ExpectImagePresent(*m_defaultSession, registryImage.c_str());
590
+ };
591
+
592
+ validatePull("debian:latest");
593
+ validatePull("alpine:latest");
594
+ validatePull("hello-world:latest");
595
+ }
596
+
597
+ WSLC_TEST_METHOD(PullImageFromDockerHub)
598
+ {
599
+ SKIP_TEST_UNSTABLE();
600
+
601
+ auto validatePull = [&](const std::string& Image, const std::optional<std::string>& ExpectedTag = {}) {
602
+ VERIFY_SUCCEEDED(m_defaultSession->PullImage(Image.c_str(), nullptr, nullptr));
603
+
604
+ auto cleanup = wil::scope_exit(
605
+ [&]() { LOG_IF_FAILED(DeleteImageNoThrow(ExpectedTag.value_or(Image), WSLCDeleteImageFlagsForce).first); });
606
+
607
+ if (!ExpectedTag.has_value())
608
+ {
609
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
610
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
611
+
612
+ for (const auto& e : images)
613
+ {
614
+ wil::unique_cotaskmem_ansistring json;
615
+ VERIFY_SUCCEEDED(m_defaultSession->InspectImage(e.Hash, &json));
616
+
617
+ auto parsed = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(json.get());
618
+
619
+ for (const auto& repoTag : parsed.RepoDigests.value_or({}))
620
+ {
621
+ if (Image == repoTag)
622
+ {
623
+ return;
624
+ }
625
+ }
626
+ }
627
+
628
+ LogError("Expected digest '%hs' not found ", Image.c_str());
629
+
630
+ VERIFY_FAIL();
631
+ }
632
+ else
633
+ {
634
+ ExpectImagePresent(*m_defaultSession, ExpectedTag->c_str());
635
+ }
636
+ };
637
+
638
+ validatePull("ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", {});
639
+ validatePull("ubuntu", "ubuntu:latest");
640
+ validatePull("debian:bookworm", "debian:bookworm");
641
+ validatePull("pytorch/pytorch", "pytorch/pytorch:latest");
642
+ validatePull("registry.k8s.io/pause:3.2", "registry.k8s.io/pause:3.2");
643
+
644
+ // Validate that PullImage() fails appropriately when the session runs out of space.
645
+ {
646
+ auto settings = GetDefaultSessionSettings(L"wslc-pull-image-out-of-space", false);
647
+ settings.NetworkingMode = WSLCNetworkingModeVirtioProxy;
648
+ settings.MemoryMb = 1024;
649
+ auto session = CreateSession(settings);
650
+
651
+ VERIFY_ARE_EQUAL(session->PullImage("pytorch/pytorch", nullptr, nullptr), E_FAIL);
652
+
653
+ ValidateCOMErrorMessageContains(L"no space left on device");
654
+ }
655
+ }
656
+
657
+ WSLC_TEST_METHOD(PushImage)
658
+ {
659
+ auto emptyAuth = BuildRegistryAuthHeader("", "");
660
+
661
+ // Validate that pushing a non-existent image fails.
662
+ {
663
+ VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", emptyAuth.c_str(), nullptr), E_FAIL);
664
+ ValidateCOMErrorMessage(L"An image does not exist locally with the tag: does-not-exist");
665
+ }
666
+
667
+ // Validate passing empty auth string returns an appropriate error.
668
+ {
669
+ VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", "", nullptr), E_INVALIDARG);
670
+ }
671
+
672
+ // Validate that PushImage() returns the appropriate error if the session is terminated.
673
+ {
674
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
675
+ auto cleanup = wil::scope_exit([&]() { ResetTestSession(); });
676
+
677
+ VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", emptyAuth.c_str(), nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
678
+ }
679
+ }
680
+
681
+ WSLC_TEST_METHOD(Authenticate)
682
+ {
683
+ constexpr auto c_username = "wslctest";
684
+ constexpr auto c_password = "password";
685
+
686
+ auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
687
+
688
+ wil::unique_cotaskmem_ansistring token;
689
+ VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, "wrong-password", &token), E_FAIL);
690
+ ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized");
691
+
692
+ VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token));
693
+ VERIFY_IS_NOT_NULL(token.get());
694
+
695
+ auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password);
696
+ auto image = PushImageToRegistry("hello-world:latest", registryAddress, xRegistryAuth);
697
+
698
+ // Pulling without credentials should fail.
699
+ VERIFY_ARE_EQUAL(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr), E_FAIL);
700
+ ValidateCOMErrorMessageContains(L"no basic auth credentials");
701
+
702
+ // Pulling with credentials should succeed.
703
+ VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), xRegistryAuth.c_str(), nullptr));
704
+ ExpectImagePresent(*m_defaultSession, image.c_str());
705
+ }
706
+
707
+ WSLC_TEST_METHOD(ListImages)
708
+ {
709
+ // Setup: Ensure debian:latest is available
710
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
711
+
712
+ // Create additional tags for testing
713
+ WSLCTagImageOptions tagOptions{};
714
+ tagOptions.Image = "debian:latest";
715
+ tagOptions.Repo = "debian";
716
+ tagOptions.Tag = "test-tag1";
717
+ VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
718
+ tagOptions.Tag = "test-tag2";
719
+ VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
720
+
721
+ auto cleanup = wil::scope_exit([&]() {
722
+ LOG_IF_FAILED(DeleteImageNoThrow("debian:test-tag1", WSLCDeleteImageFlagsNone).first);
723
+ LOG_IF_FAILED(DeleteImageNoThrow("debian:test-tag2", WSLCDeleteImageFlagsNone).first);
724
+ });
725
+
726
+ LogInfo("Test: Basic listing with nullptr options");
727
+ {
728
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
729
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
730
+
731
+ VERIFY_IS_TRUE(images.size() > 0);
732
+
733
+ // Find debian images and verify they exist
734
+ bool foundLatest = false, foundTag1 = false, foundTag2 = false;
735
+ for (const auto& image : images)
736
+ {
737
+ std::string imageName = image.Image;
738
+ if (imageName == "debian:latest")
739
+ {
740
+ foundLatest = true;
741
+ }
742
+ if (imageName == "debian:test-tag1")
743
+ {
744
+ foundTag1 = true;
745
+ }
746
+ if (imageName == "debian:test-tag2")
747
+ {
748
+ foundTag2 = true;
749
+ }
750
+ }
751
+
752
+ VERIFY_IS_TRUE(foundLatest);
753
+ VERIFY_IS_TRUE(foundTag1);
754
+ VERIFY_IS_TRUE(foundTag2);
755
+ }
756
+
757
+ LogInfo("Test: Verify all fields are populated");
758
+ {
759
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
760
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
761
+
762
+ std::string commonHash;
763
+ int debianTagCount = 0;
764
+
765
+ for (const auto& image : images)
766
+ {
767
+ std::string imageName = image.Image;
768
+ if (imageName.starts_with("debian:"))
769
+ {
770
+ debianTagCount++;
771
+
772
+ // Verify Hash field
773
+ VERIFY_IS_TRUE(strlen(image.Hash) > 0);
774
+ VERIFY_IS_TRUE(std::string(image.Hash).starts_with("sha256:"));
775
+
776
+ // All debian tags should have the same hash (same underlying image)
777
+ if (commonHash.empty())
778
+ {
779
+ commonHash = image.Hash;
780
+ }
781
+ else
782
+ {
783
+ VERIFY_ARE_EQUAL(commonHash, std::string(image.Hash));
784
+ }
785
+
786
+ // Verify Size field
787
+ VERIFY_IS_TRUE(image.Size > 0);
788
+
789
+ // Verify Created timestamp
790
+ VERIFY_IS_TRUE(image.Created > 0);
791
+ }
792
+ }
793
+
794
+ VERIFY_IS_TRUE(debianTagCount >= 3); // At least debian:latest, test-tag1, test-tag2
795
+ }
796
+
797
+ LogInfo("Test: Multiple tags for same image return separate entries");
798
+ {
799
+ WSLCListImageOptions options{};
800
+ options.Flags = WSLCListImagesFlagsNone;
801
+ options.Reference = "debian";
802
+
803
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
804
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
805
+
806
+ // Should find at least our 3 debian tags
807
+ VERIFY_IS_TRUE(images.size() >= 3);
808
+
809
+ // Verify each tag is a separate entry
810
+ std::set<std::string> imageTags;
811
+ for (const auto& image : images)
812
+ {
813
+ imageTags.insert(image.Image);
814
+ }
815
+
816
+ VERIFY_IS_TRUE(imageTags.contains("debian:latest"));
817
+ VERIFY_IS_TRUE(imageTags.contains("debian:test-tag1"));
818
+ VERIFY_IS_TRUE(imageTags.contains("debian:test-tag2"));
819
+ }
820
+
821
+ LogInfo("Test: Filter by specific reference");
822
+ {
823
+ WSLCListImageOptions options{};
824
+ options.Flags = WSLCListImagesFlagsNone;
825
+ options.Reference = "debian:test-tag1";
826
+
827
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
828
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
829
+
830
+ // When filtering by exact tag, Docker returns all tags for that image
831
+ // So we should get debian:latest, debian:test-tag1, debian:test-tag2
832
+ bool foundTag1 = false;
833
+ for (const auto& image : images)
834
+ {
835
+ std::string imageName = image.Image;
836
+ if (imageName == "debian:test-tag1")
837
+ {
838
+ foundTag1 = true;
839
+ }
840
+ }
841
+ VERIFY_IS_TRUE(foundTag1);
842
+ }
843
+
844
+ LogInfo("Test: Digests flag");
845
+ {
846
+ WSLCListImageOptions options{};
847
+ options.Flags = WSLCListImagesFlagsDigests;
848
+ options.Reference = "debian:latest";
849
+
850
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
851
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
852
+
853
+ // Check if digests are available (they may not be for all images)
854
+ bool hasDigest = false;
855
+ for (const auto& image : images)
856
+ {
857
+ if (strlen(image.Digest) > 0)
858
+ {
859
+ hasDigest = true;
860
+ // Digest should be in format repo@sha256:...
861
+ VERIFY_IS_TRUE(std::string(image.Digest).find("@sha256:") != std::string::npos);
862
+ }
863
+ }
864
+ // Note: Pulled images from registry should have digests, locally built may not
865
+ }
866
+
867
+ LogInfo("Test: Before/Since filters");
868
+ {
869
+ // Get all images to find their IDs
870
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> allImages;
871
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, allImages.addressof(), allImages.size_address<ULONG>()));
872
+
873
+ std::string debianId, pythonId;
874
+ for (const auto& image : allImages)
875
+ {
876
+ std::string imageName = image.Image;
877
+ if (imageName == "debian:latest")
878
+ {
879
+ debianId = image.Hash;
880
+ }
881
+ else if (imageName == "python:3.12-alpine")
882
+ {
883
+ pythonId = image.Hash;
884
+ }
885
+ }
886
+
887
+ VERIFY_IS_FALSE(debianId.empty());
888
+ VERIFY_IS_FALSE(pythonId.empty());
889
+
890
+ // Test 'since' filter - images created after debian
891
+ {
892
+ WSLCListImageOptions options{};
893
+ options.Flags = WSLCListImagesFlagsNone;
894
+ options.Since = debianId.c_str();
895
+
896
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
897
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
898
+ VERIFY_IS_TRUE(images.size() > 0);
899
+
900
+ bool foundPython = false;
901
+ for (const auto& image : images)
902
+ {
903
+ LogInfo("Image: %hs, Hash: %hs, Created: %lld", image.Image, image.Hash, image.Created);
904
+ if (std::string{image.Image} == "python:3.12-alpine")
905
+ {
906
+ foundPython = true;
907
+ }
908
+ }
909
+
910
+ VERIFY_IS_TRUE(foundPython);
911
+ }
912
+
913
+ // Test 'before' filter - images created before python
914
+ {
915
+ WSLCListImageOptions options{};
916
+ options.Flags = WSLCListImagesFlagsNone;
917
+ options.Before = pythonId.c_str();
918
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
919
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
920
+ VERIFY_IS_TRUE(images.size() > 0);
921
+
922
+ bool foundDebian = false;
923
+ for (const auto& image : images)
924
+ {
925
+ if (std::string{image.Image} == "debian:latest")
926
+ {
927
+ foundDebian = true;
928
+ }
929
+ }
930
+
931
+ VERIFY_IS_TRUE(foundDebian);
932
+ }
933
+ }
934
+
935
+ LogInfo("Test: Dangling filter");
936
+ {
937
+ // Setup a dangling image
938
+ LoadTestImage("alpine:latest");
939
+ WSLCTagImageOptions tagOptions{};
940
+ tagOptions.Image = "debian:latest";
941
+ tagOptions.Repo = "alpine";
942
+ tagOptions.Tag = "latest";
943
+ VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
944
+
945
+ auto alpineCleanup = wil::scope_exit([&]() {
946
+ RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "image", "prune", "-f"});
947
+ LOG_IF_FAILED(DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsNone).first);
948
+ });
949
+
950
+ // List only dangling images
951
+ WSLCListImageOptions options{};
952
+ options.Flags = WSLCListImagesFlagsDanglingTrue;
953
+
954
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> danglingImages;
955
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, danglingImages.addressof(), danglingImages.size_address<ULONG>()));
956
+
957
+ VERIFY_ARE_EQUAL(1, danglingImages.size());
958
+
959
+ // All dangling images should have <none>:<none> as the tag
960
+ for (const auto& image : danglingImages)
961
+ {
962
+ std::string imageName = image.Image;
963
+ VERIFY_ARE_EQUAL(imageName, std::string("<none>:<none>"));
964
+ }
965
+
966
+ // List non-dangling images
967
+ options.Flags = WSLCListImagesFlagsDanglingFalse;
968
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> nonDanglingImages;
969
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, nonDanglingImages.addressof(), nonDanglingImages.size_address<ULONG>()));
970
+ VERIFY_IS_TRUE(nonDanglingImages.size() > 0);
971
+
972
+ // None of these should be <none>:<none>
973
+ for (const auto& image : nonDanglingImages)
974
+ {
975
+ std::string imageName = image.Image;
976
+ VERIFY_ARE_NOT_EQUAL(imageName, std::string("<none>:<none>"));
977
+ }
978
+ }
979
+
980
+ LogInfo("Test: Label filter");
981
+ {
982
+ // Test with nullptr (no label filter)
983
+ WSLCListImageOptions options{};
984
+ options.Flags = WSLCListImagesFlagsNone;
985
+ options.Labels = nullptr;
986
+ options.LabelsCount = 0;
987
+
988
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
989
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
990
+
991
+ // Test with single label filter
992
+ {
993
+ WSLCLabel labels[] = {{.Key = "test.label", .Value = nullptr}};
994
+ options.Labels = labels;
995
+ options.LabelsCount = 1;
996
+
997
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
998
+ }
999
+
1000
+ // Test with multiple label filters (labels are AND'ed together)
1001
+ {
1002
+ WSLCLabel labels[] = {{.Key = "test.label1", .Value = nullptr}, {.Key = "test.label2", .Value = "value"}};
1003
+ options.Labels = labels;
1004
+ options.LabelsCount = 2;
1005
+
1006
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
1007
+ }
1008
+
1009
+ // Note: To fully test label filtering with actual matches, would need to:
1010
+ // 1. Build an image with specific labels using docker build --label
1011
+ // 2. Filter with matching labels
1012
+ // 3. Verify the filtered image appears
1013
+ // This only tests the API usage not fail without requiring image builds
1014
+ }
1015
+
1016
+ cleanup.reset();
1017
+ ExpectImagePresent(*m_defaultSession, "debian:test-tag1", false);
1018
+ ExpectImagePresent(*m_defaultSession, "debian:test-tag2", false);
1019
+ ExpectImagePresent(*m_defaultSession, "debian:latest", true);
1020
+ }
1021
+
1022
+ WSLC_TEST_METHOD(LoadImage)
1023
+ {
1024
+ // This test case is hanging on Windows Server SKUs. Skip the test until the issue is resolved.
1025
+ // TODO: Remove once the fix is available.
1026
+ if (IsWindowsServer())
1027
+ {
1028
+ SKIP_TEST_UNSTABLE();
1029
+ }
1030
+
1031
+ std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
1032
+ wil::unique_handle imageTarFileHandle{
1033
+ CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1034
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
1035
+
1036
+ LARGE_INTEGER fileSize{};
1037
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1038
+
1039
+ VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
1040
+
1041
+ // Verify that the image is in the list of images.
1042
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest");
1043
+
1044
+ // Validate container launch from the loaded image
1045
+ {
1046
+ WSLCContainerLauncher launcher("hello-world:latest", "wslc-load-image-container");
1047
+
1048
+ auto container = launcher.Launch(*m_defaultSession);
1049
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1050
+
1051
+ VERIFY_ARE_EQUAL(0, result.Code);
1052
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
1053
+ }
1054
+
1055
+ // Validate that invalid tars fail with proper error message and code.
1056
+ {
1057
+ auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
1058
+ VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
1059
+
1060
+ VERIFY_ARE_EQUAL(m_defaultSession->LoadImage(ToCOMInputHandle(currentExecutableHandle.get()), nullptr, fileSize.QuadPart), E_FAIL);
1061
+
1062
+ ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
1063
+ }
1064
+
1065
+ // Validate that LoadImage fails when the input pipe is closed during reading.
1066
+ {
1067
+ wil::unique_handle pipeRead;
1068
+ wil::unique_handle pipeWrite;
1069
+ VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1070
+
1071
+ std::promise<HRESULT> loadResult;
1072
+ std::thread operationThread([&]() {
1073
+ loadResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024));
1074
+ });
1075
+
1076
+ auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1077
+
1078
+ // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes).
1079
+ DWORD bytesWritten{};
1080
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1081
+
1082
+ // Close the write end.
1083
+ pipeWrite.reset();
1084
+
1085
+ VERIFY_ARE_EQUAL(E_FAIL, loadResult.get_future().get());
1086
+ }
1087
+
1088
+ // Validate that LoadImage is aborted when the session terminates.
1089
+ {
1090
+ wil::unique_handle pipeRead;
1091
+ wil::unique_handle pipeWrite;
1092
+ VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1093
+
1094
+ std::promise<HRESULT> terminateResult;
1095
+ wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1096
+ std::thread operationThread([&]() {
1097
+ terminateResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024));
1098
+ WI_ASSERT(testCompleted.is_signaled());
1099
+ });
1100
+
1101
+ auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1102
+
1103
+ // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes).
1104
+ DWORD bytesWritten{};
1105
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1106
+
1107
+ testCompleted.SetEvent();
1108
+
1109
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
1110
+
1111
+ auto restore = ResetTestSession();
1112
+
1113
+ auto hr = terminateResult.get_future().get();
1114
+ VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED));
1115
+ }
1116
+ }
1117
+
1118
+ WSLC_TEST_METHOD(ImportImage)
1119
+ {
1120
+ auto cleanup =
1121
+ wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow("my-hello-world:test", WSLCDeleteImageFlagsNone).first); });
1122
+
1123
+ std::filesystem::path imageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
1124
+ wil::unique_handle imageTarFileHandle{
1125
+ CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1126
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
1127
+
1128
+ LARGE_INTEGER fileSize{};
1129
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1130
+
1131
+ VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
1132
+ ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart));
1133
+
1134
+ ExpectImagePresent(*m_defaultSession, "my-hello-world:test");
1135
+
1136
+ // Validate that containers can be started from the imported image.
1137
+ {
1138
+ WSLCContainerLauncher launcher("my-hello-world:test", "wslc-import-image-container", {"/hello"});
1139
+
1140
+ auto container = launcher.Launch(*m_defaultSession);
1141
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1142
+
1143
+ VERIFY_ARE_EQUAL(0, result.Code);
1144
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
1145
+ }
1146
+
1147
+ // Validate that ImportImage fails if no tag is passed
1148
+ {
1149
+ VERIFY_ARE_EQUAL(
1150
+ m_defaultSession->ImportImage(ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart),
1151
+ E_INVALIDARG);
1152
+ }
1153
+
1154
+ // Validate that invalid tars fail with proper error message and code.
1155
+ {
1156
+ auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
1157
+
1158
+ VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
1159
+
1160
+ VERIFY_ARE_EQUAL(
1161
+ m_defaultSession->ImportImage(
1162
+ ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart),
1163
+ E_FAIL);
1164
+
1165
+ ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
1166
+ }
1167
+
1168
+ // Validate that ImportImage fails when the input pipe is closed during reading.
1169
+ {
1170
+ wil::unique_handle pipeRead;
1171
+ wil::unique_handle pipeWrite;
1172
+ VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1173
+
1174
+ std::promise<HRESULT> importResult;
1175
+ std::thread operationThread([&]() {
1176
+ importResult.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024));
1177
+ });
1178
+
1179
+ auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1180
+
1181
+ // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes).
1182
+ DWORD bytesWritten{};
1183
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1184
+
1185
+ // Close the write end.
1186
+ pipeWrite.reset();
1187
+
1188
+ VERIFY_ARE_EQUAL(E_FAIL, importResult.get_future().get());
1189
+ }
1190
+
1191
+ // Validate that ImportImage is aborted when the session terminates.
1192
+ {
1193
+ wil::unique_handle pipeRead;
1194
+ wil::unique_handle pipeWrite;
1195
+ VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
1196
+
1197
+ std::promise<HRESULT> terminateResult;
1198
+ wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1199
+ std::thread operationThread([&]() {
1200
+ terminateResult.set_value(
1201
+ m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024));
1202
+ WI_ASSERT(testCompleted.is_signaled());
1203
+ });
1204
+
1205
+ auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
1206
+
1207
+ // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes).
1208
+ DWORD bytesWritten{};
1209
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
1210
+
1211
+ testCompleted.SetEvent();
1212
+
1213
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
1214
+
1215
+ auto restore = ResetTestSession();
1216
+
1217
+ auto hr = terminateResult.get_future().get();
1218
+ VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED));
1219
+ }
1220
+ }
1221
+
1222
+ WSLC_TEST_METHOD(DeleteImage)
1223
+ {
1224
+ // Prepare alpine image to delete.
1225
+ LoadTestImage("alpine:latest");
1226
+
1227
+ // Verify that the image is in the list of images.
1228
+ ExpectImagePresent(*m_defaultSession, "alpine:latest");
1229
+
1230
+ // Launch a container to ensure that image deletion fails when in use.
1231
+ WSLCContainerLauncher launcher(
1232
+ "alpine:latest", "test-delete-container-in-use", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
1233
+
1234
+ auto container = launcher.Launch(*m_defaultSession);
1235
+
1236
+ // Verify that the container is in running state.
1237
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
1238
+
1239
+ // Test delete failed if image in use.
1240
+ VERIFY_ARE_EQUAL(
1241
+ HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION), DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsNone).first);
1242
+
1243
+ // Force should succeed.
1244
+ auto deletedImages = DeleteImage("alpine:latest", WSLCDeleteImageFlagsForce);
1245
+ VERIFY_IS_TRUE(deletedImages.size() > 0);
1246
+ VERIFY_IS_TRUE(std::strlen(deletedImages[0].Image) > 0);
1247
+
1248
+ // Verify that the image is no longer in the list of images.
1249
+ ExpectImagePresent(*m_defaultSession, "alpine:latest", false);
1250
+
1251
+ // Test delete failed if image does not exist.
1252
+ VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, DeleteImageNoThrow("alpine:latest", WSLCDeleteImageFlagsForce).first);
1253
+
1254
+ // Validate that invalid flags are rejected.
1255
+ {
1256
+ WSLCDeleteImageOptions invalidOptions{.Image = "alpine:latest", .Flags = 0x4};
1257
+ VERIFY_ARE_EQUAL(
1258
+ m_defaultSession->DeleteImage(&invalidOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()), E_INVALIDARG);
1259
+ }
1260
+ }
1261
+
1262
+ void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected, const std::source_location& Source = std::source_location::current())
1263
+ {
1264
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1265
+
1266
+ if (comError.has_value())
1267
+ {
1268
+ if (!Expected.has_value())
1269
+ {
1270
+ LogError("Unexpected COM error: '%ls'. Source: %hs", comError->Message.get(), std::format("{}", Source).c_str());
1271
+ VERIFY_FAIL();
1272
+ }
1273
+
1274
+ VERIFY_ARE_EQUAL(Expected.value(), comError->Message.get());
1275
+ }
1276
+ else
1277
+ {
1278
+ if (Expected.has_value())
1279
+ {
1280
+ LogError("Expected COM error: '%ls' but none was set. Source: %hs", Expected->c_str(), std::format("{}", Source).c_str());
1281
+ VERIFY_FAIL();
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring)
1287
+ {
1288
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1289
+
1290
+ if (comError.has_value())
1291
+ {
1292
+ if (!comError->Message)
1293
+ {
1294
+ LogError("Expected COM error containing: '%ls', but COM error message was null", ExpectedSubstring.c_str());
1295
+ VERIFY_FAIL();
1296
+ }
1297
+
1298
+ if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr)
1299
+ {
1300
+ LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get());
1301
+ VERIFY_FAIL();
1302
+ }
1303
+ }
1304
+ else
1305
+ {
1306
+ LogError("Expected COM error containing: '%ls' but none was set", ExpectedSubstring.c_str());
1307
+ VERIFY_FAIL();
1308
+ }
1309
+ }
1310
+
1311
+ class CapturingProgressCallback
1312
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1313
+ {
1314
+ public:
1315
+ CapturingProgressCallback(std::string& output) : m_output(output)
1316
+ {
1317
+ }
1318
+
1319
+ HRESULT OnProgress(LPCSTR status, LPCSTR, ULONGLONG, ULONGLONG) override
1320
+ {
1321
+ m_output.append(status);
1322
+ return S_OK;
1323
+ }
1324
+
1325
+ private:
1326
+ std::string& m_output;
1327
+ };
1328
+
1329
+ HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const WSLCBuildImageOptions* options, IProgressCallback* callback = nullptr)
1330
+ {
1331
+ auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1332
+
1333
+ auto contextPathStr = contextDir.wstring();
1334
+ WSLCBuildImageOptions optionsCopy = *options;
1335
+ optionsCopy.ContextPath = contextPathStr.c_str();
1336
+ optionsCopy.DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get());
1337
+
1338
+ auto buildResult = m_defaultSession->BuildImage(&optionsCopy, callback, nullptr);
1339
+
1340
+ if (FAILED(buildResult))
1341
+ {
1342
+ LogInfo("BuildImage failed: 0x%08x", buildResult);
1343
+ }
1344
+
1345
+ return buildResult;
1346
+ }
1347
+
1348
+ HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const char* imageTag)
1349
+ {
1350
+ LPCSTR tag = imageTag;
1351
+ WSLCBuildImageOptions options{
1352
+ .Tags = {&tag, 1},
1353
+ };
1354
+ return BuildImageFromContext(contextDir, &options);
1355
+ }
1356
+
1357
+ WSLC_TEST_METHOD(BuildImage)
1358
+ {
1359
+ auto contextDir = std::filesystem::current_path() / "build-context";
1360
+ std::filesystem::create_directories(contextDir);
1361
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1362
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build:latest", WSLCDeleteImageFlagsForce).first);
1363
+
1364
+ std::error_code ec;
1365
+ std::filesystem::remove_all(contextDir, ec);
1366
+ });
1367
+
1368
+ {
1369
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1370
+ dockerfile << "FROM debian:latest\n";
1371
+ dockerfile << "CMD [\"echo\", \"Hello from a WSL container!\"]\n";
1372
+ }
1373
+
1374
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
1375
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
1376
+
1377
+ WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-build-test-container");
1378
+ auto container = launcher.Launch(*m_defaultSession);
1379
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1380
+
1381
+ VERIFY_ARE_EQUAL(0, result.Code);
1382
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container!") != std::string::npos);
1383
+ }
1384
+
1385
+ // This test validates both that we can build an image with an empty CMD, and that we can run such an image.
1386
+ WSLC_TEST_METHOD(BuildImageEntrypoint)
1387
+ {
1388
+ auto contextDir = std::filesystem::current_path() / "build-context-entrypoint";
1389
+ std::filesystem::create_directories(contextDir);
1390
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1391
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-entrypoint:latest", WSLCDeleteImageFlagsForce).first);
1392
+
1393
+ std::error_code ec;
1394
+ std::filesystem::remove_all(contextDir, ec);
1395
+ });
1396
+
1397
+ {
1398
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1399
+ dockerfile << "FROM debian:latest\n";
1400
+ dockerfile << "CMD []\n";
1401
+ dockerfile << "ENTRYPOINT [\"/bin/echo\", \"Entrypoint\"]\n";
1402
+ }
1403
+
1404
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-entrypoint:latest"));
1405
+ ExpectImagePresent(*m_defaultSession, "wslc-test-entrypoint:latest");
1406
+
1407
+ // Validate that the entrypoint is started by default.
1408
+ {
1409
+ WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-1");
1410
+ auto container = launcher.Launch(*m_defaultSession);
1411
+ auto initProcess = container.GetInitProcess();
1412
+ ValidateProcessOutput(initProcess, {{1, "Entrypoint\n"}});
1413
+ }
1414
+
1415
+ // Validate that arguments are passed to the entrypoint, and don't override it.
1416
+ {
1417
+ WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-2", {"extra-arg"});
1418
+ auto container = launcher.Launch(*m_defaultSession);
1419
+ auto initProcess = container.GetInitProcess();
1420
+ ValidateProcessOutput(initProcess, {{1, "Entrypoint extra-arg\n"}});
1421
+ }
1422
+
1423
+ // Validate that the entrypoint can be overridden.
1424
+ {
1425
+ WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-3");
1426
+ launcher.SetEntrypoint({"/bin/echo", "OverriddenEntrypoint"});
1427
+ auto container = launcher.Launch(*m_defaultSession);
1428
+ auto initProcess = container.GetInitProcess();
1429
+ ValidateProcessOutput(initProcess, {{1, "OverriddenEntrypoint\n"}});
1430
+ }
1431
+
1432
+ // Validate that the entrypoint can be overridden and that CMD args are passed to the entrypoint.
1433
+ {
1434
+ WSLCContainerLauncher launcher("wslc-test-entrypoint:latest", "wslc-entrypoint-test-4", {"extra-arg"});
1435
+ launcher.SetEntrypoint({"/bin/echo", "OverriddenEntrypoint"});
1436
+ auto container = launcher.Launch(*m_defaultSession);
1437
+ auto initProcess = container.GetInitProcess();
1438
+ ValidateProcessOutput(initProcess, {{1, "OverriddenEntrypoint extra-arg\n"}});
1439
+ }
1440
+ }
1441
+
1442
+ WSLC_TEST_METHOD(BuildImageWithContext)
1443
+ {
1444
+ auto contextDir = std::filesystem::current_path() / "build-context-file";
1445
+ std::filesystem::create_directories(contextDir);
1446
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1447
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-context:latest", WSLCDeleteImageFlagsForce).first);
1448
+
1449
+ std::error_code ec;
1450
+ std::filesystem::remove_all(contextDir, ec);
1451
+ });
1452
+
1453
+ {
1454
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1455
+ dockerfile << "FROM debian:latest\n";
1456
+ dockerfile << "COPY message.txt /message.txt\n";
1457
+ dockerfile << "CMD [\"cat\", \"/message.txt\"]\n";
1458
+ }
1459
+
1460
+ {
1461
+ std::ofstream message(contextDir / "message.txt");
1462
+ message << "Hello from a WSL container context file!\n";
1463
+ }
1464
+
1465
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-context:latest"));
1466
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-context:latest");
1467
+
1468
+ WSLCContainerLauncher launcher("wslc-test-build-context:latest", "wslc-build-context-container");
1469
+ auto container = launcher.Launch(*m_defaultSession);
1470
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1471
+
1472
+ VERIFY_ARE_EQUAL(0, result.Code);
1473
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container context file!") != std::string::npos);
1474
+ }
1475
+
1476
+ WSLC_TEST_METHOD(BuildImageManyFiles)
1477
+ {
1478
+ static constexpr int fileCount = 1024;
1479
+
1480
+ auto contextDir = std::filesystem::current_path() / "build-context-many";
1481
+ std::filesystem::create_directories(contextDir / "files");
1482
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1483
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-many:latest", WSLCDeleteImageFlagsForce).first);
1484
+
1485
+ std::error_code ec;
1486
+ std::filesystem::remove_all(contextDir, ec);
1487
+ });
1488
+
1489
+ // Generate the context files.
1490
+ for (int i = 0; i < fileCount; i++)
1491
+ {
1492
+ auto name = std::format("file{:04d}.txt", i);
1493
+ auto content = std::format("content-{:04d}\n", i);
1494
+ std::ofstream file(contextDir / "files" / name);
1495
+ file << content;
1496
+ }
1497
+
1498
+ {
1499
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1500
+ dockerfile << "FROM debian:latest\n";
1501
+ dockerfile << "COPY files/ /files/\n";
1502
+ // Verify every file is present and contains the expected content.
1503
+ // Only mismatches are printed; on success just the sentinel.
1504
+ dockerfile << "CMD [\"sh\", \"-c\", "
1505
+ << "\"cd /files && failed=0 && "
1506
+ << "for i in $(seq 0 " << (fileCount - 1) << "); do "
1507
+ << "f=$(printf 'file%04d.txt' $i); "
1508
+ << "e=$(printf 'content-%04d' $i); "
1509
+ << "if [ ! -f $f ]; then echo MISSING:$f; failed=1; "
1510
+ << "elif ! grep -q $e $f; then echo BAD:$f; failed=1; fi; "
1511
+ << "done && "
1512
+ << "[ $failed -eq 0 ] && echo all_ok_" << fileCount << "\"]\n";
1513
+ }
1514
+
1515
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-many:latest"));
1516
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-many:latest");
1517
+
1518
+ WSLCContainerLauncher launcher("wslc-test-build-many:latest", "wslc-build-many-container");
1519
+ auto container = launcher.Launch(*m_defaultSession);
1520
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1521
+
1522
+ VERIFY_ARE_EQUAL(0, result.Code);
1523
+ auto sentinel = std::format("all_ok_{}", fileCount);
1524
+ VERIFY_IS_TRUE(result.Output[1].find(sentinel) != std::string::npos);
1525
+ }
1526
+
1527
+ WSLC_TEST_METHOD(BuildImageLargeFile)
1528
+ {
1529
+ RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rmi", "-f", "wslc-test-build-large:latest"});
1530
+ ExpectCommandResult(m_defaultSession.get(), {"/usr/bin/docker", "builder", "prune", "-f"}, 0);
1531
+
1532
+ auto contextDir = std::filesystem::current_path() / "build-context-large";
1533
+ std::filesystem::create_directories(contextDir);
1534
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1535
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-large:latest", WSLCDeleteImageFlagsForce).first);
1536
+
1537
+ std::error_code ec;
1538
+ std::filesystem::remove_all(contextDir, ec);
1539
+ });
1540
+
1541
+ static constexpr int fileSizeMb = 1024;
1542
+
1543
+ {
1544
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1545
+ dockerfile << "FROM debian:latest\n";
1546
+ dockerfile << "COPY large.bin /large.bin\n";
1547
+ dockerfile << std::format(
1548
+ "CMD [\"sh\", \"-c\", \"test $(stat -c %s /large.bin) -eq {} && echo size_ok\"]\n",
1549
+ static_cast<long long>(fileSizeMb) * 1024 * 1024);
1550
+ }
1551
+
1552
+ {
1553
+ auto largePath = contextDir / "large.bin";
1554
+ wil::unique_hfile largeFile{CreateFileW(largePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
1555
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == largeFile.get());
1556
+
1557
+ std::vector<char> buffer(1024 * 1024, '\0');
1558
+ for (int i = 0; i < fileSizeMb; i++)
1559
+ {
1560
+ DWORD written = 0;
1561
+ if (!WriteFile(largeFile.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &written, nullptr) ||
1562
+ written != static_cast<DWORD>(buffer.size()))
1563
+ {
1564
+ LogError("WriteFile failed at chunk %d/%d: 0x%08x", i, fileSizeMb, GetLastError());
1565
+ VERIFY_FAIL();
1566
+ }
1567
+ }
1568
+ }
1569
+
1570
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-large:latest"));
1571
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-large:latest");
1572
+
1573
+ WSLCContainerLauncher launcher("wslc-test-build-large:latest", "wslc-build-large-container");
1574
+ auto container = launcher.Launch(*m_defaultSession);
1575
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1576
+
1577
+ VERIFY_ARE_EQUAL(0, result.Code);
1578
+ VERIFY_IS_TRUE(result.Output[1].find("size_ok") != std::string::npos);
1579
+ }
1580
+
1581
+ WSLC_TEST_METHOD(BuildImageMultiStage)
1582
+ {
1583
+ auto contextDir = std::filesystem::current_path() / "build-context-multistage";
1584
+ std::filesystem::create_directories(contextDir);
1585
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1586
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-multistage:latest", WSLCDeleteImageFlagsForce).first);
1587
+
1588
+ std::error_code ec;
1589
+ std::filesystem::remove_all(contextDir, ec);
1590
+ });
1591
+
1592
+ {
1593
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1594
+ // Two independent stages that can build in parallel, each producing
1595
+ // part of the final output. The last stage combines them.
1596
+ dockerfile << "FROM debian:latest AS greeting\n";
1597
+ dockerfile << "RUN echo -n 'WSL containers' | tee /part.txt\n";
1598
+ dockerfile << "\n";
1599
+ dockerfile << "FROM debian:latest AS description\n";
1600
+ dockerfile << "RUN echo -n 'support multi-stage builds' | tee /part.txt\n";
1601
+ dockerfile << "\n";
1602
+ dockerfile << "FROM debian:latest\n";
1603
+ dockerfile << "COPY --from=greeting /part.txt /greeting.txt\n";
1604
+ dockerfile << "COPY --from=description /part.txt /description.txt\n";
1605
+ dockerfile << "CMD [\"sh\", \"-c\", "
1606
+ << "\"echo \\\"$(cat /greeting.txt) $(cat /description.txt)\\\"\"]\n";
1607
+ }
1608
+
1609
+ std::string output;
1610
+ auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1611
+ LPCSTR tag = "wslc-test-build-multistage:latest";
1612
+ WSLCBuildImageOptions options{.Tags = {&tag, 1}, .Flags = WSLCBuildImageFlagsNoCache};
1613
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1614
+ VERIFY_IS_TRUE(output.find("[greeting] WSL containers") != std::string::npos);
1615
+ VERIFY_IS_TRUE(output.find("[description] support multi-stage builds") != std::string::npos);
1616
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-multistage:latest");
1617
+
1618
+ WSLCContainerLauncher launcher("wslc-test-build-multistage:latest", "wslc-build-multistage-container");
1619
+ auto container = launcher.Launch(*m_defaultSession);
1620
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1621
+
1622
+ VERIFY_ARE_EQUAL(0, result.Code);
1623
+ VERIFY_IS_TRUE(result.Output[1].find("WSL containers support multi-stage builds") != std::string::npos);
1624
+ }
1625
+
1626
+ WSLC_TEST_METHOD(BuildImageDockerIgnore)
1627
+ {
1628
+ auto contextDir = std::filesystem::current_path() / "build-context-dockerignore";
1629
+ std::filesystem::create_directories(contextDir / "temp");
1630
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1631
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-dockerignore:latest", WSLCDeleteImageFlagsForce).first);
1632
+
1633
+ std::error_code ec;
1634
+ std::filesystem::remove_all(contextDir, ec);
1635
+ });
1636
+
1637
+ {
1638
+ std::ofstream ignore(contextDir / ".dockerignore");
1639
+ ignore << "# Ignore log files and temp directory\n";
1640
+ ignore << "*.log\n";
1641
+ ignore << "temp/\n";
1642
+ }
1643
+
1644
+ {
1645
+ std::ofstream(contextDir / "keep.txt") << "kept\n";
1646
+ std::ofstream(contextDir / "debug.log") << "excluded\n";
1647
+ std::ofstream(contextDir / "temp" / "cache.dat") << "excluded\n";
1648
+ }
1649
+
1650
+ {
1651
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1652
+ dockerfile << "FROM debian:latest\n";
1653
+ dockerfile << "COPY . /ctx/\n";
1654
+ dockerfile << "CMD [\"sh\", \"-c\", "
1655
+ << "\"test -f /ctx/keep.txt "
1656
+ << "&& ! test -f /ctx/debug.log "
1657
+ << "&& ! test -d /ctx/temp "
1658
+ << "&& echo dockerignore_ok\"]\n";
1659
+ }
1660
+
1661
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-dockerignore:latest"));
1662
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-dockerignore:latest");
1663
+
1664
+ WSLCContainerLauncher launcher("wslc-test-build-dockerignore:latest", "wslc-build-dockerignore-container");
1665
+ auto container = launcher.Launch(*m_defaultSession);
1666
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1667
+
1668
+ VERIFY_ARE_EQUAL(0, result.Code);
1669
+ VERIFY_IS_TRUE(result.Output[1].find("dockerignore_ok") != std::string::npos);
1670
+ }
1671
+
1672
+ WSLC_TEST_METHOD(BuildImageFailure)
1673
+ {
1674
+ auto contextDir = std::filesystem::current_path() / "build-context-failure";
1675
+ std::filesystem::create_directories(contextDir);
1676
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1677
+ std::error_code ec;
1678
+ std::filesystem::remove_all(contextDir, ec);
1679
+ });
1680
+
1681
+ {
1682
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1683
+ dockerfile << "FROM does-not-exist:invalid\n";
1684
+ }
1685
+
1686
+ VERIFY_FAILED(BuildImageFromContext(contextDir, "wslc-test-build-failure:latest"));
1687
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1688
+ VERIFY_IS_TRUE(comError.has_value());
1689
+ LogInfo("Expected build error: %ls", comError->Message.get());
1690
+
1691
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-failure:latest", false);
1692
+ }
1693
+
1694
+ WSLC_TEST_METHOD(BuildImageFailureShowsBuildOutput)
1695
+ {
1696
+ auto contextDir = std::filesystem::current_path() / "build-context-failure-output";
1697
+ std::filesystem::create_directories(contextDir);
1698
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1699
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-args:latest", WSLCDeleteImageFlagsForce).first);
1700
+
1701
+ std::error_code ec;
1702
+ std::filesystem::remove_all(contextDir, ec);
1703
+ });
1704
+
1705
+ {
1706
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1707
+ dockerfile << "FROM debian:latest\n";
1708
+ dockerfile << "RUN echo 'build-log-marker' && /bin/false\n";
1709
+ }
1710
+
1711
+ class ProgressAccumulator
1712
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1713
+ {
1714
+ public:
1715
+ ProgressAccumulator(std::string& output) : m_output(output)
1716
+ {
1717
+ }
1718
+ HRESULT OnProgress(LPCSTR message, LPCSTR, ULONGLONG, ULONGLONG) override
1719
+ {
1720
+ if (message)
1721
+ {
1722
+ m_output.append(message);
1723
+ }
1724
+ return S_OK;
1725
+ }
1726
+
1727
+ private:
1728
+ std::string& m_output;
1729
+ };
1730
+
1731
+ std::string progressOutput;
1732
+ auto callback = Microsoft::WRL::Make<ProgressAccumulator>(progressOutput);
1733
+
1734
+ auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1735
+ auto contextPathStr = contextDir.wstring();
1736
+ LPCSTR tag = "wslc-test-build-failure-output:latest";
1737
+ WSLCBuildImageOptions options{
1738
+ .ContextPath = contextPathStr.c_str(),
1739
+ .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()),
1740
+ .Tags = {&tag, 1},
1741
+ };
1742
+
1743
+ VERIFY_FAILED(m_defaultSession->BuildImage(&options, callback.Get(), nullptr));
1744
+ VERIFY_IS_TRUE(progressOutput.find("build-log-marker") != std::string::npos);
1745
+ }
1746
+
1747
+ WSLC_TEST_METHOD(BuildImageStdinDockerfile)
1748
+ {
1749
+ auto contextDir = std::filesystem::current_path() / "build-context-stdin";
1750
+ std::filesystem::create_directories(contextDir);
1751
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1752
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-stdin:latest", WSLCDeleteImageFlagsForce).first);
1753
+
1754
+ std::error_code ec;
1755
+ std::filesystem::remove_all(contextDir, ec);
1756
+ });
1757
+
1758
+ auto dockerfileContent = "FROM debian:latest\nCMD [\"echo\", \"stdin-dockerfile-ok\"]\n";
1759
+
1760
+ wil::unique_hfile readHandle;
1761
+ wil::unique_hfile writeHandle;
1762
+ THROW_IF_WIN32_BOOL_FALSE(CreatePipe(readHandle.addressof(), writeHandle.addressof(), nullptr, 0));
1763
+
1764
+ DWORD bytesWritten;
1765
+ THROW_IF_WIN32_BOOL_FALSE(
1766
+ WriteFile(writeHandle.get(), dockerfileContent, static_cast<DWORD>(strlen(dockerfileContent)), &bytesWritten, nullptr));
1767
+ writeHandle.reset();
1768
+
1769
+ auto contextPathStr = contextDir.wstring();
1770
+ LPCSTR tag = "wslc-test-build-stdin:latest";
1771
+ WSLCBuildImageOptions options{
1772
+ .ContextPath = contextPathStr.c_str(),
1773
+ .DockerfileHandle = ToCOMInputHandle(readHandle.get()),
1774
+ .Tags = {&tag, 1},
1775
+ };
1776
+ VERIFY_SUCCEEDED(m_defaultSession->BuildImage(&options, nullptr, nullptr));
1777
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-stdin:latest");
1778
+
1779
+ WSLCContainerLauncher launcher("wslc-test-build-stdin:latest", "wslc-build-stdin-container");
1780
+ auto container = launcher.Launch(*m_defaultSession);
1781
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
1782
+
1783
+ VERIFY_ARE_EQUAL(0, result.Code);
1784
+ VERIFY_IS_TRUE(result.Output[1].find("stdin-dockerfile-ok") != std::string::npos);
1785
+ }
1786
+
1787
+ WSLC_TEST_METHOD(BuildImageBuildArgs)
1788
+ {
1789
+ auto contextDir = std::filesystem::current_path() / "build-context-buildargs";
1790
+ std::filesystem::create_directories(contextDir);
1791
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1792
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build-args:latest", WSLCDeleteImageFlagsForce).first);
1793
+
1794
+ std::error_code ec;
1795
+ std::filesystem::remove_all(contextDir, ec);
1796
+ });
1797
+
1798
+ {
1799
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1800
+ dockerfile << "FROM debian:latest\n";
1801
+ dockerfile << "ARG TEST_VALUE\n";
1802
+ dockerfile << "ENV TEST_VALUE=${TEST_VALUE}\n";
1803
+ dockerfile << "CMD echo \"build-arg-value=${TEST_VALUE}\"\n";
1804
+ }
1805
+
1806
+ LPCSTR tag = "wslc-test-build-args:latest";
1807
+ LPCSTR buildArg = "TEST_VALUE=hello-from-build-arg";
1808
+ WSLCBuildImageOptions options{.Tags = {&tag, 1}, .BuildArgs = {&buildArg, 1}};
1809
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
1810
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build-args:latest");
1811
+
1812
+ WSLCContainerLauncher launcher("wslc-test-build-args:latest", "wslc-build-args-container");
1813
+ auto container = launcher.Launch(*m_defaultSession);
1814
+ auto initProcess = container.GetInitProcess();
1815
+ ValidateProcessOutput(initProcess, {{1, "build-arg-value=hello-from-build-arg\n"}});
1816
+ }
1817
+
1818
+ WSLC_TEST_METHOD(BuildImageMultipleTags)
1819
+ {
1820
+ auto contextDir = std::filesystem::current_path() / "build-context-multitag";
1821
+ std::filesystem::create_directories(contextDir);
1822
+ LPCSTR tags[] = {"wslc-test-multitag:v1", "wslc-test-multitag:v2"};
1823
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1824
+ for (auto* tag : tags)
1825
+ {
1826
+ LOG_IF_FAILED(DeleteImageNoThrow(tag, WSLCDeleteImageFlagsForce).first);
1827
+ }
1828
+
1829
+ std::error_code ec;
1830
+ std::filesystem::remove_all(contextDir, ec);
1831
+ });
1832
+
1833
+ {
1834
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1835
+ dockerfile << "FROM debian:latest\n";
1836
+ dockerfile << "CMD [\"echo\", \"multi-tag-ok\"]\n";
1837
+ }
1838
+ WSLCBuildImageOptions options{.Tags = {tags, 2}};
1839
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
1840
+ ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v1");
1841
+ ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v2");
1842
+ }
1843
+
1844
+ WSLC_TEST_METHOD(BuildImageNullHandle)
1845
+ {
1846
+ WSLCBuildImageOptions options{.ContextPath = L"C:\\", .DockerfileHandle = {}, .Tags = {nullptr, 0}};
1847
+
1848
+ VERIFY_ARE_EQUAL(m_defaultSession->BuildImage(&options, nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE));
1849
+ }
1850
+
1851
+ WSLC_TEST_METHOD(BuildImageCancel)
1852
+ {
1853
+ class TestProgressCallback
1854
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1855
+ {
1856
+ public:
1857
+ TestProgressCallback(wil::unique_event& event) : m_event(event)
1858
+ {
1859
+ }
1860
+
1861
+ HRESULT OnProgress(LPCSTR, LPCSTR, ULONGLONG, ULONGLONG) override
1862
+ {
1863
+ m_event.SetEvent();
1864
+ return S_OK;
1865
+ }
1866
+
1867
+ private:
1868
+ wil::unique_event& m_event;
1869
+ };
1870
+
1871
+ auto contextDir = std::filesystem::current_path() / "build-context-cancel";
1872
+ std::filesystem::create_directories(contextDir);
1873
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1874
+ std::error_code ec;
1875
+ std::filesystem::remove_all(contextDir, ec);
1876
+ });
1877
+
1878
+ // Use a Dockerfile that takes a long time to build so we can cancel it mid-build.
1879
+ {
1880
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1881
+ dockerfile << "FROM debian:latest\n";
1882
+ dockerfile << "RUN sleep 120\n";
1883
+ }
1884
+
1885
+ wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
1886
+ wil::unique_event progressEvent{wil::EventOptions::ManualReset};
1887
+
1888
+ // Use a progress callback to detect when the build is actively running
1889
+ // before signaling cancellation, avoiding a racy Sleep().
1890
+ auto callback = Microsoft::WRL::Make<TestProgressCallback>(progressEvent);
1891
+
1892
+ auto contextPathStr = contextDir.wstring();
1893
+ auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
1894
+
1895
+ LPCSTR tag = "wslc-test-build-cancel:latest";
1896
+ WSLCBuildImageOptions options{
1897
+ .ContextPath = contextPathStr.c_str(), .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()), .Tags = {&tag, 1}};
1898
+
1899
+ std::promise<HRESULT> result;
1900
+ std::thread buildThread(
1901
+ [&]() { result.set_value(m_defaultSession->BuildImage(&options, callback.Get(), cancelEvent.get())); });
1902
+
1903
+ auto joinThread = wil::scope_exit([&]() { buildThread.join(); });
1904
+
1905
+ VERIFY_IS_TRUE(progressEvent.wait(60 * 1000));
1906
+ cancelEvent.SetEvent();
1907
+
1908
+ VERIFY_ARE_EQUAL(E_ABORT, result.get_future().get());
1909
+ }
1910
+
1911
+ WSLC_TEST_METHOD(BuildImageNoCache)
1912
+ {
1913
+ auto contextDir = std::filesystem::current_path() / "build-context-nocache";
1914
+ std::filesystem::create_directories(contextDir);
1915
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1916
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-nocache:latest", WSLCDeleteImageFlagsForce).first);
1917
+
1918
+ std::error_code ec;
1919
+ std::filesystem::remove_all(contextDir, ec);
1920
+ });
1921
+
1922
+ {
1923
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1924
+ dockerfile << "FROM debian:latest\n";
1925
+ dockerfile << "RUN echo -n Image && echo -n is && echo -n rebuilt\n";
1926
+ }
1927
+
1928
+ // First build to populate cache.
1929
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-nocache:latest"));
1930
+
1931
+ // Validate that the image isn't rebuilt when NoCache isn't set.
1932
+ {
1933
+ std::string output;
1934
+ auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1935
+ LPCSTR tag = "wslc-test-nocache:latest";
1936
+ WSLCBuildImageOptions options{.Tags = {&tag, 1}};
1937
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1938
+ VERIFY_IS_TRUE(output.find("Imageisrebuilt") == std::string::npos);
1939
+ }
1940
+
1941
+ // Validate that the image is rebuilt when WSLCBuildImageFlagsNoCache is set, and that the output from the RUN step appears in the progress callback.
1942
+ {
1943
+ std::string output;
1944
+ auto callback = Microsoft::WRL::Make<CapturingProgressCallback>(output);
1945
+ LPCSTR tag = "wslc-test-nocache:latest";
1946
+ WSLCBuildImageOptions options{.Tags = {&tag, 1}, .Flags = WSLCBuildImageFlagsNoCache};
1947
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options, callback.Get()));
1948
+ VERIFY_IS_TRUE(output.find("Imageisrebuilt") != std::string::npos);
1949
+ }
1950
+ }
1951
+
1952
+ WSLC_TEST_METHOD(BuildImageInvalidFlags)
1953
+ {
1954
+ auto dummyDockerfile = wil::create_new_file(
1955
+ (std::filesystem::current_path() / "Dockerfile").c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, FILE_FLAG_DELETE_ON_CLOSE);
1956
+
1957
+ auto contextDir = std::filesystem::current_path();
1958
+
1959
+ WSLCBuildImageOptions options{
1960
+ .ContextPath = contextDir.c_str(),
1961
+ .DockerfileHandle = ToCOMInputHandle(dummyDockerfile.get()),
1962
+ .Flags = static_cast<WSLCBuildImageFlags>(0x8)};
1963
+
1964
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->BuildImage(&options, nullptr, nullptr));
1965
+ }
1966
+
1967
+ WSLC_TEST_METHOD(AnonymousVolumes)
1968
+ {
1969
+ auto contextDir = std::filesystem::current_path() / "build-context";
1970
+ std::filesystem::create_directories(contextDir);
1971
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
1972
+ std::error_code ec;
1973
+ std::filesystem::remove_all(contextDir, ec);
1974
+
1975
+ LOG_IF_FAILED(DeleteImageNoThrow("wslc-test-build:latest", WSLCDeleteImageFlagsForce).first);
1976
+ });
1977
+
1978
+ {
1979
+ std::ofstream dockerfile(contextDir / "Dockerfile");
1980
+ dockerfile << "FROM debian:latest\n";
1981
+ dockerfile << "VOLUME /volume\n"; // Use VOLUME to force the creation of an anonymous volume.
1982
+ }
1983
+
1984
+ VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
1985
+ ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
1986
+
1987
+ // Lists anonymous docker volume names via the VM's docker CLI.
1988
+ // TODO: Add proper support so we can list via session's API instead.
1989
+ auto listAnonymousVolumes = [&]() {
1990
+ auto result = ExpectCommandResult(
1991
+ m_defaultSession.get(), {"/usr/bin/docker", "volume", "ls", "-q", "-f", "label=com.docker.volume.anonymous"}, 0);
1992
+ std::vector<std::string> names;
1993
+ std::stringstream ss(result.Output[1]);
1994
+ std::string line;
1995
+ while (std::getline(ss, line))
1996
+ {
1997
+ if (!line.empty())
1998
+ {
1999
+ names.push_back(line);
2000
+ }
2001
+ }
2002
+ return names;
2003
+ };
2004
+
2005
+ // Session-restart scenario: an anonymous volume-backed container survives a session reset.
2006
+ {
2007
+ WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-anonymous-volume", {"test", "-d", "/volume"});
2008
+ auto container = launcher.Launch(*m_defaultSession);
2009
+ auto result = container.GetInitProcess();
2010
+
2011
+ auto containerId = container.Id();
2012
+
2013
+ ValidateProcessOutput(result, {});
2014
+
2015
+ ResetTestSession();
2016
+
2017
+ container.SetDeleteOnClose(false);
2018
+
2019
+ // Manually cleanup the container and delete anonymous volumes since the session has been reset.
2020
+ auto containerCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2021
+ wil::com_ptr<IWSLCContainer> container;
2022
+ VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerId.c_str(), &container));
2023
+
2024
+ VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes));
2025
+ });
2026
+
2027
+ // Validate that the session is correctly restarted.
2028
+ wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
2029
+ wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
2030
+
2031
+ VERIFY_SUCCEEDED(
2032
+ m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
2033
+
2034
+ VERIFY_ARE_EQUAL(containers.size(), 1);
2035
+ VERIFY_ARE_EQUAL(containers[0].Id, containerId);
2036
+ }
2037
+
2038
+ // Delete container without WSLCDeleteFlagsDeleteVolumes -> anonymous volume is leaked.
2039
+ {
2040
+ WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-leak", {"test", "-d", "/volume"});
2041
+ auto container = launcher.Launch(*m_defaultSession);
2042
+ container.GetInitProcess().Wait();
2043
+ container.SetDeleteOnClose(false);
2044
+
2045
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2046
+
2047
+ VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
2048
+
2049
+ // Anonymous volume was NOT deleted by Docker.
2050
+ auto leaked = listAnonymousVolumes();
2051
+ VERIFY_ARE_EQUAL(leaked.size(), 1u);
2052
+
2053
+ RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "volume", "prune", "-f"});
2054
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2055
+ }
2056
+
2057
+ // Delete container with WSLCDeleteFlagsDeleteVolumes -> anonymous volume is cleaned up.
2058
+ {
2059
+ WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-rm", {"sleep", "99999"});
2060
+ auto container = launcher.Launch(*m_defaultSession);
2061
+ container.SetDeleteOnClose(false);
2062
+
2063
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2064
+
2065
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2066
+
2067
+ VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsDeleteVolumes));
2068
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2069
+ }
2070
+
2071
+ // Container with WSLCContainerFlagsRm -> anonymous volume cleaned up when the container auto-removes on exit.
2072
+ {
2073
+ WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-test-delete-vol-rm", {"sleep", "99999"});
2074
+ launcher.SetContainerFlags(WSLCContainerFlagsRm);
2075
+
2076
+ auto container = launcher.Launch(*m_defaultSession);
2077
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 1u);
2078
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
2079
+
2080
+ VERIFY_ARE_EQUAL(listAnonymousVolumes().size(), 0u);
2081
+ }
2082
+ }
2083
+
2084
+ WSLC_TEST_METHOD(TagImage)
2085
+ {
2086
+ auto runTagImage = [&](LPCSTR Image, LPCSTR Repo, LPCSTR Tag) {
2087
+ WSLCTagImageOptions options{};
2088
+ options.Image = Image;
2089
+ options.Repo = Repo;
2090
+ options.Tag = Tag;
2091
+
2092
+ return m_defaultSession->TagImage(&options);
2093
+ };
2094
+
2095
+ // Positive test: Tag an existing image with a new tag in the same repository.
2096
+ {
2097
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
2098
+
2099
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2100
+ DeleteImage("debian:test-tag", WSLCDeleteImageFlagsNoPrune);
2101
+
2102
+ ExpectImagePresent(*m_defaultSession, "debian:test-tag", false);
2103
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
2104
+ });
2105
+
2106
+ VERIFY_SUCCEEDED(runTagImage("debian:latest", "debian", "test-tag"));
2107
+
2108
+ // Verify both tags exist and point to the same image.
2109
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
2110
+ ExpectImagePresent(*m_defaultSession, "debian:test-tag");
2111
+
2112
+ // Verify they have the same image hash.
2113
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
2114
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
2115
+
2116
+ std::string latestHash;
2117
+ std::string testTagHash;
2118
+ for (const auto& image : images)
2119
+ {
2120
+ if (std::strcmp(image.Image, "debian:latest") == 0)
2121
+ {
2122
+ latestHash = image.Hash;
2123
+ }
2124
+ else if (std::strcmp(image.Image, "debian:test-tag") == 0)
2125
+ {
2126
+ testTagHash = image.Hash;
2127
+ }
2128
+ }
2129
+
2130
+ VERIFY_IS_FALSE(latestHash.empty());
2131
+ VERIFY_IS_FALSE(testTagHash.empty());
2132
+ VERIFY_ARE_EQUAL(latestHash, testTagHash);
2133
+ }
2134
+
2135
+ // Positive test: Tag with a different repository name.
2136
+ {
2137
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
2138
+
2139
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2140
+ DeleteImage("myrepo/myimage:v1.0.0", WSLCDeleteImageFlagsNoPrune);
2141
+
2142
+ ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0", false);
2143
+ });
2144
+
2145
+ VERIFY_SUCCEEDED(runTagImage("debian:latest", "myrepo/myimage", "v1.0.0"));
2146
+
2147
+ ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0");
2148
+ }
2149
+
2150
+ // Positive test: Tag using image ID.
2151
+ {
2152
+ ExpectImagePresent(*m_defaultSession, "debian:latest");
2153
+
2154
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2155
+ DeleteImage("debian:test-by-id", WSLCDeleteImageFlagsNoPrune);
2156
+
2157
+ ExpectImagePresent(*m_defaultSession, "debian:test-by-id", false);
2158
+ });
2159
+
2160
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
2161
+ VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
2162
+
2163
+ std::string imageId;
2164
+ for (const auto& image : images)
2165
+ {
2166
+ if (std::strcmp(image.Image, "debian:latest") == 0)
2167
+ {
2168
+ imageId = image.Hash;
2169
+ break;
2170
+ }
2171
+ }
2172
+ VERIFY_IS_FALSE(imageId.empty());
2173
+
2174
+ VERIFY_SUCCEEDED(runTagImage(imageId.c_str(), "debian", "test-by-id"));
2175
+
2176
+ ExpectImagePresent(*m_defaultSession, "debian:test-by-id");
2177
+ }
2178
+
2179
+ // Positive test: Overwrite existing tag.
2180
+ {
2181
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2182
+ DeleteImage("test:duplicate-tag", WSLCDeleteImageFlagsNoPrune);
2183
+
2184
+ ExpectImagePresent(*m_defaultSession, "test:duplicate-tag", false);
2185
+ });
2186
+
2187
+ VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
2188
+ VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
2189
+ }
2190
+
2191
+ // Negative test: Null options pointer.
2192
+ {
2193
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), m_defaultSession->TagImage(nullptr));
2194
+ }
2195
+
2196
+ // Negative test: Null Image field.
2197
+ {
2198
+ VERIFY_ARE_EQUAL(E_POINTER, runTagImage(nullptr, "test", "tag"));
2199
+ }
2200
+
2201
+ // Negative test: Null Repo field.
2202
+ {
2203
+ VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", nullptr, "tag"));
2204
+ }
2205
+
2206
+ // Negative test: Null Tag field.
2207
+ {
2208
+ VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", "test", nullptr));
2209
+ }
2210
+
2211
+ // Negative test: Tag a non-existent image.
2212
+ {
2213
+ VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, runTagImage("nonexistent:notfound", "test", "fail"));
2214
+ ValidateCOMErrorMessage(L"No such image: nonexistent:notfound");
2215
+ }
2216
+
2217
+ // Negative test: Invalid tag format with spaces.
2218
+ {
2219
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), runTagImage("debian:latest", "test", "invalid tag"));
2220
+ ValidateCOMErrorMessage(L"invalid tag format");
2221
+ }
2222
+ }
2223
+
2224
+ WSLC_TEST_METHOD(InspectImage)
2225
+ {
2226
+ // Test inspect debian:latest
2227
+ {
2228
+ wil::unique_cotaskmem_ansistring output;
2229
+ VERIFY_SUCCEEDED(m_defaultSession->InspectImage("debian:latest", &output));
2230
+
2231
+ // Verify output is valid JSON
2232
+ VERIFY_IS_NOT_NULL(output.get());
2233
+ VERIFY_IS_TRUE(std::strlen(output.get()) > 0);
2234
+ LogInfo("Inspect output: %hs", output.get());
2235
+
2236
+ // Parse and validate JSON structure
2237
+ auto inspectResult = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(output.get());
2238
+
2239
+ // Verify all fields exposed in wslc_schema::InspectImage
2240
+ VERIFY_IS_TRUE(inspectResult.Id.find("sha256:") == 0);
2241
+
2242
+ VERIFY_IS_TRUE(inspectResult.RepoTags.has_value());
2243
+ VERIFY_IS_FALSE(inspectResult.RepoTags->empty());
2244
+ bool foundTag = false;
2245
+ for (const auto& tag : inspectResult.RepoTags.value())
2246
+ {
2247
+ if (tag.find("debian:latest") != std::string::npos)
2248
+ {
2249
+ foundTag = true;
2250
+ break;
2251
+ }
2252
+ }
2253
+ VERIFY_IS_TRUE(foundTag);
2254
+
2255
+ // skip testing RepoDigests for loaded test image.
2256
+ VERIFY_IS_FALSE(inspectResult.Created.empty());
2257
+ VERIFY_IS_TRUE(inspectResult.Architecture == "amd64" || inspectResult.Architecture == "arm64");
2258
+ VERIFY_ARE_EQUAL("linux", inspectResult.Os);
2259
+ VERIFY_IS_TRUE(inspectResult.Size > 0);
2260
+ VERIFY_IS_TRUE(inspectResult.Metadata.has_value());
2261
+ VERIFY_IS_TRUE(inspectResult.Metadata->size() > 0);
2262
+
2263
+ VERIFY_IS_TRUE(inspectResult.Config.has_value());
2264
+ const auto& config = inspectResult.Config.value();
2265
+ VERIFY_IS_TRUE(config.Cmd.has_value());
2266
+ VERIFY_IS_TRUE(config.Cmd->size() > 0);
2267
+ VERIFY_IS_TRUE(config.Entrypoint.has_value());
2268
+ VERIFY_ARE_EQUAL(0, config.Entrypoint->size());
2269
+ VERIFY_IS_TRUE(config.Env.has_value());
2270
+ VERIFY_IS_TRUE(config.Env->size() > 0);
2271
+ VERIFY_IS_FALSE(config.Labels.has_value());
2272
+ }
2273
+
2274
+ // Negative test: Image not found
2275
+ {
2276
+ wil::unique_cotaskmem_ansistring output;
2277
+ VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("nonexistent:image", &output));
2278
+ ValidateCOMErrorMessage(L"No such image: nonexistent:image");
2279
+ }
2280
+
2281
+ // Negative test: Bad image name input
2282
+ {
2283
+ wil::unique_cotaskmem_ansistring output;
2284
+
2285
+ std::string longImageName(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a');
2286
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->InspectImage(longImageName.c_str(), &output));
2287
+
2288
+ // Invalid name.
2289
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), m_defaultSession->InspectImage("debian latest", &output));
2290
+ ValidateCOMErrorMessage(L"invalid reference format");
2291
+
2292
+ // Attempt to fake to call search endpoint. Our implementation escaped the image name correctly.
2293
+ VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("search/debian:latest", &output));
2294
+ ValidateCOMErrorMessage(L"No such image: search/debian:latest");
2295
+ }
2296
+ }
2297
+
2298
+ struct BlockingOperation
2299
+ {
2300
+ NON_COPYABLE(BlockingOperation);
2301
+ NON_MOVABLE(BlockingOperation);
2302
+
2303
+ BlockingOperation(std::function<HRESULT(HANDLE)>&& Operation, HRESULT ExpectedResult = S_OK, bool AllowEarlyCompletion = false, bool UseOverlappedWritePipe = false) :
2304
+ m_operation(std::move(Operation)), m_expectedResult(ExpectedResult), m_allowEarlyCompletion(AllowEarlyCompletion)
2305
+ {
2306
+ auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(100000, false, UseOverlappedWritePipe);
2307
+
2308
+ m_operationThread = std::thread(&BlockingOperation::RunOperation, this, std::move(pipeWrite));
2309
+ m_ioThread = std::thread(&BlockingOperation::RunIO, this, std::move(pipeRead));
2310
+
2311
+ // Wait for the operation to be running before continuing.
2312
+ VERIFY_IS_TRUE(m_startedEvent.wait(60 * 1000));
2313
+ }
2314
+
2315
+ ~BlockingOperation()
2316
+ {
2317
+ if (m_operationThread.joinable())
2318
+ {
2319
+ m_operationThread.join();
2320
+ }
2321
+
2322
+ if (m_ioThread.joinable())
2323
+ {
2324
+ m_ioThread.join();
2325
+ }
2326
+ }
2327
+
2328
+ void RunOperation(wil::unique_hfile Handle)
2329
+ {
2330
+ m_result.set_value(m_operation(Handle.get()));
2331
+
2332
+ // Fail if the operation completed before the test signaled completion
2333
+ // (unless early completion is expected, e.g. session termination).
2334
+ // Don't use VERIFY macros since this is running in a separate thread.
2335
+ WI_ASSERT(m_allowEarlyCompletion || m_testCompleteEvent.is_signaled());
2336
+ }
2337
+
2338
+ void RunIO(wil::unique_hfile Handle)
2339
+ {
2340
+ std::vector<char> buffer(1024 * 1024);
2341
+ while (true)
2342
+ {
2343
+ DWORD bytesRead{};
2344
+ if (!ReadFile(Handle.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &bytesRead, nullptr))
2345
+ {
2346
+ if (GetLastError() != ERROR_BROKEN_PIPE)
2347
+ {
2348
+ LogError("Unexpected ReadFile() error: %u", GetLastError());
2349
+ }
2350
+
2351
+ break;
2352
+ }
2353
+
2354
+ if (bytesRead == 0)
2355
+ {
2356
+ break;
2357
+ }
2358
+
2359
+ if (!m_startedEvent.is_signaled())
2360
+ {
2361
+ m_startedEvent.SetEvent();
2362
+ }
2363
+
2364
+ // Block until the test completes.
2365
+ if (!m_testCompleteEvent.wait(60 * 1000))
2366
+ {
2367
+ LogError("Timed out waiting for test completion");
2368
+ break;
2369
+ }
2370
+ }
2371
+ }
2372
+
2373
+ void Complete()
2374
+ {
2375
+ m_testCompleteEvent.SetEvent();
2376
+
2377
+ VERIFY_ARE_EQUAL(m_expectedResult, m_result.get_future().get());
2378
+ }
2379
+
2380
+ std::function<HRESULT(HANDLE)> m_operation;
2381
+ wil::unique_event m_startedEvent{wil::EventOptions::ManualReset};
2382
+ wil::unique_event m_testCompleteEvent{wil::EventOptions::ManualReset};
2383
+ std::thread m_operationThread;
2384
+ std::thread m_ioThread;
2385
+ std::promise<HRESULT> m_result;
2386
+ HRESULT m_expectedResult{};
2387
+ bool m_allowEarlyCompletion{};
2388
+ };
2389
+
2390
+ WSLC_TEST_METHOD(SaveImage)
2391
+ {
2392
+ {
2393
+ std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
2394
+ wil::unique_handle imageTarFileHandle{
2395
+ CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2396
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2397
+ LARGE_INTEGER fileSize{};
2398
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2399
+ // Load the image from a saved tar
2400
+ VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2401
+ // Verify that the image is in the list of images.
2402
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2403
+ WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2404
+ auto container = launcher.Launch(*m_defaultSession);
2405
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
2406
+ VERIFY_ARE_EQUAL(0, result.Code);
2407
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2408
+ }
2409
+
2410
+ {
2411
+ std::filesystem::path imageTar = L"HelloWorldExported.tar";
2412
+ auto cleanup =
2413
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2414
+ // Save the image to a tar file.
2415
+ {
2416
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2417
+ imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2418
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2419
+ LARGE_INTEGER fileSize{};
2420
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2421
+ VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2422
+ VERIFY_SUCCEEDED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-world:latest", nullptr, nullptr));
2423
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2424
+ VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, true);
2425
+ }
2426
+
2427
+ // Load the saved image to verify it's valid.
2428
+ {
2429
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2430
+ imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2431
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2432
+ LARGE_INTEGER fileSize{};
2433
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2434
+ // Load the image from a saved tar
2435
+ VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2436
+ // Verify that the image is in the list of images.
2437
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2438
+ WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2439
+ auto container = launcher.Launch(*m_defaultSession);
2440
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
2441
+ VERIFY_ARE_EQUAL(0, result.Code);
2442
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2443
+ }
2444
+ }
2445
+
2446
+ // Try to save an invalid image.
2447
+ {
2448
+ std::filesystem::path imageTar = L"HelloWorldError.tar";
2449
+ auto cleanfile =
2450
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
2451
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2452
+ imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2453
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2454
+ LARGE_INTEGER fileSize{};
2455
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2456
+ VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2457
+ VERIFY_FAILED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-wld:latest", nullptr, nullptr));
2458
+ ValidateCOMErrorMessage(L"reference does not exist");
2459
+
2460
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2461
+ VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
2462
+ }
2463
+
2464
+ // Validate that cancellation works.
2465
+ {
2466
+ wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
2467
+
2468
+ BlockingOperation operation(
2469
+ [&](HANDLE handle) {
2470
+ return m_defaultSession->SaveImage(ToCOMInputHandle(handle), "debian:latest", nullptr, cancelEvent.get());
2471
+ },
2472
+ E_ABORT);
2473
+
2474
+ cancelEvent.SetEvent();
2475
+ operation.Complete();
2476
+ }
2477
+ }
2478
+
2479
+ WSLC_TEST_METHOD(SynchronousIoCancellation)
2480
+ {
2481
+ // Create a blocked operation that will cause the service to get stuck on a ReadFile() call.
2482
+ // Because the pipe handle that we're passing in doesn't support overlapped IO, the service will get stuck in a
2483
+ // synchronous ReadFile() call. Validate that terminating the session correctly cancels the IO.
2484
+
2485
+ wil::unique_handle pipeRead;
2486
+ wil::unique_handle pipeWrite;
2487
+ VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
2488
+
2489
+ std::promise<HRESULT> result;
2490
+
2491
+ wil::unique_event testCompleted{wil::EventOptions::ManualReset};
2492
+ std::thread operationThread([&]() {
2493
+ result.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024));
2494
+
2495
+ WI_ASSERT(testCompleted.is_signaled()); // Sanity check.
2496
+ });
2497
+
2498
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
2499
+
2500
+ // Write 4 bytes to validate that the service has started reading from the pipe (since the pipe buffer is 2).
2501
+ DWORD bytesWritten{};
2502
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
2503
+
2504
+ testCompleted.SetEvent();
2505
+
2506
+ // N.B. It's not possible to deterministically wait for the service to be stuck in the ReadFile() call.
2507
+ // It's possible that the service will check the session termination event before calling ReadFile() on the pipe
2508
+ // but that's OK since we can also accept that error code here (E_ABORT).
2509
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
2510
+
2511
+ auto reset = ResetTestSession();
2512
+
2513
+ auto hr = result.get_future().get();
2514
+ if (hr != E_ABORT && hr != HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED))
2515
+ {
2516
+ LogError("Unexpected result: 0x%08X", hr);
2517
+ VERIFY_FAIL();
2518
+ }
2519
+ }
2520
+
2521
+ WSLC_TEST_METHOD(ExportContainer)
2522
+ {
2523
+ // Load an image and launch a container to verify image is valid.
2524
+ // Then export the container to a tar file.
2525
+ // Load the exported tar file to verify it's a valid image and can be launched.
2526
+ // Finally, stop and delete the container, then try to export again to verify it fails as expected.
2527
+ {
2528
+ std::filesystem::path containerTar = L"HelloWorldExported.tar";
2529
+ auto cleanup =
2530
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(containerTar.c_str())); });
2531
+
2532
+ // Load the image from a saved tar and launch a container
2533
+ {
2534
+ std::filesystem::path imageTar = GetTestImagePath("hello-world:latest");
2535
+ wil::unique_handle imageTarFileHandle{CreateFileW(
2536
+ imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2537
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2538
+ LARGE_INTEGER fileSize{};
2539
+ VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2540
+ VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
2541
+ // Verify that the image is in the list of images.
2542
+ ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2543
+ WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
2544
+ auto container = launcher.Launch(*m_defaultSession);
2545
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
2546
+ VERIFY_ARE_EQUAL(0, result.Code);
2547
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2548
+
2549
+ // Export the container to a tar file.
2550
+ wil::unique_handle containerTarFileHandle{CreateFileW(
2551
+ containerTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2552
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
2553
+ VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2554
+ VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2555
+ VERIFY_SUCCEEDED(container.Get().Export(ToCOMInputHandle(containerTarFileHandle.get())));
2556
+ VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2557
+ VERIFY_ARE_NOT_EQUAL(fileSize.QuadPart, 0);
2558
+ }
2559
+
2560
+ // Load the exported container to verify it's valid.
2561
+ {
2562
+ wil::unique_handle containerTarFileHandle{CreateFileW(
2563
+ containerTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2564
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
2565
+ LARGE_INTEGER fileSize{};
2566
+ VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
2567
+
2568
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2569
+ LOG_IF_FAILED(DeleteImageNoThrow("test-imported-container:latest", WSLCDeleteImageFlagsNone).first);
2570
+ });
2571
+
2572
+ VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
2573
+ ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart));
2574
+
2575
+ // Verify that the image is in the list of images.
2576
+ ExpectImagePresent(*m_defaultSession, "test-imported-container:latest");
2577
+ WSLCContainerLauncher launcher("test-imported-container:latest", "wslc-hello-world-container", {"/hello"});
2578
+ auto container = launcher.Launch(*m_defaultSession);
2579
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
2580
+ VERIFY_ARE_EQUAL(0, result.Code);
2581
+ VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
2582
+
2583
+ // Stop and delete the above container and try to export.
2584
+
2585
+ std::filesystem::path imageTarFile = L"HelloWorldExportError.tar";
2586
+ auto cleanfile =
2587
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTarFile.c_str())); });
2588
+ wil::unique_handle contTarFileHandle{CreateFileW(
2589
+ imageTarFile.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
2590
+ VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == contTarFileHandle.get());
2591
+ VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
2592
+ VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2593
+
2594
+ auto outFile = ToCOMInputHandle(contTarFileHandle.get());
2595
+
2596
+ container.Get().Stop(WSLCSignalSIGILL, 10);
2597
+ container.Get().Delete(WSLCDeleteFlagsNone);
2598
+ VERIFY_ARE_EQUAL(container.Get().Export(outFile), RPC_E_DISCONNECTED);
2599
+
2600
+ VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
2601
+ VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
2602
+ }
2603
+ }
2604
+ }
2605
+
2606
+ WSLC_TEST_METHOD(CustomDmesgOutput)
2607
+ {
2608
+ SKIP_TEST_ARM64();
2609
+
2610
+ auto createVmWithDmesg = [this](bool earlyBootLogging) {
2611
+ auto [read, write] = CreateSubprocessPipe(false, false);
2612
+
2613
+ auto settings = GetDefaultSessionSettings(L"dmesg-output-test");
2614
+ settings.DmesgOutput = ToCOMInputHandle(write.get());
2615
+ WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsEarlyBootDmesg, earlyBootLogging);
2616
+
2617
+ std::vector<char> dmesgContent;
2618
+ auto readDmesg = [read = read.get(), &dmesgContent]() mutable {
2619
+ DWORD Offset = 0;
2620
+
2621
+ constexpr auto bufferSize = 1024;
2622
+ while (true)
2623
+ {
2624
+ dmesgContent.resize(Offset + bufferSize);
2625
+
2626
+ DWORD Read{};
2627
+ if (!ReadFile(read, &dmesgContent[Offset], bufferSize, &Read, nullptr))
2628
+ {
2629
+ LogInfo("ReadFile() failed: %lu", GetLastError());
2630
+ }
2631
+
2632
+ if (Read == 0)
2633
+ {
2634
+ break;
2635
+ }
2636
+
2637
+ Offset += Read;
2638
+ }
2639
+ };
2640
+
2641
+ std::thread thread(readDmesg); // Needs to be created before the VM starts, to avoid a pipe deadlock.
2642
+
2643
+ // Ensure the thread is joined even if CreateSession throws, to avoid std::terminate.
2644
+ auto threadGuard = wil::scope_exit([&]() {
2645
+ write.reset();
2646
+ if (thread.joinable())
2647
+ {
2648
+ thread.join();
2649
+ }
2650
+ });
2651
+
2652
+ auto session = CreateSession(settings);
2653
+ threadGuard.release(); // CreateSession succeeded, detach scope_exit below takes over.
2654
+
2655
+ auto detach = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2656
+ session.reset();
2657
+ if (thread.joinable())
2658
+ {
2659
+ thread.join();
2660
+ }
2661
+ });
2662
+
2663
+ write.reset();
2664
+
2665
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo DmesgTest > /dev/kmsg"}, 0);
2666
+
2667
+ session.reset();
2668
+ detach.reset();
2669
+
2670
+ auto contentString = std::string(dmesgContent.begin(), dmesgContent.end());
2671
+
2672
+ VERIFY_ARE_NOT_EQUAL(contentString.find("Run /init as init process"), std::string::npos);
2673
+ VERIFY_ARE_NOT_EQUAL(contentString.find("DmesgTest"), std::string::npos);
2674
+
2675
+ return contentString;
2676
+ };
2677
+
2678
+ auto validateFirstDmesgLine = [](const std::string& dmesg, const char* expected) {
2679
+ auto firstLf = dmesg.find("\n");
2680
+ VERIFY_ARE_NOT_EQUAL(firstLf, std::string::npos);
2681
+ VERIFY_IS_TRUE(dmesg.find(expected) < firstLf);
2682
+ };
2683
+
2684
+ // Dmesg without early boot logging
2685
+ {
2686
+ auto dmesg = createVmWithDmesg(false);
2687
+
2688
+ // Verify that the first line is "brd: module loaded";
2689
+ validateFirstDmesgLine(dmesg, "brd: module loaded");
2690
+ }
2691
+
2692
+ // Dmesg with early boot logging
2693
+ {
2694
+ auto dmesg = createVmWithDmesg(true);
2695
+ validateFirstDmesgLine(dmesg, "Linux version");
2696
+ }
2697
+ }
2698
+
2699
+ WSLC_TEST_METHOD(TerminationCallback)
2700
+ {
2701
+ class DECLSPEC_UUID("7BC4E198-6531-4FA6-ADE2-5EF3D2A04DFF") CallbackInstance
2702
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, ITerminationCallback, IFastRundown>
2703
+ {
2704
+
2705
+ public:
2706
+ CallbackInstance(std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)>&& callback) :
2707
+ m_callback(std::move(callback))
2708
+ {
2709
+ }
2710
+
2711
+ HRESULT OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR Details) override
2712
+ {
2713
+ m_callback(Reason, Details);
2714
+ return S_OK;
2715
+ }
2716
+
2717
+ private:
2718
+ std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)> m_callback;
2719
+ };
2720
+
2721
+ std::promise<std::pair<WSLCVirtualMachineTerminationReason, std::wstring>> promise;
2722
+
2723
+ CallbackInstance callback{[&](WSLCVirtualMachineTerminationReason reason, LPCWSTR details) {
2724
+ promise.set_value(std::make_pair(reason, details));
2725
+ }};
2726
+
2727
+ WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(L"termination-callback-test");
2728
+ sessionSettings.TerminationCallback = &callback;
2729
+
2730
+ auto session = CreateSession(sessionSettings);
2731
+
2732
+ session.reset();
2733
+ auto future = promise.get_future();
2734
+ auto result = future.wait_for(std::chrono::seconds(30));
2735
+ VERIFY_ARE_EQUAL(result, std::future_status::ready);
2736
+ auto [reason, details] = future.get();
2737
+ VERIFY_ARE_EQUAL(reason, WSLCVirtualMachineTerminationReasonShutdown);
2738
+ VERIFY_ARE_NOT_EQUAL(details, L"");
2739
+ }
2740
+
2741
+ WSLC_TEST_METHOD(BuildImageStuckCallbackCancellation)
2742
+ {
2743
+ class StuckBuildProgressCallback
2744
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
2745
+ {
2746
+ public:
2747
+ StuckBuildProgressCallback(std::promise<void>& reachedPromise, wil::unique_event& exitEvent) :
2748
+ m_reachedPromise(reachedPromise), m_exitEvent(exitEvent)
2749
+ {
2750
+ }
2751
+
2752
+ HRESULT OnProgress(LPCSTR, LPCSTR, ULONGLONG, ULONGLONG) override
2753
+ {
2754
+ if (!m_signaled)
2755
+ {
2756
+ m_signaled = true;
2757
+ m_reachedPromise.set_value();
2758
+ m_exitEvent.wait(); // Block until this test case is complete.
2759
+ }
2760
+
2761
+ return S_OK;
2762
+ }
2763
+
2764
+ private:
2765
+ std::promise<void>& m_reachedPromise;
2766
+ wil::unique_event& m_exitEvent;
2767
+ bool m_signaled{};
2768
+ };
2769
+
2770
+ auto contextDir = std::filesystem::current_path() / "build-context-stuck-callback";
2771
+ std::filesystem::create_directories(contextDir);
2772
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
2773
+ std::error_code ec;
2774
+ std::filesystem::remove_all(contextDir, ec);
2775
+ });
2776
+
2777
+ {
2778
+ std::ofstream dockerfile(contextDir / "Dockerfile");
2779
+ dockerfile << "FROM debian:latest\n";
2780
+ dockerfile << "RUN echo hello\n";
2781
+ }
2782
+
2783
+ auto contextPathStr = contextDir.wstring();
2784
+ auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
2785
+
2786
+ WSLCBuildImageOptions options{
2787
+ .ContextPath = contextPathStr.c_str(),
2788
+ .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()),
2789
+ .Flags = WSLCBuildImageFlagsVerbose,
2790
+ };
2791
+
2792
+ std::promise<void> callbackReached;
2793
+ wil::unique_event exitEvent{wil::EventOptions::ManualReset};
2794
+ auto callback = Microsoft::WRL::Make<StuckBuildProgressCallback>(callbackReached, exitEvent);
2795
+
2796
+ std::promise<HRESULT> buildResult;
2797
+ std::thread buildThread(
2798
+ [&]() { buildResult.set_value(m_defaultSession->BuildImage(&options, callback.Get(), exitEvent.get())); });
2799
+
2800
+ auto joinThread = wil::scope_exit([&]() {
2801
+ exitEvent.SetEvent();
2802
+ buildThread.join();
2803
+ });
2804
+
2805
+ // Wait for the progress callback to be called, proving the COM call is in flight.
2806
+ auto reachedFuture = callbackReached.get_future();
2807
+ auto reachedStatus = reachedFuture.wait_for(std::chrono::seconds(60));
2808
+ VERIFY_ARE_EQUAL(reachedStatus, std::future_status::ready);
2809
+
2810
+ // Terminate the session while the callback is stuck.
2811
+ // This should cancel the pending COM call and unblock BuildImage.
2812
+ VERIFY_SUCCEEDED(m_defaultSession->Terminate());
2813
+ ResetTestSession();
2814
+
2815
+ auto buildFuture = buildResult.get_future();
2816
+ auto buildStatus = buildFuture.wait_for(std::chrono::seconds(60));
2817
+ VERIFY_ARE_EQUAL(buildStatus, std::future_status::ready);
2818
+
2819
+ // BuildImage should have failed due to COM call cancellation.
2820
+ VERIFY_FAILED(buildFuture.get());
2821
+ }
2822
+
2823
+ WSLC_TEST_METHOD(InteractiveShell)
2824
+ {
2825
+ WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
2826
+ auto process = launcher.Launch(*m_defaultSession);
2827
+
2828
+ wil::unique_handle tty = process.GetStdHandle(WSLCFDTty);
2829
+
2830
+ auto validateTtyOutput = [&](const std::string& expected) {
2831
+ std::string buffer(expected.size(), '\0');
2832
+
2833
+ DWORD offset = 0;
2834
+
2835
+ while (offset < buffer.size())
2836
+ {
2837
+ DWORD bytesRead{};
2838
+ VERIFY_IS_TRUE(ReadFile(tty.get(), buffer.data() + offset, static_cast<DWORD>(buffer.size() - offset), &bytesRead, nullptr));
2839
+
2840
+ offset += bytesRead;
2841
+ }
2842
+
2843
+ buffer.resize(offset);
2844
+ VERIFY_ARE_EQUAL(buffer, expected);
2845
+ };
2846
+
2847
+ auto writeTty = [&](const std::string& content) {
2848
+ VERIFY_IS_TRUE(WriteFile(tty.get(), content.data(), static_cast<DWORD>(content.size()), nullptr, nullptr));
2849
+ };
2850
+
2851
+ // Expect the shell prompt to be displayed
2852
+ validateTtyOutput("\033[?2004hsh-5.2# ");
2853
+ writeTty("echo OK\n");
2854
+ validateTtyOutput("echo OK\r\n\033[?2004l\rOK");
2855
+
2856
+ // Exit the shell
2857
+ writeTty("exit\n");
2858
+
2859
+ VERIFY_IS_TRUE(process.GetExitEvent().wait(30 * 1000));
2860
+ }
2861
+
2862
+ void ValidateNetworking(WSLCNetworkingMode mode, bool enableDnsTunneling = false)
2863
+ {
2864
+ // Reuse the default session if settings match (same networking mode and DNS tunneling setting).
2865
+ auto createNewSession = mode != m_defaultSessionSettings.NetworkingMode ||
2866
+ enableDnsTunneling != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsDnsTunneling);
2867
+
2868
+ auto settings = GetDefaultSessionSettings(L"networking-test", false, mode);
2869
+ WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, enableDnsTunneling);
2870
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
2871
+
2872
+ // Validate that eth0 has an ip address
2873
+ ExpectCommandResult(
2874
+ session.get(),
2875
+ {"/bin/sh",
2876
+ "-c",
2877
+ "ip a show dev eth0 | grep -iF 'inet ' | grep -E '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}'"},
2878
+ 0);
2879
+
2880
+ ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"}, 0);
2881
+
2882
+ // Verify that /etc/resolv.conf is correctly configured.
2883
+ if (enableDnsTunneling)
2884
+ {
2885
+ auto result = ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver ", "/etc/resolv.conf"}, 0);
2886
+
2887
+ VERIFY_ARE_EQUAL(result.Output[1], std::format("nameserver {}\n", LX_INIT_DNS_TUNNELING_IP_ADDRESS));
2888
+ }
2889
+
2890
+ // Verify DNS resolution.
2891
+ // Note: without DNS tunneling, NAT mode uses the ICS SharedAccess DNS proxy which only supports UDP.
2892
+ // TCP DNS queries (dig +tcp) will time out without tunneling.
2893
+ VerifyDigDnsResolution(session.get(), "getent ahosts bing.com");
2894
+ VerifyDnsQueries(session.get(), mode, enableDnsTunneling);
2895
+ }
2896
+
2897
+ TEST_METHOD(NATNetworking)
2898
+ {
2899
+ ValidateNetworking(WSLCNetworkingModeNAT);
2900
+ }
2901
+
2902
+ TEST_METHOD(NATNetworkingWithDnsTunneling)
2903
+ {
2904
+ WINDOWS_11_TEST_ONLY();
2905
+ ValidateNetworking(WSLCNetworkingModeNAT, true);
2906
+ }
2907
+
2908
+ TEST_METHOD(VirtioProxyNetworking)
2909
+ {
2910
+ ValidateNetworking(WSLCNetworkingModeVirtioProxy);
2911
+ }
2912
+
2913
+ TEST_METHOD(VirtioProxyNetworkingWithDnsTunneling)
2914
+ {
2915
+ WINDOWS_11_TEST_ONLY();
2916
+ ValidateNetworking(WSLCNetworkingModeVirtioProxy, true);
2917
+ }
2918
+
2919
+ // DNS test helpers
2920
+
2921
+ void VerifyDigDnsResolution(IWSLCSession* session, const std::string& digCommandLine)
2922
+ {
2923
+ auto result = ExpectCommandResult(session, {"/bin/sh", "-c", digCommandLine}, 0);
2924
+ VERIFY_IS_FALSE(result.Output[1].empty());
2925
+ }
2926
+
2927
+ void VerifyDnsQueries(IWSLCSession* session, WSLCNetworkingMode mode, bool enableDnsTunneling)
2928
+ {
2929
+ // TCP DNS works except for NAT without tunneling (ICS SharedAccess DNS proxy is UDP-only).
2930
+ const bool includeTcp = (mode != WSLCNetworkingModeNAT) || enableDnsTunneling;
2931
+
2932
+ // UDP queries for all record types
2933
+ VerifyDigDnsResolution(session, "dig +short +time=5 A bing.com");
2934
+ VerifyDigDnsResolution(session, "dig +short +time=5 AAAA bing.com");
2935
+ VerifyDigDnsResolution(session, "dig +short +time=5 MX bing.com");
2936
+ VerifyDigDnsResolution(session, "dig +short +time=5 NS bing.com");
2937
+ VerifyDigDnsResolution(session, "dig +short +time=5 -x 8.8.8.8");
2938
+ VerifyDigDnsResolution(session, "dig +short +time=5 SOA bing.com");
2939
+ VerifyDigDnsResolution(session, "dig +short +time=5 TXT bing.com");
2940
+ VerifyDigDnsResolution(session, "dig +time=5 CNAME bing.com");
2941
+ VerifyDigDnsResolution(session, "dig +time=5 SRV bing.com");
2942
+
2943
+ if (includeTcp)
2944
+ {
2945
+ // ANY - dig expects a large response so it queries directly over TCP
2946
+ VerifyDigDnsResolution(session, "dig +short +time=5 ANY bing.com");
2947
+
2948
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 A bing.com");
2949
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 AAAA bing.com");
2950
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 MX bing.com");
2951
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 NS bing.com");
2952
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 -x 8.8.8.8");
2953
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 SOA bing.com");
2954
+ VerifyDigDnsResolution(session, "dig +tcp +short +time=5 TXT bing.com");
2955
+ VerifyDigDnsResolution(session, "dig +tcp +time=5 CNAME bing.com");
2956
+ VerifyDigDnsResolution(session, "dig +tcp +time=5 SRV bing.com");
2957
+ }
2958
+ }
2959
+
2960
+ void ValidatePortMapping(WSLCNetworkingMode networkingMode)
2961
+ {
2962
+ auto settings = GetDefaultSessionSettings(L"port-mapping-test");
2963
+ settings.NetworkingMode = networkingMode;
2964
+
2965
+ // Reuse the default session if the networking mode matches.
2966
+ auto createNewSession = networkingMode != m_defaultSessionSettings.NetworkingMode;
2967
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
2968
+
2969
+ // Install socat in the container.
2970
+ //
2971
+ // TODO: revisit this in the future to avoid pulling packages from the network.
2972
+ auto installSocat = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "tdnf install socat -y"}).Launch(*session);
2973
+ ValidateProcessOutput(installSocat, {}, 0, 300 * 1000);
2974
+
2975
+ auto listen = [&](short port, const char* content, bool ipv6) {
2976
+ auto cmd = std::format("echo -n '{}' | /usr/bin/socat -dd TCP{}-LISTEN:{},reuseaddr -", content, ipv6 ? "6" : "", port);
2977
+ auto process = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", cmd}).Launch(*session);
2978
+ WaitForOutput(process.GetStdHandle(2), "listening on");
2979
+
2980
+ return process;
2981
+ };
2982
+
2983
+ auto connectAndRead = [&](short port, int family) -> std::string {
2984
+ SOCKADDR_INET addr{};
2985
+ addr.si_family = family;
2986
+ INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
2987
+ SS_PORT(&addr) = htons(port);
2988
+
2989
+ wil::unique_socket hostSocket{socket(family, SOCK_STREAM, IPPROTO_TCP)};
2990
+ THROW_LAST_ERROR_IF(!hostSocket);
2991
+ THROW_LAST_ERROR_IF(connect(hostSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
2992
+
2993
+ return ReadToString(hostSocket.get());
2994
+ };
2995
+
2996
+ auto expectContent = [&](short port, int family, const char* expected) {
2997
+ auto content = connectAndRead(port, family);
2998
+ VERIFY_ARE_EQUAL(content, expected);
2999
+ };
3000
+
3001
+ auto expectNotBound = [&](short port, int family) {
3002
+ auto result = wil::ResultFromException([&]() { connectAndRead(port, family); });
3003
+
3004
+ VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WSAECONNREFUSED));
3005
+ };
3006
+
3007
+ // Map port
3008
+ VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
3009
+
3010
+ // Validate that the same port can't be bound twice
3011
+ VERIFY_ARE_EQUAL(session->MapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3012
+
3013
+ // Check simple case
3014
+ listen(80, "port80", false);
3015
+ expectContent(1234, AF_INET, "port80");
3016
+
3017
+ // Validate that same port mapping can be reused
3018
+ listen(80, "port80", false);
3019
+ expectContent(1234, AF_INET, "port80");
3020
+
3021
+ // Validate that the connection is immediately reset if the port is not bound on the linux side
3022
+ expectContent(1234, AF_INET, "");
3023
+
3024
+ // Add a ipv6 binding
3025
+ VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1234, 80));
3026
+
3027
+ // Validate that ipv6 bindings work as well.
3028
+ listen(80, "port80ipv6", true);
3029
+ expectContent(1234, AF_INET6, "port80ipv6");
3030
+
3031
+ // Unmap the ipv4 port
3032
+ VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
3033
+
3034
+ // Verify that a proper error is returned if the mapping doesn't exist
3035
+ VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3036
+
3037
+ // Unmap the v6 port
3038
+ VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1234, 80));
3039
+
3040
+ // Map another port as v6 only
3041
+ VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1235, 81));
3042
+
3043
+ listen(81, "port81ipv6", true);
3044
+ expectContent(1235, AF_INET6, "port81ipv6");
3045
+ expectNotBound(1235, AF_INET);
3046
+
3047
+ VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1235, 81));
3048
+ VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET6, 1235, 81), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3049
+ expectNotBound(1235, AF_INET6);
3050
+
3051
+ // Create a forking relay and stress test
3052
+ VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
3053
+
3054
+ auto process =
3055
+ WSLCProcessLauncher{"/usr/bin/socat", {"/usr/bin/socat", "-dd", "TCP-LISTEN:80,fork,reuseaddr", "system:'echo -n OK'"}}
3056
+ .Launch(*session);
3057
+
3058
+ WaitForOutput(process.GetStdHandle(2), "listening on");
3059
+
3060
+ for (auto i = 0; i < 100; i++)
3061
+ {
3062
+ expectContent(1234, AF_INET, "OK");
3063
+ }
3064
+
3065
+ VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
3066
+
3067
+ // Validate the 63-port limit.
3068
+ // TODO: Remove the 63-port limit by switching the relay's AcceptThread from
3069
+ // WaitForMultipleObjects to IO completion ports or similar.
3070
+ constexpr int c_maxPorts = 63;
3071
+ for (int i = 0; i < c_maxPorts; i++)
3072
+ {
3073
+ VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
3074
+ }
3075
+
3076
+ VERIFY_ARE_EQUAL(
3077
+ session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)),
3078
+ HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES));
3079
+
3080
+ for (int i = 0; i < c_maxPorts; i++)
3081
+ {
3082
+ VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
3083
+ }
3084
+ }
3085
+
3086
+ TEST_METHOD(PortMappingNat)
3087
+ {
3088
+ ValidatePortMapping(WSLCNetworkingModeNAT);
3089
+ }
3090
+
3091
+ TEST_METHOD(PortMappingVirtioProxy)
3092
+ {
3093
+ ValidatePortMapping(WSLCNetworkingModeVirtioProxy);
3094
+ }
3095
+
3096
+ WSLC_TEST_METHOD(StuckVmTermination)
3097
+ {
3098
+ // Create a 'stuck' process
3099
+ auto process = WSLCProcessLauncher{"/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin}.Launch(*m_defaultSession);
3100
+
3101
+ // Stop the service
3102
+ StopWslService();
3103
+
3104
+ ResetTestSession(); // Reopen the session since the service was stopped.
3105
+ }
3106
+
3107
+ void ValidateWindowsMounts(bool enableVirtioFs)
3108
+ {
3109
+ auto settings = GetDefaultSessionSettings(L"windows-mount-tests");
3110
+ WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs, enableVirtioFs);
3111
+
3112
+ // Reuse the default session if possible.
3113
+ auto createNewSession = enableVirtioFs != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3114
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3115
+
3116
+ auto expectedMountOptions = [&](bool readOnly) -> std::string {
3117
+ if (enableVirtioFs)
3118
+ {
3119
+ return std::format("/win-path*virtiofs*{},relatime*", readOnly ? "ro" : "rw");
3120
+ }
3121
+ else
3122
+ {
3123
+ return std::format(
3124
+ "/win-path*9p*{},relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*", readOnly ? "ro" : "rw");
3125
+ }
3126
+ };
3127
+
3128
+ auto testFolder = std::filesystem::current_path() / "test-folder";
3129
+ std::filesystem::create_directories(testFolder);
3130
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
3131
+
3132
+ // Validate writeable mount.
3133
+ {
3134
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3135
+ ExpectMount(session.get(), "/win-path", expectedMountOptions(false));
3136
+
3137
+ // Validate that mount can't be stacked on each other
3138
+ VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3139
+
3140
+ // Validate that folder is writeable from linux
3141
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt && sync"}, 0);
3142
+ VERIFY_ARE_EQUAL(ReadFileContent(testFolder / "file.txt"), L"content");
3143
+
3144
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3145
+ ExpectMount(session.get(), "/win-path", {});
3146
+ }
3147
+
3148
+ // Validate read-only mount.
3149
+ {
3150
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3151
+ ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3152
+
3153
+ // Validate that folder is not writeable from linux
3154
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3155
+
3156
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3157
+ ExpectMount(session.get(), "/win-path", {});
3158
+ }
3159
+
3160
+ // Validate that a read-only share cannot be made writeable via mount -o remount,rw.
3161
+ {
3162
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3163
+ ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3164
+
3165
+ // Attempt an in-place remount to read-write from the guest.
3166
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "mount -o remount,rw /win-path"}, 0);
3167
+
3168
+ // Verify the folder is still not writeable.
3169
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3170
+
3171
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3172
+ ExpectMount(session.get(), "/win-path", {});
3173
+ }
3174
+
3175
+ // Validate that the device host enforces read-only even if the guest tries to bypass mount options.
3176
+ if (enableVirtioFs)
3177
+ {
3178
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3179
+ ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3180
+
3181
+ // Capture the mount source and type, unmount, then remount without read-only.
3182
+ ExpectCommandResult(
3183
+ session.get(),
3184
+ {"/bin/sh",
3185
+ "-c",
3186
+ "src=$(findmnt -n -o SOURCE /win-path) && "
3187
+ "fstype=$(findmnt -n -o FSTYPE /win-path) && "
3188
+ "umount /win-path && "
3189
+ "mount -t $fstype $src /win-path"},
3190
+ 0);
3191
+
3192
+ // Verify the folder is still not writeable.
3193
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3194
+
3195
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3196
+ ExpectMount(session.get(), "/win-path", {});
3197
+ }
3198
+
3199
+ // Validate various error paths
3200
+ {
3201
+ VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"relative-path", "/win-path", true), E_INVALIDARG);
3202
+ VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"C:\\does-not-exist", "/win-path", true), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
3203
+ VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/not-mounted"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3204
+ VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/proc"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3205
+
3206
+ // Validate that folders that are manually unmounted from the guest are handled properly
3207
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3208
+ ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3209
+
3210
+ ExpectCommandResult(session.get(), {"/usr/bin/umount", "/win-path"}, 0);
3211
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3212
+ }
3213
+ }
3214
+
3215
+ WSLC_TEST_METHOD(WindowsMounts)
3216
+ {
3217
+ ValidateWindowsMounts(false);
3218
+ }
3219
+
3220
+ WSLC_TEST_METHOD(WindowsMountsVirtioFs)
3221
+ {
3222
+ ValidateWindowsMounts(true);
3223
+ }
3224
+
3225
+ // Validates that VirtioFs shares are reused across mount/unmount cycles for the same Windows folder.
3226
+ WSLC_TEST_METHOD(WindowsMountsVirtioFsShareReuse)
3227
+ {
3228
+ auto settings = GetDefaultSessionSettings(L"virtiofs-share-reuse-test");
3229
+ WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3230
+
3231
+ auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3232
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3233
+
3234
+ auto testFolder = std::filesystem::current_path() / "test-folder-share-reuse";
3235
+ std::filesystem::create_directories(testFolder);
3236
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
3237
+
3238
+ auto getMountSource = [&](const char* mountPoint) -> std::string {
3239
+ auto cmd = std::format("findmnt -n -o SOURCE {}", mountPoint);
3240
+ auto result = ExpectCommandResult(session.get(), {"/bin/sh", "-c", cmd}, 0);
3241
+ return result.Output[1];
3242
+ };
3243
+
3244
+ // Mount, capture the source (share GUID), unmount, remount, verify same GUID is reused.
3245
+ {
3246
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3247
+ auto firstSource = getMountSource("/win-path");
3248
+ VERIFY_IS_FALSE(firstSource.empty());
3249
+
3250
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3251
+ ExpectMount(session.get(), "/win-path", {});
3252
+
3253
+ // Remount the same folder - should reuse the same share GUID.
3254
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3255
+ auto secondSource = getMountSource("/win-path");
3256
+
3257
+ VERIFY_ARE_EQUAL(firstSource, secondSource);
3258
+
3259
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3260
+ }
3261
+
3262
+ // Verify that changing the read-only flag produces a different share GUID.
3263
+ {
3264
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3265
+ auto rwSource = getMountSource("/win-path");
3266
+
3267
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3268
+
3269
+ VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3270
+ auto roSource = getMountSource("/win-path");
3271
+
3272
+ VERIFY_ARE_NOT_EQUAL(rwSource, roSource);
3273
+
3274
+ VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3275
+ }
3276
+ }
3277
+
3278
+ // This test case validates that no file descriptors are leaked to user processes.
3279
+ WSLC_TEST_METHOD(Fd)
3280
+ {
3281
+ auto result = ExpectCommandResult(
3282
+ m_defaultSession.get(), {"/bin/sh", "-c", "echo /proc/self/fd/* && (readlink -v /proc/self/fd/* || true)"}, 0);
3283
+
3284
+ // Note: fd/0 is opened by readlink to read the actual content of /proc/self/fd.
3285
+ if (!PathMatchSpecA(result.Output[1].c_str(), "/proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2\nsocket:*\nsocket:*"))
3286
+ {
3287
+ LogInfo("Found additional fds: %hs", result.Output[1].c_str());
3288
+ VERIFY_FAIL();
3289
+ }
3290
+ }
3291
+
3292
+ WSLC_TEST_METHOD(GPU)
3293
+ {
3294
+ // Validate that trying to mount the shares without GPU support enabled fails.
3295
+ {
3296
+ auto settings = GetDefaultSessionSettings(L"gpu-test-disabled");
3297
+ WI_ClearFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
3298
+
3299
+ auto createNewSession = WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
3300
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3301
+
3302
+ // Validate that the GPU device is not available.
3303
+ ExpectMount(session.get(), "/usr/lib/wsl/drivers", {});
3304
+ ExpectMount(session.get(), "/usr/lib/wsl/lib", {});
3305
+ }
3306
+
3307
+ // Validate that the GPU device is available when enabled.
3308
+ {
3309
+ auto settings = GetDefaultSessionSettings(L"gpu-test");
3310
+ WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
3311
+
3312
+ auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
3313
+ auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3314
+
3315
+ // Validate that the GPU device is available.
3316
+ ExpectCommandResult(session.get(), {"/bin/sh", "-c", "test -c /dev/dxg"}, 0);
3317
+
3318
+ ExpectMount(
3319
+ session.get(),
3320
+ "/usr/lib/wsl/drivers",
3321
+ "/usr/lib/wsl/drivers*9p*relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
3322
+
3323
+ ExpectMount(
3324
+ session.get(),
3325
+ "/usr/lib/wsl/lib",
3326
+ "/usr/lib/wsl/lib none*overlay ro,relatime,lowerdir=/usr/lib/wsl/lib/packaged*");
3327
+
3328
+ // Validate that the mount points are not writeable.
3329
+ VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/drivers/test"}).Code, 1L);
3330
+ VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/lib/test"}).Code, 1L);
3331
+ }
3332
+ }
3333
+
3334
+ WSLC_TEST_METHOD(Modules)
3335
+ {
3336
+ // Sanity check.
3337
+ ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 1);
3338
+
3339
+ // Validate that modules can be loaded.
3340
+ ExpectCommandResult(m_defaultSession.get(), {"/usr/sbin/modprobe", "xsk_diag"}, 0);
3341
+
3342
+ // Validate that xsk_diag is now loaded.
3343
+ ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 0);
3344
+ }
3345
+
3346
+ WSLC_TEST_METHOD(CreateRootNamespaceProcess)
3347
+ {
3348
+ // Simple case
3349
+ {
3350
+ auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo OK"}, 0);
3351
+ VERIFY_ARE_EQUAL(result.Output[1], "OK\n");
3352
+ VERIFY_ARE_EQUAL(result.Output[2], "");
3353
+ }
3354
+
3355
+ // Stdout + stderr
3356
+ {
3357
+
3358
+ auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo stdout && (echo stderr 1>& 2)"}, 0);
3359
+ VERIFY_ARE_EQUAL(result.Output[1], "stdout\n");
3360
+ VERIFY_ARE_EQUAL(result.Output[2], "stderr\n");
3361
+ }
3362
+
3363
+ // Write a large stdin buffer and expect it back on stdout.
3364
+ {
3365
+ std::vector<char> largeBuffer;
3366
+ std::string pattern = "ExpectedBufferContent";
3367
+
3368
+ for (size_t i = 0; i < 1024 * 1024; i++)
3369
+ {
3370
+ largeBuffer.insert(largeBuffer.end(), pattern.begin(), pattern.end());
3371
+ }
3372
+
3373
+ WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", "cat && (echo completed 1>& 2)"}, {}, WSLCProcessFlagsStdin);
3374
+
3375
+ auto process = launcher.Launch(*m_defaultSession);
3376
+
3377
+ std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(process.GetStdHandle(0), largeBuffer));
3378
+ std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
3379
+ extraHandles.emplace_back(std::move(writeStdin));
3380
+
3381
+ auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
3382
+
3383
+ VERIFY_IS_TRUE(std::equal(largeBuffer.begin(), largeBuffer.end(), result.Output[1].begin(), result.Output[1].end()));
3384
+ VERIFY_ARE_EQUAL(result.Output[2], "completed\n");
3385
+
3386
+ // Validate that a null out handle is rejected.
3387
+
3388
+ VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
3389
+ }
3390
+
3391
+ // Create a stuck process and kill it.
3392
+ {
3393
+ WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3394
+
3395
+ auto process = launcher.Launch(*m_defaultSession);
3396
+
3397
+ // Try to send invalid signal to the process
3398
+ VERIFY_ARE_EQUAL(process.Get().Signal(9999), E_FAIL);
3399
+
3400
+ // Send SIGKILL(9) to the process.
3401
+ VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGKILL));
3402
+
3403
+ auto result = process.WaitAndCaptureOutput();
3404
+ VERIFY_ARE_EQUAL(result.Code, WSLCSignalSIGKILL + 128);
3405
+ VERIFY_ARE_EQUAL(result.Output[1], "");
3406
+ VERIFY_ARE_EQUAL(result.Output[2], "");
3407
+
3408
+ // Validate that process can't be signalled after it exited.
3409
+ VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3410
+ }
3411
+
3412
+ // Validate that errno is correctly propagated
3413
+ {
3414
+ WSLCProcessLauncher launcher("doesnotexist", {});
3415
+
3416
+ auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
3417
+ VERIFY_ARE_EQUAL(hresult, E_FAIL);
3418
+ VERIFY_ARE_EQUAL(error, 2); // ENOENT
3419
+ VERIFY_IS_FALSE(process.has_value());
3420
+ }
3421
+
3422
+ {
3423
+ WSLCProcessLauncher launcher("/", {});
3424
+
3425
+ auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
3426
+ VERIFY_ARE_EQUAL(hresult, E_FAIL);
3427
+ VERIFY_ARE_EQUAL(error, 13); // EACCESS
3428
+ VERIFY_IS_FALSE(process.has_value());
3429
+ }
3430
+
3431
+ {
3432
+ WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3433
+
3434
+ auto process = launcher.Launch(*m_defaultSession);
3435
+ auto stdoutHandle = process.GetStdHandle(1);
3436
+
3437
+ COMOutputHandle dummyHandle;
3438
+ // Verify that the same handle can only be acquired once.
3439
+ VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, &dummyHandle), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3440
+
3441
+ // Verify that trying to acquire a std handle that doesn't exist fails as expected.
3442
+ VERIFY_ARE_EQUAL(process.Get().GetStdHandle(static_cast<WSLCFD>(3), &dummyHandle), E_INVALIDARG);
3443
+
3444
+ // Validate that the process object correctly handle requests after the VM has terminated.
3445
+ ResetTestSession();
3446
+ VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
3447
+ }
3448
+
3449
+ // Validate that empty arguments are correctly handled.
3450
+ {
3451
+ WSLCProcessLauncher launcher({"/usr/bin/echo"}, {"/usr/bin/echo", "foo", "", "bar"});
3452
+
3453
+ auto process = launcher.Launch(*m_defaultSession);
3454
+ ValidateProcessOutput(process, {{1, "foo bar\n"}}); // expect two spaces for the empty argument.
3455
+ }
3456
+
3457
+ // Validate error paths
3458
+ {
3459
+ WSLCProcessLauncher launcher("/bin/bash", {"/bin/bash"});
3460
+ launcher.SetUser("nobody"); // Custom users are not supported for root namespace processes.
3461
+
3462
+ auto [hresult, error, process] = launcher.LaunchNoThrow(*m_defaultSession);
3463
+ VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
3464
+ }
3465
+ }
3466
+
3467
+ WSLC_TEST_METHOD(CrashDumpCollection)
3468
+ {
3469
+ int processId = 0;
3470
+
3471
+ // Cache the existing crash dumps so we can check that a new one is created.
3472
+ auto crashDumpsDir = std::filesystem::temp_directory_path() / "wslc-crashes";
3473
+ std::set<std::filesystem::path> existingDumps;
3474
+
3475
+ if (std::filesystem::exists(crashDumpsDir))
3476
+ {
3477
+ existingDumps = {std::filesystem::directory_iterator(crashDumpsDir), std::filesystem::directory_iterator{}};
3478
+ }
3479
+
3480
+ // Create a stuck process and crash it.
3481
+ {
3482
+ WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
3483
+
3484
+ auto process = launcher.Launch(*m_defaultSession);
3485
+
3486
+ // Get the process id. This is need to identify the crash dump file.
3487
+ VERIFY_SUCCEEDED(process.Get().GetPid(&processId));
3488
+
3489
+ // Send SIGSEV(11) to crash the process.
3490
+ VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGSEGV));
3491
+
3492
+ auto result = process.WaitAndCaptureOutput();
3493
+ VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGSEGV);
3494
+ VERIFY_ARE_EQUAL(result.Output[1], "");
3495
+ VERIFY_ARE_EQUAL(result.Output[2], "");
3496
+
3497
+ VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
3498
+ }
3499
+
3500
+ // Dumps files are named with the format: wsl-crash-<sessionId>-<pid>-<processname>-<code>.dmp
3501
+ // Check if a new file was added in crashDumpsDir matching the pattern and not in existingDumps.
3502
+ std::string expectedPattern = std::format("wsl-crash-*-{}-_usr_bin_cat-11.dmp", processId);
3503
+
3504
+ auto dumpFile = wsl::shared::retry::RetryWithTimeout<std::filesystem::path>(
3505
+ [crashDumpsDir, expectedPattern, existingDumps]() {
3506
+ for (const auto& entry : std::filesystem::directory_iterator(crashDumpsDir))
3507
+ {
3508
+ const auto& filePath = entry.path();
3509
+ if (existingDumps.find(filePath) == existingDumps.end() &&
3510
+ PathMatchSpecA(filePath.filename().string().c_str(), expectedPattern.c_str()))
3511
+ {
3512
+ return filePath;
3513
+ }
3514
+ }
3515
+
3516
+ throw wil::ResultException(HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
3517
+ },
3518
+ std::chrono::milliseconds{100},
3519
+ std::chrono::seconds{10});
3520
+
3521
+ // Ensure that the dump file is cleaned up after test completion.
3522
+ auto cleanup = wil::scope_exit([&] {
3523
+ if (std::filesystem::exists(dumpFile))
3524
+ {
3525
+ std::filesystem::remove(dumpFile);
3526
+ }
3527
+ });
3528
+
3529
+ VERIFY_IS_TRUE(std::filesystem::exists(dumpFile));
3530
+ VERIFY_IS_TRUE(std::filesystem::file_size(dumpFile) > 0);
3531
+ }
3532
+
3533
+ WSLC_TEST_METHOD(VhdFormatting)
3534
+ {
3535
+ constexpr auto formatedVhd = L"test-format-vhd.vhdx";
3536
+
3537
+ // TODO: Replace this by a proper SDK method once it exists
3538
+ auto tokenInfo = wil::get_token_information<TOKEN_USER>();
3539
+ wsl::core::filesystem::CreateVhd(formatedVhd, 100 * 1024 * 1024, tokenInfo->User.Sid, false, false);
3540
+
3541
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(formatedVhd)); });
3542
+
3543
+ // Format the disk.
3544
+ auto absoluteVhdPath = std::filesystem::absolute(formatedVhd).wstring();
3545
+ VERIFY_SUCCEEDED(m_defaultSession->FormatVirtualDisk(absoluteVhdPath.c_str()));
3546
+
3547
+ // Validate error paths.
3548
+ VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"DoesNotExist.vhdx"), E_INVALIDARG);
3549
+ VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"C:\\DoesNotExist.vhdx"), HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
3550
+ }
3551
+
3552
+ // Exercises behavior that all volume drivers must implement identically:
3553
+ // create, duplicate-name rejection, multi-mount, cross-container read/write,
3554
+ // in-use deletion rejection, and clean deletion after the referencing container is removed.
3555
+ void ValidateNamedVolumeContract(std::string_view driver, const WSLCDriverOption* driverOpts, ULONG driverOptsCount)
3556
+ {
3557
+ const std::string driverStr(driver);
3558
+ const std::string volumeName = std::format("wslc-test-named-volume-{}", driver);
3559
+
3560
+ // Best-effort cleanup in case of leftovers from a previous failed run.
3561
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3562
+
3563
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3564
+
3565
+ WSLCVolumeOptions volumeOptions{};
3566
+ volumeOptions.Name = volumeName.c_str();
3567
+ volumeOptions.Driver = driverStr.c_str();
3568
+ volumeOptions.DriverOpts = driverOpts;
3569
+ volumeOptions.DriverOptsCount = driverOptsCount;
3570
+
3571
+ // Create volume and validate duplicate volume name handling.
3572
+ WSLCVolumeInformation volInfo{};
3573
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3574
+ VERIFY_ARE_EQUAL(std::string(volInfo.Name), volumeName);
3575
+ VERIFY_ARE_EQUAL(std::string(volInfo.Driver), driverStr);
3576
+ VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&volumeOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3577
+
3578
+ // Verify the same named volume can be mounted more than once with different container paths.
3579
+ {
3580
+ WSLCContainerLauncher duplicateNamedVolumes(
3581
+ "debian:latest",
3582
+ std::format("named-volume-dup-{}", driver),
3583
+ {"/bin/sh", "-c", "echo duplicated >/data-a/dup.txt ; cat /data-b/dup.txt"});
3584
+ duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-a", false);
3585
+ duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-b", true);
3586
+
3587
+ auto duplicateNamedVolumesContainer = duplicateNamedVolumes.Launch(*m_defaultSession);
3588
+ auto duplicateNamedVolumesProcess = duplicateNamedVolumesContainer.GetInitProcess();
3589
+ ValidateProcessOutput(duplicateNamedVolumesProcess, {{1, "duplicated\n"}});
3590
+ }
3591
+
3592
+ // Verify CreateContainer with named volume mounts the volume into the container.
3593
+ {
3594
+ WSLCContainerLauncher writer(
3595
+ "debian:latest",
3596
+ std::format("named-volume-writer-{}", driver),
3597
+ {"/bin/sh", "-c", "echo wslc-named-volume >/data/marker.txt"});
3598
+ writer.AddNamedVolume(volumeName, "/data", false);
3599
+
3600
+ auto writerContainer = writer.Launch(*m_defaultSession);
3601
+ auto writerProcess = writerContainer.GetInitProcess();
3602
+ ValidateProcessOutput(writerProcess, {});
3603
+
3604
+ WSLCContainerLauncher reader(
3605
+ "debian:latest", std::format("named-volume-reader-{}", driver), {"/bin/sh", "-c", "cat /data/marker.txt"});
3606
+ reader.AddNamedVolume(volumeName, "/data", true);
3607
+
3608
+ auto readerContainer = reader.Launch(*m_defaultSession);
3609
+ auto readerProcess = readerContainer.GetInitProcess();
3610
+ ValidateProcessOutput(readerProcess, {{1, "wslc-named-volume\n"}});
3611
+ }
3612
+
3613
+ // Verify we cannot delete a named volume while a container references it.
3614
+ WSLCContainerLauncher holder("debian:latest", std::format("named-volume-holder-{}", driver), {"sleep", "99999"});
3615
+ holder.AddNamedVolume(volumeName, "/data", false);
3616
+
3617
+ auto [holderCreateResult, holderContainerResult] = holder.CreateNoThrow(*m_defaultSession);
3618
+ VERIFY_SUCCEEDED(holderCreateResult);
3619
+ VERIFY_IS_TRUE(holderContainerResult.has_value());
3620
+
3621
+ auto holderContainer = std::move(holderContainerResult.value());
3622
+ holderContainer.SetDeleteOnClose(false);
3623
+
3624
+ VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION));
3625
+
3626
+ // Verify that after deleting the container, the volume can be deleted.
3627
+ VERIFY_SUCCEEDED(holderContainer.Get().Delete(WSLCDeleteFlagsNone));
3628
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3629
+
3630
+ cleanup.release();
3631
+ }
3632
+
3633
+ WSLC_TEST_METHOD(NamedVolumesVhd)
3634
+ {
3635
+ WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3636
+ ValidateNamedVolumeContract("vhd", driverOpts, ARRAYSIZE(driverOpts));
3637
+
3638
+ // VHD-driver-specific: validate the host-side .vhdx artifact and the
3639
+ // /mnt/wslc-volumes ext4 mount inside the VM appear and disappear with
3640
+ // the volume.
3641
+ const std::string volumeName = "wslc-test-named-volume-vhd-host";
3642
+ const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
3643
+
3644
+ WSLCVolumeOptions volumeOptions{};
3645
+ volumeOptions.Name = volumeName.c_str();
3646
+ volumeOptions.Driver = "vhd";
3647
+ volumeOptions.DriverOpts = driverOpts;
3648
+ volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3649
+
3650
+ WSLCVolumeInformation volInfo{};
3651
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3652
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3653
+
3654
+ VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
3655
+ ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::optional<std::string>{"*ext4*"});
3656
+
3657
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3658
+ cleanup.release();
3659
+
3660
+ ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::nullopt);
3661
+ VERIFY_IS_FALSE(std::filesystem::exists(volumeVhdPath));
3662
+ }
3663
+
3664
+ WSLC_TEST_METHOD(NamedVolumesGuest)
3665
+ {
3666
+ ValidateNamedVolumeContract("guest", nullptr, 0);
3667
+ }
3668
+
3669
+ // Verifies that a container using a named volume survives a session restart and the volume's data is preserved.
3670
+ void ValidateNamedVolumeRecoveryContract(std::string_view driver, const WSLCDriverOption* driverOpts, ULONG driverOptsCount)
3671
+ {
3672
+ const std::string driverStr(driver);
3673
+ const std::string volumeName = std::format("wslc-test-named-volume-{}", driver);
3674
+ const std::string containerName = std::format("wslc-test-container-{}", driver);
3675
+
3676
+ // Best-effort cleanup in case prior failed runs left artifacts behind.
3677
+ RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
3678
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3679
+
3680
+ auto cleanup = wil::scope_exit([&]() {
3681
+ RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
3682
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3683
+ });
3684
+
3685
+ WSLCVolumeOptions volumeOptions{};
3686
+ volumeOptions.Name = volumeName.c_str();
3687
+ volumeOptions.Driver = driverStr.c_str();
3688
+ volumeOptions.DriverOpts = driverOpts;
3689
+ volumeOptions.DriverOptsCount = driverOptsCount;
3690
+
3691
+ WSLCVolumeInformation volInfo{};
3692
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3693
+
3694
+ // Create a container that uses the named volume and writes a marker.
3695
+ {
3696
+ WSLCContainerLauncher writer(
3697
+ "debian:latest", containerName, {"/bin/sh", "-c", "echo named-volume-recovery >/data/marker.txt"});
3698
+ writer.AddNamedVolume(volumeName, "/data", false);
3699
+
3700
+ auto writerContainer = writer.Launch(*m_defaultSession);
3701
+ writerContainer.SetDeleteOnClose(false);
3702
+
3703
+ auto writerProcess = writerContainer.GetInitProcess();
3704
+ ValidateProcessOutput(writerProcess, {});
3705
+ }
3706
+
3707
+ // Restart the session and verify the container is recovered.
3708
+ ResetTestSession();
3709
+
3710
+ auto recoveredContainer = OpenContainer(m_defaultSession.get(), containerName);
3711
+ recoveredContainer.SetDeleteOnClose(false);
3712
+
3713
+ // Verify the named volume still contains the marker after restart.
3714
+ {
3715
+ WSLCContainerLauncher reader(
3716
+ "debian:latest", std::format("{}-reader", containerName), {"/bin/sh", "-c", "cat /data/marker.txt"});
3717
+ reader.AddNamedVolume(volumeName, "/data", true);
3718
+
3719
+ auto readerContainer = reader.Launch(*m_defaultSession);
3720
+ auto readerProcess = readerContainer.GetInitProcess();
3721
+ ValidateProcessOutput(readerProcess, {{1, "named-volume-recovery\n"}});
3722
+ }
3723
+ }
3724
+
3725
+ WSLC_TEST_METHOD(NamedVolumeRecovery)
3726
+ {
3727
+ ValidateNamedVolumeRecoveryContract("guest", nullptr, 0);
3728
+ }
3729
+
3730
+ WSLC_TEST_METHOD(NamedVolumesVhdSessionRecovery)
3731
+ {
3732
+
3733
+ WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3734
+ ValidateNamedVolumeRecoveryContract("vhd", driverOpts, ARRAYSIZE(driverOpts));
3735
+
3736
+ // Re-create the volume (the recovery helper cleans up on exit) so we
3737
+ // can test the "delete VHD while session is down" scenario.
3738
+ const std::string volumeName = "wslc-test-named-volume-vhd";
3739
+ const std::string containerName = "wslc-test-container-vhd";
3740
+
3741
+ // Prune containers on exit so this test doesn't leak "wslc-test-container-vhd" on exit.
3742
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
3743
+ PruneResult result;
3744
+ LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
3745
+ });
3746
+
3747
+ WSLCVolumeOptions volumeOptions{};
3748
+ volumeOptions.Name = volumeName.c_str();
3749
+ volumeOptions.Driver = "vhd";
3750
+ volumeOptions.DriverOpts = driverOpts;
3751
+ volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3752
+
3753
+ WSLCVolumeInformation volInfo{};
3754
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3755
+
3756
+ // Create a container that depends on the volume so we can verify it
3757
+ // gets dropped when the backing .vhdx is removed.
3758
+ {
3759
+ WSLCContainerLauncher writer("debian:latest", containerName, {"/bin/sh", "-c", "echo vhd-recovery >/data/marker.txt"});
3760
+ writer.AddNamedVolume(volumeName, "/data", false);
3761
+
3762
+ auto writerContainer = writer.Launch(*m_defaultSession);
3763
+ writerContainer.SetDeleteOnClose(false);
3764
+
3765
+ auto writerProcess = writerContainer.GetInitProcess();
3766
+ ValidateProcessOutput(writerProcess, {});
3767
+ }
3768
+
3769
+ const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
3770
+
3771
+ {
3772
+ auto restartSession = ResetTestSession();
3773
+
3774
+ VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
3775
+
3776
+ std::error_code error;
3777
+ VERIFY_IS_TRUE(std::filesystem::remove(volumeVhdPath, error));
3778
+ VERIFY_ARE_EQUAL(error, std::error_code{});
3779
+ }
3780
+
3781
+ wil::com_ptr<IWSLCContainer> notFound;
3782
+ VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(containerName.c_str(), ¬Found), E_UNEXPECTED);
3783
+
3784
+ // Deleting the named volume should fail since the volume was not recovered.
3785
+ VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), WSLC_E_VOLUME_NOT_FOUND);
3786
+ }
3787
+
3788
+ WSLC_TEST_METHOD(NamedVolumeGuestDriverOptsTest)
3789
+ {
3790
+ const std::string volumeName = "wslc-test-vol";
3791
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3792
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3793
+
3794
+ auto expectReject = [&](const WSLCDriverOption* opts, ULONG optsCount, const std::wstring& expectedMessage) {
3795
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3796
+
3797
+ WSLCVolumeOptions volumeOptions{};
3798
+ volumeOptions.Name = volumeName.c_str();
3799
+ volumeOptions.Driver = "guest";
3800
+ volumeOptions.DriverOpts = opts;
3801
+ volumeOptions.DriverOptsCount = optsCount;
3802
+
3803
+ WSLCVolumeInformation volInfo{};
3804
+ VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&volumeOptions, &volInfo), E_INVALIDARG);
3805
+ ValidateCOMErrorMessageContains(expectedMessage);
3806
+ };
3807
+
3808
+ auto expectAccept = [&](const WSLCDriverOption* opts, ULONG optsCount) {
3809
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3810
+
3811
+ WSLCVolumeOptions volumeOptions{};
3812
+ volumeOptions.Name = volumeName.c_str();
3813
+ volumeOptions.Driver = "guest";
3814
+ volumeOptions.DriverOpts = opts;
3815
+ volumeOptions.DriverOptsCount = optsCount;
3816
+
3817
+ WSLCVolumeInformation volInfo{};
3818
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
3819
+ };
3820
+
3821
+ // Allowed: no options (nullptr).
3822
+ expectAccept(nullptr, 0);
3823
+
3824
+ // Allowed: type=tmpfs with device=tmpfs.
3825
+ {
3826
+ WSLCDriverOption opts[] = {{"type", "tmpfs"}, {"device", "tmpfs"}};
3827
+ expectAccept(opts, ARRAYSIZE(opts));
3828
+ }
3829
+
3830
+ // Allowed: type=tmpfs with device=tmpfs and o= suboptions.
3831
+ {
3832
+ WSLCDriverOption opts[] = {{"type", "tmpfs"}, {"device", "tmpfs"}, {"o", "size=100m,uid=1000"}};
3833
+ expectAccept(opts, ARRAYSIZE(opts));
3834
+ }
3835
+
3836
+ // Blocked: type=none (bind mount).
3837
+ {
3838
+ WSLCDriverOption opts[] = {{"type", "none"}};
3839
+ expectReject(opts, ARRAYSIZE(opts), L"unsupported volume driver options: type=none");
3840
+ }
3841
+
3842
+ // Blocked: type=nfs.
3843
+ {
3844
+ WSLCDriverOption opts[] = {{"type", "nfs"}};
3845
+ expectReject(opts, ARRAYSIZE(opts), L"unsupported volume driver options: type=nfs");
3846
+ }
3847
+
3848
+ // Blocked by Docker: device without type.
3849
+ {
3850
+ WSLCDriverOption opts[] = {{"device", "/some/path"}};
3851
+ expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3852
+ }
3853
+
3854
+ // Blocked by Docker: device=tmpfs without type.
3855
+ {
3856
+ WSLCDriverOption opts[] = {{"device", "tmpfs"}};
3857
+ expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3858
+ }
3859
+
3860
+ // Blocked by Docker: device and o without type.
3861
+ {
3862
+ WSLCDriverOption opts[] = {{"device", "tmpfs"}, {"o", "size=100m"}};
3863
+ expectReject(opts, ARRAYSIZE(opts), L"create wslc-test-vol: missing required option: \"type\"");
3864
+ }
3865
+ }
3866
+
3867
+ WSLC_TEST_METHOD(NamedVolumeVhdOptionsParseTest)
3868
+ {
3869
+ const std::string volumeName = "wslc-volume-name";
3870
+
3871
+ auto validateInvalidOptionsFailure = [&](const WSLCDriverOption* opts,
3872
+ ULONG optsCount,
3873
+ HRESULT expectedResult,
3874
+ const std::optional<std::wstring>& expectedMessage = std::nullopt) {
3875
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
3876
+
3877
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
3878
+
3879
+ WSLCVolumeOptions volumeOptions{};
3880
+ volumeOptions.Name = volumeName.c_str();
3881
+ volumeOptions.Driver = "vhd";
3882
+ volumeOptions.DriverOpts = opts;
3883
+ volumeOptions.DriverOptsCount = optsCount;
3884
+
3885
+ WSLCVolumeInformation volInfo{};
3886
+ const auto result = m_defaultSession->CreateVolume(&volumeOptions, &volInfo);
3887
+
3888
+ if (result != expectedResult)
3889
+ {
3890
+ LogInfo("CreateVolume mismatch result=0x%08x expected=0x%08x", static_cast<unsigned int>(result), static_cast<unsigned int>(expectedResult));
3891
+ }
3892
+
3893
+ VERIFY_ARE_EQUAL(result, expectedResult);
3894
+ if (expectedMessage.has_value())
3895
+ {
3896
+ ValidateCOMErrorMessage(expectedMessage);
3897
+ }
3898
+ };
3899
+
3900
+ // Missing SizeBytes.
3901
+ validateInvalidOptionsFailure(nullptr, 0, E_INVALIDARG, L"Missing required option: 'SizeBytes'");
3902
+
3903
+ WSLCDriverOption wrongOption[] = {{"WrongOption", "value"}};
3904
+ validateInvalidOptionsFailure(wrongOption, ARRAYSIZE(wrongOption), E_INVALIDARG, L"Missing required option: 'SizeBytes'");
3905
+
3906
+ // Invalid SizeBytes values.
3907
+ WSLCDriverOption emptySize[] = {{"SizeBytes", ""}};
3908
+ validateInvalidOptionsFailure(emptySize, ARRAYSIZE(emptySize), E_INVALIDARG, L"Invalid size: ");
3909
+
3910
+ WSLCDriverOption zeroSize[] = {{"SizeBytes", "0"}};
3911
+ validateInvalidOptionsFailure(zeroSize, ARRAYSIZE(zeroSize), E_INVALIDARG, L"Invalid size: 0");
3912
+
3913
+ WSLCDriverOption invalidSizeAbc[] = {{"SizeBytes", "abc"}};
3914
+ validateInvalidOptionsFailure(invalidSizeAbc, ARRAYSIZE(invalidSizeAbc), E_INVALIDARG, L"Invalid size: abc");
3915
+
3916
+ WSLCDriverOption invalidSizeMixed[] = {{"SizeBytes", "123abc"}};
3917
+ validateInvalidOptionsFailure(invalidSizeMixed, ARRAYSIZE(invalidSizeMixed), E_INVALIDARG, L"Invalid size: 123abc");
3918
+
3919
+ WSLCDriverOption invalidSizeSign[] = {{"SizeBytes", "+-1"}};
3920
+ validateInvalidOptionsFailure(invalidSizeSign, ARRAYSIZE(invalidSizeSign), E_INVALIDARG, L"Invalid size: +-1");
3921
+
3922
+ WSLCDriverOption invalidSizeOverflow[] = {{"SizeBytes", "18446744073709551616"}};
3923
+ validateInvalidOptionsFailure(
3924
+ invalidSizeOverflow, ARRAYSIZE(invalidSizeOverflow), E_INVALIDARG, L"Invalid size: 18446744073709551616");
3925
+
3926
+ WSLCDriverOption invalidSizeNeg[] = {{"SizeBytes", "-1"}};
3927
+ validateInvalidOptionsFailure(invalidSizeNeg, ARRAYSIZE(invalidSizeNeg), E_INVALIDARG, L"Invalid size: -1");
3928
+ }
3929
+
3930
+ WSLC_TEST_METHOD(ListAndInspectNamedVolumesTest)
3931
+ {
3932
+ const std::string vhdVolumeName = "wsla-test-vol-vhd";
3933
+ const std::string guestVolumeName = "wsla-test-vol-guest";
3934
+
3935
+ auto cleanup = wil::scope_exit([&]() {
3936
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(vhdVolumeName.c_str()));
3937
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(guestVolumeName.c_str()));
3938
+ });
3939
+
3940
+ // Verify empty list is returned when no volumes exist.
3941
+ wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
3942
+ VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3943
+ VERIFY_ARE_EQUAL(0u, volumes.size());
3944
+
3945
+ // Create a VHD volume and verify list returns one entry.
3946
+ WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
3947
+
3948
+ WSLCVolumeOptions vhdOptions{};
3949
+ vhdOptions.Name = vhdVolumeName.c_str();
3950
+ vhdOptions.Driver = "vhd";
3951
+ vhdOptions.DriverOpts = driverOpts;
3952
+ vhdOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3953
+
3954
+ WSLCVolumeInformation volInfo{};
3955
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&vhdOptions, &volInfo));
3956
+
3957
+ VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3958
+ VERIFY_ARE_EQUAL(1u, volumes.size());
3959
+ VERIFY_ARE_EQUAL(std::string(volumes[0].Name), vhdVolumeName);
3960
+ VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("vhd"));
3961
+
3962
+ // Verify that a guest volume cannot be created with the same name as an existing vhd volume.
3963
+ WSLCVolumeOptions duplicateGuestOptions{};
3964
+ duplicateGuestOptions.Name = vhdVolumeName.c_str();
3965
+ duplicateGuestOptions.Driver = "guest";
3966
+ VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&duplicateGuestOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3967
+
3968
+ // Create a guest volume and verify both drivers show up in the list.
3969
+ WSLCVolumeOptions guestOptions{};
3970
+ guestOptions.Name = guestVolumeName.c_str();
3971
+ guestOptions.Driver = "guest";
3972
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&guestOptions, &volInfo));
3973
+
3974
+ // Verify that a vhd volume cannot be created with the same name as an existing guest volume.
3975
+ WSLCVolumeOptions duplicateVhdOptions{};
3976
+ duplicateVhdOptions.Name = guestVolumeName.c_str();
3977
+ duplicateVhdOptions.Driver = "vhd";
3978
+ duplicateVhdOptions.DriverOpts = driverOpts;
3979
+ duplicateVhdOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
3980
+ VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&duplicateVhdOptions, &volInfo), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
3981
+
3982
+ VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
3983
+ VERIFY_ARE_EQUAL(2u, volumes.size());
3984
+
3985
+ std::map<std::string, std::string> namesToDrivers;
3986
+ for (const auto& v : volumes)
3987
+ {
3988
+ namesToDrivers.emplace(v.Name, v.Driver);
3989
+ }
3990
+
3991
+ VERIFY_ARE_EQUAL(namesToDrivers[vhdVolumeName], std::string("vhd"));
3992
+ VERIFY_ARE_EQUAL(namesToDrivers[guestVolumeName], std::string("guest"));
3993
+
3994
+ // Verify InspectVolume returns correct details for the VHD volume (driver opts present).
3995
+ wil::unique_cotaskmem_ansistring output;
3996
+ VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(vhdVolumeName.c_str(), &output));
3997
+ VERIFY_IS_NOT_NULL(output.get());
3998
+
3999
+ auto vhdInspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
4000
+ VERIFY_ARE_EQUAL(vhdInspect.Name, vhdVolumeName);
4001
+ VERIFY_ARE_EQUAL(vhdInspect.Driver, std::string("vhd"));
4002
+ VERIFY_IS_TRUE(vhdInspect.DriverOpts.contains("SizeBytes"));
4003
+
4004
+ // Verify InspectVolume returns correct details for the guest volume (no driver opts).
4005
+ output.reset();
4006
+ VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(guestVolumeName.c_str(), &output));
4007
+ VERIFY_IS_NOT_NULL(output.get());
4008
+
4009
+ auto guestInspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
4010
+ VERIFY_ARE_EQUAL(guestInspect.Name, guestVolumeName);
4011
+ VERIFY_ARE_EQUAL(guestInspect.Driver, std::string("guest"));
4012
+ VERIFY_IS_TRUE(guestInspect.DriverOpts.empty());
4013
+
4014
+ // Verify InspectVolume fails for a non-existent volume.
4015
+ output.reset();
4016
+ VERIFY_ARE_EQUAL(m_defaultSession->InspectVolume("does-not-exist", &output), WSLC_E_VOLUME_NOT_FOUND);
4017
+
4018
+ // Delete the VHD volume and verify only the guest volume remains.
4019
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(vhdVolumeName.c_str()));
4020
+ VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
4021
+ VERIFY_ARE_EQUAL(1u, volumes.size());
4022
+ VERIFY_ARE_EQUAL(std::string(volumes[0].Name), guestVolumeName);
4023
+ VERIFY_ARE_EQUAL(std::string(volumes[0].Driver), std::string("guest"));
4024
+ }
4025
+
4026
+ WSLC_TEST_METHOD(NetworkCreateDeleteListTest)
4027
+ {
4028
+ const std::string networkName = "test-network";
4029
+
4030
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4031
+
4032
+ // List should start empty.
4033
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4034
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4035
+ VERIFY_ARE_EQUAL(0u, networks.size());
4036
+
4037
+ WSLCNetworkOptions options{};
4038
+ options.Name = networkName.c_str();
4039
+ options.Driver = "bridge";
4040
+ options.DriverOpts = nullptr;
4041
+ options.DriverOptsCount = 0;
4042
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4043
+
4044
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4045
+
4046
+ // Verify it appears in the list with correct fields.
4047
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4048
+ VERIFY_ARE_EQUAL(1u, networks.size());
4049
+ VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4050
+ VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
4051
+ VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
4052
+
4053
+ // Duplicate name should fail.
4054
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_defaultSession->CreateNetwork(&options));
4055
+
4056
+ cleanup.release();
4057
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4058
+
4059
+ // List should be empty again.
4060
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4061
+ VERIFY_ARE_EQUAL(0u, networks.size());
4062
+
4063
+ // Delete non-existent should fail.
4064
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->DeleteNetwork(networkName.c_str()));
4065
+ }
4066
+
4067
+ WSLC_TEST_METHOD(NetworkCreateWithSubnetTest)
4068
+ {
4069
+ const std::string networkName = "subnet-test-net";
4070
+
4071
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4072
+
4073
+ WSLCDriverOption subnetOpt[] = {{"Subnet", "172.28.0.0/16"}};
4074
+
4075
+ WSLCNetworkOptions options{};
4076
+ options.Name = networkName.c_str();
4077
+ options.Driver = "bridge";
4078
+ options.DriverOpts = subnetOpt;
4079
+ options.DriverOptsCount = ARRAYSIZE(subnetOpt);
4080
+
4081
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4082
+
4083
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4084
+
4085
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4086
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4087
+ VERIFY_ARE_EQUAL(1u, networks.size());
4088
+ VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4089
+ }
4090
+
4091
+ WSLC_TEST_METHOD(NetworkCreateInternalTest)
4092
+ {
4093
+ const std::string networkName = "internal-test-net";
4094
+
4095
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4096
+
4097
+ WSLCDriverOption internalOpt[] = {{"Internal", "true"}};
4098
+
4099
+ WSLCNetworkOptions options{};
4100
+ options.Name = networkName.c_str();
4101
+ options.Driver = "bridge";
4102
+ options.DriverOpts = internalOpt;
4103
+ options.DriverOptsCount = ARRAYSIZE(internalOpt);
4104
+
4105
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4106
+
4107
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4108
+
4109
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4110
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4111
+ VERIFY_ARE_EQUAL(1u, networks.size());
4112
+ VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4113
+ }
4114
+
4115
+ WSLC_TEST_METHOD(NetworkCreateWithLabelsTest)
4116
+ {
4117
+ const std::string networkName = "labels-test-net";
4118
+
4119
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4120
+
4121
+ WSLCLabel labels[] = {
4122
+ {.Key = "com.example.env", .Value = "test"},
4123
+ {.Key = "com.example.team", .Value = "infra"},
4124
+ };
4125
+
4126
+ WSLCNetworkOptions options{};
4127
+ options.Name = networkName.c_str();
4128
+ options.Driver = "bridge";
4129
+ options.DriverOpts = nullptr;
4130
+ options.DriverOptsCount = 0;
4131
+ options.Labels = labels;
4132
+ options.LabelsCount = ARRAYSIZE(labels);
4133
+
4134
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4135
+
4136
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4137
+
4138
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4139
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4140
+ VERIFY_ARE_EQUAL(1u, networks.size());
4141
+ VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4142
+ }
4143
+
4144
+ WSLC_TEST_METHOD(NetworkCreateInvalidDriverTest)
4145
+ {
4146
+ WSLCNetworkOptions options{};
4147
+ options.Name = "bad-driver-net";
4148
+ options.DriverOpts = nullptr;
4149
+ options.DriverOptsCount = 0;
4150
+
4151
+ for (const char* driver : {"overlay", "Bridge", ""})
4152
+ {
4153
+ options.Driver = driver;
4154
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4155
+ ValidateCOMErrorMessageContains(L"Unsupported network driver:");
4156
+ }
4157
+ }
4158
+
4159
+ WSLC_TEST_METHOD(NetworkCreateReservedNameTest)
4160
+ {
4161
+ WSLCNetworkOptions options{};
4162
+ options.Driver = "bridge";
4163
+ options.DriverOpts = nullptr;
4164
+ options.DriverOptsCount = 0;
4165
+
4166
+ options.Name = "bridge";
4167
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4168
+ ValidateCOMErrorMessageContains(L"bridge");
4169
+
4170
+ options.Name = "host";
4171
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4172
+ ValidateCOMErrorMessageContains(L"host");
4173
+
4174
+ options.Name = "none";
4175
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4176
+ ValidateCOMErrorMessageContains(L"none");
4177
+ }
4178
+
4179
+ WSLC_TEST_METHOD(NetworkCreateInvalidNameTest)
4180
+ {
4181
+ WSLCNetworkOptions options{};
4182
+ options.Name = "invalid name!";
4183
+ options.Driver = "bridge";
4184
+ options.DriverOpts = nullptr;
4185
+ options.DriverOptsCount = 0;
4186
+
4187
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4188
+ ValidateCOMErrorMessageContains(L"invalid name!");
4189
+ }
4190
+
4191
+ WSLC_TEST_METHOD(NetworkCreateInvalidSubnetTest)
4192
+ {
4193
+ const std::string networkName = "bad-subnet-net";
4194
+
4195
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4196
+
4197
+ WSLCDriverOption opts[] = {{"Subnet", "not-a-cidr"}};
4198
+
4199
+ WSLCNetworkOptions options{};
4200
+ options.Name = networkName.c_str();
4201
+ options.Driver = "bridge";
4202
+ options.DriverOpts = opts;
4203
+ options.DriverOptsCount = ARRAYSIZE(opts);
4204
+
4205
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4206
+ ValidateCOMErrorMessageContains(L"invalid subnet");
4207
+
4208
+ wil::unique_cotaskmem_ansistring output;
4209
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4210
+ }
4211
+
4212
+ WSLC_TEST_METHOD(NetworkCreateInvalidGatewayTest)
4213
+ {
4214
+ const std::string networkName = "bad-gateway-net";
4215
+
4216
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4217
+
4218
+ WSLCDriverOption opts[] = {{"Subnet", "172.27.0.0/16"}, {"Gateway", "999.999.999.999"}};
4219
+
4220
+ WSLCNetworkOptions options{};
4221
+ options.Name = networkName.c_str();
4222
+ options.Driver = "bridge";
4223
+ options.DriverOpts = opts;
4224
+ options.DriverOptsCount = ARRAYSIZE(opts);
4225
+
4226
+ VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->CreateNetwork(&options));
4227
+ ValidateCOMErrorMessageContains(L"invalid gateway");
4228
+
4229
+ wil::unique_cotaskmem_ansistring output;
4230
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4231
+ }
4232
+
4233
+ WSLC_TEST_METHOD(NetworkCreateWithGatewayTest)
4234
+ {
4235
+ const std::string networkName = "gateway-test-net";
4236
+
4237
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4238
+
4239
+ WSLCDriverOption opts[] = {{"Subnet", "172.31.0.0/16"}, {"Gateway", "172.31.0.1"}};
4240
+
4241
+ WSLCNetworkOptions options{};
4242
+ options.Name = networkName.c_str();
4243
+ options.Driver = "bridge";
4244
+ options.DriverOpts = opts;
4245
+ options.DriverOptsCount = ARRAYSIZE(opts);
4246
+
4247
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4248
+
4249
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4250
+
4251
+ wil::unique_cotaskmem_ansistring output;
4252
+ VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4253
+ VERIFY_IS_NOT_NULL(output.get());
4254
+
4255
+ auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4256
+ VERIFY_IS_TRUE(inspect.IPAM.Config.has_value());
4257
+ VERIFY_ARE_EQUAL(1u, inspect.IPAM.Config->size());
4258
+ VERIFY_ARE_EQUAL(std::string("172.31.0.0/16"), inspect.IPAM.Config->at(0).Subnet);
4259
+ VERIFY_ARE_EQUAL(std::string("172.31.0.1"), inspect.IPAM.Config->at(0).Gateway);
4260
+ }
4261
+
4262
+ WSLC_TEST_METHOD(NetworkSessionRecoveryTest)
4263
+ {
4264
+ const std::string networkName = "recovery-test-net";
4265
+
4266
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4267
+
4268
+ WSLCNetworkOptions options{};
4269
+ options.Name = networkName.c_str();
4270
+ options.Driver = "bridge";
4271
+ options.DriverOpts = nullptr;
4272
+ options.DriverOptsCount = 0;
4273
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4274
+
4275
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4276
+
4277
+ // Reset the session (simulates session restart).
4278
+ ResetTestSession();
4279
+
4280
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4281
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4282
+ VERIFY_ARE_EQUAL(1u, networks.size());
4283
+ VERIFY_ARE_EQUAL(networkName, std::string(networks[0].Name));
4284
+ VERIFY_ARE_EQUAL(std::string("bridge"), std::string(networks[0].Driver));
4285
+ VERIFY_IS_TRUE(strlen(networks[0].Id) > 0);
4286
+ }
4287
+
4288
+ WSLC_TEST_METHOD(NetworkMultipleCreateListDeleteTest)
4289
+ {
4290
+ const std::string networkNameA = "net-a";
4291
+ const std::string networkNameB = "net-b";
4292
+ const std::string networkNameC = "net-c";
4293
+
4294
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameA.c_str()));
4295
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4296
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameC.c_str()));
4297
+
4298
+ auto cleanup = wil::scope_exit([&]() {
4299
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameA.c_str()));
4300
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4301
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkNameC.c_str()));
4302
+ });
4303
+
4304
+ WSLCNetworkOptions optionsA{};
4305
+ optionsA.Name = networkNameA.c_str();
4306
+ optionsA.Driver = "bridge";
4307
+ optionsA.DriverOpts = nullptr;
4308
+ optionsA.DriverOptsCount = 0;
4309
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsA));
4310
+
4311
+ WSLCDriverOption subnetOpt[] = {{"Subnet", "172.29.0.0/16"}};
4312
+ WSLCNetworkOptions optionsB{};
4313
+ optionsB.Name = networkNameB.c_str();
4314
+ optionsB.Driver = "bridge";
4315
+ optionsB.DriverOpts = subnetOpt;
4316
+ optionsB.DriverOptsCount = ARRAYSIZE(subnetOpt);
4317
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsB));
4318
+
4319
+ WSLCDriverOption internalOpt[] = {{"Internal", "true"}};
4320
+ WSLCNetworkOptions optionsC{};
4321
+ optionsC.Name = networkNameC.c_str();
4322
+ optionsC.Driver = "bridge";
4323
+ optionsC.DriverOpts = internalOpt;
4324
+ optionsC.DriverOptsCount = ARRAYSIZE(internalOpt);
4325
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&optionsC));
4326
+
4327
+ wil::unique_cotaskmem_array_ptr<WSLCNetworkInformation> networks;
4328
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4329
+ VERIFY_ARE_EQUAL(3u, networks.size());
4330
+
4331
+ VERIFY_SUCCEEDED(m_defaultSession->DeleteNetwork(networkNameB.c_str()));
4332
+ VERIFY_SUCCEEDED(m_defaultSession->ListNetworks(networks.addressof(), networks.size_address<ULONG>()));
4333
+ VERIFY_ARE_EQUAL(2u, networks.size());
4334
+ }
4335
+
4336
+ WSLC_TEST_METHOD(NetworkInspectTest)
4337
+ {
4338
+ const std::string networkName = "test-inspect-network";
4339
+
4340
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4341
+
4342
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4343
+
4344
+ WSLCNetworkOptions options{};
4345
+ options.Name = networkName.c_str();
4346
+ options.Driver = "bridge";
4347
+ options.DriverOpts = nullptr;
4348
+ options.DriverOptsCount = 0;
4349
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4350
+
4351
+ wil::unique_cotaskmem_ansistring output;
4352
+ VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4353
+ VERIFY_IS_NOT_NULL(output.get());
4354
+
4355
+ auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4356
+ VERIFY_ARE_EQUAL(inspect.Name, networkName);
4357
+ VERIFY_ARE_EQUAL(inspect.Driver, std::string("bridge"));
4358
+ VERIFY_IS_FALSE(inspect.Id.empty());
4359
+ VERIFY_IS_FALSE(inspect.Internal);
4360
+ }
4361
+
4362
+ WSLC_TEST_METHOD(NetworkInspectWithSubnetTest)
4363
+ {
4364
+ const std::string networkName = "test-inspect-subnet-net";
4365
+
4366
+ LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str()));
4367
+
4368
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteNetwork(networkName.c_str())); });
4369
+
4370
+ WSLCDriverOption subnetOpt[] = {{"Subnet", "172.30.0.0/16"}};
4371
+
4372
+ WSLCNetworkOptions options{};
4373
+ options.Name = networkName.c_str();
4374
+ options.Driver = "bridge";
4375
+ options.DriverOpts = subnetOpt;
4376
+ options.DriverOptsCount = ARRAYSIZE(subnetOpt);
4377
+ VERIFY_SUCCEEDED(m_defaultSession->CreateNetwork(&options));
4378
+
4379
+ wil::unique_cotaskmem_ansistring output;
4380
+ VERIFY_SUCCEEDED(m_defaultSession->InspectNetwork(networkName.c_str(), &output));
4381
+ VERIFY_IS_NOT_NULL(output.get());
4382
+
4383
+ auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectNetwork>(output.get());
4384
+ VERIFY_ARE_EQUAL(inspect.Name, networkName);
4385
+ VERIFY_ARE_EQUAL(inspect.Driver, std::string("bridge"));
4386
+ VERIFY_IS_TRUE(inspect.IPAM.Config.has_value());
4387
+ VERIFY_ARE_EQUAL(1u, inspect.IPAM.Config->size());
4388
+ VERIFY_ARE_EQUAL(std::string("172.30.0.0/16"), inspect.IPAM.Config->at(0).Subnet);
4389
+ }
4390
+
4391
+ WSLC_TEST_METHOD(NetworkInspectNotFoundTest)
4392
+ {
4393
+ wil::unique_cotaskmem_ansistring output;
4394
+ auto hr = m_defaultSession->InspectNetwork("nonexistent-network", &output);
4395
+ VERIFY_ARE_EQUAL(WSLC_E_NETWORK_NOT_FOUND, hr);
4396
+ ValidateCOMErrorMessageContains(L"nonexistent-network");
4397
+ }
4398
+
4399
+ WSLC_TEST_METHOD(CreateContainer)
4400
+ {
4401
+ // Test a simple container start.
4402
+ {
4403
+ WSLCContainerLauncher launcher("debian:latest", "test-simple", {"echo", "OK"});
4404
+ auto container = launcher.Launch(*m_defaultSession);
4405
+ auto process = container.GetInitProcess();
4406
+
4407
+ ValidateProcessOutput(process, {{1, "OK\n"}});
4408
+
4409
+ // Validate that GetInitProcess fails with the process argument is null.
4410
+ VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), container.Get().GetInitProcess(nullptr));
4411
+ }
4412
+
4413
+ // Validate that env is correctly wired.
4414
+ {
4415
+ WSLCContainerLauncher launcher("debian:latest", "test-env", {"/bin/sh", "-c", "echo $testenv"}, {{"testenv=testvalue"}});
4416
+ auto container = launcher.Launch(*m_defaultSession);
4417
+ auto process = container.GetInitProcess();
4418
+
4419
+ ValidateProcessOutput(process, {{1, "testvalue\n"}});
4420
+ }
4421
+
4422
+ // Validate that exit codes are correctly wired.
4423
+ {
4424
+ WSLCContainerLauncher launcher("debian:latest", "test-exit-code", {"/bin/sh", "-c", "exit 12"});
4425
+ auto container = launcher.Launch(*m_defaultSession);
4426
+ auto process = container.GetInitProcess();
4427
+
4428
+ ValidateProcessOutput(process, {}, 12);
4429
+ }
4430
+
4431
+ // Validate that stdin is correctly wired
4432
+ {
4433
+ WSLCContainerLauncher launcher(
4434
+ "debian:latest", "test-default-entrypoint", {"/bin/cat"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost, WSLCProcessFlagsStdin);
4435
+
4436
+ auto container = launcher.Launch(*m_defaultSession);
4437
+
4438
+ auto process = container.GetInitProcess();
4439
+ auto input = process.GetStdHandle(0);
4440
+
4441
+ std::string shellInput = "foo";
4442
+ std::vector<char> inputBuffer{shellInput.begin(), shellInput.end()};
4443
+
4444
+ std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(std::move(input), inputBuffer));
4445
+
4446
+ std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
4447
+ extraHandles.emplace_back(std::move(writeStdin));
4448
+
4449
+ auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
4450
+
4451
+ VERIFY_ARE_EQUAL(result.Output[2], "");
4452
+ VERIFY_ARE_EQUAL(result.Output[1], "foo");
4453
+ }
4454
+
4455
+ // Validate that stdin behaves correctly if closed without any input.
4456
+ {
4457
+ WSLCContainerLauncher launcher("debian:latest", "test-stdin", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4458
+ auto container = launcher.Launch(*m_defaultSession);
4459
+ auto process = container.GetInitProcess();
4460
+ process.GetStdHandle(0); // Close stdin;
4461
+
4462
+ ValidateProcessOutput(process, {{1, ""}});
4463
+ }
4464
+
4465
+ // Validate that the default stop signal is respected.
4466
+ {
4467
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4468
+ launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
4469
+ launcher.SetContainerFlags(WSLCContainerFlagsInit);
4470
+
4471
+ auto container = launcher.Launch(*m_defaultSession);
4472
+ auto process = container.GetInitProcess();
4473
+
4474
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, 60));
4475
+
4476
+ // Validate that the init process exited with the expected signal.
4477
+ VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGHUP + 128);
4478
+ }
4479
+
4480
+ // Validate that the default stop signal can be overriden.
4481
+ {
4482
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-2", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4483
+ launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
4484
+ launcher.SetContainerFlags(WSLCContainerFlagsInit);
4485
+
4486
+ auto container = launcher.Launch(*m_defaultSession);
4487
+ auto process = container.GetInitProcess();
4488
+
4489
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 60));
4490
+
4491
+ // Validate that the init process exited with the expected signal.
4492
+ VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGKILL + 128);
4493
+ }
4494
+
4495
+ // Validate that entrypoint is respected.
4496
+ {
4497
+ WSLCContainerLauncher launcher("debian:latest", "test-entrypoint", {"OK"});
4498
+ launcher.SetEntrypoint({"/bin/echo", "-n"});
4499
+
4500
+ auto container = launcher.Launch(*m_defaultSession);
4501
+ auto process = container.GetInitProcess();
4502
+ ValidateProcessOutput(process, {{1, "OK"}});
4503
+ }
4504
+
4505
+ // Validate that the working directory is correctly wired.
4506
+ {
4507
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"pwd"});
4508
+ launcher.SetWorkingDirectory("/tmp");
4509
+
4510
+ auto container = launcher.Launch(*m_defaultSession);
4511
+ auto process = container.GetInitProcess();
4512
+ ValidateProcessOutput(process, {{1, "/tmp\n"}});
4513
+ }
4514
+
4515
+ // Validate that the current directory is created if it doesn't exist.
4516
+ {
4517
+ WSLCContainerLauncher launcher("debian:latest", "test-bad-cwd", {"pwd"});
4518
+ launcher.SetWorkingDirectory("/new-dir");
4519
+
4520
+ auto container = launcher.Launch(*m_defaultSession);
4521
+ auto process = container.GetInitProcess();
4522
+
4523
+ ValidateProcessOutput(process, {{1, "/new-dir\n"}});
4524
+ }
4525
+
4526
+ // Validate that hostname and domainanme are correctly wired.
4527
+ {
4528
+ WSLCContainerLauncher launcher("debian:latest", "test-hostname", {"/bin/sh", "-c", "echo $(hostname).$(domainname)"});
4529
+
4530
+ launcher.SetHostname("my-host-name");
4531
+ launcher.SetDomainname("my-domain-name");
4532
+
4533
+ auto container = launcher.Launch(*m_defaultSession);
4534
+ auto process = container.GetInitProcess();
4535
+ ValidateProcessOutput(process, {{1, "my-host-name.my-domain-name\n"}});
4536
+ }
4537
+
4538
+ // Validate that containers without DNS configuration use default DNS.
4539
+ {
4540
+ WSLCContainerLauncher launcher("debian:latest", "test-no-dns", {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"});
4541
+
4542
+ auto container = launcher.Launch(*m_defaultSession);
4543
+ auto process = container.GetInitProcess();
4544
+ ValidateProcessOutput(process, {}, 0);
4545
+ }
4546
+
4547
+ // Validate that custom DNS servers are correctly wired.
4548
+ {
4549
+ WSLCContainerLauncher launcher(
4550
+ "debian:latest", "test-dns-custom", {"/bin/grep", "-iF", "nameserver 1.2.3.4", "/etc/resolv.conf"});
4551
+
4552
+ launcher.SetDnsServers({"1.2.3.4"});
4553
+
4554
+ auto container = launcher.Launch(*m_defaultSession);
4555
+ auto process = container.GetInitProcess();
4556
+ ValidateProcessOutput(process, {}, 0);
4557
+ }
4558
+
4559
+ // Validate that custom DNS search domains are correctly wired.
4560
+ {
4561
+ WSLCContainerLauncher launcher(
4562
+ "debian:latest", "test-dns-search", {"/bin/grep", "-iF", "test.local", "/etc/resolv.conf"});
4563
+
4564
+ launcher.SetDnsSearchDomains({"test.local"});
4565
+
4566
+ auto container = launcher.Launch(*m_defaultSession);
4567
+ auto process = container.GetInitProcess();
4568
+ ValidateProcessOutput(process, {}, 0);
4569
+ }
4570
+
4571
+ // Validate that custom DNS options are correctly wired.
4572
+ {
4573
+ WSLCContainerLauncher launcher(
4574
+ "debian:latest", "test-dns-options", {"/bin/grep", "-iF", "timeout:1", "/etc/resolv.conf"});
4575
+
4576
+ launcher.SetDnsOptions({"timeout:1"});
4577
+
4578
+ auto container = launcher.Launch(*m_defaultSession);
4579
+ auto process = container.GetInitProcess();
4580
+ ValidateProcessOutput(process, {}, 0);
4581
+ }
4582
+
4583
+ // Validate that multiple DNS options are correctly wired.
4584
+ {
4585
+ WSLCContainerLauncher launcher(
4586
+ "debian:latest", "test-dns-options-multiple", {"/bin/grep", "-iF", "timeout:2", "/etc/resolv.conf"});
4587
+
4588
+ launcher.SetDnsOptions({"timeout:1", "timeout:2"});
4589
+
4590
+ auto container = launcher.Launch(*m_defaultSession);
4591
+ auto process = container.GetInitProcess();
4592
+ ValidateProcessOutput(process, {}, 0);
4593
+ }
4594
+
4595
+ // Validate that the username is correctly wired.
4596
+ {
4597
+ WSLCContainerLauncher launcher("debian:latest", "test-username", {"whoami"});
4598
+
4599
+ launcher.SetUser("nobody");
4600
+
4601
+ auto container = launcher.Launch(*m_defaultSession);
4602
+ auto process = container.GetInitProcess();
4603
+ ValidateProcessOutput(process, {{1, "nobody\n"}});
4604
+ }
4605
+
4606
+ // Validate that the group is correctly wired.
4607
+ {
4608
+ WSLCContainerLauncher launcher("debian:latest", "test-group", {"groups"});
4609
+
4610
+ launcher.SetUser("nobody:www-data");
4611
+
4612
+ auto container = launcher.Launch(*m_defaultSession);
4613
+ auto process = container.GetInitProcess();
4614
+ ValidateProcessOutput(process, {{1, "www-data\n"}});
4615
+ }
4616
+
4617
+ // Validate that the container behaves correctly if the caller keeps a reference to an init process during termination.
4618
+ {
4619
+ WSLCContainerLauncher launcher("debian:latest", "test-init-ref", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
4620
+
4621
+ auto container = launcher.Launch(*m_defaultSession);
4622
+ auto containerId = container.Id();
4623
+
4624
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
4625
+ wil::com_ptr<IWSLCContainer> openedContainer;
4626
+ VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerId.c_str(), &openedContainer));
4627
+ VERIFY_SUCCEEDED(openedContainer->Delete(WSLCDeleteFlagsNone));
4628
+ });
4629
+
4630
+ auto process = container.GetInitProcess();
4631
+
4632
+ VERIFY_ARE_EQUAL(process.State(), WslcProcessStateRunning);
4633
+
4634
+ // Terminate the session.
4635
+ ResetTestSession();
4636
+
4637
+ WSLCProcessState processState{};
4638
+ int exitCode{};
4639
+ VERIFY_ARE_EQUAL(process.Get().GetState(&processState, &exitCode), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
4640
+
4641
+ WSLCContainerState state{};
4642
+ VERIFY_ARE_EQUAL(container.Get().GetState(&state), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
4643
+ }
4644
+
4645
+ // Validate error handling when the username / group doesn't exist
4646
+ {
4647
+ WSLCContainerLauncher launcher("debian:latest", "test-no-missing-user", {"groups"});
4648
+
4649
+ launcher.SetUser("does-not-exist");
4650
+
4651
+ auto [result, _] = launcher.LaunchNoThrow(*m_defaultSession);
4652
+ VERIFY_ARE_EQUAL(result, E_FAIL);
4653
+
4654
+ ValidateCOMErrorMessage(L"unable to find user does-not-exist: no matching entries in passwd file");
4655
+ }
4656
+
4657
+ // Validate that empty arguments are correctly handled.
4658
+ {
4659
+ WSLCContainerLauncher launcher("debian:latest", "test-empty-args", {"echo", "foo", "", "bar"});
4660
+
4661
+ auto container = launcher.Launch(*m_defaultSession);
4662
+ auto process = container.GetInitProcess();
4663
+ ValidateProcessOutput(process, {{1, "foo bar\n"}}); // Expect two spaces for the empty argument.
4664
+ }
4665
+
4666
+ // Validate that tmpfs mounts are correctly wired.
4667
+ {
4668
+ WSLCContainerLauncher launcher(
4669
+ "debian:latest",
4670
+ "test-tmpfs",
4671
+ {"/bin/sh", "-c", "mount | grep 'tmpfs on /mnt/wslc-tmpfs1' && mount | grep 'tmpfs on /mnt/wslc-tmpfs2'"});
4672
+
4673
+ launcher.AddTmpfs("/mnt/wslc-tmpfs1", "rw,noexec,nosuid,size=65536k");
4674
+ launcher.AddTmpfs("/mnt/wslc-tmpfs2", "");
4675
+
4676
+ auto container = launcher.Launch(*m_defaultSession);
4677
+ auto process = container.GetInitProcess();
4678
+ ValidateProcessOutput(process, {}, 0);
4679
+ }
4680
+
4681
+ // Validate that relative tmpfs paths are rejected by Docker.
4682
+ {
4683
+ WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-relative", {"/bin/cat"});
4684
+ launcher.AddTmpfs("relative-path", "");
4685
+
4686
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4687
+ VERIFY_ARE_EQUAL(hresult, E_FAIL);
4688
+
4689
+ ValidateCOMErrorMessage(L"invalid mount path: 'relative-path' mount path must be absolute");
4690
+ }
4691
+
4692
+ // Validate that invalid tmpfs options are rejected by Docker.
4693
+ {
4694
+ WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-invalid-opts", {"/bin/cat"});
4695
+ launcher.AddTmpfs("/mnt/wslc-tmpfs", "invalid_option_xyz");
4696
+
4697
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4698
+ VERIFY_ARE_EQUAL(hresult, E_FAIL);
4699
+
4700
+ ValidateCOMErrorMessage(L"invalid tmpfs option [\"invalid_option_xyz\"]");
4701
+ }
4702
+
4703
+ // Validate error paths
4704
+ {
4705
+ WSLCContainerLauncher launcher("debian:latest", std::string(WSLC_MAX_CONTAINER_NAME_LENGTH + 1, 'a'), {"/bin/cat"});
4706
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4707
+ VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4708
+ }
4709
+
4710
+ {
4711
+ WSLCContainerLauncher launcher(std::string(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a'), "dummy", {"/bin/cat"});
4712
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4713
+ VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4714
+ }
4715
+
4716
+ {
4717
+ WSLCContainerLauncher launcher("invalid-image-name", "dummy", {"/bin/cat"});
4718
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4719
+ VERIFY_ARE_EQUAL(hresult, WSLC_E_IMAGE_NOT_FOUND);
4720
+ }
4721
+
4722
+ {
4723
+ WSLCContainerLauncher launcher("debian:latest", "dummy", {"/does-not-exist"});
4724
+ auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
4725
+ VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
4726
+
4727
+ ValidateCOMErrorMessage(
4728
+ L"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create "
4729
+ L"failed: unable to start container process: error during container init: exec: \"/does-not-exist\": stat "
4730
+ L"/does-not-exist: no such file or directory: unknown");
4731
+ }
4732
+
4733
+ // Test null image name
4734
+ {
4735
+ WSLCContainerOptions options{};
4736
+ options.Image = nullptr;
4737
+ options.Name = "test-container";
4738
+ options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
4739
+
4740
+ wil::com_ptr<IWSLCContainer> container;
4741
+ auto hr = m_defaultSession->CreateContainer(&options, &container);
4742
+ VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
4743
+ }
4744
+
4745
+ // Test null container name
4746
+ {
4747
+ WSLCContainerOptions options{};
4748
+ options.Image = "debian:latest";
4749
+ options.Name = nullptr;
4750
+ options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
4751
+
4752
+ wil::com_ptr<IWSLCContainer> container;
4753
+ VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, &container));
4754
+ VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsNone));
4755
+ }
4756
+ }
4757
+
4758
+ WSLC_TEST_METHOD(ContainerStartAfterStop)
4759
+ {
4760
+ {
4761
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-start", {"echo", "OK"});
4762
+ auto container = launcher.Launch(*m_defaultSession);
4763
+ auto process = container.GetInitProcess();
4764
+
4765
+ ValidateProcessOutput(process, {{1, "OK\n"}});
4766
+
4767
+ {
4768
+ // Validate that the container can be restarted.
4769
+ VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr), S_OK);
4770
+ auto restartedProcess = container.GetInitProcess();
4771
+ ValidateProcessOutput(restartedProcess, {{1, "OK\n"}});
4772
+ }
4773
+
4774
+ {
4775
+ // Validate that the container can be restarted without the attach flag.
4776
+ VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), S_OK);
4777
+ auto restartedProcess = container.GetInitProcess();
4778
+ VERIFY_ARE_EQUAL(restartedProcess.Wait(), 0);
4779
+
4780
+ COMOutputHandle stdoutLogs{};
4781
+ COMOutputHandle stderrLogs{};
4782
+ VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutLogs, &stderrLogs, 0, 0, 0));
4783
+
4784
+ ValidateHandleOutput(stdoutLogs.Get(), "OK\nOK\nOK\n");
4785
+ ValidateHandleOutput(stderrLogs.Get(), "");
4786
+ }
4787
+ }
4788
+
4789
+ // Validate that containers can be restarted after being explicitly stopped.
4790
+ {
4791
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-start-2", {"sleep", "99999"});
4792
+ auto container = launcher.Launch(*m_defaultSession);
4793
+
4794
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4795
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4796
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
4797
+
4798
+ VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
4799
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4800
+
4801
+ auto initProcess = container.GetInitProcess();
4802
+ initProcess.Get().Signal(WSLCSignalSIGKILL);
4803
+ VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128);
4804
+
4805
+ VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
4806
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4807
+
4808
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4809
+ VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
4810
+
4811
+ // Validate that deleted containers can't be started.
4812
+ VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
4813
+ }
4814
+
4815
+ // Validate restart behavior for a container with the autorm flag set
4816
+ {
4817
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-start-3", {"sleep", "99999"});
4818
+ launcher.SetContainerFlags(WSLCContainerFlagsRm);
4819
+ auto container = launcher.Launch(*m_defaultSession);
4820
+
4821
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4822
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
4823
+
4824
+ // Validate that deleted containers can't be started.
4825
+ VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
4826
+ }
4827
+
4828
+ // Validate that invalid start flags are rejected.
4829
+ {
4830
+ WSLCContainerLauncher launcher("debian:latest", "test-stop-start-invalid-flags", {"echo", "OK"});
4831
+ auto container = launcher.Create(*m_defaultSession);
4832
+ VERIFY_ARE_EQUAL(container.Get().Start(static_cast<WSLCContainerStartFlags>(0x2), nullptr), E_INVALIDARG);
4833
+ }
4834
+ }
4835
+
4836
+ WSLC_TEST_METHOD(OpenContainer)
4837
+ {
4838
+ auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {
4839
+ wil::com_ptr<IWSLCContainer> container;
4840
+ auto result = m_defaultSession->OpenContainer(Id, &container);
4841
+
4842
+ VERIFY_ARE_EQUAL(result, expectedResult);
4843
+
4844
+ return container;
4845
+ };
4846
+
4847
+ {
4848
+ WSLCContainerLauncher launcher("debian:latest", "named-container", {"echo", "OK"});
4849
+ auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
4850
+ VERIFY_SUCCEEDED(result);
4851
+
4852
+ VERIFY_ARE_EQUAL(container->Id().length(), WSLC_CONTAINER_ID_LENGTH);
4853
+
4854
+ VERIFY_ARE_EQUAL(container->Name(), "named-container");
4855
+
4856
+ // Validate that the container can be opened by name.
4857
+ expectOpen("named-container");
4858
+
4859
+ // Validate that the container can be opened by ID.
4860
+ expectOpen(container->Id().c_str());
4861
+
4862
+ // Validate that the container can be opened by a prefix of the ID.
4863
+ expectOpen(container->Id().substr(0, 8).c_str());
4864
+ expectOpen(container->Id().substr(0, 1).c_str());
4865
+
4866
+ // Validate that prefix conflicts are correctly handled.
4867
+ std::vector<RunningWSLCContainer> createdContainers;
4868
+ createdContainers.emplace_back(std::move(container.value()));
4869
+
4870
+ auto findConflict = [&]() {
4871
+ for (auto& e : createdContainers)
4872
+ {
4873
+ auto firstChar = e.Id()[0];
4874
+
4875
+ if (std::ranges::count_if(createdContainers, [&](auto& container) { return container.Id()[0] == firstChar; }) > 1)
4876
+ {
4877
+ return firstChar;
4878
+ }
4879
+ }
4880
+
4881
+ return '\0';
4882
+ };
4883
+
4884
+ // Create containers until we get two containers with the same first character in their ID.
4885
+ while (true)
4886
+ {
4887
+ VERIFY_IS_LESS_THAN(createdContainers.size(), 16);
4888
+
4889
+ auto [result, newContainer] = WSLCContainerLauncher("debian:latest").CreateNoThrow(*m_defaultSession);
4890
+ VERIFY_SUCCEEDED(result);
4891
+
4892
+ createdContainers.emplace_back(std::move(newContainer.value()));
4893
+ char conflictChar = findConflict();
4894
+ if (conflictChar == '\0')
4895
+ {
4896
+ continue;
4897
+ }
4898
+
4899
+ expectOpen(std::string{&conflictChar, 1}.c_str(), WSLC_E_CONTAINER_PREFIX_AMBIGUOUS);
4900
+ break;
4901
+ }
4902
+ }
4903
+
4904
+ // Test error paths
4905
+ {
4906
+ expectOpen("", E_INVALIDARG);
4907
+ ValidateCOMErrorMessage(L"Invalid name: ''");
4908
+
4909
+ expectOpen("non-existing-container", WSLC_E_CONTAINER_NOT_FOUND);
4910
+ ValidateCOMErrorMessage(L"Container 'non-existing-container' not found.");
4911
+
4912
+ expectOpen("/", E_INVALIDARG);
4913
+ ValidateCOMErrorMessage(L"Invalid name: '/'");
4914
+
4915
+ expectOpen("?foo=bar", E_INVALIDARG);
4916
+ ValidateCOMErrorMessage(L"Invalid name: '?foo=bar'");
4917
+
4918
+ expectOpen("\n", E_INVALIDARG);
4919
+ ValidateCOMErrorMessage(L"Invalid name: '\n'");
4920
+
4921
+ expectOpen(" ", E_INVALIDARG);
4922
+ ValidateCOMErrorMessage(L"Invalid name: ' '");
4923
+ }
4924
+ }
4925
+
4926
+ WSLC_TEST_METHOD(ContainerState)
4927
+ {
4928
+ auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
4929
+ wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
4930
+ wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
4931
+
4932
+ VERIFY_SUCCEEDED(
4933
+ m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
4934
+ VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
4935
+
4936
+ for (size_t i = 0; i < expectedContainers.size(); i++)
4937
+ {
4938
+ const auto& [expectedName, expectedImage, expectedState] = expectedContainers[i];
4939
+ VERIFY_ARE_EQUAL(expectedName, containers[i].Name);
4940
+ VERIFY_ARE_EQUAL(expectedImage, containers[i].Image);
4941
+ VERIFY_ARE_EQUAL(expectedState, containers[i].State);
4942
+ VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLC_CONTAINER_ID_LENGTH);
4943
+ VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
4944
+ VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
4945
+ }
4946
+ };
4947
+
4948
+ {
4949
+ // Validate that the container list is initially empty.
4950
+ expectContainerList({});
4951
+
4952
+ // Start one container and wait for it to exit.
4953
+ {
4954
+ WSLCContainerLauncher launcher("debian:latest", "exited-container", {"echo", "OK"});
4955
+ auto container = launcher.Launch(*m_defaultSession);
4956
+ auto process = container.GetInitProcess();
4957
+
4958
+ ValidateProcessOutput(process, {{1, "OK\n"}});
4959
+ expectContainerList({{"exited-container", "debian:latest", WslcContainerStateExited}});
4960
+ }
4961
+
4962
+ // Create a stuck container.
4963
+ WSLCContainerLauncher launcher("debian:latest", "test-container-1", {"sleep", "99999"});
4964
+
4965
+ auto container = launcher.Launch(*m_defaultSession);
4966
+
4967
+ // Verify that the container is in running state.
4968
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
4969
+ expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateRunning}});
4970
+
4971
+ // Capture StateChangedAt and CreatedAt while the container is running.
4972
+ ULONGLONG runningStateChangedAt{};
4973
+ ULONGLONG runningCreatedAt{};
4974
+ {
4975
+ wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
4976
+ wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
4977
+ VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
4978
+ &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
4979
+ VERIFY_ARE_EQUAL(containers.size(), 1);
4980
+ runningStateChangedAt = containers[0].StateChangedAt;
4981
+ runningCreatedAt = containers[0].CreatedAt;
4982
+ VERIFY_IS_TRUE(runningStateChangedAt > 0);
4983
+ VERIFY_IS_TRUE(runningCreatedAt > 0);
4984
+ }
4985
+
4986
+ // Kill the container init process and expect it to be in exited state.
4987
+ auto initProcess = container.GetInitProcess();
4988
+ VERIFY_SUCCEEDED(initProcess.Get().Signal(WSLCSignalSIGKILL));
4989
+
4990
+ // Wait for the process to actually exit.
4991
+ wsl::shared::retry::RetryWithTimeout<void>(
4992
+ [&]() {
4993
+ initProcess.GetExitCode(); // Throw if the process hasn't exited yet.
4994
+ },
4995
+ std::chrono::milliseconds{100},
4996
+ std::chrono::seconds{30});
4997
+
4998
+ // Expect the container to be in exited state.
4999
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
This file is too large to show in full.