/*++ Copyright (c) Microsoft. All rights reserved. Module Name: WslcSdkTests.cpp Abstract: This file contains test cases for the WSLC SDK. --*/ #include "precomp.h" #include "Common.h" #include "wslcsdk.h" #include "WslcsdkPrivate.h" #include "WSLCContainerLauncher.h" #include "wslc_schema.h" #include extern std::wstring g_testDataPath; extern bool g_fastTestRun; using namespace std::chrono_literals; namespace { // // RAII guards for opaque WSLC handle types. // void CloseSession(WslcSession session) { if (session) { WslcTerminateSession(session); WslcReleaseSession(session); } } using UniqueSession = wil::unique_any; void CloseContainer(WslcContainer container) { if (container) { WslcStopContainer(container, WSLC_SIGNAL_SIGKILL, 0, nullptr); WslcDeleteContainer(container, WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr); WslcReleaseContainer(container); } } using UniqueContainer = wil::unique_any; void CloseProcess(WslcProcess process) { if (process) { WslcReleaseProcess(process); } } using UniqueProcess = wil::unique_any; struct ProcessOutput { std::string stdoutOutput; std::string stderrOutput; }; ProcessOutput WaitForProcessOutput(WslcProcess process, std::chrono::milliseconds timeout = 2min) { // Borrow the exit-event handle (lifetime tied to the process object; do NOT close it). HANDLE exitEvent = nullptr; THROW_IF_FAILED(WslcGetProcessExitEvent(process, &exitEvent)); // Acquire stdout / stderr pipe handles (caller owns these). wil::unique_handle ownedStdout; THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout)); wil::unique_handle ownedStderr; THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDERR, &ownedStderr)); // Read stdout / stderr concurrently so that full pipe buffers do not stall the process. ProcessOutput output; wsl::windows::common::io::MultiHandleWait io; io.AddHandle(std::make_unique( std::move(ownedStdout), [&](const auto& buffer) { output.stdoutOutput.append(buffer.data(), buffer.size()); })); io.AddHandle(std::make_unique( std::move(ownedStderr), [&](const auto& buffer) { output.stderrOutput.append(buffer.data(), buffer.size()); })); auto timeoutTime = std::chrono::steady_clock::now() + timeout; io.Run(timeout); auto remaining = timeoutTime - std::chrono::steady_clock::now(); if (remaining < 0ns) { remaining = {}; } // Check that the process exits within the timeout. THROW_HR_IF( HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(exitEvent, static_cast(std::chrono::duration_cast(remaining).count())) != WAIT_OBJECT_0); return output; } // // Runs a container with the given argv, waits up to timeoutMs for it to exit, // and returns the captured stdout / stderr output. // ProcessOutput RunContainerAndCapture(WslcSession session, const WslcContainerSettings& containerSettings, std::chrono::milliseconds timeout = 2min) { // Create and start the container. UniqueContainer container; THROW_IF_FAILED(WslcCreateContainer(session, &containerSettings, &container, nullptr)); THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); // Acquire the init process handle. UniqueProcess process; THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process)); return WaitForProcessOutput(process.get()); } ProcessOutput RunContainerAndCapture( WslcSession session, const char* image, const std::vector& argv, WslcContainerFlags flags = WSLC_CONTAINER_FLAG_NONE, const char* name = nullptr, std::chrono::milliseconds timeout = 2min, std::optional networkingMode = std::nullopt) { // Build process settings. WslcProcessSettings procSettings; THROW_IF_FAILED(WslcInitProcessSettings(&procSettings)); if (!argv.empty()) { THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv.data(), argv.size())); } // Build container settings. WslcContainerSettings containerSettings; THROW_IF_FAILED(WslcInitContainerSettings(image, &containerSettings)); THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); THROW_IF_FAILED(WslcSetContainerSettingsFlags(&containerSettings, flags)); if (name) { THROW_IF_FAILED(WslcSetContainerSettingsName(&containerSettings, name)); } if (networkingMode.has_value()) { THROW_IF_FAILED(WslcSetContainerSettingsNetworkingMode(&containerSettings, *networkingMode)); } return RunContainerAndCapture(session, containerSettings, timeout); } } // namespace class WslcSdkTests { WSLC_TEST_CLASS(WslcSdkTests) WSADATA m_wsadata; std::filesystem::path m_storagePath; WslcSession m_defaultSession = nullptr; static inline auto c_testSessionName = L"wslc-test"; void LoadTestImage(std::string_view imageName) { std::filesystem::path imagePath = GetTestImagePath(imageName); THROW_IF_FAILED(WslcLoadSessionImageFromFile(m_defaultSession, imagePath.c_str(), nullptr, nullptr)); } TEST_CLASS_SETUP(TestClassSetup) { THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata)); // 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 WSLC SDK. WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(c_testSessionName, m_storagePath.c_str(), &sessionSettings)); VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 4)); VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 2048)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000)); WslcVhdRequirements vhdReqs{}; vhdReqs.sizeBytes = 4096ull * 1024 * 1024; // 4 GB vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs)); VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &m_defaultSession, nullptr)); // Pull images required by the tests (no-op if already present). for (const char* image : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"}) { LoadTestImage(image); } return true; } TEST_CLASS_CLEANUP(TestClassCleanup) { if (m_defaultSession) { WslcTerminateSession(m_defaultSession); WslcReleaseSession(m_defaultSession); 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) { std::filesystem::path extraStorage = m_storagePath / "wslc-extra-session-storage"; WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-extra-session", extraStorage.c_str(), &sessionSettings)); VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 2)); VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 1024)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000)); WslcVhdRequirements vhdReqs{}; vhdReqs.sizeBytes = 1024ull * 1024 * 1024; // 1 GB vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs)); UniqueSession session; VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr)); VERIFY_IS_NOT_NULL(session.get()); // Null output pointer must fail. VERIFY_ARE_EQUAL(WslcCreateSession(&sessionSettings, nullptr, nullptr), E_POINTER); // Null settings pointer must fail. UniqueSession session2; VERIFY_ARE_EQUAL(WslcCreateSession(nullptr, &session2, nullptr), E_POINTER); } WSLC_TEST_METHOD(TerminationCallbackViaTerminate) { std::promise promise; auto callback = [](WslcSessionTerminationReason reason, PVOID context) { auto* p = static_cast*>(context); p->set_value(reason); }; std::filesystem::path extraStorage = m_storagePath / "wslc-termcb-term-storage"; WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termcb-term-test", extraStorage.c_str(), &sessionSettings)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTerminationCallback(&sessionSettings, callback, &promise)); UniqueSession session; VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr)); // Terminating the session should trigger a graceful shutdown and fire the callback. VERIFY_SUCCEEDED(WslcTerminateSession(session.get())); auto future = promise.get_future(); VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(30)), std::future_status::ready); VERIFY_ARE_EQUAL(future.get(), WSLC_SESSION_TERMINATION_REASON_SHUTDOWN); } WSLC_TEST_METHOD(TerminationCallbackViaRelease) { std::promise promise; auto callback = [](WslcSessionTerminationReason reason, PVOID context) { auto* p = static_cast*>(context); p->set_value(reason); }; std::filesystem::path extraStorage = m_storagePath / "wslc-termcb-release-storage"; WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termcb-release-test", extraStorage.c_str(), &sessionSettings)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000)); VERIFY_SUCCEEDED(WslcSetSessionSettingsTerminationCallback(&sessionSettings, callback, &promise)); UniqueSession session; VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr)); // Releasing the session should trigger a graceful shutdown and fire the callback. VERIFY_SUCCEEDED(WslcReleaseSession(session.get())); // Calling WslcSessionRelease will destroy the session session.release(); auto future = promise.get_future(); VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(30)), std::future_status::ready); VERIFY_ARE_EQUAL(future.get(), WSLC_SESSION_TERMINATION_REASON_SHUTDOWN); } // ----------------------------------------------------------------------- // Image tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ImageList) { // Positive: session has images pre-loaded — list must return at least one entry. { WslcImageInfo* images = nullptr; uint32_t count = 0; VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, &images, &count)); auto cleanupImages = wil::scope_exit([images]() { CoTaskMemFree(images); }); VERIFY_IS_TRUE(count >= 1); VERIFY_IS_NOT_NULL(images); // At least one image must have a non-empty name. bool foundNonEmpty = false; for (uint32_t i = 0; i < count; ++i) { if (images[i].name[0] != '\0' && (images[i].sha256[0] != 0 || images[i].sha256[31] != 0) && images[i].sizeBytes != 0 && images[i].createdUnixTime != 0) { foundNonEmpty = true; break; } } VERIFY_IS_TRUE(foundNonEmpty); } // Negative: null images pointer must fail. { uint32_t count = 0; VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, nullptr, &count), E_POINTER); } // Negative: null count pointer must fail. { WslcImageInfo* images = nullptr; VERIFY_ARE_EQUAL(WslcListSessionImages(m_defaultSession, &images, nullptr), E_POINTER); } } WSLC_TEST_METHOD(LoadImage) { // Positive: load a saved image tar and verify the image can be run. { // Remove the image first (ignore failure if it wasn't present). WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr); std::filesystem::path imageTar = GetTestImagePath("hello-world:latest"); wil::unique_handle imageTarFileHandle{ CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get()); LARGE_INTEGER fileSize{}; VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize)); VERIFY_SUCCEEDED(WslcLoadSessionImage( m_defaultSession, imageTarFileHandle.get(), static_cast(fileSize.QuadPart), nullptr, nullptr)); // Verify the loaded image is usable. auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {}); VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); } // Positive: load a saved image tar and verify the image can be run. { // Remove the image first (ignore failure if it wasn't present). WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr); std::filesystem::path imageTar = GetTestImagePath("hello-world:latest"); VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(m_defaultSession, imageTar.c_str(), nullptr, nullptr)); // Verify the loaded image is usable. auto output = RunContainerAndCapture(m_defaultSession, "hello-world:latest", {}); VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); } WslcLoadImageOptions opts{}; // Negative: null ImageHandle must fail. VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, nullptr, 1, &opts, nullptr), E_INVALIDARG); // Negative: INVALID_HANDLE_VALUE must fail. VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, INVALID_HANDLE_VALUE, 1, &opts, nullptr), E_INVALIDARG); // Negative: zero ContentLength must fail. VERIFY_ARE_EQUAL(WslcLoadSessionImage(m_defaultSession, GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG); // Negative: null path must fail. VERIFY_ARE_EQUAL(WslcLoadSessionImageFromFile(m_defaultSession, nullptr, &opts, nullptr), E_POINTER); } WSLC_TEST_METHOD(ImportImage) { const auto exportedImageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar"; constexpr auto c_handleImportedImageName = "my-hello-world-handle:test"; constexpr auto c_pathImportedImageName = "my-hello-world-path:test"; // Positive: import an exported image tar via handle+length and verify the image can be run. { WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr); auto cleanup = wil::scope_exit( [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_handleImportedImageName, nullptr)); }); wil::unique_handle imageTarFileHandle{CreateFileW( exportedImageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get()); LARGE_INTEGER fileSize{}; VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize)); VERIFY_SUCCEEDED(WslcImportSessionImage( m_defaultSession, c_handleImportedImageName, imageTarFileHandle.get(), static_cast(fileSize.QuadPart), nullptr, nullptr)); auto output = RunContainerAndCapture(m_defaultSession, c_handleImportedImageName, {"/hello"}); VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); } // Positive: import an exported image tar via path and verify the image can be run. { WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr); auto cleanup = wil::scope_exit( [this]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, c_pathImportedImageName, nullptr)); }); VERIFY_SUCCEEDED(WslcImportSessionImageFromFile(m_defaultSession, c_pathImportedImageName, exportedImageTar.c_str(), nullptr, nullptr)); auto output = RunContainerAndCapture(m_defaultSession, c_pathImportedImageName, {"/hello"}); VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); } WslcImportImageOptions opts{}; // Negative: null image name must fail. VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, nullptr, exportedImageTar.c_str(), &opts, nullptr), E_POINTER); // Negative: missing file input must fail. VERIFY_ARE_EQUAL(WslcImportSessionImageFromFile(m_defaultSession, "missing-file-input:test", nullptr, &opts, nullptr), E_POINTER); // Negative: zero ContentLength must fail. VERIFY_ARE_EQUAL(WslcImportSessionImage(m_defaultSession, "zero-length:test", GetCurrentThreadEffectiveToken(), 0, &opts, nullptr), E_INVALIDARG); } WSLC_TEST_METHOD(LoadImageNonTar) { // The load should fail but it just silently ignores the load currently. SKIP_TEST_NOT_IMPL(); // Negative: attempt to load a non-tar file. { std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW(GetCurrentProcess()); wil::unique_handle selfFileHandle{ CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get()); LARGE_INTEGER fileSize{}; VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize)); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL( WslcLoadSessionImage(m_defaultSession, selfFileHandle.get(), static_cast(fileSize.QuadPart), nullptr, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); } } WSLC_TEST_METHOD(ImportImageNonTar) { // Negative: attempt to load a non-tar file. { std::filesystem::path pathToSelf = wil::QueryFullProcessImageNameW(GetCurrentProcess()); wil::unique_handle selfFileHandle{ CreateFileW(pathToSelf.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == selfFileHandle.get()); LARGE_INTEGER fileSize{}; VERIFY_IS_TRUE(GetFileSizeEx(selfFileHandle.get(), &fileSize)); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL( WslcImportSessionImage( m_defaultSession, "import-self:test", selfFileHandle.get(), static_cast(fileSize.QuadPart), nullptr, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); LogInfo("Import error: %ws", errorMsg.get()); } } WSLC_TEST_METHOD(ImageDelete) { VERIFY_IS_TRUE(HasImage("hello-world:latest")); // Positive: delete an existing image. wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", &errorMsg)); // Verify the image is no longer present in the list. VERIFY_IS_FALSE(HasImage("hello-world:latest")); // Reload the image for subsequent tests. LoadTestImage("hello-world:latest"); // Negative: null name must fail. VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); } // ----------------------------------------------------------------------- // Container lifecycle tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(CreateContainer) { // Simple echo — verify stdout is captured correctly. { auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/echo", "OK"}); VERIFY_ARE_EQUAL(output.stdoutOutput, "OK\n"); VERIFY_ARE_EQUAL(output.stderrOutput, ""); } // Verify stdout and stderr are routed independently. { auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/sh", "-c", "echo stdout && echo stderr >&2"}); VERIFY_ARE_EQUAL(output.stdoutOutput, "stdout\n"); VERIFY_ARE_EQUAL(output.stderrOutput, "stderr\n"); } // Verify that creating a container with a non-existent image fails. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("invalid-image:notfound", &containerSettings)); WslcContainer container = nullptr; wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, &errorMsg), WSLC_E_IMAGE_NOT_FOUND); VERIFY_IS_NULL(container); } // Verify that a null image name is rejected. { WslcContainerSettings containerSettings; VERIFY_ARE_EQUAL(WslcInitContainerSettings(nullptr, &containerSettings), E_POINTER); } // Verify that a null settings pointer is rejected. { VERIFY_ARE_EQUAL(WslcInitContainerSettings("debian:latest", nullptr), E_POINTER); } // Verify that a null container output pointer is rejected. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, nullptr, nullptr), E_POINTER); } } WSLC_TEST_METHOD(ContainerGetID) { UniqueContainer container; WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); // Positive: ID is returned and is the expected length of hex characters. CHAR id[WSLC_CONTAINER_ID_BUFFER_SIZE]{}; VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), id)); VERIFY_ARE_EQUAL(strnlen(id, WSLC_CONTAINER_ID_BUFFER_SIZE), static_cast(WSLC_CONTAINER_ID_BUFFER_SIZE - 1)); // Negative: null ID buffer must fail. VERIFY_ARE_EQUAL(WslcGetContainerID(container.get(), nullptr), E_POINTER); VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr)); } WSLC_TEST_METHOD(ContainerGetState) { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); // State after creation: CREATED. { WslcContainerState state{}; VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state)); VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_CREATED); } VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); // State while running: RUNNING. { WslcContainerState state{}; VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state)); VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING); } VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr)); // State after stop: EXITED. { WslcContainerState state{}; VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state)); VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_EXITED); } // Negative: null state pointer must fail. VERIFY_ARE_EQUAL(WslcGetContainerState(container.get(), nullptr), E_POINTER); VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr)); } WSLC_TEST_METHOD(ContainerStopAndDelete) { // Build a long-running container. WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "999"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "wslc-stop-delete-test")); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); // Acquire and release the init process handle — we won't read its I/O. { UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); } // Stop the container gracefully (after the timeout). VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGKILL, 0, nullptr)); // Delete the stopped container. VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr)); } WSLC_TEST_METHOD(ProcessIOHandles) { // Verify that stdout and stderr can each be read, and are independent streams. WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "printf 'stdout-line\n' ; printf 'stderr-line\n' >&2"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_NONE)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); HANDLE rawStdout = nullptr; VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &rawStdout)); wil::unique_handle ownedStdout(rawStdout); HANDLE rawStderr = nullptr; VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &rawStderr)); wil::unique_handle ownedStderr(rawStderr); // Verify that each handle can only be acquired once. { HANDLE duplicate = nullptr; VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &duplicate), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), WAIT_OBJECT_0); } WSLC_TEST_METHOD(ContainerNetworkingMode) { // BRIDGED: container should have an eth0 interface in sysfs. { auto output = RunContainerAndCapture( m_defaultSession, "debian:latest", {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"}, WSLC_CONTAINER_FLAG_NONE, nullptr, 60s, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED); VERIFY_ARE_EQUAL(output.stdoutOutput, "HAS_ETH0\n"); } // NONE: container should not have an eth0 interface. { auto output = RunContainerAndCapture( m_defaultSession, "debian:latest", {"/bin/sh", "-c", "[ -d /sys/class/net/eth0 ] && echo 'HAS_ETH0' || echo 'NO_ETH0'"}, WSLC_CONTAINER_FLAG_NONE, nullptr, 60s, WSLC_CONTAINER_NETWORKING_MODE_NONE); VERIFY_ARE_EQUAL(output.stdoutOutput, "NO_ETH0\n"); } // Invalid networking mode must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_ARE_EQUAL(WslcSetContainerSettingsNetworkingMode(&containerSettings, static_cast(99)), E_INVALIDARG); } } WSLC_TEST_METHOD(ContainerPortMapping) { // Negative: null mappings with nonzero count must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 1), E_INVALIDARG); } // Negative: non-null pointer with zero count must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcContainerPortMapping portMappings[1] = {}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings, portMappings, 0), E_INVALIDARG); } // Positive: null mappings with zero count must succeed (clears the mapping). { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings, nullptr, 0)); } // Negative: port mappings with NONE networking must fail at container creation. { WslcContainerSettings containerSettings1; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings1)); VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings1, WSLC_CONTAINER_NETWORKING_MODE_NONE)); WslcContainerPortMapping mapping{}; mapping.windowsPort = 12342; mapping.containerPort = 8000; mapping.protocol = WSLC_PORT_PROTOCOL_TCP; VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings1, &mapping, 1)); WslcContainer rawContainer = nullptr; VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings1, &rawContainer, nullptr), E_INVALIDARG); VERIFY_IS_NULL(rawContainer); } // Functional: create a container with BRIDGED networking and a port mapping; // verify that a TCP connection from the host reaches the container. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"python3", "-m", "http.server", "8000"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); const char* env[] = {"PYTHONUNBUFFERED=1"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env))); WslcContainerSettings containerSettings2; VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings2)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings2, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings2, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED)); WslcContainerPortMapping mapping{}; mapping.windowsPort = 12341; mapping.containerPort = 8000; mapping.protocol = WSLC_PORT_PROTOCOL_TCP; VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings2, &mapping, 1)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings2, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); wil::unique_handle ownedStdout; VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout)); WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s); ExpectHttpResponse(L"http://127.0.0.1:12341", 200); } // Functional: port mapping with explicit IPv4 windowsAddress (127.0.0.1). { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"python3", "-m", "http.server", "8000"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); const char* env[] = {"PYTHONUNBUFFERED=1"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env))); WslcContainerSettings containerSettings3; VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings3)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings3, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings3, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED)); sockaddr_storage addr4{}; auto* sin4 = reinterpret_cast(&addr4); sin4->sin_family = AF_INET; VERIFY_ARE_EQUAL(inet_pton(AF_INET, "127.0.0.1", &sin4->sin_addr), 1); WslcContainerPortMapping mapping{}; mapping.windowsPort = 12343; mapping.containerPort = 8000; mapping.protocol = WSLC_PORT_PROTOCOL_TCP; mapping.windowsAddress = &addr4; VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings3, &mapping, 1)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings3, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); wil::unique_handle ownedStdout; VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout)); WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s); ExpectHttpResponse(L"http://127.0.0.1:12343", 200); } // Functional: port mapping with explicit IPv6 windowsAddress (::1). { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"python3", "-m", "http.server", "8000", "--bind", "::"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); const char* env[] = {"PYTHONUNBUFFERED=1"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, env, ARRAYSIZE(env))); WslcContainerSettings containerSettings4; VERIFY_SUCCEEDED(WslcInitContainerSettings("python:3.12-alpine", &containerSettings4)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings4, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings4, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED)); sockaddr_storage addr6{}; auto* sin6 = reinterpret_cast(&addr6); sin6->sin6_family = AF_INET6; VERIFY_ARE_EQUAL(inet_pton(AF_INET6, "::1", &sin6->sin6_addr), 1); WslcContainerPortMapping mapping{}; mapping.windowsPort = 12344; mapping.containerPort = 8000; mapping.protocol = WSLC_PORT_PROTOCOL_TCP; mapping.windowsAddress = &addr6; VERIFY_SUCCEEDED(WslcSetContainerSettingsPortMappings(&containerSettings4, &mapping, 1)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings4, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); wil::unique_handle ownedStdout; VERIFY_SUCCEEDED(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &ownedStdout)); WaitForOutput(std::move(ownedStdout), "Serving HTTP on", 30s); ExpectHttpResponse(L"http://[::1]:12344", 200); } // Negative: unsupported address family must fail when setting container portmapping values. { WslcContainerSettings containerSettings5; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings5)); VERIFY_SUCCEEDED(WslcSetContainerSettingsNetworkingMode(&containerSettings5, WSLC_CONTAINER_NETWORKING_MODE_BRIDGED)); sockaddr_storage badAddr{}; badAddr.ss_family = AF_UNIX; // unsupported for port mapping WslcContainerPortMapping mapping{}; mapping.windowsPort = 12345; mapping.containerPort = 8000; mapping.protocol = WSLC_PORT_PROTOCOL_TCP; mapping.windowsAddress = &badAddr; VERIFY_ARE_EQUAL(WslcSetContainerSettingsPortMappings(&containerSettings5, &mapping, 1), E_INVALIDARG); } } WSLC_TEST_METHOD(ContainerVolumeUnit) { // Negative: null volumes with nonzero count must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 1), E_INVALIDARG); } // Negative: non-null pointer with zero count must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcContainerVolume containerVolumes[1] = {}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, 0), E_INVALIDARG); } // Negative: null paths must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcContainerVolume containerVolumes[1] = {nullptr, "/mnt/path"}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG); } { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); auto currentDirectory = std::filesystem::current_path(); WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), nullptr}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG); } // Relative paths must fail. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcContainerVolume containerVolumes[1] = {L"relative", "/mnt/path"}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG); } { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); auto currentDirectory = std::filesystem::current_path(); WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "./mnt/path"}; VERIFY_ARE_EQUAL(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes)), E_INVALIDARG); } // Positive: null volumes with zero count must succeed (clears volumes). { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, nullptr, 0)); } // Absolute paths should succeed { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); auto currentDirectory = std::filesystem::current_path(); WslcContainerVolume containerVolumes[1] = {currentDirectory.c_str(), "/mnt/path"}; VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, containerVolumes, ARRAYSIZE(containerVolumes))); } } WSLC_TEST_METHOD(ContainerVolumeFunctional) { // Functional: mount a read-write and a read-only directory into the container. { auto hostRwDir = std::filesystem::current_path() / "wslc-test-vol-rw"; auto hostRoDir = std::filesystem::current_path() / "wslc-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); }); // Write sentinel files into both host directories. { std::ofstream rwSentinel(hostRwDir / "hello.txt"); rwSentinel << "hello-rw"; } { std::ofstream roSentinel(hostRoDir / "hello.txt"); roSentinel << "hello-ro"; } WslcContainerVolume volumes[2]{}; volumes[0].windowsPath = hostRwDir.c_str(); volumes[0].containerPath = "/mnt/rw"; volumes[0].readOnly = FALSE; volumes[1].windowsPath = hostRoDir.c_str(); volumes[1].containerPath = "/mnt/ro"; volumes[1].readOnly = TRUE; // Container script: // 1. Read from the rw mount. // 2. Read from the ro mount. // 3. Write a file to the rw mount; print WRITE_OK on success. // 4. Try to write to the ro mount; print RO_WRITE_BLOCKED if correctly rejected. const char* script = "cat /mnt/rw/hello.txt && " "cat /mnt/ro/hello.txt && " "echo 'container-write' > /mnt/rw/written.txt && echo 'WRITE_OK' && " "if touch /mnt/ro/probe 2>/dev/null; then echo 'RO_WRITE_ALLOWED'; else echo 'RO_WRITE_BLOCKED'; fi"; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", script}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsVolumes(&containerSettings, volumes, 2)); ProcessOutput output = RunContainerAndCapture(m_defaultSession, containerSettings); // Verify all four outcomes. VERIFY_IS_TRUE(output.stdoutOutput.find("hello-rw") != std::string::npos); VERIFY_IS_TRUE(output.stdoutOutput.find("hello-ro") != std::string::npos); VERIFY_IS_TRUE(output.stdoutOutput.find("WRITE_OK") != std::string::npos); VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_BLOCKED") != std::string::npos); VERIFY_IS_TRUE(output.stdoutOutput.find("RO_WRITE_ALLOWED") == std::string::npos); // 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) { UniqueContainer container; WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); wil::unique_cotaskmem_ansistring inspectData; VERIFY_SUCCEEDED(WslcInspectContainer(container.get(), &inspectData)); VERIFY_IS_NOT_NULL(inspectData); auto inspectObject = wsl::shared::FromJson(inspectData.get()); CHAR containerId[WSLC_CONTAINER_ID_BUFFER_SIZE]; VERIFY_SUCCEEDED(WslcGetContainerID(container.get(), containerId)); VERIFY_ARE_EQUAL(containerId, inspectObject.Id); } WSLC_TEST_METHOD(ContainerExec) { // Start a long-running container so we can exec into it. WslcProcessSettings initProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings)); const char* initArgv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); // Positive: exec an echo command and verify its output. { WslcProcessSettings execProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings)); const char* execArgv[] = {"/bin/echo", "exec-hello"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv))); UniqueProcess execProcess; VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr)); auto output = WaitForProcessOutput(execProcess.get()); VERIFY_ARE_EQUAL(output.stdoutOutput, "exec-hello\n"); } // Negative: process settings with no command line must fail. { WslcProcessSettings emptyProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&emptyProcSettings)); WslcProcess newProcess = nullptr; VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &emptyProcSettings, &newProcess, nullptr), E_INVALIDARG); VERIFY_IS_NULL(newProcess); } // Negative: null newProcess output pointer must fail. { WslcProcessSettings execProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings)); const char* execArgv[] = {"/bin/echo", "x"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv))); VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execProcSettings, nullptr, nullptr), E_POINTER); } } WSLC_TEST_METHOD(ContainerHostName) { // Unit: setting a hostname succeeds. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "test-host")); } // Functional: container process should see the configured hostname. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/hostname"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsHostName(&containerSettings, "my-test-host")); auto output = RunContainerAndCapture(m_defaultSession, containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "my-test-host\n"); } } WSLC_TEST_METHOD(ContainerDomainName) { // Unit: setting a domain name succeeds. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "my.domain")); } // Functional: container should see the configured domain name. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "echo $(domainname)"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsDomainName(&containerSettings, "test.local")); auto output = RunContainerAndCapture(m_defaultSession, containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "test.local\n"); } } WSLC_TEST_METHOD(ProcessEnvVariables) { // Negative: null pointer with nonzero count must fail. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 1), E_INVALIDARG); } // Negative: non-null pointer with zero count must fail. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* envVars[] = {"FOO=bar"}; VERIFY_ARE_EQUAL(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, 0), E_INVALIDARG); } // Positive: null pointer with zero count must succeed (clears env vars). { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, nullptr, 0)); } // Functional: set an env var and verify it is visible inside the container. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "echo $MY_TEST_VAR"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); const char* envVars[] = {"MY_TEST_VAR=hello-from-test"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsEnvVariables(&procSettings, envVars, ARRAYSIZE(envVars))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); auto output = RunContainerAndCapture(m_defaultSession, containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "hello-from-test\n"); } } WSLC_TEST_METHOD(ProcessSignal) { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); // Positive: SIGKILL the running process. VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL)); // The process exit event should fire after the signal. VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); // Negative: null process handle must return an error. VERIFY_ARE_EQUAL(WslcSignalProcess(nullptr, WSLC_SIGNAL_SIGKILL), E_POINTER); } WSLC_TEST_METHOD(ProcessGetPid) { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); // Positive: PID of a running process must be non-zero. uint32_t pid = 0; VERIFY_SUCCEEDED(WslcGetProcessPid(process.get(), &pid)); VERIFY_IS_TRUE(pid > 0); // Negative: null pid pointer must fail. VERIFY_ARE_EQUAL(WslcGetProcessPid(process.get(), nullptr), E_POINTER); // Negative: null process handle must return an error. WslcProcess nullProcess = nullptr; VERIFY_ARE_EQUAL(WslcGetProcessPid(nullProcess, &pid), E_POINTER); } WSLC_TEST_METHOD(ProcessGetExitCode) { auto RunAndGetProcess = [&](int exitCodeArg) -> UniqueProcess { std::string script = "exit " + std::to_string(exitCodeArg); const char* argv[] = {"/bin/sh", "-c", script.c_str()}; WslcProcessSettings procSettings; THROW_IF_FAILED(WslcInitProcessSettings(&procSettings)); THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings)); THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; THROW_IF_FAILED(WslcGetProcessExitEvent(process.get(), &exitEvent)); THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(exitEvent, 30 * 1000) != WAIT_OBJECT_0); return process; }; auto RunAndGetExitCode = [&](int exitCodeArg) -> INT32 { UniqueProcess process = RunAndGetProcess(exitCodeArg); INT32 code = -1; THROW_IF_FAILED(WslcGetProcessExitCode(process.get(), &code)); return code; }; // Positive: verify exit 0 and exit 42 are reported correctly. VERIFY_ARE_EQUAL(RunAndGetExitCode(0), 0); VERIFY_ARE_EQUAL(RunAndGetExitCode(42), 42); // Negative: null exit code pointer must fail. { auto process = RunAndGetProcess(0); VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), nullptr), E_POINTER); } // Negative: null process handle must return an error. { WslcProcess nullProcess = nullptr; INT32 code = 0; VERIFY_ARE_EQUAL(WslcGetProcessExitCode(nullProcess, &code), E_POINTER); } } WSLC_TEST_METHOD(ProcessGetState) { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); // State while running: RUNNING. { WslcProcessState state{}; VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state)); VERIFY_ARE_EQUAL(state, WSLC_PROCESS_STATE_RUNNING); } // Bonus test for exit code while running { INT32 exitCode{}; VERIFY_ARE_EQUAL(WslcGetProcessExitCode(process.get(), &exitCode), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); VERIFY_ARE_EQUAL(exitCode, -1); } // Kill the process and wait for the exit event. VERIFY_SUCCEEDED(WslcSignalProcess(process.get(), WSLC_SIGNAL_SIGKILL)); VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); // State after kill: SIGNALLED or EXITED. { WslcProcessState state{}; VERIFY_SUCCEEDED(WslcGetProcessState(process.get(), &state)); VERIFY_IS_TRUE(state == WSLC_PROCESS_STATE_SIGNALLED || state == WSLC_PROCESS_STATE_EXITED); } // Negative: null state pointer must fail. VERIFY_ARE_EQUAL(WslcGetProcessState(process.get(), nullptr), E_POINTER); // Negative: null process handle must return an error. { WslcProcess nullProcess = nullptr; WslcProcessState state{}; VERIFY_ARE_EQUAL(WslcGetProcessState(nullProcess, &state), E_POINTER); } } WSLC_TEST_METHOD(ProcessWorkingDirectory) { // Unit: setting a working directory returns S_OK. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp")); } // Negative: null processSettings must fail. VERIFY_ARE_EQUAL(WslcSetProcessSettingsWorkingDirectory(nullptr, "/tmp"), E_POINTER); // Functional: verify pwd reports the configured working directory. { auto output = RunContainerAndCapture(m_defaultSession, "debian:latest", {"/bin/pwd"}); // Default working directory baseline — just verify pwd succeeds. VERIFY_IS_FALSE(output.stdoutOutput.empty()); } // Functional: set working directory to /tmp and verify pwd output. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/pwd"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); VERIFY_SUCCEEDED(WslcSetProcessSettingsWorkingDirectory(&procSettings, "/tmp")); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); auto output = RunContainerAndCapture(m_defaultSession, containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "/tmp\n"); } } WSLC_TEST_METHOD(GetVersion) { // Positive: returns S_OK and fills in a non-zero version. { WslcVersion version{}; VERIFY_SUCCEEDED(WslcGetVersion(&version)); VERIFY_IS_TRUE(version.major > 0 || version.minor > 0 || version.revision > 0); } // Negative: null pointer must fail. VERIFY_ARE_EQUAL(WslcGetVersion(nullptr), E_POINTER); } WSLC_TEST_METHOD(GetMissingComponents) { WslcComponentFlags missing{}; VERIFY_SUCCEEDED(WslcGetMissingComponents(&missing)); // Presumably anywhere that we run the tests we should get these results. // The levels of OS state modification required to test beyond this are beyond the scope of these tests. VERIFY_ARE_EQUAL(missing, WSLC_COMPONENT_FLAG_NONE); } // ----------------------------------------------------------------------- // WslcSetProcessSettingsCallbacks tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(ProcessIoCallbackUnit) { auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {}; auto noopExitCb = [](INT32, PVOID) {}; // Negative: null processSettings must fail. { WslcProcessCallbacks callbacks{}; callbacks.onStdOut = noopIoCb; VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(nullptr, &callbacks, nullptr), E_POINTER); } // Negative: null callbacks pointer with non-null context must fail. { int ctx = 0; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); VERIFY_ARE_EQUAL(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, &ctx), E_INVALIDARG); } // Positive: null callbacks pointer with null context clears all callbacks. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, nullptr, nullptr)); } // Positive: set onStdOut only. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = noopIoCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr)); } // Positive: set onStdErr only. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); WslcProcessCallbacks callbacks{}; callbacks.onStdErr = noopIoCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr)); } // Positive: set onExit only. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); WslcProcessCallbacks callbacks{}; callbacks.onExit = noopExitCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr)); } // Positive: set all three callbacks with a context. { int ctx = 0; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = noopIoCb; callbacks.onStdErr = noopIoCb; callbacks.onExit = noopExitCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx)); } // Negative: StartContainer without ATTACH fails when callbacks are registered. // The ATTACH flag is required so the IOCallback can claim the init process pipe handles. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "1"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = noopIoCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr)); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_ARE_EQUAL(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr), E_INVALIDARG); } } WSLC_TEST_METHOD(ProcessIoCallbackInitProcess) { struct IOContext { std::string stdoutData; std::string stderrData; } ioContext; // Both streams share one callback; ioHandle distinguishes which accumulator to use. auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) { auto* c = static_cast(ctx); auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData; target.append(reinterpret_cast(data), size); }; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; callbacks.onStdErr = ioCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ioContext)); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); // Release the process handle first, then the container handle. // Releasing the container destroys the WslcContainerImpl which joins the IOCallback // thread, guaranteeing all bytes have been delivered before the assertions below. process.reset(); container.reset(); VERIFY_ARE_EQUAL(ioContext.stdoutData, "STDOUT\n"); VERIFY_ARE_EQUAL(ioContext.stderrData, "STDERR\n"); } WSLC_TEST_METHOD(ProcessIoCallbackExecProcess) { // Start a long-running container so we can exec into it. WslcProcessSettings initProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings)); const char* initArgv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); struct IOContext { std::string stdoutData; std::string stderrData; } ioContext; auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID ctx) { auto* c = static_cast(ctx); auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData; target.append(reinterpret_cast(data), size); }; WslcProcessSettings execProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings)); const char* execArgv[] = {"/bin/sh", "-c", "echo EXEC_OUT && echo EXEC_ERR >&2"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; callbacks.onStdErr = ioCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ioContext)); UniqueProcess execProcess; VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(execProcess.get(), &exitEvent)); VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 30 * 1000), static_cast(WAIT_OBJECT_0)); // Releasing the exec process handle destroys WslcProcessImpl and joins its IOCallback // thread, ensuring all bytes are delivered before assertions. execProcess.reset(); VERIFY_ARE_EQUAL(ioContext.stdoutData, "EXEC_OUT\n"); VERIFY_ARE_EQUAL(ioContext.stderrData, "EXEC_ERR\n"); } WSLC_TEST_METHOD(ProcessIoCallbackHandleExclusion) { // Register a stdout callback only. IOCallback always acquires ALL pipe handles // (draining uncallbacked streams to prevent deadlock), so both stdout and stderr // handles are consumed and neither can be obtained via WslcGetProcessIOHandle. auto noopIoCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID) {}; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sleep", "99"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = noopIoCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, nullptr)); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); // stdout handle was consumed by the IOCallback — must not be obtainable. { HANDLE h = nullptr; VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDOUT, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } // stderr handle was also consumed in order to drain it despite not being given a callback. { HANDLE h = nullptr; VERIFY_ARE_EQUAL(WslcGetProcessIOHandle(process.get(), WSLC_PROCESS_IO_HANDLE_STDERR, &h), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } } WSLC_TEST_METHOD(ProcessIoCallbackExitCallback) { // Verify the onExit callback fires with the correct exit code after IO has been flushed. // We test both exit 0 and a non-zero exit code. auto RunAndCaptureExit = [&](int exitCodeArg) -> std::pair { std::string stdoutData; std::atomic capturedExitCode{-999}; struct Context { std::string* stdoutData; std::atomic* capturedExitCode; wil::unique_event exitEvent{wil::EventOptions::ManualReset}; } ctx{&stdoutData, &capturedExitCode}; auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID c) { static_cast(c)->stdoutData->append(reinterpret_cast(data), size); }; auto exitCb = [](INT32 code, PVOID c) { static_cast(c)->capturedExitCode->store(code); static_cast(c)->exitEvent.SetEvent(); }; std::string script = "echo HELLO && exit " + std::to_string(exitCodeArg); const char* argv[] = {"/bin/sh", "-c", script.c_str()}; WslcProcessSettings procSettings; THROW_IF_FAILED(WslcInitProcessSettings(&procSettings)); THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; callbacks.onExit = exitCb; THROW_IF_FAILED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx)); WslcContainerSettings containerSettings; THROW_IF_FAILED(WslcInitContainerSettings("debian:latest", &containerSettings)); THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; THROW_IF_FAILED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); THROW_IF_FAILED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; THROW_IF_FAILED(WslcGetContainerInitProcess(container.get(), &process)); THROW_HR_IF(HRESULT_FROM_WIN32(WAIT_TIMEOUT), WaitForSingleObject(ctx.exitEvent.get(), 60 * 1000) != WAIT_OBJECT_0); return {capturedExitCode.load(), stdoutData}; }; // Exit 0: onExit 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: onExit must report the correct code. { auto [exitCode, output] = RunAndCaptureExit(42); VERIFY_ARE_EQUAL(exitCode, 42); VERIFY_ARE_EQUAL(output, "HELLO\n"); } } WSLC_TEST_METHOD(ProcessIoCallbackCancelOnRelease) { // Verify that releasing the process handle while an exec'd process is still running // and writing IO cancels the IOCallback pump: // - No IO callbacks arrive after the handle is released. // - onExit is never invoked (cancellation returns runResult=false, suppressing it). // // A secondary (exec'd) process is used so that the long-lived init process keeps the // container alive, allowing UniqueContainer to clean up normally at scope exit. // Start a long-running init process to keep the container alive. WslcProcessSettings initProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&initProcSettings)); const char* initArgv[] = {"/bin/sleep", "999"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&initProcSettings, initArgv, ARRAYSIZE(initArgv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); struct Context { std::atomic callbackCount{0}; std::atomic exitFired{false}; } ctx; auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) { static_cast(c)->callbackCount.fetch_add(1); }; auto exitCb = [](INT32, PVOID c) { static_cast(c)->exitFired.store(true); }; // Continuous writer: emits one line every 50 ms indefinitely. WslcProcessSettings execProcSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&execProcSettings)); const char* execArgv[] = {"/bin/sh", "-c", "while true; do echo LINE; sleep 0.05; done"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execProcSettings, execArgv, ARRAYSIZE(execArgv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; callbacks.onExit = exitCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&execProcSettings, &callbacks, &ctx)); UniqueProcess execProcess; VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr)); // Wait long enough for at least several callbacks to arrive. Sleep(500); VERIFY_IS_TRUE(ctx.callbackCount.load() > 0); // Release the exec process handle while the process is still running and writing. // This destructs WslcProcessImpl → cancels the IOCallback → joins its thread. // By the time execProcess.reset() returns, the pump thread has exited. execProcess.reset(); // Snapshot the count now that the thread is confirmed stopped. int countAtRelease = ctx.callbackCount.load(); // onExit must not have fired: cancellation sets runResult=false, suppressing the call. VERIFY_IS_FALSE(ctx.exitFired.load()); // Wait another interval — no further callbacks can arrive after the thread has joined. Sleep(200); VERIFY_ARE_EQUAL(ctx.callbackCount.load(), countAtRelease); VERIFY_IS_FALSE(ctx.exitFired.load()); } WSLC_TEST_METHOD(ProcessIoCallbackLargeOutput) { // 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 (ceil(1048576/3)*4). static constexpr size_t c_expectedBytes = 1'398'104; std::string stdoutData; stdoutData.reserve(c_expectedBytes + 4096); auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID ctx) { static_cast(ctx)->append(reinterpret_cast(data), size); }; WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "dd if=/dev/zero bs=1024 count=1024 2>/dev/null | base64 -w 0"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &stdoutData)); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(process.get(), &exitEvent)); VERIFY_ARE_EQUAL(WaitForSingleObject(exitEvent, 60 * 1000), static_cast(WAIT_OBJECT_0)); // Join the IOCallback thread before inspecting the accumulator. process.reset(); container.reset(); VERIFY_ARE_EQUAL(stdoutData.size(), c_expectedBytes); } WSLC_TEST_METHOD(ReleaseFromIOCallbackFails) { struct Context { std::atomic process{nullptr}; std::atomic container{nullptr}; std::atomic releaseProcessHr{S_OK}; std::atomic releaseContainerHr{S_OK}; std::atomic captured{false}; wil::unique_event done{wil::EventOptions::ManualReset}; } ctx; auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) { auto* cx = static_cast(c); // Wait until the test thread has published both handles before sampling. auto process = cx->process.load(std::memory_order_acquire); auto container = cx->container.load(std::memory_order_acquire); if (!process || !container) { return; } // Only capture on the first eligible callback; later callbacks no-op. bool expected = false; if (!cx->captured.compare_exchange_strong(expected, true)) { return; } // Both calls should fail with ERROR_INVALID_HANDLE_STATE without consuming the handles. cx->releaseProcessHr.store(WslcReleaseProcess(process)); cx->releaseContainerHr.store(WslcReleaseContainer(container)); cx->done.SetEvent(); }; // Continuous writer for the init process so onStdOut fires repeatedly. WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "while true; do echo LINE; sleep 0.05; done"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcProcessCallbacks callbacks{}; callbacks.onStdOut = ioCb; VERIFY_SUCCEEDED(WslcSetProcessSettingsCallbacks(&procSettings, &callbacks, &ctx)); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); UniqueContainer container; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); UniqueProcess process; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process)); // Publish handles to the callback now that both are valid. ctx.container.store(container.get(), std::memory_order_release); ctx.process.store(process.get(), std::memory_order_release); VERIFY_ARE_EQUAL(WaitForSingleObject(ctx.done.get(), 30 * 1000), static_cast(WAIT_OBJECT_0)); VERIFY_ARE_EQUAL(ctx.releaseProcessHr.load(), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE_STATE)); VERIFY_ARE_EQUAL(ctx.releaseContainerHr.load(), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE_STATE)); } // ----------------------------------------------------------------------- // Storage tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(SessionCreateVhd) { constexpr auto c_volumeName = "wslc-test-data-vol"; constexpr auto c_vhdSizeBytes = _1GB; std::filesystem::path vhdSessionStorage = m_storagePath / "wslc-vhd-test-storage"; auto removeStorage = wil::scope_exit([&]() { std::error_code error; std::filesystem::remove_all(vhdSessionStorage, error); if (error) { LogError("Failed to remove VHD test storage %ws: %hs", vhdSessionStorage.c_str(), error.message().c_str()); } }); // Create a dedicated session so that volume creation does not affect the shared default session. WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-vhd-test", vhdSessionStorage.c_str(), &sessionSettings)); WslcVhdRequirements sessionVhd{}; sessionVhd.sizeBytes = 4 * _1GB; sessionVhd.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &sessionVhd)); UniqueSession session; VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr)); // Load debian so we have a container image to work with. std::filesystem::path debianTar = GetTestImagePath("debian:latest"); VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(session.get(), debianTar.c_str(), nullptr, nullptr)); // Positive: create a named VHD volume in the session. { WslcVhdRequirements vhd{}; vhd.name = c_volumeName; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = WSLC_VHD_TYPE_DYNAMIC; wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg)); // The backing VHD file must exist on disk. std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx"); VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath)); } // Positive: write a marker via a container that mounts the named volume. { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "echo wslc-vhd-test > /data/marker.txt"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); WslcContainerNamedVolume namedVol{}; namedVol.name = c_volumeName; namedVol.containerPath = "/data"; namedVol.readOnly = FALSE; VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1)); auto output = RunContainerAndCapture(session.get(), containerSettings); VERIFY_IS_TRUE(output.stderrOutput.empty()); } // Positive: read back the marker in a second container (read-only mount). { WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/bin/sh", "-c", "cat /data/marker.txt"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); WslcContainerNamedVolume namedVol{}; namedVol.name = c_volumeName; namedVol.containerPath = "/data"; namedVol.readOnly = TRUE; VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1)); auto output = RunContainerAndCapture(session.get(), containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "wslc-vhd-test\n"); } // Positive: delete the volume. { wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_volumeName, &errorMsg)); // The backing VHD file must not exist on disk. std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_volumeName) + ".vhdx"); VERIFY_IS_FALSE(std::filesystem::exists(expectedVhdPath)); } // Negative: null options pointer must fail. VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), nullptr, nullptr), E_POINTER); // Negative: null name must fail. { WslcVhdRequirements vhd{}; vhd.name = nullptr; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG); } // Negative: zero sizeInBytes must fail. { WslcVhdRequirements vhd{}; vhd.name = c_volumeName; vhd.sizeBytes = 0; vhd.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG); } // Negative: invalid VHD type must fail. { WslcVhdRequirements vhd{}; vhd.name = c_volumeName; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = static_cast(42); VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG); } // Positive: fixed-allocation VHD; on-disk file size must be >= SizeBytes. { constexpr auto c_fixedVolumeName = "wslc-sdk-vhd-fixed"; constexpr auto c_fixedSizeBytes = 64ull * _1MB; WslcVhdRequirements vhd{}; vhd.name = c_fixedVolumeName; vhd.sizeBytes = c_fixedSizeBytes; vhd.type = WSLC_VHD_TYPE_FIXED; wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg)); auto deleteVolume = wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_fixedVolumeName, nullptr)); }); std::filesystem::path expectedVhdPath = vhdSessionStorage / "volumes" / (std::string(c_fixedVolumeName) + ".vhdx"); VERIFY_IS_TRUE(std::filesystem::exists(expectedVhdPath)); VERIFY_IS_GREATER_THAN_OR_EQUAL(std::filesystem::file_size(expectedVhdPath), c_fixedSizeBytes); } // Positive: owner flags are honored — uid/gid baked into the volume root // inode at mkfs time. Verify by stat-ing the mount inside a container. { constexpr auto c_ownedVolumeName = "wslc-sdk-vhd-owned"; WslcVhdRequirements vhd{}; vhd.name = c_ownedVolumeName; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = WSLC_VHD_TYPE_DYNAMIC; vhd.flags = WSLC_VHD_REQ_FLAG_OWNER; vhd.uid = 65534; // nobody vhd.gid = 65534; // nogroup wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg)); auto deleteVolume = wil::scope_exit([&]() { LOG_IF_FAILED(WslcDeleteSessionVhdVolume(session.get(), c_ownedVolumeName, nullptr)); }); WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); const char* argv[] = {"/usr/bin/stat", "-c", "%u %g", "/data"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); WslcContainerNamedVolume namedVol{}; namedVol.name = c_ownedVolumeName; namedVol.containerPath = "/data"; namedVol.readOnly = FALSE; VERIFY_SUCCEEDED(WslcSetContainerSettingsNamedVolumes(&containerSettings, &namedVol, 1)); auto output = RunContainerAndCapture(session.get(), containerSettings); VERIFY_ARE_EQUAL(output.stdoutOutput, "65534 65534\n"); } // Negative: unknown flag bits are rejected. { WslcVhdRequirements vhd{}; vhd.name = c_volumeName; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = WSLC_VHD_TYPE_DYNAMIC; vhd.flags = static_cast(0x80000000); VERIFY_ARE_EQUAL(WslcCreateSessionVhdVolume(session.get(), &vhd, nullptr), E_INVALIDARG); } // Positive: flags=NONE silently ignores uid/gid (volume defaults to root:root). { constexpr auto c_unflaggedVolumeName = "wslc-sdk-vhd-unflagged"; WslcVhdRequirements vhd{}; vhd.name = c_unflaggedVolumeName; vhd.sizeBytes = c_vhdSizeBytes; vhd.type = WSLC_VHD_TYPE_DYNAMIC; vhd.flags = WSLC_VHD_REQ_FLAG_NONE; vhd.uid = 1000; vhd.gid = 1000; wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcCreateSessionVhdVolume(session.get(), &vhd, &errorMsg)); wil::unique_cotaskmem_string deleteErr; VERIFY_SUCCEEDED(WslcDeleteSessionVhdVolume(session.get(), c_unflaggedVolumeName, &deleteErr)); } } // ----------------------------------------------------------------------- // Authentication helpers // ----------------------------------------------------------------------- // Starts a local registry container with host-mode networking and returns [container, registryAddress]. // Uses the COM API (via GetInternalType) with WSLCContainerLauncher to get host-mode networking, // which the SDK doesn't expose. Host networking shares the VM's network namespace, so the registry // is reachable at 127.0.0.1: from both dockerd (inside the VM) and the host. std::pair StartLocalRegistry( const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) { VERIFY_IS_TRUE(HasImage("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& session = *reinterpret_cast(m_defaultSession)->session; auto container = launcher.Launch(session, 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) { auto imageName = std::format("{}:{}", repo, tag); auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag); auto registryRepo = std::format("{}/{}", registryAddress, repo); VERIFY_IS_TRUE(HasImage(imageName)); // Tag the image with the registry address so it can be pushed. WslcTagImageOptions tagOptions{}; tagOptions.image = imageName.c_str(); tagOptions.repo = registryRepo.c_str(); tagOptions.tag = tag.c_str(); VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr)); // Ensures the registry-prefixed tag is removed after the push. auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr)); }); WslcPushImageOptions pushOptions{}; pushOptions.image = registryImage.c_str(); pushOptions.registryAuth = registryAuth.c_str(); VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &pushOptions, nullptr)); } bool HasImage(const std::string& imageName) { wil::unique_cotaskmem_array_ptr images; VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, images.addressof(), images.size_address())); for (const auto& image : images) { if (image.name == imageName) { return true; } } return false; } // ----------------------------------------------------------------------- // Authentication tests // ----------------------------------------------------------------------- WSLC_TEST_METHOD(AuthenticateTests) { constexpr auto c_username = "wslctest"; constexpr auto c_password = "password"; auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password); // Negative: wrong password must fail. { wil::unique_cotaskmem_ansistring token; wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL( WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); } // Positive: correct credentials must succeed and return a non-null token. { wil::unique_cotaskmem_ansistring token; wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &errorMsg)); VERIFY_IS_NOT_NULL(token.get()); } auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password); PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); auto image = std::format("{}/hello-world:latest", registryAddress); // Pulling with credentials should succeed. { WslcPullImageOptions opts{}; opts.uri = image.c_str(); opts.registryAuth = xRegistryAuth.c_str(); VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); VERIFY_IS_TRUE(HasImage(image)); } // Negative: Pulling without credentials should fail. { WslcPullImageOptions opts{}; opts.uri = image.c_str(); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: Pulling with bad credentials should fail. { auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong"); WslcPullImageOptions opts{}; opts.uri = image.c_str(); opts.registryAuth = badAuth.c_str(); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: null parameters must fail. { wil::unique_cotaskmem_ansistring token; VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, nullptr, c_username, c_password, &token, nullptr), E_POINTER); VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), nullptr, c_password, &token, nullptr), E_POINTER); VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, nullptr, &token, nullptr), E_POINTER); VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, nullptr, nullptr), E_POINTER); } } WSLC_TEST_METHOD(PullImage) { // Start a local registry without auth to avoid Docker Hub rate limits. auto [registryContainer, registryAddress] = StartLocalRegistry(); auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); { // Push hello-world:latest to the local registry. PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); auto image = std::format("{}/hello-world:latest", registryAddress); // Delete the image locally so the pull is a real pull. WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr); // Pull from the local registry. { WslcPullImageOptions opts{}; opts.uri = image.c_str(); VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); } // Verify the pulled image is in the image list. VERIFY_IS_TRUE(HasImage(image)); // Verify the image is usable by running a container from it. auto output = RunContainerAndCapture(m_defaultSession, image.c_str(), {}); VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); } // Negative: pull an image that does not exist. { auto image = std::format("{}/does-not-exist", registryAddress); WslcPullImageOptions opts{}; opts.uri = image.c_str(); opts.registryAuth = xRegistryAuth.c_str(); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND); } // Negative: null URI inside options must fail. { WslcPullImageOptions opts{}; opts.uri = nullptr; wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_INVALIDARG); } } WSLC_TEST_METHOD(PushImage) { auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); // Negative: pushing a non-existent image must fail. { WslcPushImageOptions opts{}; opts.image = "does-not-exist"; opts.registryAuth = emptyRegistryAuth.c_str(); wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: null options must fail. VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); // Negative: null image inside options must fail. { WslcPushImageOptions opts{}; opts.image = nullptr; opts.registryAuth = emptyRegistryAuth.c_str(); VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); } } WSLC_TEST_METHOD(TagImage) { // Positive: tag an existing image. { WslcTagImageOptions opts{}; opts.image = "debian:latest"; opts.repo = "debian"; opts.tag = "sdk-test-tag"; VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &opts, nullptr)); // Verify the tag is present. VERIFY_IS_TRUE(HasImage("debian:sdk-test-tag")); // Cleanup: delete the tag. WslcDeleteSessionImage(m_defaultSession, "debian:sdk-test-tag", nullptr); } // Negative: null options must fail. VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); // Negative: null fields must fail. { WslcTagImageOptions opts{}; opts.image = nullptr; opts.repo = "debian"; opts.tag = "test"; VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); } { WslcTagImageOptions opts{}; opts.image = "debian:latest"; opts.repo = nullptr; opts.tag = "test"; VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); } { WslcTagImageOptions opts{}; opts.image = "debian:latest"; opts.repo = "debian"; opts.tag = nullptr; VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); } } // Negative tests: handle lifecycle and invalid state transitions WSLC_TEST_METHOD(ReleaseNullSessionHandle) { VERIFY_ARE_EQUAL(WslcReleaseSession(nullptr), E_POINTER); } WSLC_TEST_METHOD(TerminateNullSessionHandle) { VERIFY_ARE_EQUAL(WslcTerminateSession(nullptr), E_POINTER); } WSLC_TEST_METHOD(ReleaseNullContainerHandle) { VERIFY_ARE_EQUAL(WslcReleaseContainer(nullptr), E_POINTER); } WSLC_TEST_METHOD(ReleaseNullProcessHandle) { VERIFY_ARE_EQUAL(WslcReleaseProcess(nullptr), E_POINTER); } WSLC_TEST_METHOD(CreateContainerWithNullSession) { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcContainer container = nullptr; VERIFY_ARE_EQUAL(WslcCreateContainer(nullptr, &containerSettings, &container, nullptr), E_POINTER); } WSLC_TEST_METHOD(StopContainerWithInvalidSignal) { UniqueContainer container; WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); PCSTR argv[] = {"/bin/sleep", "10"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr)); // Wait for the short-lived init process to exit UniqueProcess initProcess; VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &initProcess)); HANDLE exitEvent = nullptr; VERIFY_SUCCEEDED(WslcGetProcessExitEvent(initProcess.get(), &exitEvent)); VERIFY_ARE_EQUAL(WAIT_OBJECT_0, WaitForSingleObject(exitEvent, 30000)); // Attempting to exec on a stopped container should fail WslcProcessSettings execSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&execSettings)); PCSTR execArgv[] = {"/bin/echo", "should-fail"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&execSettings, execArgv, ARRAYSIZE(execArgv))); UniqueProcess execProcess; VERIFY_ARE_EQUAL(WslcCreateContainerProcess(container.get(), &execSettings, &execProcess, nullptr), WSLC_E_CONTAINER_NOT_RUNNING); } WSLC_TEST_METHOD(DuplicateContainerName) { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); PCSTR argv[] = {"/bin/sleep", "10"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, "duplicate-name-test")); UniqueContainer container1; VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container1, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container1.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); // Creating a second container with the same name should fail UniqueContainer container2; VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container2, nullptr), static_cast(0x800700b7)); // ERROR_ALREADY_EXISTS } WSLC_TEST_METHOD(DeleteRunningContainerWithoutForce) { UniqueContainer container; WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); WslcProcessSettings procSettings; VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings)); PCSTR argv[] = {"/bin/sleep", "10"}; VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv))); VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings)); VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr)); VERIFY_SUCCEEDED(WslcStartContainer(container.get(), WSLC_CONTAINER_START_FLAG_NONE, nullptr)); // Deleting a running container without force flag should fail VERIFY_ARE_EQUAL(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_NONE, nullptr), WSLC_E_CONTAINER_IS_RUNNING); } WSLC_TEST_METHOD(DeleteNonExistentImage) { VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, "nonexistent-image:this-tag-does-not-exist", nullptr), WSLC_E_IMAGE_NOT_FOUND); } WSLC_TEST_METHOD(PullInvalidImageUri) { WslcPullImageOptions pullOptions = {}; pullOptions.uri = "///invalid-registry-url///"; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &pullOptions, nullptr), E_INVALIDARG); } WSLC_TEST_METHOD(ContainerGpu) { // Validate that creating a GPU container on a session without GPU support fails. { WslcContainerSettings containerSettings; VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings)); VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_ENABLE_GPU)); UniqueContainer container; VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } // Create a GPU-enabled session. std::filesystem::path gpuStorage = m_storagePath / "wslc-gpu-session-storage"; auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { std::error_code error; std::filesystem::remove_all(gpuStorage, error); }); WslcSessionSettings sessionSettings; VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-gpu-test", gpuStorage.c_str(), &sessionSettings)); VERIFY_SUCCEEDED(WslcSetSessionSettingsFeatureFlags(&sessionSettings, WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU)); WslcVhdRequirements vhdReqs{}; vhdReqs.sizeBytes = 4096ull * 1024 * 1024; vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC; VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs)); UniqueSession gpuSession; VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &gpuSession, nullptr)); THROW_IF_FAILED(WslcLoadSessionImageFromFile(gpuSession.get(), GetTestImagePath("debian:latest").c_str(), nullptr, nullptr)); // Validate /dev/dxg is available and the dynamic linker is configured to resolve the WSL // GPU libraries. { const char* initArgv[] = { "/bin/sh", "-c", "test -c /dev/dxg && test -r /dev/dxg && test -w /dev/dxg && cat /etc/ld.so.conf.d/ld.wsl.conf"}; auto output = RunContainerAndCapture( gpuSession.get(), "debian:latest", {initArgv[0], initArgv[1], initArgv[2]}, WSLC_CONTAINER_FLAG_ENABLE_GPU); VERIFY_ARE_EQUAL(output.stdoutOutput, "/usr/lib/wsl/lib\n"); } } };