Files
WSL/test/windows/WslcSdkTests.cpp

3205 lines
146 KiB
C++

/*++
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 "WSLCProcessLauncher.h"
#include "wslc_schema.h"
#include "wslc/e2e/WSLCE2EHelpers.h"
#include <optional>
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<WslcSession, decltype(CloseSession), CloseSession>;
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<WslcContainer, decltype(CloseContainer), CloseContainer>;
// Release-only handle: does not stop or delete the container.
// Used when a second handle to the same container is needed (e.g., OpenContainer tests).
void ReleaseContainerHandle(WslcContainer container)
{
if (container)
{
THROW_IF_FAILED(WslcReleaseContainer(container));
}
}
using UniqueContainerReleaseOnly = wil::unique_any<WslcContainer, decltype(ReleaseContainerHandle), ReleaseContainerHandle>;
void CloseProcess(WslcProcess process)
{
if (process)
{
WslcReleaseProcess(process);
}
}
using UniqueProcess = wil::unique_any<WslcProcess, decltype(CloseProcess), CloseProcess>;
using UniqueCrashDumpSubscription =
wil::unique_any<WslcCrashDumpSubscription, decltype(&WslcReleaseCrashDumpSubscription), WslcReleaseCrashDumpSubscription>;
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<wsl::windows::common::io::ReadHandle>(
std::move(ownedStdout), [&](const auto& buffer) { output.stdoutOutput.append(buffer.data(), buffer.size()); }));
io.AddHandle(std::make_unique<wsl::windows::common::io::ReadHandle>(
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<DWORD>(std::chrono::duration_cast<std::chrono::milliseconds>(remaining).count())) != WAIT_OBJECT_0);
return output;
}
//
// Initializes process settings with the given command line and flags. The command line storage must
// outlive the settings, since only a pointer to it is stored.
//
void InitProcessSettings(WslcProcessSettings& processSettings, std::span<const char* const> commandLine, WslcProcessFlags flags = WSLC_PROCESS_FLAG_NONE)
{
THROW_IF_FAILED(WslcInitProcessSettings(&processSettings));
THROW_IF_FAILED(WslcSetProcessSettingsCmdLine(&processSettings, commandLine.data(), commandLine.size()));
THROW_IF_FAILED(WslcSetProcessSettingsFlags(&processSettings, flags));
}
//
// Writes the given input to a process's stdin and closes the handle so that the process sees EOF.
// Requires WSLC_PROCESS_FLAG_STDIN.
//
void WriteToProcessStdin(WslcProcess process, std::string_view input)
{
wil::unique_handle stdinHandle;
THROW_IF_FAILED(WslcGetProcessIOHandle(process, WSLC_PROCESS_IO_HANDLE_STDIN, &stdinHandle));
const auto size = static_cast<DWORD>(input.size());
DWORD written = 0;
THROW_IF_WIN32_BOOL_FALSE(WriteFile(stdinHandle.get(), input.data(), size, &written, nullptr));
THROW_HR_IF(E_UNEXPECTED, written != size);
}
//
// Creates a container whose init process is described by the given settings and starts it.
//
UniqueContainer StartContainerWithInitProcess(WslcSession session, const char* image, WslcProcessSettings& initProcessSettings, WslcContainerStartFlags startFlags)
{
WslcContainerSettings containerSettings;
THROW_IF_FAILED(WslcInitContainerSettings(image, &containerSettings));
THROW_IF_FAILED(WslcSetContainerSettingsInitProcess(&containerSettings, &initProcessSettings));
UniqueContainer container;
THROW_IF_FAILED(WslcCreateContainer(session, &containerSettings, &container, nullptr));
THROW_IF_FAILED(WslcStartContainer(container.get(), startFlags, nullptr));
return container;
}
//
// 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<const char*>& argv,
WslcContainerFlags flags = WSLC_CONTAINER_FLAG_NONE,
const char* name = nullptr,
std::chrono::milliseconds timeout = 2min,
std::optional<WslcContainerNetworkingMode> 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(TerminationEventViaTerminate)
{
std::filesystem::path extraStorage = m_storagePath / "wslc-termevt-term-storage";
WslcSessionSettings sessionSettings;
VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termevt-term-test", extraStorage.c_str(), &sessionSettings));
VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
UniqueSession session;
VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
wil::unique_handle terminationEvent;
VERIFY_SUCCEEDED(WslcGetSessionTerminationEvent(session.get(), &terminationEvent));
VERIFY_IS_NOT_NULL(terminationEvent.get());
// Terminating the session should trigger a graceful shutdown and signal the event.
VERIFY_SUCCEEDED(WslcTerminateSession(session.get()));
VERIFY_ARE_EQUAL(WaitForSingleObject(terminationEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
WslcSessionTerminationReason reason = WSLC_SESSION_TERMINATION_REASON_UNKNOWN;
VERIFY_SUCCEEDED(WslcGetSessionTerminationReason(session.get(), &reason));
VERIFY_ARE_EQUAL(reason, WSLC_SESSION_TERMINATION_REASON_SHUTDOWN);
}
WSLC_TEST_METHOD(TerminationEventViaRelease)
{
std::filesystem::path extraStorage = m_storagePath / "wslc-termevt-release-storage";
WslcSessionSettings sessionSettings;
VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-termevt-release-test", extraStorage.c_str(), &sessionSettings));
VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
UniqueSession session;
VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
// The termination event is owned by the caller and stays valid even after the session is released.
wil::unique_handle terminationEvent;
VERIFY_SUCCEEDED(WslcGetSessionTerminationEvent(session.get(), &terminationEvent));
VERIFY_IS_NOT_NULL(terminationEvent.get());
// Releasing the session should trigger a graceful shutdown and signal the event.
VERIFY_SUCCEEDED(WslcReleaseSession(session.get()));
// Calling WslcReleaseSession will destroy the session.
session.release();
VERIFY_ARE_EQUAL(WaitForSingleObject(terminationEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
}
WSLC_TEST_METHOD(CrashDumpCallback)
{
struct Invocation
{
std::wstring DumpPath;
std::string ProcessName;
uint32_t Pid;
uint32_t Signal;
uint64_t Timestamp;
};
std::promise<Invocation> promise;
auto callback = [](const WslcSessionCrashDumpInfo* info, PVOID context) {
auto* p = static_cast<std::promise<Invocation>*>(context);
p->set_value(Invocation{
info->dumpPath ? std::wstring{info->dumpPath} : std::wstring{},
info->processName ? std::string{info->processName} : std::string{},
info->pid,
info->signal,
info->timestamp});
};
std::filesystem::path extraStorage = m_storagePath / "wslc-crash-callback-storage";
auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code ec;
std::filesystem::remove_all(extraStorage, ec);
});
WslcSessionSettings sessionSettings;
VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-crashcb-test", extraStorage.c_str(), &sessionSettings));
VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
UniqueSession session;
VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
UniqueCrashDumpSubscription subscription;
VERIFY_SUCCEEDED(WslcRegisterSessionCrashDumpCallback(session.get(), callback, &promise, &subscription, nullptr));
auto comSession = reinterpret_cast<WslcSessionImpl*>(session.get())->session.query<IWSLCSession>();
wsl::windows::common::WSLCProcessLauncher launcher{"/bin/sh", {"/bin/sh", "-c", "kill -SEGV $$"}};
auto process = launcher.Launch(*comSession);
auto result = process.WaitAndCaptureOutput();
VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGSEGV);
auto future = promise.get_future();
VERIFY_ARE_EQUAL(future.wait_for(std::chrono::seconds(60)), std::future_status::ready);
auto invocation = future.get();
VERIFY_IS_FALSE(invocation.DumpPath.empty());
VERIFY_IS_TRUE(std::filesystem::exists(invocation.DumpPath));
VERIFY_IS_GREATER_THAN(std::filesystem::file_size(invocation.DumpPath), 0ull);
VERIFY_IS_TRUE(invocation.ProcessName.find("sh") != std::string::npos);
VERIFY_ARE_EQUAL(invocation.Signal, static_cast<uint32_t>(WSLCSignalSIGSEGV));
VERIFY_IS_GREATER_THAN(invocation.Pid, 0u);
VERIFY_IS_GREATER_THAN(invocation.Timestamp, 0ull);
}
// -----------------------------------------------------------------------
// 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<uint64_t>(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<uint64_t>(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<std::wstring>(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<uint64_t>(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<std::wstring>(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<uint64_t>(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<size_t>(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<WslcContainerNetworkingMode>(99)), E_INVALIDARG);
}
}
WSLC_TEST_METHOD(SessionHostLoopbackDisabled)
{
constexpr uint16_t c_hostLoopbackTestPort = 1237;
const auto endpoint = std::format(L"http://127.0.0.1:{}/", c_hostLoopbackTestPort);
UniqueWebServer server(endpoint.c_str(), L"sdk-loopback-enabled");
ExpectHttpResponse(endpoint.c_str(), HTTP_STATUS_OK, true);
const auto command = std::format(
"if python3 -c \"import http.client,socket;"
"a=socket.getaddrinfo('host.wslc.internal',{},socket.AF_INET,socket.SOCK_STREAM)[0][4];"
"c=http.client.HTTPConnection(*a,timeout=5);"
"c.request('GET','/');"
"c.getresponse().read()\";"
"then echo enabled; else echo disabled; fi",
c_hostLoopbackTestPort);
auto output = RunContainerAndCapture(m_defaultSession, "python:3.12-alpine", {"/bin/sh", "-c", command.c_str()});
VERIFY_ARE_EQUAL("disabled\n", output.stdoutOutput);
}
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<sockaddr_in*>(&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<sockaddr_in6*>(&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<char>(written)), std::istreambuf_iterator<char>());
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<wsl::windows::common::wslc_schema::InspectContainer>(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(ProcessFlags)
{
constexpr const char* c_catArgv[] = {"/bin/cat"};
constexpr const char* c_sleepArgv[] = {"/bin/sleep", "99"};
constexpr const char* c_initInput = "hello-from-stdin\n";
constexpr const char* c_execInput = "hello-from-exec-stdin\n";
// Negative: unknown flag bits must be rejected (TTY is not exposed by the SDK).
{
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
VERIFY_ARE_EQUAL(WslcSetProcessSettingsFlags(&procSettings, static_cast<WslcProcessFlags>(0x00000002)), E_INVALIDARG);
}
// Negative: null settings pointer must fail.
VERIFY_ARE_EQUAL(WslcSetProcessSettingsFlags(nullptr, WSLC_PROCESS_FLAG_STDIN), E_POINTER);
// Functional (init process): without WSLC_PROCESS_FLAG_STDIN, stdin is closed immediately so cat produces no output.
{
WslcProcessSettings procSettings;
InitProcessSettings(procSettings, c_catArgv);
WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
auto output = RunContainerAndCapture(m_defaultSession, containerSettings);
VERIFY_ARE_EQUAL(output.stdoutOutput, "");
}
// Functional (init process): with WSLC_PROCESS_FLAG_STDIN, input written to stdin is echoed back by cat.
{
WslcProcessSettings procSettings;
InitProcessSettings(procSettings, c_catArgv, WSLC_PROCESS_FLAG_STDIN);
auto container = StartContainerWithInitProcess(m_defaultSession, "debian:latest", procSettings, WSLC_CONTAINER_START_FLAG_ATTACH);
UniqueProcess process;
VERIFY_SUCCEEDED(WslcGetContainerInitProcess(container.get(), &process));
WriteToProcessStdin(process.get(), c_initInput);
auto output = WaitForProcessOutput(process.get());
VERIFY_ARE_EQUAL(output.stdoutOutput, c_initInput);
}
// Functional (exec): the flag is also honored for processes created in an already running container.
{
WslcProcessSettings initProcSettings;
InitProcessSettings(initProcSettings, c_sleepArgv);
auto container =
StartContainerWithInitProcess(m_defaultSession, "debian:latest", initProcSettings, WSLC_CONTAINER_START_FLAG_NONE);
// Without WSLC_PROCESS_FLAG_STDIN, stdin is closed immediately so cat produces no output.
{
WslcProcessSettings execProcSettings;
InitProcessSettings(execProcSettings, c_catArgv);
UniqueProcess execProcess;
VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
auto output = WaitForProcessOutput(execProcess.get());
VERIFY_ARE_EQUAL(output.stdoutOutput, "");
}
// With WSLC_PROCESS_FLAG_STDIN, input written to stdin is echoed back by cat.
{
WslcProcessSettings execProcSettings;
InitProcessSettings(execProcSettings, c_catArgv, WSLC_PROCESS_FLAG_STDIN);
UniqueProcess execProcess;
VERIFY_SUCCEEDED(WslcCreateContainerProcess(container.get(), &execProcSettings, &execProcess, nullptr));
WriteToProcessStdin(execProcess.get(), c_execInput);
auto output = WaitForProcessOutput(execProcess.get());
VERIFY_ARE_EQUAL(output.stdoutOutput, c_execInput);
}
}
}
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<DWORD>(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<DWORD>(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);
}
WSLC_TEST_METHOD(InstallWithDependencies_NoComponents_Succeeds)
{
// Passing WSLC_COMPONENT_FLAG_NONE must return S_OK immediately without requiring elevation.
VERIFY_SUCCEEDED(WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_NONE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
}
WSLC_TEST_METHOD(InstallWithDependencies_SdkNeedsUpdate_ReturnsError)
{
// Passing SDK_NEEDS_UPDATE must always return WSLC_E_SDK_UPDATE_NEEDED — the caller must update their SDK.
VERIFY_ARE_EQUAL(
WSLC_E_SDK_UPDATE_NEEDED,
WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_SDK_NEEDS_UPDATE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
}
WSLC_TEST_METHOD(InstallWithDependencies_WslPackage_GhFallback404)
{
// Without repair semantics, DCAT uses EnsureProductRegistration so it should find no update
// (the product is already registered at the current version). The code then falls back to the
// GitHub release endpoint. This test intercepts that fallback: the fake API server returns a
// release whose asset URL points at a second local server that always responds with HTTP 404,
// so the download fails and WslcInstallWithDependencies surfaces an error HRESULT.
constexpr auto apiEndpoint = L"http://127.0.0.1:12345/";
constexpr auto assetEndpoint = L"http://127.0.0.1:12346/";
RegistryKeyChange<std::wstring> urlOverride(
HKEY_LOCAL_MACHINE,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss",
wsl::windows::common::wslutil::c_githubUrlOverrideRegistryValue,
apiEndpoint);
// Version 1.0.0 is below the current package version, so without repair=true the version
// check in UpdatePackageImpl would short-circuit and return early. Using a sub-2.0 version
// here confirms that the repair flag (always set in the GH fallback) is what drives the
// download attempt rather than the version being newer than the installed one.
constexpr auto GitHubApiResponse =
LR"([{
\"name\": \"1.0.0\",
\"created_at\": \"2023-06-14T16:56:30Z\",
\"assets\": [
{
\"url\": \"http://127.0.0.1:12346/fake.msixbundle\",
\"id\": 1,
\"name\": \"Microsoft.WSL_1.0.0.0_x64_ARM64.msixbundle\"
}
]
}])";
UniqueWebServer apiServer(apiEndpoint, GitHubApiResponse);
UniqueWebServer assetServer(assetEndpoint, L"", 404u);
VERIFY_ARE_EQUAL(
HTTP_E_STATUS_NOT_FOUND, WslcInstallWithDependencies(WSLC_COMPONENT_FLAG_WSL_PACKAGE, WSLC_INSTALL_OPTION_NONE, nullptr, nullptr));
}
// -----------------------------------------------------------------------
// 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<IOContext*>(ctx);
auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
target.append(reinterpret_cast<const char*>(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<DWORD>(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<IOContext*>(ctx);
auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? c->stdoutData : c->stderrData;
target.append(reinterpret_cast<const char*>(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<DWORD>(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<INT32, std::string> {
std::string stdoutData;
std::atomic<INT32> capturedExitCode{-999};
struct Context
{
std::string* stdoutData;
std::atomic<INT32>* capturedExitCode;
wil::unique_event exitEvent{wil::EventOptions::ManualReset};
} ctx{&stdoutData, &capturedExitCode};
auto ioCb = [](WslcProcessIOHandle, const BYTE* data, uint32_t size, PVOID c) {
static_cast<Context*>(c)->stdoutData->append(reinterpret_cast<const char*>(data), size);
};
auto exitCb = [](INT32 code, PVOID c) {
static_cast<Context*>(c)->capturedExitCode->store(code);
static_cast<Context*>(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<int> callbackCount{0};
std::atomic<bool> exitFired{false};
} ctx;
auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) {
static_cast<Context*>(c)->callbackCount.fetch_add(1);
};
auto exitCb = [](INT32, PVOID c) { static_cast<Context*>(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<std::string*>(ctx)->append(reinterpret_cast<const char*>(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<DWORD>(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<WslcProcess> process{nullptr};
std::atomic<WslcContainer> container{nullptr};
std::atomic<HRESULT> releaseProcessHr{S_OK};
std::atomic<HRESULT> releaseContainerHr{S_OK};
std::atomic<bool> captured{false};
wil::unique_event done{wil::EventOptions::ManualReset};
} ctx;
auto ioCb = [](WslcProcessIOHandle, const BYTE*, uint32_t, PVOID c) {
auto* cx = static_cast<Context*>(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<DWORD>(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<WslcVhdType>(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<WslcVhdRequirementsFlags>(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:<port> from both dockerd (inside the VM) and the host.
std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000)
{
// Get the IWSLCSession COM object from the SDK session handle and delegate to the shared helper.
auto session = reinterpret_cast<WslcSessionImpl*>(m_defaultSession)->session.query<IWSLCSession>();
return WSLCE2ETests::StartLocalRegistry(*session, username, password, port);
}
// 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<WslcImageInfo> images;
VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, images.addressof(), images.size_address<uint32_t>()));
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;
WslcIdentityTokenType tokenType{};
VERIFY_ARE_EQUAL(
WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &tokenType, &errorMsg),
E_FAIL);
VERIFY_IS_NOT_NULL(errorMsg.get());
VERIFY_ARE_EQUAL(tokenType, WSLC_IDENTITY_TOKEN_TYPE_UNKNOWN);
}
// Positive: correct credentials must succeed and return a non-null, registry-auth-ready token.
{
wil::unique_cotaskmem_ansistring token;
wil::unique_cotaskmem_string errorMsg;
WslcIdentityTokenType tokenType{};
VERIFY_SUCCEEDED(WslcSessionAuthenticate(
m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &tokenType, &errorMsg));
VERIFY_IS_NOT_NULL(token.get());
// The local test registry does not return an identity token, so credentials are embedded.
VERIFY_ARE_EQUAL(tokenType, WSLC_IDENTITY_TOKEN_TYPE_CREDENTIALS);
}
// The local registry requires auth; push the test image for the pull tests below.
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);
auto imageCleanup = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr)); });
// Positive: pulling with the identityToken from WslcSessionAuthenticate directly should succeed,
// demonstrating that the output can be passed as registryAuth without any transformation.
{
wil::unique_cotaskmem_ansistring authToken;
VERIFY_SUCCEEDED(WslcSessionAuthenticate(
m_defaultSession, registryAddress.c_str(), c_username, c_password, &authToken, nullptr, nullptr));
WslcPullImageOptions opts{};
opts.uri = image.c_str();
opts.registryAuth = authToken.get();
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; tokenType is optional and may be null.
{
wil::unique_cotaskmem_ansistring token;
VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, nullptr, c_username, c_password, &token, nullptr, nullptr), E_POINTER);
VERIFY_ARE_EQUAL(
WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), nullptr, c_password, &token, nullptr, nullptr), E_POINTER);
VERIFY_ARE_EQUAL(
WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, nullptr, &token, nullptr, nullptr), E_POINTER);
VERIFY_ARE_EQUAL(
WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, nullptr, 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);
auto imageCleanup = wil::scope_exit_log(
WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr)); });
// 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<HRESULT>(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");
}
}
WSLC_TEST_METHOD(ContainerAutoRemove)
{
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
const char* argv[] = {"/bin/sh", "-c", "echo wslc-auto-remove"};
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_AUTO_REMOVE));
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));
auto output = WaitForProcessOutput(process.get());
VERIFY_ARE_EQUAL(output.stdoutOutput, "wslc-auto-remove\n");
VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
[&]() {
WslcContainerState state{};
THROW_IF_FAILED(WslcGetContainerState(container.get(), &state));
THROW_WIN32_IF(ERROR_RETRY, state != WSLC_CONTAINER_STATE_DELETED);
},
std::chrono::milliseconds{100},
std::chrono::seconds{30}));
}
WSLC_TEST_METHOD(DeleteRunningContainerWithForce)
{
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));
{
WslcContainerState state{};
VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
}
VERIFY_SUCCEEDED(WslcDeleteContainer(container.get(), WSLC_DELETE_CONTAINER_FLAG_FORCE, nullptr));
{
WslcContainerState state{};
VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_DELETED);
}
}
WSLC_TEST_METHOD(ImageProgressCallback)
{
auto [registryContainer, registryAddress] = StartLocalRegistry();
auto registryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
struct ProgressContext
{
bool invoked = false;
bool sawKnownStatus = false;
};
auto progressCb = [](const WslcImageProgressMessage* progress, PVOID context) -> HRESULT {
auto* ctx = static_cast<ProgressContext*>(context);
ctx->invoked = true;
if (progress != nullptr && progress->status != WSLC_IMAGE_PROGRESS_STATUS_UNKNOWN)
{
ctx->sawKnownStatus = true;
}
return S_OK;
};
const auto registryImage = std::format("{}/hello-world:latest", registryAddress);
const auto registryRepo = std::format("{}/hello-world", registryAddress);
WslcTagImageOptions tagOptions{};
tagOptions.image = "hello-world:latest";
tagOptions.repo = registryRepo.c_str();
tagOptions.tag = "latest";
VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr));
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
LOG_IF_FAILED(WslcLoadSessionImageFromFile(m_defaultSession, GetTestImagePath("hello-world:latest").c_str(), nullptr, nullptr));
});
{
ProgressContext ctx;
WslcPushImageOptions opts{};
opts.image = registryImage.c_str();
opts.registryAuth = registryAuth.c_str();
opts.progressCallback = progressCb;
opts.progressCallbackContext = &ctx;
VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &opts, nullptr));
VERIFY_IS_TRUE(ctx.invoked);
}
VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr));
VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", nullptr));
{
ProgressContext ctx;
WslcPullImageOptions opts{};
opts.uri = registryImage.c_str();
opts.registryAuth = registryAuth.c_str();
opts.progressCallback = progressCb;
opts.progressCallbackContext = &ctx;
VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr));
VERIFY_IS_TRUE(HasImage(registryImage));
VERIFY_IS_TRUE(ctx.sawKnownStatus);
}
}
WSLC_TEST_METHOD(ResourceReuseAfterCleanup)
{
// Each iteration exercises the complete lifecycle: create session → load image → run container
// → terminate session → release session. Running 10 times on the same storage path verifies
// that all resources (VHD locks, image store, container overlays, process handles, VM) are
// fully freed by the time WslcReleaseSession returns, so the next iteration can reuse them
// without errors.
std::filesystem::path sessionStorage = m_storagePath / "wslc-resource-reuse-storage";
auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code ec;
std::filesystem::remove_all(sessionStorage, ec);
});
WslcSessionSettings sessionSettings;
VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-resource-reuse", sessionStorage.c_str(), &sessionSettings));
VERIFY_SUCCEEDED(WslcSetSessionSettingsCpuCount(&sessionSettings, 2));
VERIFY_SUCCEEDED(WslcSetSessionSettingsMemory(&sessionSettings, 1024));
VERIFY_SUCCEEDED(WslcSetSessionSettingsTimeout(&sessionSettings, 30 * 1000));
WslcVhdRequirements vhdReqs{};
vhdReqs.sizeBytes = 2048ull * 1024 * 1024; // 2 GB
vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
constexpr auto c_imageName = "hello-world:latest";
constexpr int c_iterationCount = 10;
for (int i = 0; i < c_iterationCount; ++i)
{
LogInfo("ResourceReuseAfterCleanup: iteration %d / %d", i + 1, c_iterationCount);
// Create the session, reusing the same storage path every iteration.
UniqueSession session;
VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &session, nullptr));
VERIFY_IS_NOT_NULL(session.get());
// Load the test image into the session.
VERIFY_SUCCEEDED(WslcLoadSessionImageFromFile(session.get(), GetTestImagePath(c_imageName).c_str(), nullptr, nullptr));
// Run a container using the image's default entrypoint and capture its output.
auto output = RunContainerAndCapture(session.get(), c_imageName, {});
VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos);
// Terminate the session, then release it. WslcReleaseSession must return only after all
// resources are freed so that the next iteration can reopen the same storage without error.
VERIFY_SUCCEEDED(WslcTerminateSession(session.get()));
VERIFY_SUCCEEDED(WslcReleaseSession(session.get()));
session.release(); // explicit calls above
}
}
WSLC_TEST_METHOD(StopContainerTimeout)
{
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
const char* argv[] = {"/bin/sh", "-c", "trap '' TERM; sleep 60"};
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));
{
WslcContainerState state{};
VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_RUNNING);
}
constexpr uint32_t c_timeoutSeconds = 1;
const auto start = std::chrono::steady_clock::now();
VERIFY_SUCCEEDED(WslcStopContainer(container.get(), WSLC_SIGNAL_SIGTERM, c_timeoutSeconds, nullptr));
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
VERIFY_IS_GREATER_THAN_OR_EQUAL(elapsedMs, 1000LL);
// Ensure it's not because of the sleep running out.
VERIFY_IS_LESS_THAN(elapsedMs, 30000LL);
{
WslcContainerState state{};
VERIFY_SUCCEEDED(WslcGetContainerState(container.get(), &state));
VERIFY_ARE_EQUAL(state, WSLC_CONTAINER_STATE_EXITED);
}
}
WSLC_TEST_METHOD(OpenContainer)
{
constexpr auto c_containerName = "wslc-sdk-open-test";
WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, c_containerName));
UniqueContainer created;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &created, nullptr));
CHAR createdId[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
VERIFY_SUCCEEDED(WslcGetContainerID(created.get(), createdId));
// Positive: open by name — IDs must match.
{
UniqueContainerReleaseOnly opened;
VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, c_containerName, &opened, nullptr));
VERIFY_IS_NOT_NULL(opened.get());
CHAR openedId[WSLC_CONTAINER_ID_BUFFER_SIZE]{};
VERIFY_SUCCEEDED(WslcGetContainerID(opened.get(), openedId));
VERIFY_ARE_EQUAL(strcmp(createdId, openedId), 0);
}
// Positive: open by full 64-character ID.
{
UniqueContainerReleaseOnly opened;
VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, createdId, &opened, nullptr));
VERIFY_IS_NOT_NULL(opened.get());
}
// Positive: open by partial ID prefix (12 hex chars — standard short ID).
{
createdId[12] = '\0';
UniqueContainerReleaseOnly opened;
VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, createdId, &opened, nullptr));
VERIFY_IS_NOT_NULL(opened.get());
}
// Negative: non-existent name must fail with WSLC_E_CONTAINER_NOT_FOUND.
{
UniqueContainerReleaseOnly opened;
VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, "no-such-container", &opened, nullptr), WSLC_E_CONTAINER_NOT_FOUND);
VERIFY_IS_NULL(opened.get());
}
// Negative: null session must fail with E_POINTER.
{
UniqueContainerReleaseOnly opened;
VERIFY_ARE_EQUAL(WslcOpenContainer(nullptr, c_containerName, &opened, nullptr), E_POINTER);
}
// Negative: null nameOrId must fail with E_POINTER.
{
UniqueContainerReleaseOnly opened;
VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, nullptr, &opened, nullptr), E_POINTER);
}
// Negative: null container output pointer must fail with E_POINTER.
VERIFY_ARE_EQUAL(WslcOpenContainer(m_defaultSession, c_containerName, nullptr, nullptr), E_POINTER);
}
WSLC_TEST_METHOD(OpenContainerIOCallbacks)
{
// Verify that IO callbacks installed via WslcSetContainerInitProcessIOCallbacks fire when
// the opened container is started with WSLC_CONTAINER_START_FLAG_ATTACH.
constexpr auto c_containerName = "wslc-sdk-opencb-test";
WslcProcessSettings procSettings;
VERIFY_SUCCEEDED(WslcInitProcessSettings(&procSettings));
PCSTR argv[] = {"/bin/sh", "-c", "echo STDOUT && echo STDERR >&2"};
VERIFY_SUCCEEDED(WslcSetProcessSettingsCmdLine(&procSettings, argv, ARRAYSIZE(argv)));
WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
VERIFY_SUCCEEDED(WslcSetContainerSettingsName(&containerSettings, c_containerName));
VERIFY_SUCCEEDED(WslcSetContainerSettingsInitProcess(&containerSettings, &procSettings));
UniqueContainer created;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &created, nullptr));
UniqueContainerReleaseOnly opened;
VERIFY_SUCCEEDED(WslcOpenContainer(m_defaultSession, c_containerName, &opened, nullptr));
struct Context
{
std::string stdoutData;
std::string stderrData;
wil::unique_event exitEvent{wil::EventOptions::ManualReset};
} ctx;
auto ioCb = [](WslcProcessIOHandle ioHandle, const BYTE* data, uint32_t size, PVOID c) {
auto& target = (ioHandle == WSLC_PROCESS_IO_HANDLE_STDOUT) ? static_cast<Context*>(c)->stdoutData
: static_cast<Context*>(c)->stderrData;
target.append(reinterpret_cast<const char*>(data), size);
};
auto exitCb = [](INT32, PVOID c) { static_cast<Context*>(c)->exitEvent.SetEvent(); };
WslcProcessCallbacks callbacks{};
callbacks.onStdOut = ioCb;
callbacks.onStdErr = ioCb;
callbacks.onExit = exitCb;
VERIFY_SUCCEEDED(WslcSetContainerInitProcessIOCallbacks(opened.get(), &callbacks, &ctx));
VERIFY_SUCCEEDED(WslcStartContainer(opened.get(), WSLC_CONTAINER_START_FLAG_ATTACH, nullptr));
VERIFY_ARE_EQUAL(WaitForSingleObject(ctx.exitEvent.get(), 30 * 1000), static_cast<DWORD>(WAIT_OBJECT_0));
// Release the opened handle first to flush all pending IO callbacks before asserting output.
opened.reset();
VERIFY_ARE_EQUAL(ctx.stdoutData, "STDOUT\n");
VERIFY_ARE_EQUAL(ctx.stderrData, "STDERR\n");
}
WSLC_TEST_METHOD(SetContainerInitProcessIOCallbacksNullCallbacks)
{
WslcContainerSettings containerSettings;
VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
UniqueContainer container;
VERIFY_SUCCEEDED(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr));
// Null callbacks pointer must fail with E_POINTER.
VERIFY_ARE_EQUAL(WslcSetContainerInitProcessIOCallbacks(container.get(), nullptr, nullptr), E_POINTER);
}
};