/*++ Copyright (c) Microsoft. All rights reserved. Module Name: WslcSdkWinRtTests.cpp Abstract: This file contains test cases for the WSLC SDK WinRT projection. --*/ #include "precomp.h" #include "Common.h" #include "wslcsdk.h" #include "WslcsdkPrivate.h" #include "WSLCContainerLauncher.h" #include "wslutil.h" #include "winrt/Session.h" #include "winrt/Helpers.h" #include #include #include #include #include using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::Storage::Streams; using namespace std::chrono_literals; namespace WSLCSDK = winrt::Microsoft::WSL::Containers; extern std::wstring g_testDataPath; extern bool g_fastTestRun; #define VERIFY_THROWS_HR(operation, expectedHr) \ VERIFY_THROWS_SPECIFIC( \ operation, winrt::hresult_error, [&](winrt::hresult_error const& e) { return e.code().value == expectedHr; }) #define IGNORE_ERRORS(operation) \ try \ { \ operation; \ } \ CATCH_LOG() #define SCOPE_CLEANUP(operation) wil::scope_exit([&]() { IGNORE_ERRORS(operation) }) #define DELETE_CONTAINER_ON_SCOPE_EXIT(container) SCOPE_CLEANUP(container.Delete(WSLCSDK::DeleteContainerFlags::Force)) #define DELETE_IMAGE_ON_SCOPE_EXIT(imageName) SCOPE_CLEANUP(m_defaultSession.DeleteImage(imageName)) struct ProcessOutput { uint32_t ExitCode; std::wstring StandardOutput; std::wstring StandardError; }; std::wstring ReadStream(IInputStream const& stream) { std::wstring output; DataReader reader{stream}; reader.UnicodeEncoding(winrt::Windows::Storage::Streams::UnicodeEncoding::Utf8); uint32_t bytesRead; do { bytesRead = reader.LoadAsync(1024).get(); output += reader.ReadString(bytesRead).c_str(); } while (bytesRead > 0); return output; } class WslcSdkWinRtTests { WSLC_TEST_CLASS(WslcSdkWinRtTests) std::filesystem::path m_storagePath; WSLCSDK::Session m_defaultSession{nullptr}; static inline constexpr auto c_testSessionName = L"wslc-winrt-test"; // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- void StartProcessAndWaitForExit(WSLCSDK::Process const& process, std::chrono::milliseconds timeout = 2min) { std::promise promise; auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); }); process.Start(); VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready); } void StartContainerAndWaitForInitProcessExit(WSLCSDK::Container const& container, std::chrono::milliseconds timeout = 2min) { auto initProcess = container.InitProcess(); std::promise promise; auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); }); container.Start(); VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready); } ProcessOutput GetProcessOutput(WSLCSDK::Process const& process) { ProcessOutput output; output.ExitCode = process.ExitCode(); output.StandardOutput = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput)); output.StandardError = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError)); return output; } struct RunContainerOptions { std::vector cmdLine = {}; WSLCSDK::ContainerFlags flags = WSLCSDK::ContainerFlags::None; std::optional name = std::nullopt; std::chrono::milliseconds timeout = 2min; std::optional networkingMode = std::nullopt; }; // Creates and starts a one-shot container, waits for the init process to // exit, and returns the exit code. ProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {}) { auto procSettings = WSLCSDK::ProcessSettings(); if (!options.cmdLine.empty()) { procSettings.CmdLine(winrt::single_threaded_vector(std::move(options.cmdLine))); } procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream); auto containerSettings = WSLCSDK::ContainerSettings(imageName); containerSettings.InitProcess(procSettings); containerSettings.Flags(options.flags); if (options.name) { containerSettings.Name(options.name.value()); } if (options.networkingMode) { containerSettings.NetworkingMode(options.networkingMode.value()); } auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); StartContainerAndWaitForInitProcessExit(container, options.timeout); auto output = GetProcessOutput(container.InitProcess()); IGNORE_ERRORS(container.Delete(WSLCSDK::DeleteContainerFlags::Force)); return output; } bool HasImage(winrt::hstring const& imageName) { auto images = m_defaultSession.Images(); return std::any_of(images.begin(), images.end(), [&](auto const& img) { return img.Name() == imageName; }); } // Starts a local wslc-registry container using host-mode networking. // Host networking is not exposed by the WinRT projection, so this helper // uses WSLCContainerLauncher with the raw session handle. std::pair StartLocalRegistry( const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) { VERIFY_IS_TRUE(HasImage(L"wslc-registry:latest")); std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; if (!username.empty()) { env.push_back(std::format("USERNAME={}", username)); env.push_back(std::format("PASSWORD={}", password)); } wsl::windows::common::WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env); launcher.SetEntrypoint({"/entrypoint.sh"}); launcher.AddPort(port, port, AF_INET); // Get the IWSLCSession COM object from the SDK session handle. auto& comSession = *reinterpret_cast(WSLCSDK::implementation::GetHandle(m_defaultSession))->session; auto container = launcher.Launch(comSession, WSLCContainerStartFlagsNone); auto registryAddress = std::format("127.0.0.1:{}", port); // Wait for the registry to be ready by probing from the host. auto hostUrl = std::format(L"http://{}", registryAddress); ExpectHttpResponse(hostUrl.c_str(), 200, true); return {std::move(container), registryAddress}; } // Tags and pushes an image to a local registry via the SDK APIs. void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth) { const auto imageName = winrt::to_hstring(std::format("{}:{}", repo, tag)); const auto registryRepo = winrt::to_hstring(std::format("{}/{}", registryAddress, repo)); const auto registryImage = winrt::to_hstring(std::format("{}/{}:{}", registryAddress, repo, tag)); VERIFY_IS_TRUE(HasImage(imageName)); m_defaultSession.TagImage(WSLCSDK::TagImageOptions(imageName, registryRepo, winrt::to_hstring(tag))); // Ensures the registry-prefixed tag is removed after the push. auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(registryImage); m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(registryImage, winrt::to_hstring(registryAuth))).get(); } TEST_CLASS_SETUP(TestClassSetup) { WSADATA wsaData; THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &wsaData)); winrt::init_apartment(); // Use the same storage path as WSLC runtime tests to reduce pull overhead. m_storagePath = std::filesystem::current_path() / "test-storage"; // Build session settings using the WinRT API auto settings = WSLCSDK::SessionSettings(c_testSessionName, m_storagePath.wstring()); settings.CpuCount(4); settings.MemoryMB(2048); settings.Timeout(std::chrono::duration_cast(30s)); settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic)); m_defaultSession = WSLCSDK::Session(settings); m_defaultSession.Start(); // Pull images required by the tests (no-op if already present). for (const auto* imageName : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"}) { const auto imagePath = GetTestImagePath(imageName); m_defaultSession.LoadImageAsync(imagePath.wstring()).get(); } return true; } TEST_CLASS_CLEANUP(TestClassCleanup) { if (m_defaultSession) { m_defaultSession.Terminate(); m_defaultSession = nullptr; } // Preserve the VHD in fast-run mode so subsequent runs skip image pulling. if (!g_fastTestRun && !m_storagePath.empty()) { std::error_code error; std::filesystem::remove_all(m_storagePath, error); if (error) { LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str()); } } return true; } // ----------------------------------------------------------------------- // Session tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(CreateSession) { const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-extra-session-storage"; auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-extra-session", extraStorage.wstring()); settings.CpuCount(2); settings.MemoryMB(1024); settings.Timeout(std::chrono::duration_cast(30s)); settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 1024ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic)); // Positive: Creation must succeed with valid settings. { auto session = WSLCSDK::Session(settings); VERIFY_IS_NOT_NULL(session); } // Negative: Must throw if used before Start() { auto session = WSLCSDK::Session(settings); VERIFY_THROWS_HR(std::ignore = session.Images(), E_ILLEGAL_METHOD_CALL); } // Positive: Starting the session must succeed. { auto session = WSLCSDK::Session(settings); VERIFY_NO_THROW(session.Start()); } // Negative: Null settings must fail. { VERIFY_THROWS_HR(WSLCSDK::Session(WSLCSDK::SessionSettings{nullptr}), E_POINTER); } } WSLC_TEST_METHOD(TerminationHandler) { // Positive: Terminating the session must trigger a graceful shutdown and fire the event std::promise promise; const std::filesystem::path extraStorage = m_storagePath / "wslc-winrt-termh-storage"; auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-termh", extraStorage.wstring()); settings.Timeout(std::chrono::duration_cast(30s)); auto session = WSLCSDK::Session(settings); session.Terminated([&](WSLCSDK::SessionTerminationReason reason) { promise.set_value(reason); }); session.Start(); session.Terminate(); auto future = promise.get_future(); VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready); VERIFY_ARE_EQUAL(future.get(), WSLCSDK::SessionTerminationReason::Shutdown); } // ----------------------------------------------------------------------- // Image tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ImageList) { // Session has images pre-loaded - list must return at least one entry. const auto images = m_defaultSession.Images(); VERIFY_IS_TRUE(images.Size() >= 1); // At least one image must be non-empty bool foundNonEmpty = false; for (auto const& img : images) { if (!img.Name().empty() && img.SizeBytes() != 0) { foundNonEmpty = true; break; } } VERIFY_IS_TRUE(foundNonEmpty); } WSLC_TEST_METHOD(LoadImage) { // Positive: load a saved image tar and verify the image can be run { // Remove the image if it already exists IGNORE_ERRORS(m_defaultSession.DeleteImage(L"hello-world:latest")); const auto imageTar = GetTestImagePath("hello-world:latest"); // Positive: load from file path. VERIFY_NO_THROW(m_defaultSession.LoadImageAsync(imageTar.wstring()).get()); // Verify the loaded image is usable VERIFY_IS_TRUE(HasImage(L"hello-world:latest")); auto output = RunContainerAndWaitForExit(L"hello-world:latest", {}); VERIFY_ARE_EQUAL(output.ExitCode, 0); VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos); } // Negative: empty path must fail { VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"").get(), E_INVALIDARG); } // Negative: non-existent path must fail. { VERIFY_THROWS_HR(m_defaultSession.LoadImageAsync(L"C:\\bogus\\image.tar").get(), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); } } WSLC_TEST_METHOD(ImportImage) { const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar"; constexpr auto c_importedImageName = L"my-hello-world-winrt:test"; IGNORE_ERRORS(m_defaultSession.DeleteImage(c_importedImageName)); // Positive: import an exported image tar via path. { auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(c_importedImageName); VERIFY_NO_THROW(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), c_importedImageName).get()); VERIFY_IS_TRUE(HasImage(c_importedImageName)); auto output = RunContainerAndWaitForExit(c_importedImageName, {.cmdLine = {L"/hello"}}); VERIFY_ARE_EQUAL(output.ExitCode, 0); VERIFY_IS_TRUE(output.StandardOutput.find(L"Hello from Docker!") != std::string::npos); } // Negative: empty path must fail. { VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(L"", c_importedImageName).get(), E_INVALIDARG); } // Negative: empty image name must fail. { VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(exportedImageTar.wstring(), L"").get(), E_INVALIDARG); } // Negative: non-tar file must fail. { std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW(GetCurrentProcess()); VERIFY_THROWS_HR(m_defaultSession.ImportImageAsync(pathToSelf.wstring(), L"import-self:test").get(), E_FAIL); } } WSLC_TEST_METHOD(ImageDelete) { VERIFY_IS_TRUE(HasImage(L"hello-world:latest")); // Positive: delete an existing image. { m_defaultSession.DeleteImage(L"hello-world:latest"); VERIFY_IS_FALSE(HasImage(L"hello-world:latest")); // Reload for subsequent tests. const auto imageTar = GetTestImagePath("hello-world:latest"); m_defaultSession.LoadImageAsync(imageTar.wstring()).get(); } // Negative: non-existent image name must throw. { VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent:no-such-tag"), WSLC_E_IMAGE_NOT_FOUND); } } // ----------------------------------------------------------------------- // Container lifecycle tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(CreateContainer) { // Positive: stdout is captured correctly. { auto output = RunContainerAndWaitForExit(L"debian:latest", {.cmdLine = {L"/bin/echo", L"OK"}}); VERIFY_ARE_EQUAL(output.ExitCode, 0); VERIFY_ARE_EQUAL(output.StandardOutput, L"OK\n"); VERIFY_ARE_EQUAL(output.StandardError, L""); } // Positive: stdout and stderr are routed independently. { auto output = RunContainerAndWaitForExit(L"debian:latest", {.cmdLine = {L"/bin/sh", L"-c", L"echo stdout && echo stderr >&2"}}); VERIFY_ARE_EQUAL(output.ExitCode, 0); VERIFY_ARE_EQUAL(output.StandardOutput, L"stdout\n"); VERIFY_ARE_EQUAL(output.StandardError, L"stderr\n"); } // Negative: creating a container with a non-existent image fails at CreateContainer. { WSLCSDK::ContainerSettings containerSettings{L"invalid-image:notfound"}; VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), WSLC_E_IMAGE_NOT_FOUND); } // Negative: an empty image name is rejected. { VERIFY_THROWS_HR(WSLCSDK::ContainerSettings{L""}, E_INVALIDARG); } // Verify that a null settings pointer is rejected. { VERIFY_THROWS_HR(m_defaultSession.CreateContainer(nullptr), E_POINTER); } } WSLC_TEST_METHOD(ContainerGetId) { auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest")); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); const auto id = container.Id(); VERIFY_IS_FALSE(id.empty()); // Container ID is a 64-character lowercase hex string. VERIFY_ARE_EQUAL(id.size(), 64u); } WSLC_TEST_METHOD(ContainerGetState) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); // State after creation: Created. VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Created); container.Start(); // State while running: Running. VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running); container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero()); // State after stop: Exited. VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited); } WSLC_TEST_METHOD(ContainerStopAndDelete) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"999"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); VERIFY_NO_THROW(container.Start()); VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Running); VERIFY_NO_THROW(container.Stop(WSLCSDK::Signal::SIGKILL, TimeSpan::zero())); VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Exited); VERIFY_NO_THROW(container.Delete(WSLCSDK::DeleteContainerFlags::None)); VERIFY_ARE_EQUAL(container.State(), WSLCSDK::ContainerState::Deleted); } WSLC_TEST_METHOD(ProcessIOHandles) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"echo STDOUT_TOKEN; echo STDERR_TOKEN >&2"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto initProcess = container.InitProcess(); std::promise promise; auto autoRevoker = initProcess.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); }); container.Start(); auto stdoutStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput); auto stderrStream = initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError); // Verify that each handle can only be acquired once. { VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); VERIFY_THROWS_HR(initProcess.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } VERIFY_ARE_EQUAL(promise.get_future().wait_for(1min), std::future_status::ready); VERIFY_ARE_EQUAL(ReadStream(stdoutStream), L"STDOUT_TOKEN\n"); VERIFY_ARE_EQUAL(ReadStream(stderrStream), L"STDERR_TOKEN\n"); } WSLC_TEST_METHOD(ContainerNetworkingMode) { // BRIDGED: eth0 interface must be present. { auto output = RunContainerAndWaitForExit( L"debian:latest", {.cmdLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"}, .flags = WSLCSDK::ContainerFlags::None, .networkingMode = WSLCSDK::ContainerNetworkingMode::Bridged}); VERIFY_ARE_EQUAL(output.StandardOutput, L"HAS_ETH0\n"); } // NONE: eth0 interface must not be present. { auto output = RunContainerAndWaitForExit( L"debian:latest", {.cmdLine = {L"/bin/sh", L"-c", L"[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"}, .flags = WSLCSDK::ContainerFlags::None, .networkingMode = WSLCSDK::ContainerNetworkingMode::None}); VERIFY_ARE_EQUAL(output.StandardOutput, L"NO_ETH0\n"); } // Invalid networking mode must fail. { WSLCSDK::ContainerSettings containerSettings{L"debian:latest"}; VERIFY_THROWS_HR(containerSettings.NetworkingMode(static_cast(99)), E_INVALIDARG); } } WSLC_TEST_METHOD(ContainerPortMapping) { // Negative: port mappings with None networking mode must fail at Start. { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::None); containerSettings.PortMappings(winrt::single_threaded_vector( {WSLCSDK::ContainerPortMapping(12342, 8000, WSLCSDK::PortProtocol::TCP)})); VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), E_INVALIDARG); } // Functional: BRIDGED networking with port mapping; HTTP server must be reachable. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"python3", L"-m", L"http.server", L"8000"})); procSettings.EnvironmentVariables( winrt::single_threaded_map(std::map{{L"PYTHONUNBUFFERED", L"1"}})); auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine"); containerSettings.InitProcess(procSettings); containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged); containerSettings.PortMappings(winrt::single_threaded_vector( {WSLCSDK::ContainerPortMapping(12341, 8000, WSLCSDK::PortProtocol::TCP)})); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); ExpectHttpResponse(L"http://127.0.0.1:12341", 200, true); } // Functional: port mapping with explicit IPv4 WindowsAddress. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"python3", L"-m", L"http.server", L"8000"})); procSettings.EnvironmentVariables( winrt::single_threaded_map(std::map{{L"PYTHONUNBUFFERED", L"1"}})); auto portMapping = WSLCSDK::ContainerPortMapping(12343, 8000, WSLCSDK::PortProtocol::TCP); portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"127.0.0.1")); auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine"); containerSettings.InitProcess(procSettings); containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged); containerSettings.PortMappings(winrt::single_threaded_vector({portMapping})); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); ExpectHttpResponse(L"http://127.0.0.1:12343", 200, true); } // Functional: port mapping with explicit IPv6 WindowsAddress. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"python3", L"-m", L"http.server", L"--bind", L"::", L"8000"})); procSettings.EnvironmentVariables( winrt::single_threaded_map(std::map{{L"PYTHONUNBUFFERED", L"1"}})); auto portMapping = WSLCSDK::ContainerPortMapping(12344, 8000, WSLCSDK::PortProtocol::TCP); portMapping.WindowsAddress(winrt::Windows::Networking::HostName(L"::1")); auto containerSettings = WSLCSDK::ContainerSettings(L"python:3.12-alpine"); containerSettings.InitProcess(procSettings); containerSettings.NetworkingMode(WSLCSDK::ContainerNetworkingMode::Bridged); containerSettings.PortMappings(winrt::single_threaded_vector({portMapping})); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); ExpectHttpResponse(L"http://[::1]:12344", 200, true); } } WSLC_TEST_METHOD(ContainerVolumeUnit) { const auto currentDirectory = std::filesystem::current_path().wstring(); // Negative: non-absolute Windows path must fail at CreateContainer. VERIFY_THROWS_HR( { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.Volumes(winrt::single_threaded_vector( {WSLCSDK::ContainerVolume(L"relative", L"/mnt/path", false)})); m_defaultSession.CreateContainer(containerSettings); }, E_INVALIDARG); // Negative: non-absolute container path must fail at CreateContainer. VERIFY_THROWS_HR( { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.Volumes(winrt::single_threaded_vector( {WSLCSDK::ContainerVolume(currentDirectory, L"./mnt/path", false)})); m_defaultSession.CreateContainer(containerSettings); }, E_INVALIDARG); // Positive: absolute paths must succeed. { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.Volumes(winrt::single_threaded_vector( {WSLCSDK::ContainerVolume(currentDirectory, L"/mnt/path", false)})); auto container = m_defaultSession.CreateContainer(containerSettings); container.Delete(WSLCSDK::DeleteContainerFlags::None); } } WSLC_TEST_METHOD(ContainerVolumeFunctional) { const auto hostRwDir = std::filesystem::current_path() / "wslc-winrt-test-vol-rw"; const auto hostRoDir = std::filesystem::current_path() / "wslc-winrt-test-vol-ro"; std::filesystem::create_directories(hostRwDir); std::filesystem::create_directories(hostRoDir); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::error_code ec; std::filesystem::remove_all(hostRwDir, ec); std::filesystem::remove_all(hostRoDir, ec); }); std::ofstream{hostRwDir / "hello.txt"} << "hello-rw"; std::ofstream{hostRoDir / "hello.txt"} << "hello-ro"; // Container script exits 0 if all checks pass: // 1. RW mount is readable (hello-rw). // 2. RO mount is readable (hello-ro). // 3. Writing to RW mount succeeds. // 4. Writing to RO mount fails (! touch). constexpr auto c_script = "test \"$(cat /mnt/rw/hello.txt)\" = hello-rw && " "test \"$(cat /mnt/ro/hello.txt)\" = hello-ro && " "echo container-write > /mnt/rw/written.txt && " "! touch /mnt/ro/probe 2>/dev/null"; auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sh", L"-c", winrt::to_hstring(c_script)})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.Volumes(winrt::single_threaded_vector({ WSLCSDK::ContainerVolume(hostRwDir.wstring(), L"/mnt/rw", false), WSLCSDK::ContainerVolume(hostRoDir.wstring(), L"/mnt/ro", true), })); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); // Verify the file written by the container is visible on the host. std::ifstream written(hostRwDir / "written.txt"); VERIFY_IS_TRUE(written.is_open()); std::string writtenContent((std::istreambuf_iterator(written)), std::istreambuf_iterator()); VERIFY_ARE_EQUAL(writtenContent, "container-write\n"); } WSLC_TEST_METHOD(ContainerInspect) { auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest")); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); const auto inspectJson = container.Inspect(); VERIFY_IS_FALSE(inspectJson.empty()); const auto id = container.Id(); VERIFY_IS_FALSE(id.empty()); // The inspect JSON must contain the container ID. VERIFY_IS_TRUE(winrt::to_string(inspectJson).find(winrt::to_string(id)) != std::string::npos); container.Delete(WSLCSDK::DeleteContainerFlags::None); cleanup.release(); } WSLC_TEST_METHOD(ContainerExec) { // Start a long-running container so we can exec into it. auto initProcSettings = WSLCSDK::ProcessSettings(); initProcSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(initProcSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); // Positive: exec a command that exits 0. { auto execSettings = WSLCSDK::ProcessSettings(); execSettings.CmdLine(winrt::single_threaded_vector({L"/bin/true"})); auto execProcess = container.CreateProcess(execSettings); StartProcessAndWaitForExit(execProcess); VERIFY_ARE_EQUAL(execProcess.ExitCode(), 0); } // Negative: no command line must fail. VERIFY_THROWS_HR(container.CreateProcess(WSLCSDK::ProcessSettings()).Start(), E_INVALIDARG); } WSLC_TEST_METHOD(ContainerHostName) { // Unit: setting a hostname must succeed. { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.HostName(L"unit-test-host"); } // Functional: container process should see the configured hostname. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"test \"$(hostname)\" = my-test-host"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.HostName(L"my-test-host"); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } } WSLC_TEST_METHOD(ContainerDomainName) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"test \"$(domainname)\" = test.local"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.DomainName(L"test.local"); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } // ----------------------------------------------------------------------- // Process tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ProcessEnvVariables) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"test \"$MY_TEST_VAR\" = hello-from-test"})); procSettings.EnvironmentVariables( winrt::single_threaded_map(std::map{{L"MY_TEST_VAR", L"hello-from-test"}})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } WSLC_TEST_METHOD(ProcessSignal) { // Negative: Signal() before Start() must throw. { auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest")); VERIFY_THROWS_HR(container.InitProcess().Signal(WSLCSDK::Signal::SIGKILL), E_ILLEGAL_METHOD_CALL); } auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto process = container.InitProcess(); std::promise promise; auto autoRevoker = process.Exited(winrt::auto_revoke, [&](int32_t) { promise.set_value(); }); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running); process.Signal(WSLCSDK::Signal::SIGKILL); VERIFY_ARE_EQUAL(promise.get_future().wait_for(2min), std::future_status::ready); const auto state = process.State(); VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited); } WSLC_TEST_METHOD(ProcessGetPid) { // Negative: Pid() before Start() must throw. { auto container = m_defaultSession.CreateContainer(WSLCSDK::ContainerSettings(L"debian:latest")); VERIFY_THROWS_HR(std::ignore = container.InitProcess().Pid(), E_ILLEGAL_METHOD_CALL); } auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); VERIFY_IS_TRUE(process.Pid() > 0); } WSLC_TEST_METHOD(ProcessGetExitCode) { auto runAndGetExitCode = [&](int code) -> int32_t { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", winrt::to_hstring(std::format("exit {}", code))})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); auto exitCode = container.InitProcess().ExitCode(); container.Delete(WSLCSDK::DeleteContainerFlags::Force); return exitCode; }; VERIFY_ARE_EQUAL(runAndGetExitCode(0), 0); VERIFY_ARE_EQUAL(runAndGetExitCode(42), 42); // Negative: querying ExitCode while process is still running must throw. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running); VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } } WSLC_TEST_METHOD(ProcessGetState) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); // State while running. VERIFY_ARE_EQUAL(process.State(), WSLCSDK::ProcessState::Running); // Querying ExitCode while running must throw ERROR_INVALID_STATE. VERIFY_THROWS_HR(std::ignore = process.ExitCode(), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); // Register the Exited event. std::promise exitPromise; auto token = process.Exited([&](int32_t code) { exitPromise.set_value(code); }); auto cleanupToken = wil::scope_exit([&]() { process.Exited(token); }); process.Signal(WSLCSDK::Signal::SIGKILL); // The Exited event must fire after the signal. auto future = exitPromise.get_future(); VERIFY_ARE_EQUAL(future.wait_for(30s), std::future_status::ready); const auto state = process.State(); VERIFY_IS_TRUE(state == WSLCSDK::ProcessState::Signalled || state == WSLCSDK::ProcessState::Exited); } WSLC_TEST_METHOD(ProcessWorkingDirectory) { // Functional: container should see the configured working directory. auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sh", L"-c", L"test \"$(pwd)\" = /tmp"})); procSettings.WorkingDirectory(L"/tmp"); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } // ----------------------------------------------------------------------- // Service tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(GetVersion) { auto version = WSLCSDK::WslcService::GetVersion(); VERIFY_IS_TRUE(version.Major() > 0 || version.Minor() > 0 || version.Revision() > 0); } WSLC_TEST_METHOD(GetMissingComponents) { const auto missing = WSLCSDK::WslcService::GetMissingComponents(); VERIFY_ARE_EQUAL(missing, WSLCSDK::ComponentFlags::None); } WSLC_TEST_METHOD(InstallWithDependencies) { WSLCSDK::WslcService::InstallWithDependenciesAsync().get(); VERIFY_ARE_EQUAL(WSLCSDK::WslcService::GetMissingComponents(), WSLCSDK::ComponentFlags::None); } // ----------------------------------------------------------------------- // Process IO event tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ProcessIoEventsUnit) { // Negative: registering OutputReceived/ErrorReceived without OutputMode::Event must throw. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"1"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view) {}), E_ILLEGAL_METHOD_CALL); VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view) {}), E_ILLEGAL_METHOD_CALL); // GetOutputStream requires OutputMode::Stream — must throw with Discard mode (even after Start). container.Start(); VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL); } // Positive: with OutputMode::Event, registering and revoking event handlers must succeed. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"1"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); auto stdoutToken = process.OutputReceived([](winrt::array_view) {}); auto stderrToken = process.ErrorReceived([](winrt::array_view) {}); auto exitToken = process.Exited([](int32_t) {}); process.OutputReceived(stdoutToken); process.ErrorReceived(stderrToken); process.Exited(exitToken); // GetOutputStream throws when OutputMode is Event. container.Start(); VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL); } // Negative: OutputReceived/ErrorReceived with OutputMode::Stream must throw. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"1"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); VERIFY_THROWS_HR(process.OutputReceived([](winrt::array_view) {}), E_ILLEGAL_METHOD_CALL); VERIFY_THROWS_HR(process.ErrorReceived([](winrt::array_view) {}), E_ILLEGAL_METHOD_CALL); } } WSLC_TEST_METHOD(ProcessIoEventsInitProcess) { std::string stdoutData, stderrData; auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"echo STDOUT && echo STDERR >&2"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto process = container.InitProcess(); process.OutputReceived([&](winrt::array_view data) { stdoutData.append(reinterpret_cast(data.data()), data.size()); }); process.ErrorReceived([&](winrt::array_view data) { stderrData.append(reinterpret_cast(data.data()), data.size()); }); // Start: claims IO handles and starts the IOCallback pump thread. StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(stdoutData, "STDOUT\n"); VERIFY_ARE_EQUAL(stderrData, "STDERR\n"); } WSLC_TEST_METHOD(ProcessIoEventsExecProcess) { // Long-running init process to keep the container alive. auto initProcSettings = WSLCSDK::ProcessSettings(); initProcSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(initProcSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); std::string stdoutData, stderrData; auto execProcSettings = WSLCSDK::ProcessSettings(); execProcSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"echo EXEC_OUT && echo EXEC_ERR >&2"})); execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto execProcess = container.CreateProcess(execProcSettings); execProcess.OutputReceived([&](winrt::array_view data) { stdoutData.append(reinterpret_cast(data.data()), data.size()); }); execProcess.ErrorReceived([&](winrt::array_view data) { stderrData.append(reinterpret_cast(data.data()), data.size()); }); StartProcessAndWaitForExit(execProcess); VERIFY_ARE_EQUAL(stdoutData, "EXEC_OUT\n"); VERIFY_ARE_EQUAL(stderrData, "EXEC_ERR\n"); } WSLC_TEST_METHOD(ProcessIoEventsHandleExclusion) { // Register an OutputReceived handler only. The IOCallback acquires ALL pipe handles // (draining uncallbacked streams to prevent deadlock), so both stdout and stderr // handles are consumed and neither can be obtained via GetOutputStream. auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"99"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); auto process = container.InitProcess(); process.OutputReceived([](winrt::array_view) {}); container.Start(); // stdout handle was consumed by the OutputReceived handler — must not be obtainable. VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput), E_ILLEGAL_METHOD_CALL); // stderr handle was also consumed in order to drain it despite not having a handler. VERIFY_THROWS_HR(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError), E_ILLEGAL_METHOD_CALL); } WSLC_TEST_METHOD(ProcessIoEventsExitCallback) { // Verify the Exited event fires with the correct exit code after IO has been flushed. auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair { std::string stdoutData; std::promise exitPromise; auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector( {L"/bin/sh", L"-c", winrt::hstring(std::format(L"echo HELLO && exit {}", exitCodeArg))})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto process = container.InitProcess(); process.OutputReceived([&](winrt::array_view data) { stdoutData.append(reinterpret_cast(data.data()), data.size()); }); process.Exited([&](int32_t code) { exitPromise.set_value(code); }); container.Start(); auto future = exitPromise.get_future(); VERIFY_ARE_EQUAL(future.wait_for(60s), std::future_status::ready); return {future.get(), stdoutData}; }; // Exit 0: Exited event must fire with code 0; IO must have been delivered first. { auto [exitCode, output] = RunAndCaptureExit(0); VERIFY_ARE_EQUAL(exitCode, 0); VERIFY_ARE_EQUAL(output, "HELLO\n"); } // Non-zero exit: Exited event must report the correct code. { auto [exitCode, output] = RunAndCaptureExit(42); VERIFY_ARE_EQUAL(exitCode, 42); VERIFY_ARE_EQUAL(output, "HELLO\n"); } } WSLC_TEST_METHOD(ProcessIoEventsCancelOnRelease) { // Verify that releasing the process handle while an exec'd process is still running // and writing IO cancels the event pump: // - No IO events arrive after the handle is released. // - Exited is never invoked (cancellation suppresses it). // Long-running init process to keep the container alive. auto initProcSettings = WSLCSDK::ProcessSettings(); initProcSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"999"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(initProcSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); std::atomic callbackCount{0}; std::atomic exitFired{false}; auto execProcSettings = WSLCSDK::ProcessSettings(); execProcSettings.CmdLine( winrt::single_threaded_vector({L"/bin/sh", L"-c", L"while true; do echo LINE; sleep 0.05; done"})); execProcSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto execProcess = container.CreateProcess(execProcSettings); execProcess.OutputReceived([&](winrt::array_view) { callbackCount.fetch_add(1); }); execProcess.Exited([&](int32_t) { exitFired.store(true); }); execProcess.Start(); // Wait for events to start arriving. Sleep(500); VERIFY_IS_TRUE(callbackCount.load() > 0); // Release the exec process handle while it is still running and writing. execProcess = nullptr; const int countAtRelease = callbackCount.load(); // Exited must not have fired: cancellation suppresses it. VERIFY_IS_FALSE(exitFired.load()); // No further events after release. Sleep(200); VERIFY_ARE_EQUAL(callbackCount.load(), countAtRelease); VERIFY_IS_FALSE(exitFired.load()); } WSLC_TEST_METHOD(ProcessIoEventsLargeOutput) { // Generate ~1 MiB of stdout via: dd if=/dev/zero bs=1024 count=1024 | base64 // 1,048,576 zero bytes → base64 output is 1,398,104 bytes. static constexpr size_t c_expectedBytes = 1'398'104; std::string stdoutData; stdoutData.reserve(c_expectedBytes + 4096); auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector( {L"/bin/sh", L"-c", L"dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Event); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); auto process = container.InitProcess(); process.OutputReceived([&](winrt::array_view data) { stdoutData.append(reinterpret_cast(data.data()), data.size()); }); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes); } // ----------------------------------------------------------------------- // Storage tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(SessionCreateVhd) { constexpr auto c_volumeName = L"wslc-winrt-test-data-vol"; constexpr uint64_t c_vhdSizeBytes = 1ull * 1024 * 1024 * 1024; // 1 GiB const std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-winrt-vhd-test-storage"; IGNORE_ERRORS(std::filesystem::remove_all(vhdSessionStorage)); auto cleanup = SCOPE_CLEANUP(std::filesystem::remove_all(vhdSessionStorage)); // Create a dedicated session so that volume creation does not affect the shared default session. auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-vhd-test", vhdSessionStorage.wstring()); settings.Timeout(std::chrono::duration_cast(30s)); settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic)); auto session = WSLCSDK::Session(settings); session.Start(); // Load debian. const auto debianTar = GetTestImagePath("debian:latest"); session.LoadImageAsync(debianTar.wstring()).get(); // Positive: create a named VHD volume. session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic)); // The backing VHD file must exist on disk. const auto expectedVhdPath = vhdSessionStorage / "volumes" / (std::wstring(c_volumeName) + L".vhdx"); VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath)); // Positive: write a marker via a container that mounts the named volume. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector( {L"/bin/sh", L"-c", L"echo wslc-winrt-vhd-test > /data/marker.txt"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.NamedVolumes(winrt::single_threaded_vector( {WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", false)})); auto container = session.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } // Positive: read back the marker in a second container (read-only mount). { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector( {L"/bin/sh", L"-c", L"test \"$(cat /data/marker.txt)\" = wslc-winrt-vhd-test"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.NamedVolumes(winrt::single_threaded_vector( {WSLCSDK::ContainerNamedVolume(c_volumeName, L"/data", true)})); auto container = session.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } // Positive: delete the volume. session.DeleteVhdVolume(c_volumeName); VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath)); // Negative: zero size must fail. VERIFY_THROWS_HR(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_volumeName, 0, WSLCSDK::VhdType::Dynamic)), E_INVALIDARG); // Positive: fixed-allocation VHD; on-disk file size must be >= SizeBytes. { constexpr auto c_fixedVolumeName = L"wslc-sdk-vhd-fixed"; constexpr auto c_fixedSizeBytes = 64ull * _1MB; VERIFY_NO_THROW(session.CreateVhdVolume(WSLCSDK::VhdOptions(c_fixedVolumeName, c_fixedSizeBytes, WSLCSDK::VhdType::Fixed))); auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_fixedVolumeName)); std::filesystem::path expectedVhdPath = vhdSessionStorage / L"volumes" / (std::wstring(c_fixedVolumeName) + L".vhdx"); VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath)); VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes); } // Positive: SetOwner() bakes uid/gid into the volume root inode at mkfs time. // Verify by stat-ing the mount inside a container. { constexpr auto c_ownedVolumeName = L"wslc-sdk-vhd-owned"; auto vhdOptions = WSLCSDK::VhdOptions(c_ownedVolumeName, c_vhdSizeBytes, WSLCSDK::VhdType::Dynamic); vhdOptions.SetOwner(65534, 65534); // nobody:nogroup session.CreateVhdVolume(vhdOptions); auto deleteVolume = SCOPE_CLEANUP(session.DeleteVhdVolume(c_ownedVolumeName)); auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/usr/bin/stat", L"-c", L"%u %g", L"/data"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.NamedVolumes(winrt::single_threaded_vector( {WSLCSDK::ContainerNamedVolume(c_ownedVolumeName, L"/data", false)})); auto container = session.CreateContainer(containerSettings); StartContainerAndWaitForInitProcessExit(container); auto output = GetProcessOutput(container.InitProcess()); VERIFY_ARE_EQUAL(container.InitProcess().ExitCode(), 0); VERIFY_ARE_EQUAL(output.StandardOutput, L"65534 65534\n"); container.Delete(WSLCSDK::DeleteContainerFlags::Force); } } // ----------------------------------------------------------------------- // Authentication / registry tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(AuthenticateTests) { constexpr auto c_username = "wslctest"; constexpr auto c_password = "password"; auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password); const auto serverUri = Uri(winrt::to_hstring(std::format("http://{}", registryAddress))); // Negative: wrong password must fail. VERIFY_THROWS_HR(m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), L"wrong-password"), E_FAIL); // Positive: correct credentials VERIFY_NO_THROW(m_defaultSession.Authenticate(serverUri, winrt::to_hstring(c_username), winrt::to_hstring(c_password))); const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password); PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress)); // Positive: pulling with correct credentials must succeed. { auto opts = WSLCSDK::PullImageOptions(image); opts.RegistryAuth(winrt::to_hstring(xRegistryAuth)); m_defaultSession.PullImageAsync(opts).get(); VERIFY_IS_TRUE(HasImage(image)); } // Negative: pulling without credentials must fail. { VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get(), E_FAIL); } // Negative: pulling with bad credentials must fail. { auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong"); auto opts = WSLCSDK::PullImageOptions(image); opts.RegistryAuth(winrt::to_hstring(badAuth)); VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), E_FAIL); } } WSLC_TEST_METHOD(PullImage) { auto [registryContainer, registryAddress] = StartLocalRegistry(); const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); { PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); const auto image = winrt::to_hstring(std::format("{}/hello-world:latest", registryAddress)); // Delete the image locally so the pull is a real network pull. IGNORE_ERRORS(m_defaultSession.DeleteImage(image)); // Positive: pull from the local registry. m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(image)).get(); VERIFY_IS_TRUE(HasImage(image)); // Verify the pulled image is runnable. auto output = RunContainerAndWaitForExit(image, {}); VERIFY_ARE_EQUAL(output.ExitCode, 0); } // Negative: image that does not exist in the registry. { const auto missing = winrt::to_hstring(std::format("{}/does-not-exist", registryAddress)); auto opts = WSLCSDK::PullImageOptions(missing); opts.RegistryAuth(winrt::to_hstring(xRegistryAuth)); VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(opts).get(), static_cast(WSLC_E_IMAGE_NOT_FOUND)); } // Negative: empty URI must fail. VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"")).get(), E_INVALIDARG); } WSLC_TEST_METHOD(PushImage) { auto [registryContainer, registryAddress] = StartLocalRegistry(); const auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); // Positive: push an existing image to the local registry. PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); // Negative: pushing a non-existent image must fail. VERIFY_THROWS_HR( m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"does-not-exist", winrt::to_hstring(xRegistryAuth))).get(), E_FAIL); // Negative: empty image name must fail. VERIFY_THROWS_HR(m_defaultSession.PushImageAsync(WSLCSDK::PushImageOptions(L"", winrt::to_hstring(xRegistryAuth))).get(), E_INVALIDARG); } WSLC_TEST_METHOD(TagImage) { // Positive: tag an existing image. m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"winrt-sdk-test-tag")); VERIFY_IS_TRUE(HasImage(L"debian:winrt-sdk-test-tag")); auto cleanup = DELETE_IMAGE_ON_SCOPE_EXIT(L"debian:winrt-sdk-test-tag"); // Negative: empty image name must fail. VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"", L"debian", L"test")), E_INVALIDARG); // Negative: empty repository must fail. VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"", L"test")), E_INVALIDARG); // Negative: empty tag must fail. VERIFY_THROWS_HR(m_defaultSession.TagImage(WSLCSDK::TagImageOptions(L"debian:latest", L"debian", L"")), E_INVALIDARG); } // ----------------------------------------------------------------------- // Negative / edge-case tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ExecOnStoppedContainer) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"10"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); // Wait for the short-lived init process to exit StartContainerAndWaitForInitProcessExit(container); // The init process has now exited. Attempting to exec on a stopped container must fail. auto execSettings = WSLCSDK::ProcessSettings(); execSettings.CmdLine(winrt::single_threaded_vector({L"/bin/echo", L"should-fail"})); VERIFY_THROWS_HR(container.CreateProcess(execSettings).Start(), static_cast(WSLC_E_CONTAINER_NOT_RUNNING)); } WSLC_TEST_METHOD(DuplicateContainerName) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"10"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.Name(L"duplicate-name-test-winrt"); auto container1 = m_defaultSession.CreateContainer(containerSettings); container1.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container1); // Creating a second container with the same name must fail. VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)); } WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce) { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector({L"/bin/sleep", L"10"})); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); auto container = m_defaultSession.CreateContainer(containerSettings); container.Start(); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); // Deleting a running container without Force must fail. VERIFY_THROWS_HR(container.Delete(WSLCSDK::DeleteContainerFlags::None), static_cast(WSLC_E_CONTAINER_IS_RUNNING)); } WSLC_TEST_METHOD(DeleteNonExistentImage) { VERIFY_THROWS_HR(m_defaultSession.DeleteImage(L"nonexistent-image:this-tag-does-not-exist"), static_cast(WSLC_E_IMAGE_NOT_FOUND)); } WSLC_TEST_METHOD(PullInvalidImageUri) { VERIFY_THROWS_HR(m_defaultSession.PullImageAsync(WSLCSDK::PullImageOptions(L"///invalid-registry-url///")).get(), E_INVALIDARG); } WSLC_TEST_METHOD(ContainerGpu) { // Negative: creating a GPU container on a session without GPU support must fail. { auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.Flags(WSLCSDK::ContainerFlags::EnableGpu); VERIFY_THROWS_HR(m_defaultSession.CreateContainer(containerSettings).Start(), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } // Create a GPU-enabled session. const std::filesystem::path gpuStorage = m_storagePath / "wslc-winrt-gpu-session-storage"; auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { std::error_code error; std::filesystem::remove_all(gpuStorage, error); }); auto settings = WSLCSDK::SessionSettings(L"wslc-winrt-gpu-test", gpuStorage.wstring()); settings.FeatureFlags(WSLCSDK::SessionFeatureFlags::EnableGpu); settings.VhdRequirements(WSLCSDK::VhdOptions(L"", 4096ull * 1024 * 1024, WSLCSDK::VhdType::Dynamic)); auto gpuSession = WSLCSDK::Session(settings); gpuSession.Start(); const auto debianTar = GetTestImagePath("debian:latest"); gpuSession.LoadImageAsync(debianTar.wstring()).get(); // Positive: /dev/dxg must be available with read/write permissions, and the dynamic linker must be configured to resolve // the WSL GPU libraries inside a GPU container. { auto procSettings = WSLCSDK::ProcessSettings(); procSettings.CmdLine(winrt::single_threaded_vector( {L"/bin/sh", L"-c", L"test -c /dev/dxg && test -r /dev/dxg && test -w /dev/dxg && cat /etc/ld.so.conf.d/ld.wsl.conf"})); procSettings.OutputMode(WSLCSDK::ProcessOutputMode::Stream); auto containerSettings = WSLCSDK::ContainerSettings(L"debian:latest"); containerSettings.InitProcess(procSettings); containerSettings.Flags(WSLCSDK::ContainerFlags::EnableGpu); auto container = gpuSession.CreateContainer(containerSettings); auto cleanup = DELETE_CONTAINER_ON_SCOPE_EXIT(container); StartContainerAndWaitForInitProcessExit(container); auto output = GetProcessOutput(container.InitProcess()); VERIFY_ARE_EQUAL(output.StandardOutput, L"/usr/lib/wsl/lib\n"); } gpuSession.Terminate(); } };