master
cpp 723 lines 26.2 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCE2EHelpers.cpp
8
9 Abstract:
10
11 This file contains helper functions for end-to-end tests of WSLC.
12 --*/
13
14 #include "precomp.h"
15 #include "WSLCSessionDefaults.h"
16 #include "ImageModel.h"
17 #include "VolumeModel.h"
18 #include "windows/Common.h"
19 #include "WSLCExecutor.h"
20 #include "WSLCE2EHelpers.h"
21 #include "TestImageRegistry.h"
22 #include <JsonUtils.h>
23 #include <wslutil.h>
24
25 extern std::wstring g_testDataPath;
26
27 namespace WSLCE2ETests {
28
29 using namespace WEX::Logging;
30
31 namespace wslc_schema = wsl::windows::common::wslc_schema;
32 using wsl::windows::common::RunningWSLCContainer;
33 using wsl::windows::common::WSLCContainerLauncher;
34
35 namespace {
36 // Lazily compute the session storage base path.
37 struct SessionStorageBasePathAccessor
38 {
39 operator const std::filesystem::path&() const
40 {
41 static const std::filesystem::path basePath =
42 std::filesystem::absolute(std::filesystem::current_path() / L"wslc-cli-test-sessions");
43 return basePath;
44 }
45 };
46
47 static wil::com_ptr<IWSLCSessionManager> OpenSessionManager()
48 {
49 wil::com_ptr<IWSLCSessionManager> sessionManager;
50 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
51 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
52 return sessionManager;
53 }
54
55 wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
56 {
57 const auto sessionManager = OpenSessionManager();
58 wil::com_ptr<IWSLCSession> session;
59 VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, nullptr, &session));
60 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
61
62 WSLCSessionState state{};
63 VERIFY_SUCCEEDED(session->GetState(&state));
64 VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
65
66 return session;
67 }
68
69 WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR name, LPCWSTR storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
70 {
71 WSLCSessionSettings settings{};
72 settings.DisplayName = name;
73 settings.CpuCount = 4;
74 settings.MemoryMb = 2048;
75 settings.BootTimeoutMs = 30 * 1000;
76 settings.StoragePath = storagePath;
77 settings.MaximumStorageSizeMb = 4096; // 4GB.
78 settings.NetworkingMode = networkingMode;
79 return settings;
80 }
81
82 wil::com_ptr<IWSLCSession> CreateCustomSession(
83 const std::wstring& sessionName, const std::filesystem::path& storagePath, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
84 {
85 WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(sessionName.c_str(), storagePath.c_str(), networkingMode);
86 return CreateSession(sessionSettings);
87 }
88
89 void CleanupCustomSession(wil::com_ptr<IWSLCSession>& session, const std::filesystem::path& storagePath)
90 {
91 if (session)
92 {
93 LOG_IF_FAILED(session->Terminate());
94 }
95
96 session.reset();
97
98 if (!storagePath.empty())
99 {
100 std::error_code error;
101 std::filesystem::remove_all(storagePath, error);
102 if (error)
103 {
104 Log::Error(std::format(L"Failed to cleanup storage path {}: {}", storagePath.wstring(), error.message()).c_str());
105 }
106 }
107 }
108 } // namespace
109
110 const TestImage& AlpineTestImage()
111 {
112 static const TestImage image{L"alpine", L"latest", std::filesystem::path{g_testDataPath} / L"alpine-latest.tar"};
113 return image;
114 }
115
116 const TestImage& DebianTestImage()
117 {
118 static const TestImage image{L"debian", L"latest", std::filesystem::path{g_testDataPath} / L"debian-latest.tar"};
119 return image;
120 }
121
122 const TestImage& HelloWorldTestImage()
123 {
124 static const TestImage image{L"hello-world", L"latest", std::filesystem::path{g_testDataPath} / L"HelloWorldSaved.tar"};
125 return image;
126 }
127
128 const TestImage& PythonTestImage()
129 {
130 static const TestImage image{L"python", L"3.12-alpine", std::filesystem::path{g_testDataPath} / L"python-3_12-alpine.tar"};
131 return image;
132 }
133
134 const TestImage& InvalidTestImage()
135 {
136 static const TestImage image{L"mcr.microsoft.com/invalid-image", L"latest", L"INVALID_PATH"};
137 return image;
138 }
139
140 TestSession TestSession::Create(const std::wstring& displayName, WSLCNetworkingMode networkingMode)
141 {
142 const std::filesystem::path& basePath = SessionStorageBasePathAccessor();
143 auto storagePath = basePath / displayName;
144 auto session = CreateCustomSession(displayName, storagePath, networkingMode);
145 return TestSession{displayName, storagePath.wstring(), std::move(session)};
146 }
147
148 TestSession::~TestSession()
149 {
150 CleanupCustomSession(m_session, m_storagePath);
151 }
152
153 void VerifyContainerIsListed(const std::wstring& containerNameOrId, const std::wstring& status, const std::wstring& sessionName)
154 {
155 // The status column reports the runtime's description, e.g. "Up 5 seconds", so map the logical
156 // state callers pass in onto the text that description starts with.
157 std::wstring expectedStatus = status;
158 if (status == L"created")
159 {
160 expectedStatus = L"Created";
161 }
162 else if (status == L"running")
163 {
164 expectedStatus = L"Up ";
165 }
166 else if (status == L"exited")
167 {
168 expectedStatus = L"Exited (";
169 }
170
171 std::wstring command = L"container list --no-trunc --all";
172 if (!sessionName.empty())
173 {
174 command = std::format(L"--session \"{}\" container list --no-trunc --all", sessionName);
175 }
176
177 auto result = RunWslc(command);
178 result.Verify({.Stderr = L"", .ExitCode = 0});
179
180 auto outputLines = result.GetStdoutLines();
181 for (const auto& line : outputLines)
182 {
183 if (line.find(containerNameOrId) != std::wstring::npos)
184 {
185 const std::wstring message = L"Container '" + containerNameOrId + L"' found in container list output but status '" +
186 expectedStatus + L"' was not found in the same line";
187 VERIFY_ARE_NOT_EQUAL(std::wstring::npos, line.find(expectedStatus), message.c_str());
188 return;
189 }
190 }
191
192 const std::wstring message = L"Container '" + containerNameOrId + L"' not found in container list output";
193 VERIFY_FAIL(message.c_str());
194 }
195
196 void VerifyImageIsUsed(const TestImage& image)
197 {
198 auto result = RunWslc(L"container list --no-trunc --all");
199 result.Verify({.Stderr = L"", .ExitCode = 0});
200 auto outputLines = result.GetStdoutLines();
201 for (const auto& line : outputLines)
202 {
203 if (line.find(image.NameAndTag()) != std::wstring::npos)
204 {
205 return;
206 }
207 }
208
209 VERIFY_FAIL(std::format(L"Image '{}' not found in container list output", image.NameAndTag()).c_str());
210 }
211
212 void VerifyImageIsNotUsed(const TestImage& image)
213 {
214 auto result = RunWslc(L"container list --no-trunc --all");
215 result.Verify({.Stderr = L"", .ExitCode = 0});
216 auto outputLines = result.GetStdoutLines();
217 for (const auto& line : outputLines)
218 {
219 if (line.find(image.NameAndTag()) != std::wstring::npos)
220 {
221 VERIFY_FAIL(std::format(L"Image '{}' found in container list output", image.NameAndTag()).c_str());
222 }
223 }
224 }
225
226 void VerifyImageIsListed(const TestImage& image)
227 {
228 auto result = RunWslc(L"image list --format json");
229 result.Verify({.Stderr = L"", .ExitCode = 0});
230 auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result);
231 for (const auto& img : images)
232 {
233 if (img.Repository == wsl::shared::string::WideToMultiByte(image.Name) &&
234 img.Tag == wsl::shared::string::WideToMultiByte(image.Tag))
235 {
236 return;
237 }
238 }
239
240 VERIFY_FAIL(std::format(L"Image '{}' not found in image list output", image.NameAndTag()).c_str());
241 }
242
243 void VerifyVolumeIsListed(const std::wstring& volumeName)
244 {
245 auto result = RunWslc(L"volume list --format json");
246 result.Verify({.Stderr = L"", .ExitCode = 0});
247 auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result);
248 for (const auto& vol : volumes)
249 {
250 if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
251 {
252 return;
253 }
254 }
255
256 VERIFY_FAIL(std::format(L"Volume '{}' not found in volume list output", volumeName).c_str());
257 }
258
259 void VerifyVolumeIsNotListed(const std::wstring& volumeName)
260 {
261 auto result = RunWslc(L"volume list --format json");
262 result.Verify({.Stderr = L"", .ExitCode = 0});
263 auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result);
264 for (const auto& vol : volumes)
265 {
266 if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
267 {
268 VERIFY_FAIL(std::format(L"Volume '{}' found in volume list output", volumeName).c_str());
269 }
270 }
271 }
272
273 void VerifyNetworkIsListed(const std::wstring& networkName)
274 {
275 auto result = RunWslc(L"network list --format json");
276 result.Verify({.Stderr = L"", .ExitCode = 0});
277 auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
278 for (const auto& net : networks)
279 {
280 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
281 {
282 return;
283 }
284 }
285
286 VERIFY_FAIL(std::format(L"Network '{}' not found in network list output", networkName).c_str());
287 }
288
289 void VerifyNetworkIsNotListed(const std::wstring& networkName)
290 {
291 auto result = RunWslc(L"network list --format json");
292 result.Verify({.Stderr = L"", .ExitCode = 0});
293 auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
294 for (const auto& net : networks)
295 {
296 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
297 {
298 VERIFY_FAIL(std::format(L"Network '{}' found in network list output", networkName).c_str());
299 }
300 }
301 }
302
303 std::string GetHashId(const std::string& id, bool fullId)
304 {
305 return wsl::windows::common::string::TruncateId(id, !fullId);
306 }
307
308 wslc_schema::InspectContainer InspectContainer(const std::wstring& containerName)
309 {
310 auto result = RunWslc(std::format(L"container inspect {}", containerName));
311 result.Verify({.Stderr = L"", .ExitCode = 0});
312 auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectContainer>>(result.Stdout.value().c_str());
313 VERIFY_ARE_EQUAL(1u, inspectData.size());
314 return inspectData[0];
315 }
316
317 wslc_schema::Health WaitForContainerHealth(const std::wstring& containerName, const std::string_view& expectedStatus, std::chrono::milliseconds timeout)
318 {
319 try
320 {
321 return wsl::shared::retry::RetryWithTimeout<wslc_schema::Health>(
322 [&]() {
323 const auto inspect = InspectContainer(containerName);
324 THROW_HR_IF(E_FAIL, !inspect.State.Health.has_value());
325 THROW_HR_IF(E_FAIL, inspect.State.Health->Status != expectedStatus);
326 return inspect.State.Health.value();
327 },
328 std::chrono::seconds(1),
329 timeout);
330 }
331 catch (...)
332 {
333 const auto inspect = InspectContainer(containerName);
334 const std::string actual = inspect.State.Health.has_value() ? inspect.State.Health->Status : "<none>";
335 VERIFY_FAIL(std::format(
336 L"Container '{}' did not reach health status '{}' (last status: '{}')",
337 containerName,
338 wsl::shared::string::MultiByteToWide(std::string(expectedStatus)),
339 wsl::shared::string::MultiByteToWide(actual))
340 .c_str());
341 throw;
342 }
343 }
344
345 wslc_schema::InspectImage InspectImage(const std::wstring& imageName)
346 {
347 auto result = RunWslc(std::format(L"image inspect {}", imageName));
348 result.Verify({.Stderr = L"", .ExitCode = 0});
349 auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectImage>>(result.Stdout.value().c_str());
350 VERIFY_ARE_EQUAL(1u, inspectData.size());
351 return inspectData[0];
352 }
353
354 wslc_schema::InspectVolume InspectVolume(const std::wstring& volumeName)
355 {
356 auto result = RunWslc(std::format(L"volume inspect {}", volumeName));
357 result.Verify({.Stderr = L"", .ExitCode = 0});
358 auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::InspectVolume>>(result.Stdout.value().c_str());
359 VERIFY_ARE_EQUAL(1u, inspectData.size());
360 return inspectData[0];
361 }
362
363 void EnsureContainerDoesNotExist(const std::wstring& containerName)
364 {
365 const auto name = wsl::shared::string::WideToMultiByte(containerName);
366 const auto containers = ListAllContainers();
367 auto it = std::ranges::find_if(containers, [&](const auto& c) { return c.Names == name; });
368 if (it == containers.end())
369 {
370 return;
371 }
372
373 auto result = RunWslc(std::format(L"container remove --force {}", containerName));
374 // Tolerate WSLC_E_CONTAINER_NOT_FOUND - container already removed
375 if (result.ExitCode != 0 && (!result.Stderr.has_value() || result.Stderr.value().find(L"WSLC_E_CONTAINER_NOT_FOUND") == std::wstring::npos))
376 {
377 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
378 }
379 }
380
381 std::vector<wsl::windows::wslc::models::ContainerOutputInformation> ListAllContainers()
382 {
383 // --no-trunc keeps the full ids, which callers use to address containers.
384 auto result = RunWslc(L"container list --all --format json --no-trunc");
385 result.Verify({.Stderr = L"", .ExitCode = 0});
386 return ParseNdjsonOutputAs<wsl::windows::wslc::models::ContainerOutputInformation>(result);
387 }
388
389 void EnsureImageContainersAreDeleted(const TestImage& image)
390 {
391 auto containers = ListAllContainers();
392 for (const auto& container : containers)
393 {
394 auto nameAndTag = wsl::shared::string::WideToMultiByte(image.NameAndTag());
395 if (container.Image.find(nameAndTag) != std::string::npos)
396 {
397 auto result = RunWslc(std::format(L"container remove --force {}", container.ID));
398 result.Verify({.Stdout = std::format(L"{}\r\n", container.ID), .Stderr = L"", .ExitCode = 0});
399 }
400 }
401 }
402
403 void DeleteImagesWithRepositoryPrefix(const std::wstring& repositoryPrefix)
404 {
405 auto result = RunWslc(L"image list --format json");
406 result.Verify({.Stderr = L"", .ExitCode = 0});
407
408 const auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result);
409 const auto prefix = wsl::shared::string::WideToMultiByte(repositoryPrefix);
410 for (const auto& image : images)
411 {
412 if (image.Repository.starts_with(prefix))
413 {
414 // No container cleanup here: the images this prunes are only ever built and inspected, never used to
415 // create containers, so image delete --force is sufficient. If a future test containerizes a built
416 // image, remove its container in that test's cleanup rather than broadening this prefix-based safety net.
417 const auto nameAndTag = wsl::shared::string::MultiByteToWide(std::format("{}:{}", image.Repository, image.Tag));
418 RunWslc(std::format(L"image delete --force {}", nameAndTag)).Verify({.Stderr = L"", .ExitCode = 0});
419 }
420 }
421 }
422
423 void EnsureNoUntaggedImages()
424 {
425 auto result = RunWslc(L"image list --format json --no-trunc --filter dangling=true");
426 result.Verify({.Stderr = L"", .ExitCode = 0});
427
428 const auto images = ParseNdjsonOutputAs<wsl::windows::wslc::models::ImageOutputInformation>(result);
429
430 for (const auto& image : images)
431 {
432 const auto id = wsl::shared::string::MultiByteToWide(GetHashId(image.ID, true));
433 auto deleteResult = RunWslc(std::format(L"image delete --force {}", id));
434
435 // Tolerate WSLC_E_IMAGE_NOT_FOUND - an untagged image may already be gone if it was a
436 // parent/child of another untagged image deleted earlier in this loop.
437 if (deleteResult.ExitCode != 0 &&
438 (!deleteResult.Stderr.has_value() || deleteResult.Stderr.value().find(L"WSLC_E_IMAGE_NOT_FOUND") == std::wstring::npos))
439 {
440 deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
441 }
442 }
443 }
444
445 void EnsureSessionIsTerminated(const std::wstring& sessionName)
446 {
447 std::wstring targetSession = sessionName;
448 if (targetSession.empty())
449 {
450 auto isElevated = wsl::windows::common::security::IsTokenElevated(wil::open_current_access_token(TOKEN_QUERY).get());
451 auto baseName = isElevated ? wsl::windows::wslc::DefaultAdminSessionName : wsl::windows::wslc::DefaultSessionName;
452
453 wchar_t username[256 + 1] = {};
454 DWORD usernameLen = ARRAYSIZE(username);
455 THROW_IF_WIN32_BOOL_FALSE(GetUserNameW(username, &usernameLen));
456
457 targetSession = std::format(L"{}-{}", baseName, username);
458 }
459
460 auto listResult = RunWslc(L"system session list");
461 listResult.Verify({.Stderr = L"", .ExitCode = 0});
462
463 auto stdoutLines = listResult.GetStdoutLines();
464 for (const auto& line : stdoutLines)
465 {
466 // Check if the line ends with the target session name
467 if (line.size() >= targetSession.size() && line.compare(line.size() - targetSession.size(), targetSession.size(), targetSession) == 0)
468 {
469 auto result = RunWslc(std::format(L"--session \"{}\" system session terminate", targetSession));
470 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
471 break;
472 }
473 }
474 }
475
476 void EnsureVolumeDoesNotExist(const std::wstring& volumeName)
477 {
478 auto result = RunWslc(L"volume list --format json");
479 result.Verify({.Stderr = L"", .ExitCode = 0});
480 auto volumes = ParseNdjsonOutputAs<VolumeListOutput>(result);
481 for (const auto& vol : volumes)
482 {
483 if (vol.Name == wsl::shared::string::WideToMultiByte(volumeName))
484 {
485 auto deleteResult = RunWslc(std::format(L"volume rm {}", volumeName));
486 deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
487 break;
488 }
489 }
490 }
491
492 void EnsureNetworkDoesNotExist(const std::wstring& networkName)
493 {
494 auto result = RunWslc(L"network list --format json");
495 result.Verify({.Stderr = L"", .ExitCode = 0});
496 auto networks = ParseNdjsonOutputAs<NetworkListOutput>(result);
497 for (const auto& net : networks)
498 {
499 if (net.Name == wsl::shared::string::WideToMultiByte(networkName))
500 {
501 auto deleteResult = RunWslc(std::format(L"network rm {}", networkName));
502 deleteResult.Verify({.Stderr = L"", .ExitCode = 0});
503 break;
504 }
505 }
506 }
507
508 wslc_schema::Network InspectNetwork(const std::wstring& networkName)
509 {
510 auto result = RunWslc(std::format(L"network inspect {}", networkName));
511 result.Verify({.Stderr = L"", .ExitCode = 0});
512 auto inspectData = wsl::shared::FromJson<std::vector<wslc_schema::Network>>(result.Stdout.value().c_str());
513 VERIFY_ARE_EQUAL(1u, inspectData.size());
514 return inspectData[0];
515 }
516
517 wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession()
518 {
519 // Ensure the default elevated session exists before opening it via COM.
520 RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0});
521
522 wil::com_ptr<IWSLCSessionManager> sessionManager;
523 VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
524 wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
525
526 wil::com_ptr<IWSLCSession> session;
527 VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(nullptr, &session));
528 wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
529
530 return std::move(session);
531 }
532
533 std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(
534 IWSLCSession& session, const std::string& username, const std::string& password, USHORT port, const std::wstring& tlsCertDir)
535 {
536 // Check if the registry image is already loaded on this session.
537 wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
538 THROW_IF_FAILED(session.ListImages(nullptr, &images, images.size_address<ULONG>()));
539
540 bool found = std::ranges::any_of(
541 std::span{images.get(), images.size()}, [](const auto& e) { return std::strcmp(e.Image, "wslc-registry:latest") == 0; });
542
543 if (!found)
544 {
545 LoadTestImage(session, "wslc-registry:latest");
546 }
547
548 const bool useTls = !tlsCertDir.empty();
549
550 std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
551
552 if (!username.empty())
553 {
554 env.push_back(std::format("USERNAME={}", username));
555 env.push_back(std::format("PASSWORD={}", password));
556 }
557
558 if (useTls)
559 {
560 env.push_back("REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt");
561 env.push_back("REGISTRY_HTTP_TLS_KEY=/certs/server.key");
562 }
563
564 // TLS needs a non-loopback address for real verification, so use bridge networking and reach the
565 // container by its bridge IP. Plain HTTP uses host networking with a published loopback port.
566 WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env, useTls ? "bridge" : "host");
567 launcher.SetEntrypoint({"/entrypoint.sh"});
568
569 if (useTls)
570 {
571 launcher.AddVolume(tlsCertDir, "/certs", true);
572 }
573 else
574 {
575 launcher.AddPort(port, port, AF_INET);
576 }
577
578 auto container = launcher.Launch(session);
579
580 // Wait for the registry to bind the port before continuing.
581 auto initProcess = container.GetInitProcess();
582 WaitForOutput(initProcess.GetStdHandle(2), std::format("listening on [::]:{}", port));
583
584 if (useTls)
585 {
586 auto inspect = container.Inspect();
587 THROW_HR_IF(E_UNEXPECTED, inspect.NetworkSettings.Networks.empty());
588 auto address = std::format("{}:{}", inspect.NetworkSettings.Networks.begin()->second.IPAddress, port);
589
590 return {std::move(container), std::move(address)};
591 }
592 else
593 {
594 auto address = std::format("127.0.0.1:{}", port);
595 auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address));
596
597 int expectedCode = username.empty() ? 200 : 401;
598 ExpectHttpResponse(url.c_str(), expectedCode, true);
599
600 return {std::move(container), std::move(address)};
601 }
602 }
603
604 std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress)
605 {
606 auto registryImage = std::format(L"{}/{}", registryAddress, imageName);
607 RunWslcAndVerify(std::format(L"image tag {} {}", imageName, registryImage), {.ExitCode = 0});
608 return registryImage;
609 }
610
611 void WriteTestFile(const std::filesystem::path& filePath, const std::vector<std::string>& lines)
612 {
613 std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
614 VERIFY_IS_TRUE(file.is_open());
615 for (const auto& line : lines)
616 {
617 file << line << "\n";
618 }
619
620 VERIFY_IS_TRUE(file.good());
621 }
622
623 void WriteTestFileContent(const std::filesystem::path& filePath, const std::string& content)
624 {
625 std::ofstream file(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
626 THROW_HR_IF_MSG(E_FAIL, !file.is_open(), "Failed to open %ls for writing", filePath.c_str());
627 file << content;
628 THROW_HR_IF_MSG(E_FAIL, !file.good(), "Failed to write to %ls", filePath.c_str());
629 }
630
631 std::wstring GetPythonHttpServerScript(uint16_t port)
632 {
633 return std::format(L"python3 -u -m http.server {}", port);
634 }
635
636 std::wstring GetPythonUdpEchoServerScript(uint16_t port)
637 {
638 // Inline Python UDP echo server: echoes each received datagram back uppercased, forever.
639 return std::format(
640 L"python3 -c \"import socket;"
641 L"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM);"
642 L"s.bind(('0.0.0.0',{}));"
643 L"[s.sendto(d.upper(),a) for d,a in iter(lambda:s.recvfrom(1024),0)]\"",
644 port);
645 }
646
647 std::string SendUdpAndReceive(uint16_t hostPort, const std::string& payload, const std::string& expectedReply, int family)
648 {
649 SOCKADDR_INET addr{};
650 addr.si_family = static_cast<ADDRESS_FAMILY>(family);
651 INETADDR_SETLOOPBACK(reinterpret_cast<PSOCKADDR>(&addr));
652 SS_PORT(&addr) = htons(hostPort);
653
654 const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
655 do
656 {
657 wil::unique_socket sock{::socket(family, SOCK_DGRAM, IPPROTO_UDP)};
658 THROW_LAST_ERROR_IF(!sock);
659
660 DWORD timeout = 1000;
661 THROW_LAST_ERROR_IF(setsockopt(sock.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout), sizeof(timeout)) == SOCKET_ERROR);
662
663 if (sendto(sock.get(), payload.data(), static_cast<int>(payload.size()), 0, reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) != SOCKET_ERROR)
664 {
665 char buf[1024];
666 const int received = recvfrom(sock.get(), buf, sizeof(buf), 0, nullptr, nullptr);
667 if (received != SOCKET_ERROR && received > 0 && std::string(buf, received) == expectedReply)
668 {
669 return std::string(buf, received);
670 }
671 }
672 } while (std::chrono::steady_clock::now() < deadline);
673
674 VERIFY_FAIL(L"Timed out waiting for expected UDP echo reply from container");
675 return {};
676 }
677
678 namespace {
679
680 void WaitForTtySize(const WSLCInteractiveSession& session, SHORT columns, SHORT rows)
681 {
682 WaitForPseudoConsoleOutput(session, std::format("{} {}\r\n", rows, columns));
683 }
684
685 } // namespace
686
687 void WaitForPseudoConsoleOutput(const WSLCInteractiveSession& session, const std::string& expected, std::chrono::seconds timeout)
688 {
689 try
690 {
691 wsl::shared::retry::RetryWithTimeout<void>(
692 [&]() {
693 const std::string data = session.GetStdoutData();
694 THROW_HR_IF(E_ABORT, data.find(expected) == std::string::npos);
695 },
696 std::chrono::milliseconds(200),
697 timeout);
698 }
699 catch (...)
700 {
701 const std::string data = session.GetStdoutData();
702 VERIFY_FAIL(std::format(
703 L"Timed out waiting for \"{}\". Captured pseudoconsole output: \"{}\"",
704 wsl::shared::string::MultiByteToWide(EscapeString(expected)),
705 wsl::shared::string::MultiByteToWide(EscapeString(data)))
706 .c_str());
707 }
708 }
709
710 void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows)
711 {
712 constexpr SHORT resizedColumns = 100;
713 constexpr SHORT resizedRows = 37;
714 VERIFY_IS_TRUE(columns != resizedColumns || rows != resizedRows, L"Resized tty size must differ from the initial size");
715
716 WaitForTtySize(session, columns, rows);
717
718 session.ResizePseudoConsole(resizedColumns, resizedRows);
719 WaitForTtySize(session, resizedColumns, resizedRows);
720
721 session.Terminate();
722 }
723 } // namespace WSLCE2ETests