mirror of
https://github.com/microsoft/WSL.git
synced 2026-04-11 10:26:27 -05:00
6553 lines
274 KiB
C++
6553 lines
274 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
WSLCTests.cpp
|
|
|
|
Abstract:
|
|
|
|
This file contains test cases for the WSLC API.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "Common.h"
|
|
#include "wslc.h"
|
|
#include "WSLCProcessLauncher.h"
|
|
#include "WSLCContainerLauncher.h"
|
|
#include "WslCoreFilesystem.h"
|
|
|
|
using namespace std::literals::chrono_literals;
|
|
using namespace wsl::windows::common::registry;
|
|
using wsl::windows::common::RunningWSLCContainer;
|
|
using wsl::windows::common::RunningWSLCProcess;
|
|
using wsl::windows::common::WSLCContainerLauncher;
|
|
using wsl::windows::common::WSLCProcessLauncher;
|
|
using wsl::windows::common::relay::OverlappedIOHandle;
|
|
using wsl::windows::common::relay::WriteHandle;
|
|
using namespace wsl::windows::common::wslutil;
|
|
|
|
extern std::wstring g_testDataPath;
|
|
extern bool g_fastTestRun;
|
|
|
|
class WSLCTests
|
|
{
|
|
WSLC_TEST_CLASS(WSLCTests)
|
|
|
|
wil::unique_mta_usage_cookie m_mtaCookie;
|
|
WSADATA m_wsadata;
|
|
std::filesystem::path m_storagePath;
|
|
WSLCSessionSettings m_defaultSessionSettings{};
|
|
wil::com_ptr<IWSLCSession> m_defaultSession;
|
|
static inline auto c_testSessionName = L"wslc-test";
|
|
|
|
void LoadTestImage(std::string_view imageName, IWSLCSession* session = nullptr)
|
|
{
|
|
std::filesystem::path imagePath = GetTestImagePath(imageName);
|
|
wil::unique_hfile imageFile{
|
|
CreateFileW(imagePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
THROW_LAST_ERROR_IF(!imageFile);
|
|
|
|
LARGE_INTEGER fileSize{};
|
|
THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
|
|
|
|
THROW_IF_FAILED(
|
|
(session ? session : m_defaultSession.get())->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
|
|
}
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie));
|
|
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
|
|
|
|
// The WSLC SDK tests use this same storage to reduce pull overhead.
|
|
m_storagePath = std::filesystem::current_path() / "test-storage";
|
|
m_defaultSessionSettings = GetDefaultSessionSettings(c_testSessionName, true, WSLCNetworkingModeVirtioProxy);
|
|
m_defaultSession = CreateSession(m_defaultSessionSettings);
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, &images, images.size_address<ULONG>()));
|
|
|
|
auto hasImage = [&](const std::string& imageName) {
|
|
return std::ranges::any_of(
|
|
images.get(), images.get() + images.size(), [&](const auto& e) { return e.Image == imageName; });
|
|
};
|
|
|
|
if (!hasImage("debian:latest"))
|
|
{
|
|
LoadTestImage("debian:latest");
|
|
}
|
|
|
|
if (!hasImage("python:3.12-alpine"))
|
|
{
|
|
LoadTestImage("python:3.12-alpine");
|
|
}
|
|
|
|
PruneResult result;
|
|
VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
|
|
if (result.result.ContainersCount > 0)
|
|
{
|
|
LogInfo("Pruned %lu containers", result.result.ContainersCount);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
m_defaultSession.reset();
|
|
|
|
// Keep the VHD when running in -f mode, to speed up subsequent test runs.
|
|
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;
|
|
}
|
|
|
|
WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR Name, bool enableStorage = false, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone)
|
|
{
|
|
WSLCSessionSettings settings{};
|
|
settings.DisplayName = Name;
|
|
settings.CpuCount = 4;
|
|
settings.MemoryMb = 2048;
|
|
settings.BootTimeoutMs = 30 * 1000;
|
|
settings.StoragePath = enableStorage ? m_storagePath.c_str() : nullptr;
|
|
settings.MaximumStorageSizeMb = 1024 * 20; // 20GB.
|
|
settings.NetworkingMode = networkingMode;
|
|
|
|
return settings;
|
|
}
|
|
|
|
auto ResetTestSession()
|
|
{
|
|
m_defaultSession.reset();
|
|
|
|
return wil::scope_exit([this]() { m_defaultSession = CreateSession(m_defaultSessionSettings); });
|
|
}
|
|
|
|
static wil::com_ptr<IWSLCSessionManager> OpenSessionManager()
|
|
{
|
|
wil::com_ptr<IWSLCSessionManager> sessionManager;
|
|
VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
|
|
wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
|
|
|
|
return sessionManager;
|
|
}
|
|
|
|
wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
|
|
{
|
|
const auto sessionManager = OpenSessionManager();
|
|
|
|
wil::com_ptr<IWSLCSession> session;
|
|
|
|
VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, &session));
|
|
wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
|
|
|
|
WSLCSessionState state{};
|
|
VERIFY_SUCCEEDED(session->GetState(&state));
|
|
VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
|
|
|
|
return session;
|
|
}
|
|
|
|
RunningWSLCContainer OpenContainer(IWSLCSession* session, const std::string& name)
|
|
{
|
|
wil::com_ptr<IWSLCContainer> rawContainer;
|
|
VERIFY_SUCCEEDED(session->OpenContainer(name.c_str(), &rawContainer));
|
|
|
|
return RunningWSLCContainer(std::move(rawContainer), {});
|
|
}
|
|
|
|
TEST_METHOD(GetVersion)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
wil::com_ptr<IWSLCSessionManager> sessionManager;
|
|
VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
|
|
|
|
WSLCVersion version{};
|
|
|
|
VERIFY_SUCCEEDED(sessionManager->GetVersion(&version));
|
|
|
|
VERIFY_ARE_EQUAL(version.Major, WSL_PACKAGE_VERSION_MAJOR);
|
|
VERIFY_ARE_EQUAL(version.Minor, WSL_PACKAGE_VERSION_MINOR);
|
|
VERIFY_ARE_EQUAL(version.Revision, WSL_PACKAGE_VERSION_REVISION);
|
|
}
|
|
|
|
static RunningWSLCProcess::ProcessResult RunCommand(IWSLCSession* session, const std::vector<std::string>& command, int timeout = 600000)
|
|
{
|
|
WSLCProcessLauncher process(command[0], command);
|
|
|
|
return process.Launch(*session).WaitAndCaptureOutput(timeout);
|
|
}
|
|
|
|
static RunningWSLCProcess::ProcessResult ExpectCommandResult(
|
|
IWSLCSession* session, const std::vector<std::string>& command, int expectResult, int timeout = 600000)
|
|
{
|
|
auto result = RunCommand(session, command, timeout);
|
|
|
|
if (result.Code != expectResult)
|
|
{
|
|
auto cmd = wsl::shared::string::Join(command, ' ');
|
|
LogError(
|
|
"Command: %hs didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
|
|
cmd.c_str(),
|
|
expectResult,
|
|
result.Code,
|
|
result.Output[1].c_str(),
|
|
result.Output[2].c_str());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void ValidateProcessOutput(RunningWSLCProcess& process, const std::map<int, std::string>& expectedOutput, int expectedResult = 0, DWORD Timeout = INFINITE)
|
|
{
|
|
auto result = process.WaitAndCaptureOutput(Timeout);
|
|
|
|
if (result.Code != expectedResult)
|
|
{
|
|
LogError(
|
|
"Command didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'",
|
|
expectedResult,
|
|
result.Code,
|
|
EscapeString(result.Output[1]).c_str(),
|
|
EscapeString(result.Output[2]).c_str());
|
|
|
|
return;
|
|
}
|
|
|
|
for (const auto& [fd, expected] : expectedOutput)
|
|
{
|
|
auto it = result.Output.find(fd);
|
|
if (it == result.Output.end())
|
|
{
|
|
LogError("Expected output on fd %i, but none found.", fd);
|
|
return;
|
|
}
|
|
|
|
if (it->second != expected)
|
|
{
|
|
LogError(
|
|
"Unexpected output on fd %i. Expected: '%hs', Actual: '%hs'",
|
|
fd,
|
|
EscapeString(expected).c_str(),
|
|
EscapeString(it->second).c_str());
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExpectMount(IWSLCSession* session, const std::string& target, const std::optional<std::string>& options)
|
|
{
|
|
auto cmd = std::format("set -o pipefail ; findmnt '{}' | tail -n 1", target);
|
|
auto result = ExpectCommandResult(session, {"/bin/sh", "-c", cmd}, options.has_value() ? 0 : 1);
|
|
|
|
const auto& output = result.Output[1];
|
|
const auto& error = result.Output[2];
|
|
|
|
if (result.Code != (options.has_value() ? 0 : 1))
|
|
{
|
|
LogError("%hs failed. code=%i, output: %hs, error: %hs", cmd.c_str(), result.Code, output.c_str(), error.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
if (options.has_value() && !PathMatchSpecA(output.c_str(), options->c_str()))
|
|
{
|
|
std::wstring message = std::format(L"Output: '{}' didn't match pattern: '{}'", output, options.value());
|
|
VERIFY_FAIL(message.c_str());
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ListSessionsReturnsSessionWithDisplayName)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto sessionManager = OpenSessionManager();
|
|
|
|
// Act: list sessions
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
|
|
VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
|
|
|
|
// Assert
|
|
VERIFY_ARE_EQUAL(sessions.size(), 1u);
|
|
const auto& info = sessions[0];
|
|
|
|
// SessionId is implementation detail (starts at 1), so we only assert DisplayName here.
|
|
VERIFY_ARE_EQUAL(std::wstring(info.DisplayName), c_testSessionName);
|
|
}
|
|
|
|
// List multiple sessions.
|
|
{
|
|
auto session2 = CreateSession(GetDefaultSessionSettings(L"wslc-test-list-2"));
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
|
|
VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
|
|
|
|
VERIFY_ARE_EQUAL(sessions.size(), 2);
|
|
|
|
std::vector<std::wstring> displayNames;
|
|
for (const auto& e : sessions)
|
|
{
|
|
displayNames.push_back(e.DisplayName);
|
|
}
|
|
|
|
std::ranges::sort(displayNames);
|
|
|
|
VERIFY_ARE_EQUAL(displayNames[0], c_testSessionName);
|
|
VERIFY_ARE_EQUAL(displayNames[1], L"wslc-test-list-2");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(OpenSessionByNameFindsExistingSession)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto sessionManager = OpenSessionManager();
|
|
|
|
// Act: open by the same display name
|
|
wil::com_ptr<IWSLCSession> opened;
|
|
VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(c_testSessionName, &opened));
|
|
VERIFY_IS_NOT_NULL(opened.get());
|
|
|
|
// And verify we get ERROR_NOT_FOUND for a nonexistent name
|
|
wil::com_ptr<IWSLCSession> notFound;
|
|
auto hr = sessionManager->OpenSessionByName(L"this-name-does-not-exist", ¬Found);
|
|
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
TEST_METHOD(CreateSessionValidation)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto sessionManager = OpenSessionManager();
|
|
|
|
// Reject NULL DisplayName.
|
|
{
|
|
auto settings = GetDefaultSessionSettings(nullptr);
|
|
wil::com_ptr<IWSLCSession> session;
|
|
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
|
|
}
|
|
|
|
// Reject DisplayName at exact boundary (no room for null terminator).
|
|
{
|
|
std::wstring boundaryName(std::size(WSLCSessionInformation{}.DisplayName), L'x');
|
|
auto settings = GetDefaultSessionSettings(boundaryName.c_str());
|
|
wil::com_ptr<IWSLCSession> session;
|
|
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
|
|
}
|
|
|
|
// Reject too long DisplayName.
|
|
{
|
|
std::wstring longName(std::size(WSLCSessionInformation{}.DisplayName) + 1, L'x');
|
|
auto settings = GetDefaultSessionSettings(longName.c_str());
|
|
wil::com_ptr<IWSLCSession> session;
|
|
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG);
|
|
}
|
|
}
|
|
|
|
void ExpectImagePresent(IWSLCSession& Session, const char* Image, bool Present = true)
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
THROW_IF_FAILED(Session.ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
std::vector<std::string> tags;
|
|
for (const auto& e : images)
|
|
{
|
|
tags.push_back(e.Image);
|
|
}
|
|
|
|
auto found = std::ranges::find(tags, Image) != tags.end();
|
|
if (Present != found)
|
|
{
|
|
LogError("Image presence check failed for image: %hs, images: %hs", Image, wsl::shared::string::Join(tags, ',').c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(PullImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
{
|
|
HRESULT pullResult = m_defaultSession->PullImage("hello-world:linux", nullptr, nullptr);
|
|
|
|
// Skip test if error is due to rate limit.
|
|
if (pullResult == E_FAIL)
|
|
{
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
if (comError.has_value())
|
|
{
|
|
if (wcsstr(comError->Message.get(), L"toomanyrequests") != nullptr)
|
|
{
|
|
LogWarning("Skipping PullImage test due to rate limiting.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(pullResult);
|
|
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "hello-world:linux");
|
|
WSLCContainerLauncher launcher("hello-world:linux", "wslc-pull-image-container");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
}
|
|
|
|
{
|
|
std::wstring expectedError =
|
|
L"pull access denied for does-not, repository does not exist or may require 'docker login': denied: requested "
|
|
L"access to the resource is denied";
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND);
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
VERIFY_IS_TRUE(comError.has_value());
|
|
|
|
VERIFY_ARE_EQUAL(expectedError, comError->Message.get());
|
|
}
|
|
|
|
// Validate that PullImage() returns the appropriate error if the session is terminated.
|
|
{
|
|
VERIFY_SUCCEEDED(m_defaultSession->Terminate());
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
ResetTestSession(); // Reopen the test session since the session was terminated.
|
|
});
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->PullImage("hello-world:linux", nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(PullImageAdvanced)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// TODO: Enable once custom registries are supported, to avoid hitting public registry rate limits.
|
|
SKIP_TEST_UNSTABLE();
|
|
|
|
auto validatePull = [&](const std::string& Image, const std::optional<std::string>& ExpectedTag = {}) {
|
|
VERIFY_SUCCEEDED(m_defaultSession->PullImage(Image.c_str(), nullptr, nullptr));
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
WSLCDeleteImageOptions options{.Flags = WSLCDeleteImageFlagsForce};
|
|
options.Image = ExpectedTag.has_value() ? ExpectedTag->c_str() : Image.c_str();
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
});
|
|
|
|
if (!ExpectedTag.has_value())
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
for (const auto& e : images)
|
|
{
|
|
wil::unique_cotaskmem_ansistring json;
|
|
VERIFY_SUCCEEDED(m_defaultSession->InspectImage(e.Hash, &json));
|
|
|
|
auto parsed = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(json.get());
|
|
|
|
for (const auto& repoTag : parsed.RepoDigests.value_or({}))
|
|
{
|
|
if (Image == repoTag)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
LogError("Expected digest '%hs' not found ", Image.c_str());
|
|
|
|
VERIFY_FAIL();
|
|
}
|
|
else
|
|
{
|
|
ExpectImagePresent(*m_defaultSession, ExpectedTag->c_str());
|
|
}
|
|
};
|
|
|
|
validatePull("ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", {});
|
|
validatePull("ubuntu", "ubuntu:latest");
|
|
validatePull("debian:bookworm", "debian:bookworm");
|
|
validatePull("pytorch/pytorch", "pytorch/pytorch:latest");
|
|
validatePull("registry.k8s.io/pause:3.2", "registry.k8s.io/pause:3.2");
|
|
|
|
// Validate that PullImage() fails appropriately when the session runs out of space.
|
|
{
|
|
auto settings = GetDefaultSessionSettings(L"wslc-pull-image-out-of-space", false);
|
|
settings.NetworkingMode = WSLCNetworkingModeVirtioProxy;
|
|
settings.MemoryMb = 1024;
|
|
auto session = CreateSession(settings);
|
|
|
|
VERIFY_ARE_EQUAL(session->PullImage("pytorch/pytorch", nullptr, nullptr), E_FAIL);
|
|
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
VERIFY_IS_TRUE(comError.has_value());
|
|
|
|
// The error message can't be compared directly because it contains an unpredicable path:
|
|
// "write /var/lib/docker/tmp/GetImageBlob1760660623: no space left on device"
|
|
if (StrStrW(comError->Message.get(), L"no space left on device") == nullptr)
|
|
{
|
|
LogError("Unexpected error message: %ls", comError->Message.get());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ListImages)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Setup: Ensure debian:latest is available
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
|
|
// Create additional tags for testing
|
|
WSLCTagImageOptions tagOptions{};
|
|
tagOptions.Image = "debian:latest";
|
|
tagOptions.Repo = "debian";
|
|
tagOptions.Tag = "test-tag1";
|
|
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
|
|
tagOptions.Tag = "test-tag2";
|
|
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
WSLCDeleteImageOptions options{.Image = "debian:test-tag1", .Flags = WSLCDeleteImageFlagsNone};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
|
|
options.Image = "debian:test-tag2";
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
});
|
|
|
|
LogInfo("Test: Basic listing with nullptr options");
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
VERIFY_IS_TRUE(images.size() > 0);
|
|
|
|
// Find debian images and verify they exist
|
|
bool foundLatest = false, foundTag1 = false, foundTag2 = false;
|
|
for (const auto& image : images)
|
|
{
|
|
std::string imageName = image.Image;
|
|
if (imageName == "debian:latest")
|
|
{
|
|
foundLatest = true;
|
|
}
|
|
if (imageName == "debian:test-tag1")
|
|
{
|
|
foundTag1 = true;
|
|
}
|
|
if (imageName == "debian:test-tag2")
|
|
{
|
|
foundTag2 = true;
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_TRUE(foundLatest);
|
|
VERIFY_IS_TRUE(foundTag1);
|
|
VERIFY_IS_TRUE(foundTag2);
|
|
}
|
|
|
|
LogInfo("Test: Verify all fields are populated");
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
std::string commonHash;
|
|
int debianTagCount = 0;
|
|
|
|
for (const auto& image : images)
|
|
{
|
|
std::string imageName = image.Image;
|
|
if (imageName.starts_with("debian:"))
|
|
{
|
|
debianTagCount++;
|
|
|
|
// Verify Hash field
|
|
VERIFY_IS_TRUE(strlen(image.Hash) > 0);
|
|
VERIFY_IS_TRUE(std::string(image.Hash).starts_with("sha256:"));
|
|
|
|
// All debian tags should have the same hash (same underlying image)
|
|
if (commonHash.empty())
|
|
{
|
|
commonHash = image.Hash;
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(commonHash, std::string(image.Hash));
|
|
}
|
|
|
|
// Verify Size field
|
|
VERIFY_IS_TRUE(image.Size > 0);
|
|
|
|
// Verify Created timestamp
|
|
VERIFY_IS_TRUE(image.Created > 0);
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_TRUE(debianTagCount >= 3); // At least debian:latest, test-tag1, test-tag2
|
|
}
|
|
|
|
LogInfo("Test: Multiple tags for same image return separate entries");
|
|
{
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsNone;
|
|
options.Reference = "debian";
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
// Should find at least our 3 debian tags
|
|
VERIFY_IS_TRUE(images.size() >= 3);
|
|
|
|
// Verify each tag is a separate entry
|
|
std::set<std::string> imageTags;
|
|
for (const auto& image : images)
|
|
{
|
|
imageTags.insert(image.Image);
|
|
}
|
|
|
|
VERIFY_IS_TRUE(imageTags.contains("debian:latest"));
|
|
VERIFY_IS_TRUE(imageTags.contains("debian:test-tag1"));
|
|
VERIFY_IS_TRUE(imageTags.contains("debian:test-tag2"));
|
|
}
|
|
|
|
LogInfo("Test: Filter by specific reference");
|
|
{
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsNone;
|
|
options.Reference = "debian:test-tag1";
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
// When filtering by exact tag, Docker returns all tags for that image
|
|
// So we should get debian:latest, debian:test-tag1, debian:test-tag2
|
|
bool foundTag1 = false;
|
|
for (const auto& image : images)
|
|
{
|
|
std::string imageName = image.Image;
|
|
if (imageName == "debian:test-tag1")
|
|
{
|
|
foundTag1 = true;
|
|
}
|
|
}
|
|
VERIFY_IS_TRUE(foundTag1);
|
|
}
|
|
|
|
LogInfo("Test: Digests flag");
|
|
{
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsDigests;
|
|
options.Reference = "debian:latest";
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
// Check if digests are available (they may not be for all images)
|
|
bool hasDigest = false;
|
|
for (const auto& image : images)
|
|
{
|
|
if (strlen(image.Digest) > 0)
|
|
{
|
|
hasDigest = true;
|
|
// Digest should be in format repo@sha256:...
|
|
VERIFY_IS_TRUE(std::string(image.Digest).find("@sha256:") != std::string::npos);
|
|
}
|
|
}
|
|
// Note: Pulled images from registry should have digests, locally built may not
|
|
}
|
|
|
|
LogInfo("Test: Before/Since filters");
|
|
{
|
|
// Get all images to find their IDs
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> allImages;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, allImages.addressof(), allImages.size_address<ULONG>()));
|
|
|
|
std::string debianId, pythonId;
|
|
for (const auto& image : allImages)
|
|
{
|
|
std::string imageName = image.Image;
|
|
if (imageName == "debian:latest")
|
|
{
|
|
debianId = image.Hash;
|
|
}
|
|
else if (imageName == "python:3.12-alpine")
|
|
{
|
|
pythonId = image.Hash;
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_FALSE(debianId.empty());
|
|
VERIFY_IS_FALSE(pythonId.empty());
|
|
|
|
// Test 'since' filter - images created after debian
|
|
{
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsNone;
|
|
options.Since = debianId.c_str();
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
VERIFY_IS_TRUE(images.size() > 0);
|
|
|
|
bool foundPython = false;
|
|
for (const auto& image : images)
|
|
{
|
|
LogInfo("Image: %hs, Hash: %hs, Created: %lld", image.Image, image.Hash, image.Created);
|
|
if (std::string{image.Image} == "python:3.12-alpine")
|
|
{
|
|
foundPython = true;
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_TRUE(foundPython);
|
|
}
|
|
|
|
// Test 'before' filter - images created before python
|
|
{
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsNone;
|
|
options.Before = pythonId.c_str();
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
VERIFY_IS_TRUE(images.size() > 0);
|
|
|
|
bool foundDebian = false;
|
|
for (const auto& image : images)
|
|
{
|
|
if (std::string{image.Image} == "debian:latest")
|
|
{
|
|
foundDebian = true;
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_TRUE(foundDebian);
|
|
}
|
|
}
|
|
|
|
LogInfo("Test: Dangling filter");
|
|
{
|
|
// Setup a dangling image
|
|
LoadTestImage("alpine:latest");
|
|
WSLCTagImageOptions tagOptions{};
|
|
tagOptions.Image = "debian:latest";
|
|
tagOptions.Repo = "alpine";
|
|
tagOptions.Tag = "latest";
|
|
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions));
|
|
|
|
auto alpineCleanup = wil::scope_exit([&]() {
|
|
RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "image", "prune", "-f"});
|
|
WSLCDeleteImageOptions options{.Image = "alpine:latest", .Flags = WSLCDeleteImageFlagsNone};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
});
|
|
|
|
// List only dangling images
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsDanglingTrue;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> danglingImages;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, danglingImages.addressof(), danglingImages.size_address<ULONG>()));
|
|
|
|
VERIFY_ARE_EQUAL(1, danglingImages.size());
|
|
|
|
// All dangling images should have <none>:<none> as the tag
|
|
for (const auto& image : danglingImages)
|
|
{
|
|
std::string imageName = image.Image;
|
|
VERIFY_ARE_EQUAL(imageName, std::string("<none>:<none>"));
|
|
}
|
|
|
|
// List non-dangling images
|
|
options.Flags = WSLCListImagesFlagsDanglingFalse;
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> nonDanglingImages;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, nonDanglingImages.addressof(), nonDanglingImages.size_address<ULONG>()));
|
|
VERIFY_IS_TRUE(nonDanglingImages.size() > 0);
|
|
|
|
// None of these should be <none>:<none>
|
|
for (const auto& image : nonDanglingImages)
|
|
{
|
|
std::string imageName = image.Image;
|
|
VERIFY_ARE_NOT_EQUAL(imageName, std::string("<none>:<none>"));
|
|
}
|
|
}
|
|
|
|
LogInfo("Test: Label filter");
|
|
{
|
|
// Test with nullptr (no label filter)
|
|
WSLCListImageOptions options{};
|
|
options.Flags = WSLCListImagesFlagsNone;
|
|
options.Labels = nullptr;
|
|
options.LabelsCount = 0;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
// Test with single label filter
|
|
{
|
|
WSLCLabel labels[] = {{.Key = "test.label", .Value = nullptr}};
|
|
options.Labels = labels;
|
|
options.LabelsCount = 1;
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
}
|
|
|
|
// Test with multiple label filters (labels are AND'ed together)
|
|
{
|
|
WSLCLabel labels[] = {{.Key = "test.label1", .Value = nullptr}, {.Key = "test.label2", .Value = "value"}};
|
|
options.Labels = labels;
|
|
options.LabelsCount = 2;
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(&options, images.addressof(), images.size_address<ULONG>()));
|
|
}
|
|
|
|
// Note: To fully test label filtering with actual matches, would need to:
|
|
// 1. Build an image with specific labels using docker build --label
|
|
// 2. Filter with matching labels
|
|
// 3. Verify the filtered image appears
|
|
// This only tests the API usage not fail without requiring image builds
|
|
}
|
|
|
|
cleanup.reset();
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-tag1", false);
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-tag2", false);
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest", true);
|
|
}
|
|
|
|
TEST_METHOD(LoadImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
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(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
|
|
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "hello-world:latest");
|
|
WSLCContainerLauncher launcher("hello-world:latest", "wslc-load-image-container");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
|
|
// Validate that invalid tars fail with proper error message and code.
|
|
{
|
|
auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
|
|
VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->LoadImage(ToCOMInputHandle(currentExecutableHandle.get()), nullptr, fileSize.QuadPart), E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ImportImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
std::filesystem::path imageTar = std::filesystem::path{g_testDataPath} / L"HelloWorldExported.tar";
|
|
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(m_defaultSession->ImportImage(
|
|
ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "my-hello-world:test");
|
|
|
|
// Validate that containers can be started from the imported image.
|
|
WSLCContainerLauncher launcher("my-hello-world:test", "wslc-import-image-container", {"/hello"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
|
|
// Validate that ImportImage fails if no tag is passed
|
|
{
|
|
VERIFY_ARE_EQUAL(
|
|
m_defaultSession->ImportImage(ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart),
|
|
E_INVALIDARG);
|
|
}
|
|
|
|
// Validate that invalid tars fail with proper error message and code.
|
|
{
|
|
auto currentExecutableHandle = wil::open_file(wil::GetModuleFileNameW<std::wstring>().c_str());
|
|
|
|
VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
m_defaultSession->ImportImage(
|
|
ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart),
|
|
E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(DeleteImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Prepare alpine image to delete.
|
|
LoadTestImage("alpine:latest");
|
|
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "alpine:latest");
|
|
|
|
// Launch a container to ensure that image deletion fails when in use.
|
|
WSLCContainerLauncher launcher(
|
|
"alpine:latest", "test-delete-container-in-use", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
// Verify that the container is in running state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
// Test delete failed if image in use.
|
|
WSLCDeleteImageOptions options{};
|
|
options.Image = "alpine:latest";
|
|
options.Flags = WSLCDeleteImageFlagsNone;
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION),
|
|
m_defaultSession->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
// Force should succeed.
|
|
options.Flags = WSLCDeleteImageFlagsForce;
|
|
VERIFY_SUCCEEDED(m_defaultSession->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
VERIFY_IS_TRUE(deletedImages.size() > 0);
|
|
VERIFY_IS_TRUE(std::strlen(deletedImages[0].Image) > 0);
|
|
|
|
// Verify that the image is no longer in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "alpine:latest", false);
|
|
|
|
// Test delete failed if image does not exist.
|
|
VERIFY_ARE_EQUAL(
|
|
WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
}
|
|
|
|
void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected)
|
|
{
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
|
|
if (comError.has_value())
|
|
{
|
|
if (!Expected.has_value())
|
|
{
|
|
LogError("Unexpected COM error: '%ls'", comError->Message.get());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(Expected.value(), comError->Message.get());
|
|
}
|
|
else
|
|
{
|
|
if (Expected.has_value())
|
|
{
|
|
LogError("Expected COM error: '%ls' but none was set", Expected->c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
}
|
|
|
|
HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const WSLCBuildImageOptions* options)
|
|
{
|
|
auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
|
|
|
|
auto contextPathStr = contextDir.wstring();
|
|
WSLCBuildImageOptions optionsCopy = *options;
|
|
optionsCopy.ContextPath = contextPathStr.c_str();
|
|
optionsCopy.DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get());
|
|
|
|
auto buildResult = m_defaultSession->BuildImage(&optionsCopy, nullptr, nullptr);
|
|
|
|
if (FAILED(buildResult))
|
|
{
|
|
LogInfo("BuildImage failed: 0x%08x", buildResult);
|
|
}
|
|
|
|
return buildResult;
|
|
}
|
|
|
|
HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const char* imageTag)
|
|
{
|
|
LPCSTR tag = imageTag;
|
|
WSLCBuildImageOptions options{
|
|
.Tags = {&tag, 1},
|
|
};
|
|
return BuildImageFromContext(contextDir, &options);
|
|
}
|
|
|
|
TEST_METHOD(BuildImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "CMD [\"echo\", \"Hello from a WSL container!\"]\n";
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build:latest", "wslc-build-test-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container!") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageWithContext)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-file";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "COPY message.txt /message.txt\n";
|
|
dockerfile << "CMD [\"cat\", \"/message.txt\"]\n";
|
|
}
|
|
|
|
{
|
|
std::ofstream message(contextDir / "message.txt");
|
|
message << "Hello from a WSL container context file!\n";
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-context:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-context:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-context:latest", "wslc-build-context-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from a WSL container context file!") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageManyFiles)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
static constexpr int fileCount = 1024;
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-many";
|
|
std::filesystem::create_directories(contextDir / "files");
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
// Generate the context files.
|
|
for (int i = 0; i < fileCount; i++)
|
|
{
|
|
auto name = std::format("file{:04d}.txt", i);
|
|
auto content = std::format("content-{:04d}\n", i);
|
|
std::ofstream file(contextDir / "files" / name);
|
|
file << content;
|
|
}
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "COPY files/ /files/\n";
|
|
// Verify every file is present and contains the expected content.
|
|
// Only mismatches are printed; on success just the sentinel.
|
|
dockerfile << "CMD [\"sh\", \"-c\", "
|
|
<< "\"cd /files && failed=0 && "
|
|
<< "for i in $(seq 0 " << (fileCount - 1) << "); do "
|
|
<< "f=$(printf 'file%04d.txt' $i); "
|
|
<< "e=$(printf 'content-%04d' $i); "
|
|
<< "if [ ! -f $f ]; then echo MISSING:$f; failed=1; "
|
|
<< "elif ! grep -q $e $f; then echo BAD:$f; failed=1; fi; "
|
|
<< "done && "
|
|
<< "[ $failed -eq 0 ] && echo all_ok_" << fileCount << "\"]\n";
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-many:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-many:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-many:latest", "wslc-build-many-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
auto sentinel = std::format("all_ok_{}", fileCount);
|
|
VERIFY_IS_TRUE(result.Output[1].find(sentinel) != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageLargeFile)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rmi", "-f", "wslc-test-build-large:latest"});
|
|
ExpectCommandResult(m_defaultSession.get(), {"/usr/bin/docker", "builder", "prune", "-f"}, 0);
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-large";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
static constexpr int fileSizeMb = 1024;
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "COPY large.bin /large.bin\n";
|
|
dockerfile << std::format(
|
|
"CMD [\"sh\", \"-c\", \"test $(stat -c %s /large.bin) -eq {} && echo size_ok\"]\n",
|
|
static_cast<long long>(fileSizeMb) * 1024 * 1024);
|
|
}
|
|
|
|
{
|
|
auto largePath = contextDir / "large.bin";
|
|
wil::unique_hfile largeFile{CreateFileW(largePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == largeFile.get());
|
|
|
|
std::vector<char> buffer(1024 * 1024, '\0');
|
|
for (int i = 0; i < fileSizeMb; i++)
|
|
{
|
|
DWORD written = 0;
|
|
if (!WriteFile(largeFile.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &written, nullptr) ||
|
|
written != static_cast<DWORD>(buffer.size()))
|
|
{
|
|
LogError("WriteFile failed at chunk %d/%d: 0x%08x", i, fileSizeMb, GetLastError());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-large:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-large:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-large:latest", "wslc-build-large-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("size_ok") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageMultiStage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-multistage";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
// Two independent stages that can build in parallel, each producing
|
|
// part of the final output. The last stage combines them.
|
|
dockerfile << "FROM debian:latest AS greeting\n";
|
|
dockerfile << "RUN echo -n 'WSL containers' > /part.txt\n";
|
|
dockerfile << "\n";
|
|
dockerfile << "FROM debian:latest AS description\n";
|
|
dockerfile << "RUN echo -n 'support multi-stage builds' > /part.txt\n";
|
|
dockerfile << "\n";
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "COPY --from=greeting /part.txt /greeting.txt\n";
|
|
dockerfile << "COPY --from=description /part.txt /description.txt\n";
|
|
dockerfile << "CMD [\"sh\", \"-c\", "
|
|
<< "\"echo \\\"$(cat /greeting.txt) $(cat /description.txt)\\\"\"]\n";
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-multistage:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-multistage:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-multistage:latest", "wslc-build-multistage-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("WSL containers support multi-stage builds") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageDockerIgnore)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-dockerignore";
|
|
std::filesystem::create_directories(contextDir / "temp");
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream ignore(contextDir / ".dockerignore");
|
|
ignore << "# Ignore log files and temp directory\n";
|
|
ignore << "*.log\n";
|
|
ignore << "temp/\n";
|
|
}
|
|
|
|
{
|
|
std::ofstream(contextDir / "keep.txt") << "kept\n";
|
|
std::ofstream(contextDir / "debug.log") << "excluded\n";
|
|
std::ofstream(contextDir / "temp" / "cache.dat") << "excluded\n";
|
|
}
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "COPY . /ctx/\n";
|
|
dockerfile << "CMD [\"sh\", \"-c\", "
|
|
<< "\"test -f /ctx/keep.txt "
|
|
<< "&& ! test -f /ctx/debug.log "
|
|
<< "&& ! test -d /ctx/temp "
|
|
<< "&& echo dockerignore_ok\"]\n";
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, "wslc-test-build-dockerignore:latest"));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-dockerignore:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-dockerignore:latest", "wslc-build-dockerignore-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("dockerignore_ok") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageFailure)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-failure";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM does-not-exist:invalid\n";
|
|
}
|
|
|
|
VERIFY_FAILED(BuildImageFromContext(contextDir, "wslc-test-build-failure:latest"));
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
VERIFY_IS_TRUE(comError.has_value());
|
|
LogInfo("Expected build error: %ls", comError->Message.get());
|
|
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-failure:latest", false);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageFailureShowsBuildOutput)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-failure-output";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "RUN echo 'build-log-marker' && /bin/false\n";
|
|
}
|
|
|
|
class ProgressAccumulator
|
|
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
|
|
{
|
|
public:
|
|
ProgressAccumulator(std::string& output) : m_output(output)
|
|
{
|
|
}
|
|
HRESULT OnProgress(LPCSTR message, LPCSTR, ULONGLONG, ULONGLONG) override
|
|
{
|
|
if (message)
|
|
{
|
|
m_output.append(message);
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
private:
|
|
std::string& m_output;
|
|
};
|
|
|
|
std::string progressOutput;
|
|
auto callback = Microsoft::WRL::Make<ProgressAccumulator>(progressOutput);
|
|
|
|
auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
|
|
auto contextPathStr = contextDir.wstring();
|
|
LPCSTR tag = "wslc-test-build-failure-output:latest";
|
|
WSLCBuildImageOptions options{
|
|
.ContextPath = contextPathStr.c_str(),
|
|
.DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()),
|
|
.Tags = {&tag, 1},
|
|
};
|
|
|
|
VERIFY_FAILED(m_defaultSession->BuildImage(&options, callback.Get(), nullptr));
|
|
VERIFY_IS_TRUE(progressOutput.find("build-log-marker") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageStdinDockerfile)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-stdin";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
auto dockerfileContent = "FROM debian:latest\nCMD [\"echo\", \"stdin-dockerfile-ok\"]\n";
|
|
|
|
wil::unique_hfile readHandle;
|
|
wil::unique_hfile writeHandle;
|
|
THROW_IF_WIN32_BOOL_FALSE(CreatePipe(readHandle.addressof(), writeHandle.addressof(), nullptr, 0));
|
|
|
|
DWORD bytesWritten;
|
|
THROW_IF_WIN32_BOOL_FALSE(
|
|
WriteFile(writeHandle.get(), dockerfileContent, static_cast<DWORD>(strlen(dockerfileContent)), &bytesWritten, nullptr));
|
|
writeHandle.reset();
|
|
|
|
auto contextPathStr = contextDir.wstring();
|
|
LPCSTR tag = "wslc-test-build-stdin:latest";
|
|
WSLCBuildImageOptions options{
|
|
.ContextPath = contextPathStr.c_str(),
|
|
.DockerfileHandle = ToCOMInputHandle(readHandle.get()),
|
|
.Tags = {&tag, 1},
|
|
};
|
|
VERIFY_SUCCEEDED(m_defaultSession->BuildImage(&options, nullptr, nullptr));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-stdin:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-stdin:latest", "wslc-build-stdin-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("stdin-dockerfile-ok") != std::string::npos);
|
|
}
|
|
|
|
TEST_METHOD(BuildImageBuildArgs)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-buildargs";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions deleteOptions{.Image = "wslc-test-build-args:latest", .Flags = WSLCDeleteImageFlagsForce};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "ARG TEST_VALUE\n";
|
|
dockerfile << "ENV TEST_VALUE=${TEST_VALUE}\n";
|
|
dockerfile << "CMD echo \"build-arg-value=${TEST_VALUE}\"\n";
|
|
}
|
|
|
|
LPCSTR tag = "wslc-test-build-args:latest";
|
|
LPCSTR buildArg = "TEST_VALUE=hello-from-build-arg";
|
|
WSLCBuildImageOptions options{.Tags = {&tag, 1}, .BuildArgs = {&buildArg, 1}};
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-build-args:latest");
|
|
|
|
WSLCContainerLauncher launcher("wslc-test-build-args:latest", "wslc-build-args-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "build-arg-value=hello-from-build-arg\n"}});
|
|
}
|
|
|
|
TEST_METHOD(BuildImageMultipleTags)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-multitag";
|
|
std::filesystem::create_directories(contextDir);
|
|
LPCSTR tags[] = {"wslc-test-multitag:v1", "wslc-test-multitag:v2"};
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
for (auto* tag : tags)
|
|
{
|
|
WSLCDeleteImageOptions deleteOptions{.Image = tag, .Flags = WSLCDeleteImageFlagsForce};
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "CMD [\"echo\", \"multi-tag-ok\"]\n";
|
|
}
|
|
WSLCBuildImageOptions options{.Tags = {tags, 2}};
|
|
VERIFY_SUCCEEDED(BuildImageFromContext(contextDir, &options));
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v1");
|
|
ExpectImagePresent(*m_defaultSession, "wslc-test-multitag:v2");
|
|
}
|
|
|
|
TEST_METHOD(BuildImageNullHandle)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
WSLCBuildImageOptions options{.ContextPath = L"C:\\", .DockerfileHandle = {}, .Tags = {nullptr, 0}};
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->BuildImage(&options, nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE));
|
|
}
|
|
|
|
TEST_METHOD(BuildImageCancel)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
class TestProgressCallback
|
|
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
|
|
{
|
|
public:
|
|
TestProgressCallback(wil::unique_event& event) : m_event(event)
|
|
{
|
|
}
|
|
|
|
HRESULT OnProgress(LPCSTR, LPCSTR, ULONGLONG, ULONGLONG) override
|
|
{
|
|
m_event.SetEvent();
|
|
return S_OK;
|
|
}
|
|
|
|
private:
|
|
wil::unique_event& m_event;
|
|
};
|
|
|
|
auto contextDir = std::filesystem::current_path() / "build-context-cancel";
|
|
std::filesystem::create_directories(contextDir);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(contextDir, ec);
|
|
});
|
|
|
|
// Use a Dockerfile that takes a long time to build so we can cancel it mid-build.
|
|
{
|
|
std::ofstream dockerfile(contextDir / "Dockerfile");
|
|
dockerfile << "FROM debian:latest\n";
|
|
dockerfile << "RUN sleep 120\n";
|
|
}
|
|
|
|
wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
|
|
wil::unique_event progressEvent{wil::EventOptions::ManualReset};
|
|
|
|
// Use a progress callback to detect when the build is actively running
|
|
// before signaling cancellation, avoiding a racy Sleep().
|
|
auto callback = Microsoft::WRL::Make<TestProgressCallback>(progressEvent);
|
|
|
|
auto contextPathStr = contextDir.wstring();
|
|
auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str());
|
|
|
|
LPCSTR tag = "wslc-test-build-cancel:latest";
|
|
WSLCBuildImageOptions options{
|
|
.ContextPath = contextPathStr.c_str(), .DockerfileHandle = ToCOMInputHandle(dockerfileHandle.get()), .Tags = {&tag, 1}};
|
|
|
|
std::promise<HRESULT> result;
|
|
std::thread buildThread(
|
|
[&]() { result.set_value(m_defaultSession->BuildImage(&options, callback.Get(), cancelEvent.get())); });
|
|
|
|
auto joinThread = wil::scope_exit([&]() { buildThread.join(); });
|
|
|
|
VERIFY_IS_TRUE(progressEvent.wait(60 * 1000));
|
|
cancelEvent.SetEvent();
|
|
|
|
VERIFY_ARE_EQUAL(E_ABORT, result.get_future().get());
|
|
}
|
|
|
|
TEST_METHOD(TagImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto runTagImage = [&](LPCSTR Image, LPCSTR Repo, LPCSTR Tag) {
|
|
WSLCTagImageOptions options{};
|
|
options.Image = Image;
|
|
options.Repo = Repo;
|
|
options.Tag = Tag;
|
|
|
|
return m_defaultSession->TagImage(&options);
|
|
};
|
|
|
|
// Positive test: Tag an existing image with a new tag in the same repository.
|
|
{
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions deleteOptions{};
|
|
deleteOptions.Image = "debian:test-tag";
|
|
deleteOptions.Flags = WSLCDeleteImageFlagsNoPrune;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->DeleteImage(&deleteOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-tag", false);
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
});
|
|
|
|
VERIFY_SUCCEEDED(runTagImage("debian:latest", "debian", "test-tag"));
|
|
|
|
// Verify both tags exist and point to the same image.
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-tag");
|
|
|
|
// Verify they have the same image hash.
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
std::string latestHash;
|
|
std::string testTagHash;
|
|
for (const auto& image : images)
|
|
{
|
|
if (std::strcmp(image.Image, "debian:latest") == 0)
|
|
{
|
|
latestHash = image.Hash;
|
|
}
|
|
else if (std::strcmp(image.Image, "debian:test-tag") == 0)
|
|
{
|
|
testTagHash = image.Hash;
|
|
}
|
|
}
|
|
|
|
VERIFY_IS_FALSE(latestHash.empty());
|
|
VERIFY_IS_FALSE(testTagHash.empty());
|
|
VERIFY_ARE_EQUAL(latestHash, testTagHash);
|
|
}
|
|
|
|
// Positive test: Tag with a different repository name.
|
|
{
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions deleteOptions{};
|
|
deleteOptions.Image = "myrepo/myimage:v1.0.0";
|
|
deleteOptions.Flags = WSLCDeleteImageFlagsNoPrune;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->DeleteImage(&deleteOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0", false);
|
|
});
|
|
|
|
VERIFY_SUCCEEDED(runTagImage("debian:latest", "myrepo/myimage", "v1.0.0"));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "myrepo/myimage:v1.0.0");
|
|
}
|
|
|
|
// Positive test: Tag using image ID.
|
|
{
|
|
ExpectImagePresent(*m_defaultSession, "debian:latest");
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions deleteOptions{};
|
|
deleteOptions.Image = "debian:test-by-id";
|
|
deleteOptions.Flags = WSLCDeleteImageFlagsNoPrune;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->DeleteImage(&deleteOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-by-id", false);
|
|
});
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
|
|
|
|
std::string imageId;
|
|
for (const auto& image : images)
|
|
{
|
|
if (std::strcmp(image.Image, "debian:latest") == 0)
|
|
{
|
|
imageId = image.Hash;
|
|
break;
|
|
}
|
|
}
|
|
VERIFY_IS_FALSE(imageId.empty());
|
|
|
|
VERIFY_SUCCEEDED(runTagImage(imageId.c_str(), "debian", "test-by-id"));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "debian:test-by-id");
|
|
}
|
|
|
|
// Positive test: Overwrite existing tag.
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions deleteOptions{};
|
|
deleteOptions.Image = "test:duplicate-tag";
|
|
deleteOptions.Flags = WSLCDeleteImageFlagsNoPrune;
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->DeleteImage(&deleteOptions, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
ExpectImagePresent(*m_defaultSession, "test:duplicate-tag", false);
|
|
});
|
|
|
|
VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
|
|
VERIFY_SUCCEEDED(runTagImage("debian:latest", "test", "duplicate-tag"));
|
|
}
|
|
|
|
// Negative test: Null options pointer.
|
|
{
|
|
VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), m_defaultSession->TagImage(nullptr));
|
|
}
|
|
|
|
// Negative test: Null Image field.
|
|
{
|
|
VERIFY_ARE_EQUAL(E_POINTER, runTagImage(nullptr, "test", "tag"));
|
|
}
|
|
|
|
// Negative test: Null Repo field.
|
|
{
|
|
VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", nullptr, "tag"));
|
|
}
|
|
|
|
// Negative test: Null Tag field.
|
|
{
|
|
VERIFY_ARE_EQUAL(E_POINTER, runTagImage("debian:latest", "test", nullptr));
|
|
}
|
|
|
|
// Negative test: Tag a non-existent image.
|
|
{
|
|
VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, runTagImage("nonexistent:notfound", "test", "fail"));
|
|
ValidateCOMErrorMessage(L"No such image: nonexistent:notfound");
|
|
}
|
|
|
|
// Negative test: Invalid tag format with spaces.
|
|
{
|
|
VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), runTagImage("debian:latest", "test", "invalid tag"));
|
|
ValidateCOMErrorMessage(L"invalid tag format");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(InspectImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Test inspect debian:latest
|
|
{
|
|
wil::unique_cotaskmem_ansistring output;
|
|
VERIFY_SUCCEEDED(m_defaultSession->InspectImage("debian:latest", &output));
|
|
|
|
// Verify output is valid JSON
|
|
VERIFY_IS_NOT_NULL(output.get());
|
|
VERIFY_IS_TRUE(std::strlen(output.get()) > 0);
|
|
LogInfo("Inspect output: %hs", output.get());
|
|
|
|
// Parse and validate JSON structure
|
|
auto inspectResult = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(output.get());
|
|
|
|
// Verify all fields exposed in wslc_schema::InspectImage
|
|
VERIFY_IS_TRUE(inspectResult.Id.find("sha256:") == 0);
|
|
|
|
VERIFY_IS_TRUE(inspectResult.RepoTags.has_value());
|
|
VERIFY_IS_FALSE(inspectResult.RepoTags->empty());
|
|
bool foundTag = false;
|
|
for (const auto& tag : inspectResult.RepoTags.value())
|
|
{
|
|
if (tag.find("debian:latest") != std::string::npos)
|
|
{
|
|
foundTag = true;
|
|
break;
|
|
}
|
|
}
|
|
VERIFY_IS_TRUE(foundTag);
|
|
|
|
// skip testing RepoDigests for loaded test image.
|
|
VERIFY_IS_FALSE(inspectResult.Created.empty());
|
|
VERIFY_IS_TRUE(inspectResult.Architecture == "amd64" || inspectResult.Architecture == "arm64");
|
|
VERIFY_ARE_EQUAL("linux", inspectResult.Os);
|
|
VERIFY_IS_TRUE(inspectResult.Size > 0);
|
|
VERIFY_IS_TRUE(inspectResult.Metadata.has_value());
|
|
VERIFY_IS_TRUE(inspectResult.Metadata->size() > 0);
|
|
|
|
VERIFY_IS_TRUE(inspectResult.Config.has_value());
|
|
const auto& config = inspectResult.Config.value();
|
|
VERIFY_IS_TRUE(config.Cmd.has_value());
|
|
VERIFY_IS_TRUE(config.Cmd->size() > 0);
|
|
VERIFY_IS_TRUE(config.Entrypoint.has_value());
|
|
VERIFY_ARE_EQUAL(0, config.Entrypoint->size());
|
|
VERIFY_IS_TRUE(config.Env.has_value());
|
|
VERIFY_IS_TRUE(config.Env->size() > 0);
|
|
VERIFY_IS_FALSE(config.Labels.has_value());
|
|
}
|
|
|
|
// Negative test: Image not found
|
|
{
|
|
wil::unique_cotaskmem_ansistring output;
|
|
VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("nonexistent:image", &output));
|
|
ValidateCOMErrorMessage(L"No such image: nonexistent:image");
|
|
}
|
|
|
|
// Negative test: Bad image name input
|
|
{
|
|
wil::unique_cotaskmem_ansistring output;
|
|
|
|
std::string longImageName(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a');
|
|
VERIFY_ARE_EQUAL(E_INVALIDARG, m_defaultSession->InspectImage(longImageName.c_str(), &output));
|
|
|
|
// Invalid name.
|
|
VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(ERROR_BAD_ARGUMENTS), m_defaultSession->InspectImage("debian latest", &output));
|
|
ValidateCOMErrorMessage(L"invalid reference format");
|
|
|
|
// Attempt to fake to call search endpoint. Our implementation escaped the image name correctly.
|
|
VERIFY_ARE_EQUAL(WSLC_E_IMAGE_NOT_FOUND, m_defaultSession->InspectImage("search/debian:latest", &output));
|
|
ValidateCOMErrorMessage(L"No such image: search/debian:latest");
|
|
}
|
|
}
|
|
|
|
struct BlockingOperation
|
|
{
|
|
NON_COPYABLE(BlockingOperation);
|
|
NON_MOVABLE(BlockingOperation);
|
|
|
|
BlockingOperation(std::function<HRESULT(HANDLE)>&& Operation, HRESULT ExpectedResult = S_OK, bool AllowEarlyCompletion = false, bool UseOverlappedWritePipe = false) :
|
|
m_operation(std::move(Operation)), m_expectedResult(ExpectedResult), m_allowEarlyCompletion(AllowEarlyCompletion)
|
|
{
|
|
auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(100000, false, UseOverlappedWritePipe);
|
|
|
|
m_operationThread = std::thread(&BlockingOperation::RunOperation, this, std::move(pipeWrite));
|
|
m_ioThread = std::thread(&BlockingOperation::RunIO, this, std::move(pipeRead));
|
|
|
|
// Wait for the operation to be running before continuing.
|
|
VERIFY_IS_TRUE(m_startedEvent.wait(60 * 1000));
|
|
}
|
|
|
|
~BlockingOperation()
|
|
{
|
|
if (m_operationThread.joinable())
|
|
{
|
|
m_operationThread.join();
|
|
}
|
|
|
|
if (m_ioThread.joinable())
|
|
{
|
|
m_ioThread.join();
|
|
}
|
|
}
|
|
|
|
void RunOperation(wil::unique_hfile Handle)
|
|
{
|
|
m_result.set_value(m_operation(Handle.get()));
|
|
|
|
// Fail if the operation completed before the test signaled completion
|
|
// (unless early completion is expected, e.g. session termination).
|
|
// Don't use VERIFY macros since this is running in a separate thread.
|
|
WI_ASSERT(m_allowEarlyCompletion || m_testCompleteEvent.is_signaled());
|
|
}
|
|
|
|
void RunIO(wil::unique_hfile Handle)
|
|
{
|
|
std::vector<char> buffer(1024 * 1024);
|
|
while (true)
|
|
{
|
|
DWORD bytesRead{};
|
|
if (!ReadFile(Handle.get(), buffer.data(), static_cast<DWORD>(buffer.size()), &bytesRead, nullptr))
|
|
{
|
|
if (GetLastError() != ERROR_BROKEN_PIPE)
|
|
{
|
|
LogError("Unexpected ReadFile() error: %u", GetLastError());
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (bytesRead == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!m_startedEvent.is_signaled())
|
|
{
|
|
m_startedEvent.SetEvent();
|
|
}
|
|
|
|
// Block until the test completes.
|
|
if (!m_testCompleteEvent.wait(60 * 1000))
|
|
{
|
|
LogError("Timed out waiting for test completion");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Complete()
|
|
{
|
|
m_testCompleteEvent.SetEvent();
|
|
|
|
VERIFY_ARE_EQUAL(m_expectedResult, m_result.get_future().get());
|
|
}
|
|
|
|
std::function<HRESULT(HANDLE)> m_operation;
|
|
wil::unique_event m_startedEvent{wil::EventOptions::ManualReset};
|
|
wil::unique_event m_testCompleteEvent{wil::EventOptions::ManualReset};
|
|
std::thread m_operationThread;
|
|
std::thread m_ioThread;
|
|
std::promise<HRESULT> m_result;
|
|
HRESULT m_expectedResult{};
|
|
bool m_allowEarlyCompletion{};
|
|
};
|
|
|
|
TEST_METHOD(SaveImage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
{
|
|
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));
|
|
// Load the image from a saved tar
|
|
VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "hello-world:latest");
|
|
WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
}
|
|
|
|
{
|
|
std::filesystem::path imageTar = L"HelloWorldExported.tar";
|
|
auto cleanup =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
|
|
// Save the image to a tar file.
|
|
{
|
|
wil::unique_handle imageTarFileHandle{CreateFileW(
|
|
imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
|
|
LARGE_INTEGER fileSize{};
|
|
VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
|
|
VERIFY_SUCCEEDED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-world:latest", nullptr, nullptr));
|
|
VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, true);
|
|
}
|
|
|
|
// Load the saved image to verify it's valid.
|
|
{
|
|
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));
|
|
// Load the image from a saved tar
|
|
VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "hello-world:latest");
|
|
WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
}
|
|
}
|
|
|
|
// Try to save an invalid image.
|
|
{
|
|
std::filesystem::path imageTar = L"HelloWorldError.tar";
|
|
auto cleanfile =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
|
|
wil::unique_handle imageTarFileHandle{CreateFileW(
|
|
imageTar.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
|
|
LARGE_INTEGER fileSize{};
|
|
VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
|
|
VERIFY_FAILED(m_defaultSession->SaveImage(ToCOMInputHandle(imageTarFileHandle.get()), "hello-wld:latest", nullptr, nullptr));
|
|
ValidateCOMErrorMessage(L"reference does not exist");
|
|
|
|
VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart > 0, false);
|
|
}
|
|
|
|
// Validate that cancellation works.
|
|
{
|
|
wil::unique_event cancelEvent{wil::EventOptions::ManualReset};
|
|
|
|
BlockingOperation operation(
|
|
[&](HANDLE handle) {
|
|
return m_defaultSession->SaveImage(ToCOMInputHandle(handle), "debian:latest", nullptr, cancelEvent.get());
|
|
},
|
|
E_ABORT);
|
|
|
|
cancelEvent.SetEvent();
|
|
operation.Complete();
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(SynchronousIoCancellation)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Create a blocked operation that will cause the service to get stuck on a ReadFile() call.
|
|
// Because the pipe handle that we're passing in doesn't support overlapped IO, the service will get stuck in a
|
|
// synchronous ReadFile() call. Validate that terminating the session correctly cancels the IO.
|
|
|
|
wil::unique_handle pipeRead;
|
|
wil::unique_handle pipeWrite;
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2));
|
|
|
|
std::promise<HRESULT> result;
|
|
|
|
wil::unique_event testCompleted{wil::EventOptions::ManualReset};
|
|
std::thread operationThread([&]() {
|
|
result.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024));
|
|
|
|
WI_ASSERT(testCompleted.is_signaled()); // Sanity check.
|
|
});
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
|
|
|
|
// Write 4 bytes to validate that the service has started reading from the pipe (since the pipe buffer is 2).
|
|
DWORD bytesWritten{};
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr));
|
|
|
|
testCompleted.SetEvent();
|
|
|
|
// N.B. It's not possible to deterministically wait for the service to be stuck in the ReadFile() call.
|
|
// It's possible that the service will check the session termination event before calling ReadFile() on the pipe
|
|
// but that's OK since we can also accept that error code here (E_ABORT).
|
|
VERIFY_SUCCEEDED(m_defaultSession->Terminate());
|
|
|
|
auto reset = ResetTestSession();
|
|
|
|
auto hr = result.get_future().get();
|
|
if (hr != E_ABORT && hr != HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED))
|
|
{
|
|
LogError("Unexpected result: 0x%08X", hr);
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ExportContainer)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Load an image and launch a container to verify image is valid.
|
|
// Then export the container to a tar file.
|
|
// Load the exported tar file to verify it's a valid image and can be launched.
|
|
// Finally, stop and delete the container, then try to export again to verify it fails as expected.
|
|
{
|
|
std::filesystem::path containerTar = L"HelloWorldExported.tar";
|
|
auto cleanup =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(containerTar.c_str())); });
|
|
|
|
// Load the image from a saved tar and launch a container
|
|
{
|
|
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(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart));
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "hello-world:latest");
|
|
WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
|
|
// Export the container to a tar file.
|
|
wil::unique_handle containerTarFileHandle{CreateFileW(
|
|
containerTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
|
|
VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
|
|
VERIFY_SUCCEEDED(container.Get().Export(ToCOMInputHandle(containerTarFileHandle.get())));
|
|
VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_NOT_EQUAL(fileSize.QuadPart, 0);
|
|
}
|
|
|
|
// Load the exported container to verify it's valid.
|
|
{
|
|
wil::unique_handle containerTarFileHandle{CreateFileW(
|
|
containerTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == containerTarFileHandle.get());
|
|
LARGE_INTEGER fileSize{};
|
|
VERIFY_IS_TRUE(GetFileSizeEx(containerTarFileHandle.get(), &fileSize));
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
WSLCDeleteImageOptions options{.Image = "test-imported-container:latest", .Flags = WSLCDeleteImageFlagsNone};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
LOG_IF_FAILED(m_defaultSession->DeleteImage(&options, &deletedImages, deletedImages.size_address<ULONG>()));
|
|
});
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
|
|
ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart));
|
|
|
|
// Verify that the image is in the list of images.
|
|
ExpectImagePresent(*m_defaultSession, "test-imported-container:latest");
|
|
WSLCContainerLauncher launcher("test-imported-container:latest", "wslc-hello-world-container", {"/hello"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto result = container.GetInitProcess().WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
|
|
|
|
// Stop and delete the above container and try to export.
|
|
|
|
std::filesystem::path imageTarFile = L"HelloWorldExportError.tar";
|
|
auto cleanfile =
|
|
wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTarFile.c_str())); });
|
|
wil::unique_handle contTarFileHandle{CreateFileW(
|
|
imageTarFile.c_str(), GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == contTarFileHandle.get());
|
|
VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
|
|
|
|
auto outFile = ToCOMInputHandle(contTarFileHandle.get());
|
|
|
|
container.Get().Stop(WSLCSignalSIGILL, 10);
|
|
container.Get().Delete(WSLCDeleteFlagsNone);
|
|
VERIFY_ARE_EQUAL(container.Get().Export(outFile), RPC_E_DISCONNECTED);
|
|
|
|
VERIFY_IS_TRUE(GetFileSizeEx(contTarFileHandle.get(), &fileSize));
|
|
VERIFY_ARE_EQUAL(fileSize.QuadPart, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(CustomDmesgOutput)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
SKIP_TEST_ARM64();
|
|
|
|
auto createVmWithDmesg = [this](bool earlyBootLogging) {
|
|
auto [read, write] = CreateSubprocessPipe(false, false);
|
|
|
|
auto settings = GetDefaultSessionSettings(L"dmesg-output-test");
|
|
settings.DmesgOutput = ToCOMInputHandle(write.get());
|
|
WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsEarlyBootDmesg, earlyBootLogging);
|
|
|
|
std::vector<char> dmesgContent;
|
|
auto readDmesg = [read = read.get(), &dmesgContent]() mutable {
|
|
DWORD Offset = 0;
|
|
|
|
constexpr auto bufferSize = 1024;
|
|
while (true)
|
|
{
|
|
dmesgContent.resize(Offset + bufferSize);
|
|
|
|
DWORD Read{};
|
|
if (!ReadFile(read, &dmesgContent[Offset], bufferSize, &Read, nullptr))
|
|
{
|
|
LogInfo("ReadFile() failed: %lu", GetLastError());
|
|
}
|
|
|
|
if (Read == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Offset += Read;
|
|
}
|
|
};
|
|
|
|
std::thread thread(readDmesg); // Needs to be created before the VM starts, to avoid a pipe deadlock.
|
|
|
|
auto session = CreateSession(settings);
|
|
auto detach = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
session.reset();
|
|
if (thread.joinable())
|
|
{
|
|
thread.join();
|
|
}
|
|
});
|
|
|
|
write.reset();
|
|
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo DmesgTest > /dev/kmsg"}, 0);
|
|
|
|
session.reset();
|
|
detach.reset();
|
|
|
|
auto contentString = std::string(dmesgContent.begin(), dmesgContent.end());
|
|
|
|
VERIFY_ARE_NOT_EQUAL(contentString.find("Run /init as init process"), std::string::npos);
|
|
VERIFY_ARE_NOT_EQUAL(contentString.find("DmesgTest"), std::string::npos);
|
|
|
|
return contentString;
|
|
};
|
|
|
|
auto validateFirstDmesgLine = [](const std::string& dmesg, const char* expected) {
|
|
auto firstLf = dmesg.find("\n");
|
|
VERIFY_ARE_NOT_EQUAL(firstLf, std::string::npos);
|
|
VERIFY_IS_TRUE(dmesg.find(expected) < firstLf);
|
|
};
|
|
|
|
// Dmesg without early boot logging
|
|
{
|
|
auto dmesg = createVmWithDmesg(false);
|
|
|
|
// Verify that the first line is "brd: module loaded";
|
|
validateFirstDmesgLine(dmesg, "brd: module loaded");
|
|
}
|
|
|
|
// Dmesg with early boot logging
|
|
{
|
|
auto dmesg = createVmWithDmesg(true);
|
|
validateFirstDmesgLine(dmesg, "Linux version");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(TerminationCallback)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
class DECLSPEC_UUID("7BC4E198-6531-4FA6-ADE2-5EF3D2A04DFF") CallbackInstance
|
|
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, ITerminationCallback, IFastRundown>
|
|
{
|
|
|
|
public:
|
|
CallbackInstance(std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)>&& callback) :
|
|
m_callback(std::move(callback))
|
|
{
|
|
}
|
|
|
|
HRESULT OnTermination(WSLCVirtualMachineTerminationReason Reason, LPCWSTR Details) override
|
|
{
|
|
m_callback(Reason, Details);
|
|
return S_OK;
|
|
}
|
|
|
|
private:
|
|
std::function<void(WSLCVirtualMachineTerminationReason, LPCWSTR)> m_callback;
|
|
};
|
|
|
|
std::promise<std::pair<WSLCVirtualMachineTerminationReason, std::wstring>> promise;
|
|
|
|
CallbackInstance callback{[&](WSLCVirtualMachineTerminationReason reason, LPCWSTR details) {
|
|
promise.set_value(std::make_pair(reason, details));
|
|
}};
|
|
|
|
WSLCSessionSettings sessionSettings = GetDefaultSessionSettings(L"termination-callback-test");
|
|
sessionSettings.TerminationCallback = &callback;
|
|
|
|
auto session = CreateSession(sessionSettings);
|
|
|
|
session.reset();
|
|
auto future = promise.get_future();
|
|
auto result = future.wait_for(std::chrono::seconds(30));
|
|
VERIFY_ARE_EQUAL(result, std::future_status::ready);
|
|
auto [reason, details] = future.get();
|
|
VERIFY_ARE_EQUAL(reason, WSLCVirtualMachineTerminationReasonShutdown);
|
|
VERIFY_ARE_NOT_EQUAL(details, L"");
|
|
}
|
|
|
|
TEST_METHOD(InteractiveShell)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh"}, {"TERM=xterm-256color"}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
|
|
wil::unique_handle tty = process.GetStdHandle(WSLCFDTty);
|
|
|
|
auto validateTtyOutput = [&](const std::string& expected) {
|
|
std::string buffer(expected.size(), '\0');
|
|
|
|
DWORD offset = 0;
|
|
|
|
while (offset < buffer.size())
|
|
{
|
|
DWORD bytesRead{};
|
|
VERIFY_IS_TRUE(ReadFile(tty.get(), buffer.data() + offset, static_cast<DWORD>(buffer.size() - offset), &bytesRead, nullptr));
|
|
|
|
offset += bytesRead;
|
|
}
|
|
|
|
buffer.resize(offset);
|
|
VERIFY_ARE_EQUAL(buffer, expected);
|
|
};
|
|
|
|
auto writeTty = [&](const std::string& content) {
|
|
VERIFY_IS_TRUE(WriteFile(tty.get(), content.data(), static_cast<DWORD>(content.size()), nullptr, nullptr));
|
|
};
|
|
|
|
// Expect the shell prompt to be displayed
|
|
validateTtyOutput("\033[?2004hsh-5.2# ");
|
|
writeTty("echo OK\n");
|
|
validateTtyOutput("echo OK\r\n\033[?2004l\rOK");
|
|
|
|
// Exit the shell
|
|
writeTty("exit\n");
|
|
|
|
VERIFY_IS_TRUE(process.GetExitEvent().wait(30 * 1000));
|
|
}
|
|
|
|
void ValidateNetworking(WSLCNetworkingMode mode, bool enableDnsTunneling = false)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Reuse the default session if settings match (same networking mode and DNS tunneling setting).
|
|
auto createNewSession = mode != m_defaultSessionSettings.NetworkingMode ||
|
|
enableDnsTunneling != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsDnsTunneling);
|
|
|
|
auto settings = GetDefaultSessionSettings(L"networking-test", false, mode);
|
|
WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, enableDnsTunneling);
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
// Validate that eth0 has an ip address
|
|
ExpectCommandResult(
|
|
session.get(),
|
|
{"/bin/sh",
|
|
"-c",
|
|
"ip a show dev eth0 | grep -iF 'inet ' | grep -E '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}'"},
|
|
0);
|
|
|
|
ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"}, 0);
|
|
|
|
// Verify that /etc/resolv.conf is correctly configured.
|
|
if (enableDnsTunneling)
|
|
{
|
|
auto result = ExpectCommandResult(session.get(), {"/bin/grep", "-iF", "nameserver ", "/etc/resolv.conf"}, 0);
|
|
|
|
VERIFY_ARE_EQUAL(result.Output[1], std::format("nameserver {}\n", LX_INIT_DNS_TUNNELING_IP_ADDRESS));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(NATNetworking)
|
|
{
|
|
ValidateNetworking(WSLCNetworkingModeNAT);
|
|
}
|
|
|
|
TEST_METHOD(NATNetworkingWithDnsTunneling)
|
|
{
|
|
WINDOWS_11_TEST_ONLY();
|
|
ValidateNetworking(WSLCNetworkingModeNAT, true);
|
|
}
|
|
|
|
TEST_METHOD(VirtioProxyNetworking)
|
|
{
|
|
ValidateNetworking(WSLCNetworkingModeVirtioProxy);
|
|
}
|
|
|
|
void ValidatePortMapping(WSLCNetworkingMode networkingMode)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto settings = GetDefaultSessionSettings(L"port-mapping-test");
|
|
settings.NetworkingMode = networkingMode;
|
|
|
|
// Reuse the default session if the networking mode matches.
|
|
auto createNewSession = networkingMode != m_defaultSessionSettings.NetworkingMode;
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
// Install socat in the container.
|
|
//
|
|
// TODO: revisit this in the future to avoid pulling packages from the network.
|
|
auto installSocat = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", "tdnf install socat -y"}).Launch(*session);
|
|
ValidateProcessOutput(installSocat, {}, 0, 300 * 1000);
|
|
|
|
auto listen = [&](short port, const char* content, bool ipv6) {
|
|
auto cmd = std::format("echo -n '{}' | /usr/bin/socat -dd TCP{}-LISTEN:{},reuseaddr -", content, ipv6 ? "6" : "", port);
|
|
auto process = WSLCProcessLauncher("/bin/sh", {"/bin/sh", "-c", cmd}).Launch(*session);
|
|
WaitForOutput(process.GetStdHandle(2), "listening on");
|
|
|
|
return process;
|
|
};
|
|
|
|
auto connectAndRead = [&](short port, int family) -> std::string {
|
|
SOCKADDR_INET addr{};
|
|
addr.si_family = family;
|
|
INETADDR_SETLOOPBACK((PSOCKADDR)&addr);
|
|
SS_PORT(&addr) = htons(port);
|
|
|
|
wil::unique_socket hostSocket{socket(family, SOCK_STREAM, IPPROTO_TCP)};
|
|
THROW_LAST_ERROR_IF(!hostSocket);
|
|
THROW_LAST_ERROR_IF(connect(hostSocket.get(), reinterpret_cast<SOCKADDR*>(&addr), sizeof(addr)) == SOCKET_ERROR);
|
|
|
|
return ReadToString(hostSocket.get());
|
|
};
|
|
|
|
auto expectContent = [&](short port, int family, const char* expected) {
|
|
auto content = connectAndRead(port, family);
|
|
VERIFY_ARE_EQUAL(content, expected);
|
|
};
|
|
|
|
auto expectNotBound = [&](short port, int family) {
|
|
auto result = wil::ResultFromException([&]() { connectAndRead(port, family); });
|
|
|
|
VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(WSAECONNREFUSED));
|
|
};
|
|
|
|
// Map port
|
|
VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
|
|
|
|
// Validate that the same port can't be bound twice
|
|
VERIFY_ARE_EQUAL(session->MapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
// Check simple case
|
|
listen(80, "port80", false);
|
|
expectContent(1234, AF_INET, "port80");
|
|
|
|
// Validate that same port mapping can be reused
|
|
listen(80, "port80", false);
|
|
expectContent(1234, AF_INET, "port80");
|
|
|
|
// Validate that the connection is immediately reset if the port is not bound on the linux side
|
|
expectContent(1234, AF_INET, "");
|
|
|
|
// Add a ipv6 binding
|
|
VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1234, 80));
|
|
|
|
// Validate that ipv6 bindings work as well.
|
|
listen(80, "port80ipv6", true);
|
|
expectContent(1234, AF_INET6, "port80ipv6");
|
|
|
|
// Unmap the ipv4 port
|
|
VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
|
|
|
|
// Verify that a proper error is returned if the mapping doesn't exist
|
|
VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET, 1234, 80), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
// Unmap the v6 port
|
|
VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1234, 80));
|
|
|
|
// Map another port as v6 only
|
|
VERIFY_SUCCEEDED(session->MapVmPort(AF_INET6, 1235, 81));
|
|
|
|
listen(81, "port81ipv6", true);
|
|
expectContent(1235, AF_INET6, "port81ipv6");
|
|
expectNotBound(1235, AF_INET);
|
|
|
|
VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET6, 1235, 81));
|
|
VERIFY_ARE_EQUAL(session->UnmapVmPort(AF_INET6, 1235, 81), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
expectNotBound(1235, AF_INET6);
|
|
|
|
// Create a forking relay and stress test
|
|
VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, 1234, 80));
|
|
|
|
auto process =
|
|
WSLCProcessLauncher{"/usr/bin/socat", {"/usr/bin/socat", "-dd", "TCP-LISTEN:80,fork,reuseaddr", "system:'echo -n OK'"}}
|
|
.Launch(*session);
|
|
|
|
WaitForOutput(process.GetStdHandle(2), "listening on");
|
|
|
|
for (auto i = 0; i < 100; i++)
|
|
{
|
|
expectContent(1234, AF_INET, "OK");
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, 1234, 80));
|
|
|
|
// Validate the 63-port limit.
|
|
// TODO: Remove the 63-port limit by switching the relay's AcceptThread from
|
|
// WaitForMultipleObjects to IO completion ports or similar.
|
|
constexpr int c_maxPorts = 63;
|
|
for (int i = 0; i < c_maxPorts; i++)
|
|
{
|
|
VERIFY_SUCCEEDED(session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(
|
|
session->MapVmPort(AF_INET, static_cast<uint16_t>(20000 + c_maxPorts), static_cast<uint16_t>(80 + c_maxPorts)),
|
|
HRESULT_FROM_WIN32(ERROR_TOO_MANY_OPEN_FILES));
|
|
|
|
for (int i = 0; i < c_maxPorts; i++)
|
|
{
|
|
VERIFY_SUCCEEDED(session->UnmapVmPort(AF_INET, static_cast<uint16_t>(20000 + i), static_cast<uint16_t>(80 + i)));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(PortMappingNat)
|
|
{
|
|
ValidatePortMapping(WSLCNetworkingModeNAT);
|
|
}
|
|
|
|
TEST_METHOD(PortMappingVirtioProxy)
|
|
{
|
|
ValidatePortMapping(WSLCNetworkingModeVirtioProxy);
|
|
}
|
|
|
|
TEST_METHOD(StuckVmTermination)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Create a 'stuck' process
|
|
auto process = WSLCProcessLauncher{"/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin}.Launch(*m_defaultSession);
|
|
|
|
// Stop the service
|
|
StopWslService();
|
|
|
|
ResetTestSession(); // Reopen the session since the service was stopped.
|
|
}
|
|
|
|
void ValidateWindowsMounts(bool enableVirtioFs)
|
|
{
|
|
auto settings = GetDefaultSessionSettings(L"windows-mount-tests");
|
|
WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs, enableVirtioFs);
|
|
|
|
// Reuse the default session if possible.
|
|
auto createNewSession = enableVirtioFs != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
auto expectedMountOptions = [&](bool readOnly) -> std::string {
|
|
if (enableVirtioFs)
|
|
{
|
|
return std::format("/win-path*virtiofs*{},relatime*", readOnly ? "ro" : "rw");
|
|
}
|
|
else
|
|
{
|
|
return std::format(
|
|
"/win-path*9p*{},relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*", readOnly ? "ro" : "rw");
|
|
}
|
|
};
|
|
|
|
auto testFolder = std::filesystem::current_path() / "test-folder";
|
|
std::filesystem::create_directories(testFolder);
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
|
|
|
|
// Validate writeable mount.
|
|
{
|
|
VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
|
|
ExpectMount(session.get(), "/win-path", expectedMountOptions(false));
|
|
|
|
// Validate that mount can't be stacked on each other
|
|
VERIFY_ARE_EQUAL(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
// Validate that folder is writeable from linux
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt && sync"}, 0);
|
|
VERIFY_ARE_EQUAL(ReadFileContent(testFolder / "file.txt"), L"content");
|
|
|
|
VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
|
|
ExpectMount(session.get(), "/win-path", {});
|
|
}
|
|
|
|
// Validate read-only mount.
|
|
{
|
|
VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
|
|
ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
|
|
|
|
// Validate that folder is not writeable from linux
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
|
|
|
|
VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
|
|
ExpectMount(session.get(), "/win-path", {});
|
|
}
|
|
|
|
// Validate that a read-only share cannot be made writeable via mount -o remount,rw.
|
|
{
|
|
VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
|
|
ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
|
|
|
|
// Attempt an in-place remount to read-write from the guest.
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "mount -o remount,rw /win-path"}, 0);
|
|
|
|
// Verify the folder is still not writeable.
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
|
|
|
|
VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
|
|
ExpectMount(session.get(), "/win-path", {});
|
|
}
|
|
|
|
// Validate that the device host enforces read-only even if the guest tries to bypass mount options.
|
|
if (enableVirtioFs)
|
|
{
|
|
VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
|
|
ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
|
|
|
|
// Capture the mount source and type, unmount, then remount without read-only.
|
|
ExpectCommandResult(
|
|
session.get(),
|
|
{"/bin/sh",
|
|
"-c",
|
|
"src=$(findmnt -n -o SOURCE /win-path) && "
|
|
"fstype=$(findmnt -n -o FSTYPE /win-path) && "
|
|
"umount /win-path && "
|
|
"mount -t $fstype $src /win-path"},
|
|
0);
|
|
|
|
// Verify the folder is still not writeable.
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
|
|
|
|
VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
|
|
ExpectMount(session.get(), "/win-path", {});
|
|
}
|
|
|
|
// Validate various error paths
|
|
{
|
|
VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"relative-path", "/win-path", true), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(session->MountWindowsFolder(L"C:\\does-not-exist", "/win-path", true), HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND));
|
|
VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/not-mounted"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
VERIFY_ARE_EQUAL(session->UnmountWindowsFolder("/proc"), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
// Validate that folders that are manually unmounted from the guest are handled properly
|
|
VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
|
|
ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
|
|
|
|
ExpectCommandResult(session.get(), {"/usr/bin/umount", "/win-path"}, 0);
|
|
VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(WindowsMounts)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
ValidateWindowsMounts(false);
|
|
}
|
|
|
|
TEST_METHOD(WindowsMountsVirtioFs)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
ValidateWindowsMounts(true);
|
|
}
|
|
|
|
// This test case validates that no file descriptors are leaked to user processes.
|
|
TEST_METHOD(Fd)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto result = ExpectCommandResult(
|
|
m_defaultSession.get(), {"/bin/sh", "-c", "echo /proc/self/fd/* && (readlink -v /proc/self/fd/* || true)"}, 0);
|
|
|
|
// Note: fd/0 is opened by readlink to read the actual content of /proc/self/fd.
|
|
if (!PathMatchSpecA(result.Output[1].c_str(), "/proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2\nsocket:*\nsocket:*"))
|
|
{
|
|
LogInfo("Found additional fds: %hs", result.Output[1].c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(GPU)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Validate that trying to mount the shares without GPU support enabled fails.
|
|
{
|
|
auto settings = GetDefaultSessionSettings(L"gpu-test-disabled");
|
|
WI_ClearFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
|
|
|
|
auto createNewSession = WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
// Validate that the GPU device is not available.
|
|
ExpectMount(session.get(), "/usr/lib/wsl/drivers", {});
|
|
ExpectMount(session.get(), "/usr/lib/wsl/lib", {});
|
|
}
|
|
|
|
// Validate that the GPU device is available when enabled.
|
|
{
|
|
auto settings = GetDefaultSessionSettings(L"gpu-test");
|
|
WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
|
|
|
|
auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsGPU);
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
// Validate that the GPU device is available.
|
|
ExpectCommandResult(session.get(), {"/bin/sh", "-c", "test -c /dev/dxg"}, 0);
|
|
|
|
ExpectMount(
|
|
session.get(),
|
|
"/usr/lib/wsl/drivers",
|
|
"/usr/lib/wsl/drivers*9p*relatime,aname=*,cache=5,access=client,msize=65536,trans=fd,rfd=*,wfd=*");
|
|
|
|
ExpectMount(
|
|
session.get(),
|
|
"/usr/lib/wsl/lib",
|
|
"/usr/lib/wsl/lib none*overlay ro,relatime,lowerdir=/usr/lib/wsl/lib/packaged*");
|
|
|
|
// Validate that the mount points are not writeable.
|
|
VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/drivers/test"}).Code, 1L);
|
|
VERIFY_ARE_EQUAL(RunCommand(session.get(), {"/usr/bin/touch", "/usr/lib/wsl/lib/test"}).Code, 1L);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(Modules)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Sanity check.
|
|
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 1);
|
|
|
|
// Validate that modules can be loaded.
|
|
ExpectCommandResult(m_defaultSession.get(), {"/usr/sbin/modprobe", "xsk_diag"}, 0);
|
|
|
|
// Validate that xsk_diag is now loaded.
|
|
ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "lsmod | grep ^xsk_diag"}, 0);
|
|
}
|
|
|
|
TEST_METHOD(CreateRootNamespaceProcess)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Simple case
|
|
{
|
|
auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo OK"}, 0);
|
|
VERIFY_ARE_EQUAL(result.Output[1], "OK\n");
|
|
VERIFY_ARE_EQUAL(result.Output[2], "");
|
|
}
|
|
|
|
// Stdout + stderr
|
|
{
|
|
|
|
auto result = ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "echo stdout && (echo stderr 1>& 2)"}, 0);
|
|
VERIFY_ARE_EQUAL(result.Output[1], "stdout\n");
|
|
VERIFY_ARE_EQUAL(result.Output[2], "stderr\n");
|
|
}
|
|
|
|
// Write a large stdin buffer and expect it back on stdout.
|
|
{
|
|
std::vector<char> largeBuffer;
|
|
std::string pattern = "ExpectedBufferContent";
|
|
|
|
for (size_t i = 0; i < 1024 * 1024; i++)
|
|
{
|
|
largeBuffer.insert(largeBuffer.end(), pattern.begin(), pattern.end());
|
|
}
|
|
|
|
WSLCProcessLauncher launcher("/bin/sh", {"/bin/sh", "-c", "cat && (echo completed 1>& 2)"}, {}, WSLCProcessFlagsStdin);
|
|
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
|
|
std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(process.GetStdHandle(0), largeBuffer));
|
|
std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
|
|
extraHandles.emplace_back(std::move(writeStdin));
|
|
|
|
auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
|
|
|
|
VERIFY_IS_TRUE(std::equal(largeBuffer.begin(), largeBuffer.end(), result.Output[1].begin(), result.Output[1].end()));
|
|
VERIFY_ARE_EQUAL(result.Output[2], "completed\n");
|
|
|
|
// Validate that a null out handle is rejected.
|
|
|
|
VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
|
|
}
|
|
|
|
// Create a stuck process and kill it.
|
|
{
|
|
WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
|
|
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
|
|
// Try to send invalid signal to the process
|
|
VERIFY_ARE_EQUAL(process.Get().Signal(9999), E_FAIL);
|
|
|
|
// Send SIGKILL(9) to the process.
|
|
VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGKILL));
|
|
|
|
auto result = process.WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(result.Code, WSLCSignalSIGKILL + 128);
|
|
VERIFY_ARE_EQUAL(result.Output[1], "");
|
|
VERIFY_ARE_EQUAL(result.Output[2], "");
|
|
|
|
// Validate that process can't be signalled after it exited.
|
|
VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
}
|
|
|
|
// Validate that errno is correctly propagated
|
|
{
|
|
WSLCProcessLauncher launcher("doesnotexist", {});
|
|
|
|
auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_FAIL);
|
|
VERIFY_ARE_EQUAL(error, 2); // ENOENT
|
|
VERIFY_IS_FALSE(process.has_value());
|
|
}
|
|
|
|
{
|
|
WSLCProcessLauncher launcher("/", {});
|
|
|
|
auto [hresult, process, error] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_FAIL);
|
|
VERIFY_ARE_EQUAL(error, 13); // EACCESS
|
|
VERIFY_IS_FALSE(process.has_value());
|
|
}
|
|
|
|
{
|
|
WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
|
|
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
auto stdoutHandle = process.GetStdHandle(1);
|
|
|
|
COMOutputHandle dummyHandle;
|
|
// Verify that the same handle can only be acquired once.
|
|
VERIFY_ARE_EQUAL(process.Get().GetStdHandle(WSLCFDStdout, &dummyHandle), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Verify that trying to acquire a std handle that doesn't exist fails as expected.
|
|
VERIFY_ARE_EQUAL(process.Get().GetStdHandle(static_cast<WSLCFD>(3), &dummyHandle), E_INVALIDARG);
|
|
|
|
// Validate that the process object correctly handle requests after the VM has terminated.
|
|
ResetTestSession();
|
|
VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
|
|
}
|
|
|
|
// Validate that empty arguments are correctly handled.
|
|
{
|
|
WSLCProcessLauncher launcher({"/usr/bin/echo"}, {"/usr/bin/echo", "foo", "", "bar"});
|
|
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
ValidateProcessOutput(process, {{1, "foo bar\n"}}); // expect two spaces for the empty argument.
|
|
}
|
|
|
|
// Validate error paths
|
|
{
|
|
WSLCProcessLauncher launcher("/bin/bash", {"/bin/bash"});
|
|
launcher.SetUser("nobody"); // Custom users are not supported for root namespace processes.
|
|
|
|
auto [hresult, error, process] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(CrashDumpCollection)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
int processId = 0;
|
|
|
|
// Cache the existing crash dumps so we can check that a new one is created.
|
|
auto crashDumpsDir = std::filesystem::temp_directory_path() / "wslc-crashes";
|
|
std::set<std::filesystem::path> existingDumps;
|
|
|
|
if (std::filesystem::exists(crashDumpsDir))
|
|
{
|
|
existingDumps = {std::filesystem::directory_iterator(crashDumpsDir), std::filesystem::directory_iterator{}};
|
|
}
|
|
|
|
// Create a stuck process and crash it.
|
|
{
|
|
WSLCProcessLauncher launcher("/bin/cat", {"/bin/cat"}, {}, WSLCProcessFlagsStdin);
|
|
|
|
auto process = launcher.Launch(*m_defaultSession);
|
|
|
|
// Get the process id. This is need to identify the crash dump file.
|
|
VERIFY_SUCCEEDED(process.Get().GetPid(&processId));
|
|
|
|
// Send SIGSEV(11) to crash the process.
|
|
VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGSEGV));
|
|
|
|
auto result = process.WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGSEGV);
|
|
VERIFY_ARE_EQUAL(result.Output[1], "");
|
|
VERIFY_ARE_EQUAL(result.Output[2], "");
|
|
|
|
VERIFY_ARE_EQUAL(process.Get().Signal(WSLCSignalSIGKILL), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
}
|
|
|
|
// Dumps files are named with the format: wsl-crash-<sessionId>-<pid>-<processname>-<code>.dmp
|
|
// Check if a new file was added in crashDumpsDir matching the pattern and not in existingDumps.
|
|
std::string expectedPattern = std::format("wsl-crash-*-{}-_usr_bin_cat-11.dmp", processId);
|
|
|
|
auto dumpFile = wsl::shared::retry::RetryWithTimeout<std::filesystem::path>(
|
|
[crashDumpsDir, expectedPattern, existingDumps]() {
|
|
for (const auto& entry : std::filesystem::directory_iterator(crashDumpsDir))
|
|
{
|
|
const auto& filePath = entry.path();
|
|
if (existingDumps.find(filePath) == existingDumps.end() &&
|
|
PathMatchSpecA(filePath.filename().string().c_str(), expectedPattern.c_str()))
|
|
{
|
|
return filePath;
|
|
}
|
|
}
|
|
|
|
throw wil::ResultException(HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
},
|
|
std::chrono::milliseconds{100},
|
|
std::chrono::seconds{10});
|
|
|
|
// Ensure that the dump file is cleaned up after test completion.
|
|
auto cleanup = wil::scope_exit([&] {
|
|
if (std::filesystem::exists(dumpFile))
|
|
{
|
|
std::filesystem::remove(dumpFile);
|
|
}
|
|
});
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(dumpFile));
|
|
VERIFY_IS_TRUE(std::filesystem::file_size(dumpFile) > 0);
|
|
}
|
|
|
|
TEST_METHOD(VhdFormatting)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
constexpr auto formatedVhd = L"test-format-vhd.vhdx";
|
|
|
|
// TODO: Replace this by a proper SDK method once it exists
|
|
auto tokenInfo = wil::get_token_information<TOKEN_USER>();
|
|
wsl::core::filesystem::CreateVhd(formatedVhd, 100 * 1024 * 1024, tokenInfo->User.Sid, false, false);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(formatedVhd)); });
|
|
|
|
// Format the disk.
|
|
auto absoluteVhdPath = std::filesystem::absolute(formatedVhd).wstring();
|
|
VERIFY_SUCCEEDED(m_defaultSession->FormatVirtualDisk(absoluteVhdPath.c_str()));
|
|
|
|
// Validate error paths.
|
|
VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"DoesNotExist.vhdx"), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(m_defaultSession->FormatVirtualDisk(L"C:\\DoesNotExist.vhdx"), HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
|
|
}
|
|
|
|
TEST_METHOD(NamedVolumesTest)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
const std::string volumeName = "wslc-test-named-volume";
|
|
const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
|
|
|
|
// Best-effort cleanup in case of leftovers from a previous failed run.
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
std::error_code ec;
|
|
std::filesystem::remove(volumeVhdPath, ec);
|
|
});
|
|
|
|
WSLCVolumeOptions volumeOptions{};
|
|
volumeOptions.Name = volumeName.c_str();
|
|
volumeOptions.Type = "vhd";
|
|
volumeOptions.Options = R"({"SizeBytes":"1073741824"})";
|
|
|
|
// Create volume and validate duplicate volume name handling.
|
|
VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions));
|
|
VERIFY_ARE_EQUAL(m_defaultSession->CreateVolume(&volumeOptions), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
// Verify volume VHD exists and mount point is present in the VM.
|
|
VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
|
|
ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::optional<std::string>{"*ext4*"});
|
|
|
|
// Verify the same named volume can be mounted more than once with different container paths.
|
|
{
|
|
WSLCContainerLauncher duplicateNamedVolumes(
|
|
"debian:latest", "named-volume-dup", {"/bin/sh", "-c", "echo duplicated >/data-a/dup.txt ; cat /data-b/dup.txt"});
|
|
duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-a", false);
|
|
duplicateNamedVolumes.AddNamedVolume(volumeName, "/data-b", true);
|
|
|
|
auto duplicateNamedVolumesContainer = duplicateNamedVolumes.Launch(*m_defaultSession);
|
|
auto duplicateNamedVolumesProcess = duplicateNamedVolumesContainer.GetInitProcess();
|
|
ValidateProcessOutput(duplicateNamedVolumesProcess, {{1, "duplicated\n"}});
|
|
}
|
|
|
|
// Verify CreateContainer with named volume mounts the volume into the container.
|
|
{
|
|
WSLCContainerLauncher writer(
|
|
"debian:latest", "named-volume-writer", {"/bin/sh", "-c", "echo wslc-named-volume >/data/marker.txt"});
|
|
writer.AddNamedVolume(volumeName, "/data", false);
|
|
|
|
auto writerContainer = writer.Launch(*m_defaultSession);
|
|
auto writerProcess = writerContainer.GetInitProcess();
|
|
ValidateProcessOutput(writerProcess, {});
|
|
|
|
WSLCContainerLauncher reader("debian:latest", "named-volume-reader", {"/bin/sh", "-c", "cat /data/marker.txt"});
|
|
reader.AddNamedVolume(volumeName, "/data", true);
|
|
|
|
auto readerContainer = reader.Launch(*m_defaultSession);
|
|
auto readerProcess = readerContainer.GetInitProcess();
|
|
ValidateProcessOutput(readerProcess, {{1, "wslc-named-volume\n"}});
|
|
}
|
|
|
|
// Verify we cannot delete a named volume while a container references it.
|
|
WSLCContainerLauncher holder("debian:latest", "named-volume-holder", {"sleep", "99999"});
|
|
holder.AddNamedVolume(volumeName, "/data", false);
|
|
|
|
auto [holderCreateResult, holderContainerResult] = holder.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(holderCreateResult);
|
|
VERIFY_IS_TRUE(holderContainerResult.has_value());
|
|
|
|
auto holderContainer = std::move(holderContainerResult.value());
|
|
holderContainer.SetDeleteOnClose(false);
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION));
|
|
|
|
// Verify that after deleting the container, the volume can be deleted.
|
|
VERIFY_SUCCEEDED(holderContainer.Get().Delete(WSLCDeleteFlagsNone));
|
|
VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
|
|
ExpectMount(m_defaultSession.get(), std::format("/mnt/wslc-volumes/{}", volumeName), std::nullopt);
|
|
VERIFY_IS_FALSE(std::filesystem::exists(volumeVhdPath));
|
|
|
|
cleanup.release();
|
|
}
|
|
|
|
TEST_METHOD(NamedVolumesSessionRecovery)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
const std::string volumeName = "wslc-test-named-volume";
|
|
const std::string containerName = "wslc-test-container";
|
|
const std::filesystem::path volumeVhdPath = m_storagePath / "volumes" / (volumeName + ".vhdx");
|
|
|
|
// Best-effort cleanup in case prior failed runs left artifacts behind.
|
|
RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
{
|
|
std::error_code ec;
|
|
std::filesystem::remove(volumeVhdPath, ec);
|
|
}
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "rm", "-f", containerName});
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove(volumeVhdPath, ec);
|
|
});
|
|
|
|
WSLCVolumeOptions volumeOptions{};
|
|
volumeOptions.Name = volumeName.c_str();
|
|
volumeOptions.Type = "vhd";
|
|
volumeOptions.Options = R"({"SizeBytes":"1073741824"})";
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions));
|
|
VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
|
|
|
|
// Create a container that uses the named volume and writes a marker.
|
|
{
|
|
WSLCContainerLauncher writer(
|
|
"debian:latest", containerName, {"/bin/sh", "-c", "echo named-volume-recovery >/data/marker.txt"});
|
|
writer.AddNamedVolume(volumeName, "/data", false);
|
|
|
|
auto writerContainer = writer.Launch(*m_defaultSession);
|
|
writerContainer.SetDeleteOnClose(false);
|
|
|
|
auto writerProcess = writerContainer.GetInitProcess();
|
|
ValidateProcessOutput(writerProcess, {});
|
|
}
|
|
|
|
// Restart the session and verify the container is recovered.
|
|
ResetTestSession();
|
|
|
|
auto recoveredContainer = OpenContainer(m_defaultSession.get(), containerName);
|
|
recoveredContainer.SetDeleteOnClose(false);
|
|
|
|
// Verify the named volume still contains the marker after restart.
|
|
{
|
|
WSLCContainerLauncher reader("debian:latest", "wslc-test-container-reader", {"/bin/sh", "-c", "cat /data/marker.txt"});
|
|
reader.AddNamedVolume(volumeName, "/data", true);
|
|
|
|
auto readerContainer = reader.Launch(*m_defaultSession);
|
|
auto readerProcess = readerContainer.GetInitProcess();
|
|
ValidateProcessOutput(readerProcess, {{1, "named-volume-recovery\n"}});
|
|
}
|
|
|
|
// Stop the session, delete the backing VHD, and restart.
|
|
{
|
|
auto restartSession = ResetTestSession();
|
|
|
|
VERIFY_IS_TRUE(std::filesystem::exists(volumeVhdPath));
|
|
|
|
std::error_code error;
|
|
VERIFY_IS_TRUE(std::filesystem::remove(volumeVhdPath, error));
|
|
VERIFY_ARE_EQUAL(error, std::error_code{});
|
|
}
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(containerName.c_str(), ¬Found), E_UNEXPECTED);
|
|
|
|
// Deleting the named volume should fail since the volume was not recovered.
|
|
VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), WSLC_E_VOLUME_NOT_FOUND);
|
|
}
|
|
|
|
TEST_METHOD(NamedVolumeOptionsParseTest)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
const std::string volumeName = "wslc-volume-name";
|
|
|
|
auto validateInvalidOptionsFailure =
|
|
[&](const std::string& options, HRESULT expectedResult, const std::optional<std::wstring>& expectedMessage = std::nullopt) {
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
|
|
|
|
auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
|
|
|
|
WSLCVolumeOptions volumeOptions{};
|
|
volumeOptions.Name = volumeName.c_str();
|
|
volumeOptions.Type = "vhd";
|
|
volumeOptions.Options = options.c_str();
|
|
|
|
const auto result = m_defaultSession->CreateVolume(&volumeOptions);
|
|
|
|
if (result != expectedResult)
|
|
{
|
|
LogInfo(
|
|
"CreateVolume mismatch options='%hs' result=0x%08x expected=0x%08x",
|
|
options.c_str(),
|
|
static_cast<unsigned int>(result),
|
|
static_cast<unsigned int>(expectedResult));
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(result, expectedResult);
|
|
if (expectedMessage.has_value())
|
|
{
|
|
ValidateCOMErrorMessage(expectedMessage);
|
|
}
|
|
};
|
|
|
|
validateInvalidOptionsFailure("not-json", WSL_E_INVALID_JSON);
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"abc"})", WSL_E_INVALID_JSON);
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"+-1"})", WSL_E_INVALID_JSON);
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"123abc"})", WSL_E_INVALID_JSON);
|
|
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"18446744073709551616"})", E_INVALIDARG);
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"-1"})", E_INVALIDARG);
|
|
validateInvalidOptionsFailure(R"({"SizeBytes":"0"})", E_INVALIDARG, L"Invalid size: 0");
|
|
validateInvalidOptionsFailure("{}", E_INVALIDARG, L"Invalid volume options: '{}'");
|
|
validateInvalidOptionsFailure("", WSL_E_INVALID_JSON);
|
|
}
|
|
|
|
TEST_METHOD(ListAndInspectNamedVolumesTest)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
const std::string volumeName1 = "wsla-test-vol1";
|
|
const std::string volumeName2 = "wsla-test-vol2";
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName1.c_str()));
|
|
LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName2.c_str()));
|
|
});
|
|
|
|
// Verify empty list is returned when no volumes exist.
|
|
wil::unique_cotaskmem_array_ptr<WSLCVolumeInformation> volumes;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(0u, volumes.size());
|
|
|
|
// Create first volume and verify list returns one entry.
|
|
WSLCVolumeOptions volumeOptions{};
|
|
volumeOptions.Name = volumeName1.c_str();
|
|
volumeOptions.Type = "vhd";
|
|
volumeOptions.Options = R"({"SizeBytes":"1073741824"})";
|
|
VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions));
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(1u, volumes.size());
|
|
VERIFY_ARE_EQUAL(std::string(volumes[0].Name), volumeName1);
|
|
VERIFY_ARE_EQUAL(std::string(volumes[0].Type), std::string("vhd"));
|
|
|
|
// Create second volume and verify list returns two entries.
|
|
volumeOptions.Name = volumeName2.c_str();
|
|
VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions));
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(2u, volumes.size());
|
|
|
|
std::set<std::string> names;
|
|
for (const auto& v : volumes)
|
|
{
|
|
names.insert(v.Name);
|
|
VERIFY_ARE_EQUAL(std::string(v.Type), std::string("vhd"));
|
|
}
|
|
|
|
VERIFY_IS_TRUE(names.contains(volumeName1));
|
|
VERIFY_IS_TRUE(names.contains(volumeName2));
|
|
|
|
// Verify InspectVolume returns correct details.
|
|
wil::unique_cotaskmem_ansistring output;
|
|
VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(volumeName1.c_str(), &output));
|
|
VERIFY_IS_NOT_NULL(output.get());
|
|
|
|
auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(output.get());
|
|
VERIFY_ARE_EQUAL(inspect.Name, volumeName1);
|
|
VERIFY_ARE_EQUAL(inspect.Type, std::string("vhd"));
|
|
VERIFY_IS_TRUE(inspect.VhdVolume.has_value());
|
|
VERIFY_ARE_EQUAL(inspect.VhdVolume->SizeBytes, 1073741824ull);
|
|
VERIFY_IS_FALSE(inspect.VhdVolume->HostPath.empty());
|
|
|
|
// Verify InspectVolume fails for a non-existent volume.
|
|
output.reset();
|
|
VERIFY_ARE_EQUAL(m_defaultSession->InspectVolume("does-not-exist", &output), WSLC_E_VOLUME_NOT_FOUND);
|
|
|
|
// Delete first volume and verify list returns one entry.
|
|
VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName1.c_str()));
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListVolumes(volumes.addressof(), volumes.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(1u, volumes.size());
|
|
VERIFY_ARE_EQUAL(std::string(volumes[0].Name), volumeName2);
|
|
}
|
|
|
|
TEST_METHOD(CreateContainer)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Test a simple container start.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-simple", {"echo", "OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
// Validate that GetInitProcess fails with the process argument is null.
|
|
VERIFY_ARE_EQUAL(HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER), container.Get().GetInitProcess(nullptr));
|
|
}
|
|
|
|
// Validate that env is correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-env", {"/bin/sh", "-c", "echo $testenv"}, {{"testenv=testvalue"}});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "testvalue\n"}});
|
|
}
|
|
|
|
// Validate that exit codes are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-exit-code", {"/bin/sh", "-c", "exit 12"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {}, 12);
|
|
}
|
|
|
|
// Validate that stdin is correctly wired
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-default-entrypoint", {"/bin/cat"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost, WSLCProcessFlagsStdin);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
auto process = container.GetInitProcess();
|
|
auto input = process.GetStdHandle(0);
|
|
|
|
std::string shellInput = "foo";
|
|
std::vector<char> inputBuffer{shellInput.begin(), shellInput.end()};
|
|
|
|
std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(std::move(input), inputBuffer));
|
|
|
|
std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
|
|
extraHandles.emplace_back(std::move(writeStdin));
|
|
|
|
auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
|
|
|
|
VERIFY_ARE_EQUAL(result.Output[2], "");
|
|
VERIFY_ARE_EQUAL(result.Output[1], "foo");
|
|
}
|
|
|
|
// Validate that stdin behaves correctly if closed without any input.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stdin", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
process.GetStdHandle(0); // Close stdin;
|
|
|
|
ValidateProcessOutput(process, {{1, ""}});
|
|
}
|
|
|
|
// Validate that the default stop signal is respected.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsInit);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, 60));
|
|
|
|
// Validate that the init process exited with the expected signal.
|
|
VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGHUP + 128);
|
|
}
|
|
|
|
// Validate that the default stop signal can be overriden.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-2", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetDefaultStopSignal(WSLCSignalSIGHUP);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsInit);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 60));
|
|
|
|
// Validate that the init process exited with the expected signal.
|
|
VERIFY_ARE_EQUAL(process.Wait(), WSLCSignalSIGKILL + 128);
|
|
}
|
|
|
|
// Validate that entrypoint is respected.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-entrypoint", {"OK"});
|
|
launcher.SetEntrypoint({"/bin/echo", "-n"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "OK"}});
|
|
}
|
|
|
|
// Validate that the working directory is correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-signal-1", {"pwd"});
|
|
launcher.SetWorkingDirectory("/tmp");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "/tmp\n"}});
|
|
}
|
|
|
|
// Validate that the current directory is created if it doesn't exist.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-bad-cwd", {"pwd"});
|
|
launcher.SetWorkingDirectory("/new-dir");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "/new-dir\n"}});
|
|
}
|
|
|
|
// Validate that hostname and domainanme are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-hostname", {"/bin/sh", "-c", "echo $(hostname).$(domainname)"});
|
|
|
|
launcher.SetHostname("my-host-name");
|
|
launcher.SetDomainname("my-domain-name");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "my-host-name.my-domain-name\n"}});
|
|
}
|
|
|
|
// Validate that containers without DNS configuration use default DNS.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-no-dns", {"/bin/grep", "-iF", "nameserver", "/etc/resolv.conf"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that custom DNS servers are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-dns-custom", {"/bin/grep", "-iF", "nameserver 1.2.3.4", "/etc/resolv.conf"});
|
|
|
|
launcher.SetDnsServers({"1.2.3.4"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that custom DNS search domains are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-dns-search", {"/bin/grep", "-iF", "test.local", "/etc/resolv.conf"});
|
|
|
|
launcher.SetDnsSearchDomains({"test.local"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that custom DNS options are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-dns-options", {"/bin/grep", "-iF", "timeout:1", "/etc/resolv.conf"});
|
|
|
|
launcher.SetDnsOptions({"timeout:1"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that multiple DNS options are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-dns-options-multiple", {"/bin/grep", "-iF", "timeout:2", "/etc/resolv.conf"});
|
|
|
|
launcher.SetDnsOptions({"timeout:1", "timeout:2"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that the username is correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-username", {"whoami"});
|
|
|
|
launcher.SetUser("nobody");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "nobody\n"}});
|
|
}
|
|
|
|
// Validate that the group is correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-group", {"groups"});
|
|
|
|
launcher.SetUser("nobody:www-data");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "www-data\n"}});
|
|
}
|
|
|
|
// Validate that the container behaves correctly if the caller keeps a reference to an init process during termination.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-init-ref", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto containerId = container.Id();
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
wil::com_ptr<IWSLCContainer> openedContainer;
|
|
VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerId.c_str(), &openedContainer));
|
|
VERIFY_SUCCEEDED(openedContainer->Delete(WSLCDeleteFlagsNone));
|
|
});
|
|
|
|
auto process = container.GetInitProcess();
|
|
|
|
VERIFY_ARE_EQUAL(process.State(), WslcProcessStateRunning);
|
|
|
|
// Terminate the session.
|
|
ResetTestSession();
|
|
|
|
WSLCProcessState processState{};
|
|
int exitCode{};
|
|
VERIFY_ARE_EQUAL(process.Get().GetState(&processState, &exitCode), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
|
|
|
|
WSLCContainerState state{};
|
|
VERIFY_ARE_EQUAL(container.Get().GetState(&state), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
|
|
}
|
|
|
|
// Validate error handling when the username / group doesn't exist
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-no-missing-user", {"groups"});
|
|
|
|
launcher.SetUser("does-not-exist");
|
|
|
|
auto [result, _] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(result, E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"unable to find user does-not-exist: no matching entries in passwd file");
|
|
}
|
|
|
|
// Validate that empty arguments are correctly handled.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-empty-args", {"echo", "foo", "", "bar"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "foo bar\n"}}); // Expect two spaces for the empty argument.
|
|
}
|
|
|
|
// Validate that tmpfs mounts are correctly wired.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest",
|
|
"test-tmpfs",
|
|
{"/bin/sh", "-c", "mount | grep 'tmpfs on /mnt/wslc-tmpfs1' && mount | grep 'tmpfs on /mnt/wslc-tmpfs2'"});
|
|
|
|
launcher.AddTmpfs("/mnt/wslc-tmpfs1", "rw,noexec,nosuid,size=65536k");
|
|
launcher.AddTmpfs("/mnt/wslc-tmpfs2", "");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {}, 0);
|
|
}
|
|
|
|
// Validate that relative tmpfs paths are rejected by Docker.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-relative", {"/bin/cat"});
|
|
launcher.AddTmpfs("relative-path", "");
|
|
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"invalid mount path: 'relative-path' mount path must be absolute");
|
|
}
|
|
|
|
// Validate that invalid tmpfs options are rejected by Docker.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-tmpfs-invalid-opts", {"/bin/cat"});
|
|
launcher.AddTmpfs("/mnt/wslc-tmpfs", "invalid_option_xyz");
|
|
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"invalid tmpfs option [\"invalid_option_xyz\"]");
|
|
}
|
|
|
|
// Validate error paths
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", std::string(WSLC_MAX_CONTAINER_NAME_LENGTH + 1, 'a'), {"/bin/cat"});
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher(std::string(WSLC_MAX_IMAGE_NAME_LENGTH + 1, 'a'), "dummy", {"/bin/cat"});
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("invalid-image-name", "dummy", {"/bin/cat"});
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, WSLC_E_IMAGE_NOT_FOUND);
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "dummy", {"/does-not-exist"});
|
|
auto [hresult, container] = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
|
|
|
|
ValidateCOMErrorMessage(
|
|
L"failed to create task for container: failed to create shim task: OCI runtime create failed: runc create "
|
|
L"failed: unable to start container process: error during container init: exec: \"/does-not-exist\": stat "
|
|
L"/does-not-exist: no such file or directory: unknown");
|
|
}
|
|
|
|
// Test null image name
|
|
{
|
|
WSLCContainerOptions options{};
|
|
options.Image = nullptr;
|
|
options.Name = "test-container";
|
|
options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = m_defaultSession->CreateContainer(&options, &container);
|
|
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
|
|
}
|
|
|
|
// Test null container name
|
|
{
|
|
WSLCContainerOptions options{};
|
|
options.Image = "debian:latest";
|
|
options.Name = nullptr;
|
|
options.InitProcessOptions.CommandLine = {.Values = nullptr, .Count = 0};
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
VERIFY_SUCCEEDED(m_defaultSession->CreateContainer(&options, &container));
|
|
VERIFY_SUCCEEDED(container->Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerStartAfterStop)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-start", {"echo", "OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
{
|
|
// Validate that the container can be restarted.
|
|
VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr), S_OK);
|
|
auto restartedProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(restartedProcess, {{1, "OK\n"}});
|
|
}
|
|
|
|
{
|
|
// Validate that the container can be restarted without the attach flag.
|
|
VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), S_OK);
|
|
auto restartedProcess = container.GetInitProcess();
|
|
VERIFY_ARE_EQUAL(restartedProcess.Wait(), 0);
|
|
|
|
COMOutputHandle stdoutLogs{};
|
|
COMOutputHandle stderrLogs{};
|
|
VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutLogs, &stderrLogs, 0, 0, 0));
|
|
|
|
ValidateHandleOutput(stdoutLogs.Get(), "OK\nOK\nOK\n");
|
|
ValidateHandleOutput(stderrLogs.Get(), "");
|
|
}
|
|
}
|
|
|
|
// Validate that containers can be restarted after being explicitly stopped.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-start-2", {"sleep", "99999"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
auto initProcess = container.GetInitProcess();
|
|
initProcess.Get().Signal(WSLCSignalSIGKILL);
|
|
VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
// Validate that deleted containers can't be started.
|
|
VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
|
|
}
|
|
|
|
// Validate restart behavior for a container with the autorm flag set
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-stop-start-3", {"sleep", "99999"});
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
// Validate that deleted containers can't be started.
|
|
VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr), RPC_E_DISCONNECTED);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(OpenContainer)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectOpen = [&](const char* Id, HRESULT expectedResult = S_OK) {
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto result = m_defaultSession->OpenContainer(Id, &container);
|
|
|
|
VERIFY_ARE_EQUAL(result, expectedResult);
|
|
|
|
return container;
|
|
};
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "named-container", {"echo", "OK"});
|
|
auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(result);
|
|
|
|
VERIFY_ARE_EQUAL(container->Id().length(), WSLC_CONTAINER_ID_LENGTH);
|
|
|
|
VERIFY_ARE_EQUAL(container->Name(), "named-container");
|
|
|
|
// Validate that the container can be opened by name.
|
|
expectOpen("named-container");
|
|
|
|
// Validate that the container can be opened by ID.
|
|
expectOpen(container->Id().c_str());
|
|
|
|
// Validate that the container can be opened by a prefix of the ID.
|
|
expectOpen(container->Id().substr(0, 8).c_str());
|
|
expectOpen(container->Id().substr(0, 1).c_str());
|
|
|
|
// Validate that prefix conflicts are correctly handled.
|
|
std::vector<RunningWSLCContainer> createdContainers;
|
|
createdContainers.emplace_back(std::move(container.value()));
|
|
|
|
auto findConflict = [&]() {
|
|
for (auto& e : createdContainers)
|
|
{
|
|
auto firstChar = e.Id()[0];
|
|
|
|
if (std::ranges::count_if(createdContainers, [&](auto& container) { return container.Id()[0] == firstChar; }) > 1)
|
|
{
|
|
return firstChar;
|
|
}
|
|
}
|
|
|
|
return '\0';
|
|
};
|
|
|
|
// Create containers until we get two containers with the same first character in their ID.
|
|
while (true)
|
|
{
|
|
VERIFY_IS_LESS_THAN(createdContainers.size(), 16);
|
|
|
|
auto [result, newContainer] = WSLCContainerLauncher("debian:latest").CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(result);
|
|
|
|
createdContainers.emplace_back(std::move(newContainer.value()));
|
|
char conflictChar = findConflict();
|
|
if (conflictChar == '\0')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
expectOpen(std::string{&conflictChar, 1}.c_str(), WSLC_E_CONTAINER_PREFIX_AMBIGUOUS);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Test error paths
|
|
{
|
|
expectOpen("", E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid name: ''");
|
|
|
|
expectOpen("non-existing-container", HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
expectOpen("/", E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid name: '/'");
|
|
|
|
expectOpen("?foo=bar", E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid name: '?foo=bar'");
|
|
|
|
expectOpen("\n", E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid name: '\n'");
|
|
|
|
expectOpen(" ", E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid name: ' '");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerState)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
|
|
|
|
for (size_t i = 0; i < expectedContainers.size(); i++)
|
|
{
|
|
const auto& [expectedName, expectedImage, expectedState] = expectedContainers[i];
|
|
VERIFY_ARE_EQUAL(expectedName, containers[i].Name);
|
|
VERIFY_ARE_EQUAL(expectedImage, containers[i].Image);
|
|
VERIFY_ARE_EQUAL(expectedState, containers[i].State);
|
|
VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLC_CONTAINER_ID_LENGTH);
|
|
VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
|
|
VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
|
|
}
|
|
};
|
|
|
|
{
|
|
// Validate that the container list is initially empty.
|
|
expectContainerList({});
|
|
|
|
// Start one container and wait for it to exit.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "exited-container", {"echo", "OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
expectContainerList({{"exited-container", "debian:latest", WslcContainerStateExited}});
|
|
}
|
|
|
|
// Create a stuck container.
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-1", {"sleep", "99999"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
// Verify that the container is in running state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateRunning}});
|
|
|
|
// Capture StateChangedAt and CreatedAt while the container is running.
|
|
ULONGLONG runningStateChangedAt{};
|
|
ULONGLONG runningCreatedAt{};
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
|
|
&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 1);
|
|
runningStateChangedAt = containers[0].StateChangedAt;
|
|
runningCreatedAt = containers[0].CreatedAt;
|
|
VERIFY_IS_TRUE(runningStateChangedAt > 0);
|
|
VERIFY_IS_TRUE(runningCreatedAt > 0);
|
|
}
|
|
|
|
// Kill the container init process and expect it to be in exited state.
|
|
auto initProcess = container.GetInitProcess();
|
|
VERIFY_SUCCEEDED(initProcess.Get().Signal(WSLCSignalSIGKILL));
|
|
|
|
// Wait for the process to actually exit.
|
|
wsl::shared::retry::RetryWithTimeout<void>(
|
|
[&]() {
|
|
initProcess.GetExitCode(); // Throw if the process hasn't exited yet.
|
|
},
|
|
std::chrono::milliseconds{100},
|
|
std::chrono::seconds{30});
|
|
|
|
// Expect the container to be in exited state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
expectContainerList({{"test-container-1", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that StateChangedAt was updated after the state transition.
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListContainers(
|
|
&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 1);
|
|
|
|
auto now = static_cast<ULONGLONG>(time(nullptr));
|
|
VERIFY_IS_TRUE(containers[0].StateChangedAt <= now);
|
|
VERIFY_IS_TRUE(containers[0].StateChangedAt >= runningStateChangedAt);
|
|
|
|
// CreatedAt must not change after state transitions.
|
|
VERIFY_ARE_EQUAL(containers[0].CreatedAt, runningCreatedAt);
|
|
}
|
|
|
|
// Open a new reference to the same container.
|
|
wil::com_ptr<IWSLCContainer> sameContainer;
|
|
VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("test-container-1", &sameContainer));
|
|
|
|
// Verify that the state matches.
|
|
WSLCContainerState state{};
|
|
VERIFY_SUCCEEDED(sameContainer->GetState(&state));
|
|
VERIFY_ARE_EQUAL(state, WslcContainerStateExited);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
|
|
// Test StopContainer
|
|
{
|
|
// Create a container
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-2", {"sleep", "99999"});
|
|
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
|
|
// Validate that a created container cannot be stopped.
|
|
VERIFY_ARE_EQUAL(container.Get().Stop(WSLCSignalSIGKILL, 0), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Verify that the container is in running state.
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGTERM, 0));
|
|
|
|
expectContainerList({{"test-container-2", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that the container is in exited state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that deleting a container stopped via Stop() works.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
expectContainerList({});
|
|
}
|
|
|
|
// Validate that Kill() works as expected
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-kill", {"sleep", "99999"}, {});
|
|
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
|
|
// Validate that a created container cannot be killed.
|
|
VERIFY_ARE_EQUAL(container.Get().Kill(WSLCSignalNone), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalNone));
|
|
|
|
// Verify that the container is in exited state.
|
|
expectContainerList({{"test-container-kill", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Validate that killing a non-running container fails (unlike Stop())
|
|
VERIFY_ARE_EQUAL(container.Get().Kill(WSLCSignalNone), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Verify that deleting a container stopped via Kill() works.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
expectContainerList({});
|
|
}
|
|
|
|
// Validate that Kill() works with non-sigkill signals.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-kill-2", {"sleep", "99999"}, {});
|
|
launcher.SetContainerFlags(WSLCContainerFlagsInit);
|
|
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGTERM));
|
|
|
|
VERIFY_ARE_EQUAL(container.GetInitProcess().Wait(120 * 1000), WSLCSignalSIGTERM + 128);
|
|
|
|
// Verify that the container is in exited state.
|
|
expectContainerList({{"test-container-kill-2", "debian:latest", WslcContainerStateExited}});
|
|
}
|
|
|
|
// Verify that trying to open a non existing container fails.
|
|
{
|
|
wil::com_ptr<IWSLCContainer> sameContainer;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("does-not-exist", &sameContainer), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Validate that container names are unique.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-unique-name", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
// Validate that a container with the same name cannot be started
|
|
VERIFY_ARE_EQUAL(
|
|
WSLCContainerLauncher("debian:latest", "test-unique-name", {"echo", "OK"}).LaunchNoThrow(*m_defaultSession).first,
|
|
HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
// Validate that running containers can't be deleted.
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Kill the container.
|
|
auto initProcess = container.GetInitProcess();
|
|
initProcess.Get().Signal(WSLCSignalSIGKILL);
|
|
|
|
// Wait for the process to actually exit.
|
|
wsl::shared::retry::RetryWithTimeout<void>(
|
|
[&]() {
|
|
initProcess.GetExitCode(); // Throw if the process hasn't exited yet.
|
|
},
|
|
std::chrono::milliseconds{100},
|
|
std::chrono::seconds{30});
|
|
|
|
expectContainerList({{"test-unique-name", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that calling Stop() on exited containers is a no-op and state remains as WslcContainerStateExited.
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGTERM, 0));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that stopped containers can be deleted.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
// Verify that stopping a deleted container returns ERROR_INVALID_STATE.
|
|
VERIFY_ARE_EQUAL(container.Get().Stop(WSLCSignalSIGTERM, 0), HRESULT_FROM_WIN32(RPC_E_DISCONNECTED));
|
|
|
|
// Verify that deleted containers can't be deleted again.
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), HRESULT_FROM_WIN32(RPC_E_DISCONNECTED));
|
|
|
|
// Verify that deleted containers don't show up in the container list.
|
|
expectContainerList({});
|
|
|
|
// Verify that the same name can be reused now that the container is deleted.
|
|
WSLCContainerLauncher otherLauncher(
|
|
"debian:latest", "test-unique-name", {"echo", "OK"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
|
|
|
|
auto result = otherLauncher.Launch(*m_defaultSession).GetInitProcess().WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(result.Output[1], "OK\n");
|
|
VERIFY_ARE_EQUAL(result.Code, 0);
|
|
}
|
|
|
|
// Validate that creating and starting a container separately behaves as expected
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-create", {"sleep", "99999"}, {});
|
|
auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(result);
|
|
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateCreated);
|
|
VERIFY_SUCCEEDED(container->Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
|
|
// Verify that Start() can't be called again on a running container.
|
|
VERIFY_ARE_EQUAL(container->Get().Start(WSLCContainerStartFlagsNone, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateRunning);
|
|
|
|
VERIFY_SUCCEEDED(container->Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateExited);
|
|
|
|
VERIFY_SUCCEEDED(container->Get().Delete(WSLCDeleteFlagsNone));
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateDeleted);
|
|
|
|
VERIFY_ARE_EQUAL(container->Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
}
|
|
|
|
// Validate that containers behave correctly if they outlive their session.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-dangling-ref", {"sleep", "99999"}, {});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
// Delete the container to avoid leaving it dangling after test completion.
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
// Terminate the session
|
|
ResetTestSession();
|
|
|
|
// Validate that calling into the container returns RPC_S_SERVER_UNAVAILABLE.
|
|
WSLCContainerState state = WslcContainerStateRunning;
|
|
VERIFY_ARE_EQUAL(container.Get().GetState(&state), HRESULT_FROM_WIN32(RPC_S_SERVER_UNAVAILABLE));
|
|
VERIFY_ARE_EQUAL(state, WslcContainerStateInvalid);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(DeleteContainer)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-delete", {"sleep", "99999"});
|
|
|
|
{
|
|
// Verify that a created container can be deleted.
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
// Verify that a deleted container can't be deleted again.
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), HRESULT_FROM_WIN32(RPC_E_DISCONNECTED));
|
|
}
|
|
|
|
{
|
|
// Verify that a running container can't be deleted by default.
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Verify that a running container can be deleted with the force flag.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsForce));
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsForce), HRESULT_FROM_WIN32(RPC_E_DISCONNECTED));
|
|
|
|
// Validate that invalid flags are rejected.
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(static_cast<WSLCDeleteFlags>(0x2)), E_INVALIDARG);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerNetwork)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLCContainerState>>& expectedContainers) {
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(expectedContainers.size(), containers.size());
|
|
|
|
for (size_t i = 0; i < expectedContainers.size(); i++)
|
|
{
|
|
const auto& [expectedName, expectedImage, expectedState] = expectedContainers[i];
|
|
VERIFY_ARE_EQUAL(expectedName, containers[i].Name);
|
|
VERIFY_ARE_EQUAL(expectedImage, containers[i].Image);
|
|
VERIFY_ARE_EQUAL(expectedState, containers[i].State);
|
|
VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLC_CONTAINER_ID_LENGTH);
|
|
VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
|
|
VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
|
|
}
|
|
};
|
|
|
|
// Verify that containers launch successfully when host and none are used as network modes
|
|
// TODO: Test bridge network container launch when VHD with bridge cni is ready
|
|
// TODO: Add port mapping related tests when port mapping is implemented
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-network", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeHost);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
auto details = container.Inspect();
|
|
VERIFY_ARE_EQUAL(details.HostConfig.NetworkMode, "host");
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGTERM, 0));
|
|
|
|
expectContainerList({{"test-network", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that the container is in exited state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that deleting a container stopped via Stop() works.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
expectContainerList({});
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-network", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
VERIFY_ARE_EQUAL(container.Inspect().HostConfig.NetworkMode, "none");
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGTERM, 0));
|
|
|
|
expectContainerList({{"test-network", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that the container is in exited state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that deleting a container stopped via Stop() works.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
expectContainerList({});
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest",
|
|
"test-network",
|
|
{"sleep", "99999"},
|
|
{},
|
|
(WSLCContainerNetworkType)6 // WSLCContainerNetworkType::WSLCContainerNetworkTypeNone
|
|
);
|
|
|
|
auto retVal = launcher.LaunchNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(retVal.first, E_INVALIDARG);
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-network", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_ARE_EQUAL(container.Inspect().HostConfig.NetworkMode, "bridge");
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGTERM, 0));
|
|
|
|
expectContainerList({{"test-network", "debian:latest", WslcContainerStateExited}});
|
|
|
|
// Verify that the container is in exited state.
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that deleting a container stopped via Stop() works.
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
expectContainerList({});
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerInspect)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Helper to verify port mappings.
|
|
auto expectPorts = [&](const auto& actualPorts, const std::map<std::string, std::set<std::string>>& expectedPorts) {
|
|
VERIFY_ARE_EQUAL(actualPorts.size(), expectedPorts.size());
|
|
|
|
for (const auto& [expectedPort, expectedHostPorts] : expectedPorts)
|
|
{
|
|
auto it = actualPorts.find(expectedPort);
|
|
if (it == actualPorts.end())
|
|
{
|
|
LogError("Expected port key not found: %hs", expectedPort.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
std::set<std::string> actualHostPorts;
|
|
for (const auto& binding : it->second)
|
|
{
|
|
VERIFY_IS_FALSE(binding.HostPort.empty());
|
|
|
|
// WSLC always binds to localhost.
|
|
VERIFY_ARE_EQUAL(binding.HostIp, "127.0.0.1");
|
|
|
|
auto [_, inserted] = actualHostPorts.insert(binding.HostPort);
|
|
if (!inserted)
|
|
{
|
|
LogError("Duplicate host port %hs found for port %hs", binding.HostPort.c_str(), expectedPort.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(actualHostPorts, expectedHostPorts);
|
|
}
|
|
};
|
|
|
|
// Helper to verify mounts.
|
|
auto expectMounts = [&](const auto& actualMounts, const std::vector<std::tuple<std::string, std::string, bool>>& expectedMounts) {
|
|
VERIFY_ARE_EQUAL(actualMounts.size(), expectedMounts.size());
|
|
|
|
for (const auto& [expectedDest, expectedType, expectedReadWrite] : expectedMounts)
|
|
{
|
|
auto it = std::ranges::find_if(actualMounts, [&](const auto& mount) { return mount.Destination == expectedDest; });
|
|
if (it == actualMounts.end())
|
|
{
|
|
LogError("Expected mount destination not found: %hs", expectedDest.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
VERIFY_IS_FALSE(it->Type.empty());
|
|
VERIFY_ARE_EQUAL(it->Type, expectedType);
|
|
|
|
if (expectedType != "tmpfs")
|
|
{
|
|
VERIFY_IS_FALSE(it->Source.empty());
|
|
}
|
|
VERIFY_ARE_EQUAL(it->ReadWrite, expectedReadWrite);
|
|
}
|
|
};
|
|
|
|
// Test a running container with port mappings and volumes.
|
|
{
|
|
auto testFolder = std::filesystem::current_path() / "test-inspect-volume";
|
|
auto testFolderReadOnly = std::filesystem::current_path() / "test-inspect-volume-ro";
|
|
|
|
std::filesystem::create_directories(testFolder);
|
|
std::filesystem::create_directories(testFolderReadOnly);
|
|
|
|
auto cleanup = wil::scope_exit([&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(testFolder, ec);
|
|
std::filesystem::remove_all(testFolderReadOnly, ec);
|
|
});
|
|
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-container-inspect", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeBridged);
|
|
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
launcher.AddPort(1235, 8000, AF_INET);
|
|
launcher.AddPort(1236, 8001, AF_INET);
|
|
launcher.AddVolume(testFolder.wstring(), "/test-volume", false);
|
|
launcher.AddVolume(testFolderReadOnly.wstring(), "/test-volume-ro", true);
|
|
launcher.AddTmpfs("/mnt/wslc-tmpfs-inspect", "");
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto details = container.Inspect();
|
|
|
|
// Verify basic container metadata.
|
|
VERIFY_IS_FALSE(details.Id.empty());
|
|
VERIFY_ARE_EQUAL(details.Name, "test-container-inspect");
|
|
VERIFY_ARE_EQUAL(details.Image, "debian:latest");
|
|
VERIFY_IS_FALSE(details.Created.empty());
|
|
|
|
// Verify container state.
|
|
VERIFY_ARE_EQUAL(details.HostConfig.NetworkMode, "bridge");
|
|
VERIFY_IS_TRUE(details.State.Running);
|
|
VERIFY_ARE_EQUAL(details.State.Status, "running");
|
|
VERIFY_IS_FALSE(details.State.StartedAt.empty());
|
|
|
|
// Verify port mappings match what we configured.
|
|
expectPorts(details.Ports, {{"8000/tcp", {"1234", "1235"}}, {"8001/tcp", {"1236"}}});
|
|
|
|
// Verify mounts match what we configured.
|
|
expectMounts(
|
|
details.Mounts,
|
|
{{"/test-volume", "bind", true}, {"/test-volume-ro", "bind", false}, {"/mnt/wslc-tmpfs-inspect", "tmpfs", true}});
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
|
|
// Test an exited container still returns correct schema shape.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container-inspect-exited", {"echo", "OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
auto details = container.Inspect();
|
|
|
|
// Verify basic container metadata is present.
|
|
VERIFY_IS_FALSE(details.Id.empty());
|
|
VERIFY_ARE_EQUAL(details.Name, "test-container-inspect-exited");
|
|
VERIFY_ARE_EQUAL(details.Image, "debian:latest");
|
|
VERIFY_IS_FALSE(details.Created.empty());
|
|
|
|
// Verify exited state is correct.
|
|
VERIFY_IS_FALSE(details.State.Running);
|
|
VERIFY_ARE_EQUAL(details.State.Status, "exited");
|
|
VERIFY_ARE_EQUAL(details.State.ExitCode, 0);
|
|
VERIFY_IS_FALSE(details.State.StartedAt.empty());
|
|
VERIFY_IS_FALSE(details.State.FinishedAt.empty());
|
|
|
|
// Verify no ports or mounts for this simple container.
|
|
expectPorts(details.Ports, {});
|
|
expectMounts(details.Mounts, {});
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(Exec)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Create a container.
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "test-container-exec", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
// Simple exec case.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"echo", "OK"}).Launch(container.Get());
|
|
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
}
|
|
|
|
// Validate that the working directory is correctly wired.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"pwd"});
|
|
launcher.SetWorkingDirectory("/tmp");
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(process, {{1, "/tmp\n"}});
|
|
}
|
|
|
|
// Validate that the username is correctly wired.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"whoami"});
|
|
launcher.SetUser("nobody");
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(process, {{1, "nobody\n"}});
|
|
}
|
|
|
|
// Validate that the group is correctly wired.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"groups"});
|
|
launcher.SetUser("nobody:www-data");
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(process, {{1, "www-data\n"}});
|
|
}
|
|
|
|
// Validate that stdin is correctly wired.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"/bin/cat"}, {}, WSLCProcessFlagsStdin).Launch(container.Get());
|
|
|
|
std::string shellInput = "foo";
|
|
std::vector<char> inputBuffer{shellInput.begin(), shellInput.end()};
|
|
|
|
std::unique_ptr<OverlappedIOHandle> writeStdin(new WriteHandle(process.GetStdHandle(0), inputBuffer));
|
|
|
|
std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
|
|
extraHandles.emplace_back(std::move(writeStdin));
|
|
|
|
auto result = process.WaitAndCaptureOutput(INFINITE, std::move(extraHandles));
|
|
|
|
VERIFY_ARE_EQUAL(result.Output[2], "");
|
|
VERIFY_ARE_EQUAL(result.Output[1], "foo");
|
|
VERIFY_ARE_EQUAL(result.Code, 0);
|
|
}
|
|
|
|
// Validate that behavior is correct when stdin is closed without any input.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"/bin/cat"}, {}, WSLCProcessFlagsStdin).Launch(container.Get());
|
|
|
|
process.GetStdHandle(0); // Close stdin.
|
|
ValidateProcessOutput(process, {{1, ""}, {2, ""}});
|
|
}
|
|
|
|
// Validate that exit codes are correctly wired.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"/bin/sh", "-c", "exit 12"}, {}).Launch(container.Get());
|
|
ValidateProcessOutput(process, {}, 12);
|
|
}
|
|
|
|
// Validate that environment is correctly wired.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"/bin/sh", "-c", "echo $testenv"}, {{"testenv=testvalue"}}).Launch(container.Get());
|
|
|
|
ValidateProcessOutput(process, {{1, "testvalue\n"}});
|
|
}
|
|
|
|
// Validate that empty arguments are correctly handled.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"echo", "foo", "", "bar"});
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(process, {{1, "foo bar\n"}}); // Expect two spaces for the empty argument.
|
|
}
|
|
|
|
// Validate that launching a non-existing command returns the correct error.
|
|
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"/not-found"});
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(
|
|
process,
|
|
{{1,
|
|
"OCI runtime exec failed: exec failed: unable to start container process: exec: \"/not-found\": stat "
|
|
"/not-found: no such file or directory: unknown\r\n"}},
|
|
126);
|
|
}
|
|
|
|
// Validate that setting invalid current directory returns the correct error.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"/bin/cat"});
|
|
launcher.SetWorkingDirectory("/notfound");
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(
|
|
process,
|
|
{{1,
|
|
"OCI runtime exec failed: exec failed: unable to start container process: chdir to cwd (\"/notfound\") set in "
|
|
"config.json failed: no such file or directory: unknown\r\n"}},
|
|
126);
|
|
}
|
|
|
|
// Validate that invalid usernames are correctly handled.
|
|
{
|
|
WSLCProcessLauncher launcher({}, {"/bin/cat"});
|
|
launcher.SetUser("does-not-exist");
|
|
|
|
auto process = launcher.Launch(container.Get());
|
|
ValidateProcessOutput(process, {{1, "unable to find user does-not-exist: no matching entries in passwd file\r\n"}}, 126);
|
|
}
|
|
|
|
// Validate that an exec'd command returns when the container is stopped.
|
|
{
|
|
auto process = WSLCProcessLauncher({}, {"/bin/cat"}, {}, WSLCProcessFlagsStdin).Launch(container.Get());
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
auto result = process.WaitAndCaptureOutput();
|
|
VERIFY_ARE_EQUAL(result.Code, 128 + WSLCSignalSIGKILL);
|
|
}
|
|
|
|
// Validate that processes can't be launched in stopped containers.
|
|
{
|
|
auto [result, _] = WSLCProcessLauncher({}, {"/bin/cat"}).LaunchNoThrow(container.Get());
|
|
VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ExecContainerDelete)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "test-exec-dtor", {"sleep", "99999"}, {}, WSLCContainerNetworkType::WSLCContainerNetworkTypeNone);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
auto process = WSLCProcessLauncher({}, {"sleep", "99999"}).Launch(container.Get());
|
|
auto exitEvent = process.GetExitEvent();
|
|
|
|
// Destroy the container (Stop + Delete + release COM reference).
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
container.Reset();
|
|
|
|
// The exec process exit event must be signaled within a reasonable timeout.
|
|
VERIFY_IS_TRUE(exitEvent.wait(30 * 1000));
|
|
VERIFY_ARE_EQUAL(process.GetExitCode(), 128 + WSLCSignalSIGKILL);
|
|
}
|
|
|
|
void RunPortMappingsTest(IWSLCSession& session, WSLCContainerNetworkType containerNetworkType)
|
|
{
|
|
LogInfo("Container network type: %d", static_cast<int>(containerNetworkType));
|
|
|
|
auto expectBoundPorts = [&](RunningWSLCContainer& Container, const std::vector<std::string>& expectedBoundPorts) {
|
|
auto ports = Container.Inspect().Ports;
|
|
|
|
std::vector<std::string> boundPorts;
|
|
for (const auto& e : ports)
|
|
{
|
|
boundPorts.emplace_back(e.first);
|
|
}
|
|
|
|
if (!std::ranges::equal(boundPorts, expectedBoundPorts))
|
|
{
|
|
LogError(
|
|
"Port bindings do not match expected values. Expected: [%hs], Actual: [%hs]",
|
|
wsl::shared::string::Join(expectedBoundPorts, ',').c_str(),
|
|
wsl::shared::string::Join(boundPorts, ',').c_str());
|
|
|
|
VERIFY_FAIL();
|
|
}
|
|
};
|
|
|
|
// Test a simple port mapping.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports", {"python3", "-m", "http.server", "--bind", "::"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
launcher.AddPort(1234, 8000, AF_INET6, IPPROTO_TCP, "::1");
|
|
|
|
auto container = launcher.Launch(session);
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
// Wait for the container bind() to be completed.
|
|
WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on");
|
|
|
|
expectBoundPorts(container, {"8000/tcp"});
|
|
|
|
ExpectHttpResponse(L"http://127.0.0.1:1234", 200);
|
|
|
|
ExpectHttpResponse(L"http://[::1]:1234", 200);
|
|
|
|
// Verify that ListContainers returns the port data for a running container.
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
|
|
// Find the container ID for "test-ports"
|
|
std::string testPortsId;
|
|
for (const auto& entry : containers)
|
|
{
|
|
if (std::string(entry.Name) == "test-ports")
|
|
{
|
|
testPortsId = entry.Id;
|
|
break;
|
|
}
|
|
}
|
|
VERIFY_IS_FALSE(testPortsId.empty());
|
|
|
|
// Filter ports for this container
|
|
std::vector<WSLCPortMapping> containerPorts;
|
|
for (const auto& port : ports)
|
|
{
|
|
if (testPortsId == port.Id)
|
|
{
|
|
containerPorts.push_back(port.PortMapping);
|
|
}
|
|
}
|
|
|
|
VERIFY_ARE_EQUAL(2, containerPorts.size());
|
|
VERIFY_ARE_EQUAL(1234, containerPorts[0].HostPort);
|
|
VERIFY_ARE_EQUAL(8000, containerPorts[0].ContainerPort);
|
|
VERIFY_ARE_EQUAL(AF_INET, containerPorts[0].Family);
|
|
VERIFY_ARE_EQUAL(1234, containerPorts[1].HostPort);
|
|
VERIFY_ARE_EQUAL(8000, containerPorts[1].ContainerPort);
|
|
VERIFY_ARE_EQUAL(AF_INET6, containerPorts[1].Family);
|
|
VERIFY_ARE_EQUAL(IPPROTO_TCP, containerPorts[0].Protocol);
|
|
VERIFY_ARE_EQUAL(IPPROTO_TCP, containerPorts[1].Protocol);
|
|
}
|
|
|
|
// Verify that a created (not yet started) container returns no ports.
|
|
{
|
|
WSLCContainerLauncher createdLauncher("debian:latest", "test-ports-created", {"echo", "OK"}, {}, containerNetworkType);
|
|
createdLauncher.AddPort(1235, 8000, AF_INET);
|
|
|
|
auto createdContainer = createdLauncher.Create(session);
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
|
|
std::string createdId = createdContainer.Id();
|
|
for (const auto& port : ports)
|
|
{
|
|
VERIFY_ARE_NOT_EQUAL(createdId, std::string(port.Id));
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(createdContainer.Get().Delete(WSLCDeleteFlagsNone));
|
|
createdContainer.Reset();
|
|
}
|
|
|
|
// Validate that the port cannot be reused while the container is running.
|
|
WSLCContainerLauncher subLauncher(
|
|
"python:3.12-alpine", "test-ports-2", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
|
|
subLauncher.AddPort(1234, 8000, AF_INET);
|
|
|
|
auto [hresult, newContainer] = subLauncher.LaunchNoThrow(session);
|
|
VERIFY_ARE_EQUAL(hresult, HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
// Verify that a stopped container returns no ports.
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
|
|
std::string stoppedId = container.Id();
|
|
for (const auto& port : ports)
|
|
{
|
|
VERIFY_ARE_NOT_EQUAL(stoppedId, std::string(port.Id));
|
|
}
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
container.Reset(); // TODO: Re-think container lifetime management.
|
|
|
|
// Validate that the port can be reused now that the container is stopped.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports-3", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
|
|
auto container = launcher.Launch(session);
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
// Wait for the container bind() to be completed.
|
|
WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on 0.0.0.0 port 8000");
|
|
|
|
expectBoundPorts(container, {"8000/tcp"});
|
|
ExpectHttpResponse(L"http://127.0.0.1:1234", 200);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
container.Reset(); // TODO: Re-think container lifetime management.
|
|
}
|
|
}
|
|
|
|
// Validate that the same host port can't be bound twice in the same Create() call.
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports-fail", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
}
|
|
|
|
auto bindSocket = [](auto port) {
|
|
wil::unique_socket socket(WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, 0));
|
|
sockaddr_in address{};
|
|
address.sin_family = AF_INET;
|
|
address.sin_port = htons(port);
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
VERIFY_ARE_NOT_EQUAL(bind(socket.get(), (sockaddr*)&address, sizeof(address)), SOCKET_ERROR);
|
|
return socket;
|
|
};
|
|
|
|
// Validate that Create() fails if the port is already bound.
|
|
{
|
|
auto boundSocket = bindSocket(1235);
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports-fail", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
|
|
launcher.AddPort(1235, 8000, AF_INET);
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEACCES));
|
|
|
|
// Validate that Create() correctly cleans up bound ports after a port fails to map
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports-fail", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
|
|
launcher.AddPort(1236, 8000, AF_INET); // Should succeed
|
|
launcher.AddPort(1235, 8000, AF_INET); // Should fail.
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(WSAEACCES));
|
|
|
|
// Validate that port 1234 is still available.
|
|
VERIFY_IS_TRUE(!!bindSocket(1236));
|
|
}
|
|
}
|
|
|
|
// Validate error paths
|
|
{
|
|
// Invalid IP address
|
|
{
|
|
WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
|
|
launcher.AddPort(1234, 8000, AF_INET, IPPROTO_TCP, "invalid-ip");
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, E_INVALIDARG);
|
|
ValidateCOMErrorMessage(L"Invalid IP address 'invalid-ip'");
|
|
}
|
|
|
|
// Invalid protocol
|
|
{
|
|
WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
|
|
launcher.AddPort(1234, 8000, AF_INET, 1);
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, E_INVALIDARG);
|
|
}
|
|
|
|
// Invalid address family (launched manually because AddPort() throws on unsupported family).
|
|
{
|
|
WSLCPortMapping port{};
|
|
strcpy_s(port.BindingAddress, "127.0.0.1");
|
|
port.HostPort = 1234;
|
|
port.ContainerPort = 1234;
|
|
port.Protocol = IPPROTO_TCP;
|
|
port.Family = AF_UNIX; // Unsupported
|
|
|
|
WSLCContainerOptions options{};
|
|
options.Image = "python:3.12-alpine";
|
|
options.Ports = &port;
|
|
options.PortsCount = 1;
|
|
options.ContainerNetwork.ContainerNetworkType = containerNetworkType;
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
VERIFY_ARE_EQUAL(session.CreateContainer(&options, &container), E_INVALIDARG);
|
|
}
|
|
|
|
// TODO: Update once UDP is supported.
|
|
{
|
|
WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
|
|
launcher.AddPort(1234, 8000, AF_INET, IPPROTO_UDP);
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
}
|
|
|
|
// TODO: Update once custom binding addresses are supported.
|
|
{
|
|
WSLCContainerLauncher launcher("python:3.12-alpine", {}, {}, {}, containerNetworkType);
|
|
launcher.AddPort(1234, 8000, AF_INET, IPPROTO_TCP, "1.1.1.1");
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(session).first, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
}
|
|
}
|
|
}
|
|
|
|
auto SetupPortMappingsTest(WSLCNetworkingMode networkingMode)
|
|
{
|
|
auto settings = GetDefaultSessionSettings(L"networking-session", true, networkingMode);
|
|
|
|
auto createNewSession = settings.NetworkingMode != m_defaultSessionSettings.NetworkingMode;
|
|
auto restore = createNewSession ? std::optional{ResetTestSession()} : std::nullopt;
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
return std::make_pair(std::move(restore), std::move(session));
|
|
}
|
|
|
|
TEST_METHOD(PortMappingsNat)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeNAT);
|
|
|
|
RunPortMappingsTest(*session, WSLCContainerNetworkTypeBridged);
|
|
RunPortMappingsTest(*session, WSLCContainerNetworkTypeHost);
|
|
}
|
|
|
|
TEST_METHOD(PortMappingsVirtioProxy)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto [restore, session] = SetupPortMappingsTest(WSLCNetworkingModeVirtioProxy);
|
|
|
|
RunPortMappingsTest(*session, WSLCContainerNetworkTypeBridged);
|
|
RunPortMappingsTest(*session, WSLCContainerNetworkTypeHost);
|
|
}
|
|
|
|
TEST_METHOD(PortMappingsNone)
|
|
{
|
|
// Validate that trying to map ports without network fails.
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", "test-ports-fail", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, WSLCContainerNetworkTypeNone);
|
|
|
|
launcher.AddPort(1234, 8000, AF_INET);
|
|
|
|
VERIFY_ARE_EQUAL(launcher.LaunchNoThrow(*m_defaultSession).first, E_INVALIDARG);
|
|
}
|
|
|
|
void ValidateContainerVolumes(bool enableVirtioFs)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto restore = ResetTestSession();
|
|
auto hostFolder = std::filesystem::current_path() / "test-volume";
|
|
auto hostFolderReadOnly = std::filesystem::current_path() / "test-volume-ro";
|
|
|
|
std::filesystem::create_directories(hostFolder);
|
|
std::filesystem::create_directories(hostFolderReadOnly);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(hostFolder, ec);
|
|
std::filesystem::remove_all(hostFolderReadOnly, ec);
|
|
});
|
|
|
|
auto settings = GetDefaultSessionSettings(L"volumes-tests", true);
|
|
WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs, enableVirtioFs);
|
|
|
|
auto session = CreateSession(settings);
|
|
|
|
// Validate both folders exist in the container and that the readonly one cannot be written to.
|
|
std::string containerName = "test-container";
|
|
std::string containerPath = "/volume";
|
|
std::string containerReadOnlyPath = "/volume-ro";
|
|
|
|
// Container init script to validate volumes are mounted correctly.
|
|
const std::string script =
|
|
"set -e; "
|
|
|
|
// Test that volumes are available in the container
|
|
"test -d " +
|
|
containerPath +
|
|
"; "
|
|
"test -d " +
|
|
containerReadOnlyPath +
|
|
"; "
|
|
|
|
// Test that the container cannot write to the read-only volume
|
|
"if touch " +
|
|
containerReadOnlyPath +
|
|
"/.ro-test 2>/dev/null;"
|
|
"then echo 'FAILED'; "
|
|
"else echo 'OK'; "
|
|
"fi ";
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", containerName, {"/bin/sh", "-c", script});
|
|
launcher.AddVolume(hostFolder.wstring(), containerPath, false);
|
|
launcher.AddVolume(hostFolderReadOnly.wstring(), containerReadOnlyPath, true);
|
|
|
|
{
|
|
auto container = launcher.Launch(*session);
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
|
|
// Validate that the volumes are not mounted after container exits.
|
|
ExpectMount(session.get(), std::format("/mnt/wslc/{}/volumes/{}", containerName, 0), {});
|
|
ExpectMount(session.get(), std::format("/mnt/wslc/{}/volumes/{}", containerName, 1), {});
|
|
}
|
|
|
|
TEST_METHOD(ContainerVolume)
|
|
{
|
|
ValidateContainerVolumes(false);
|
|
}
|
|
|
|
TEST_METHOD(ContainerVolumeVirtioFs)
|
|
{
|
|
ValidateContainerVolumes(true);
|
|
}
|
|
|
|
void ValidateContainerVolumeUnmountAllFoldersOnError(bool enableVirtioFs)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto hostFolder = std::filesystem::current_path() / "test-volume";
|
|
auto storage = std::filesystem::current_path() / "storage";
|
|
|
|
std::filesystem::create_directories(hostFolder);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(hostFolder, ec);
|
|
std::filesystem::remove_all(storage, ec);
|
|
});
|
|
|
|
auto settings = GetDefaultSessionSettings(L"unmount-test");
|
|
WI_UpdateFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs, enableVirtioFs);
|
|
|
|
// Reuse the default session if possible.
|
|
auto createNewSession = enableVirtioFs != WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
|
|
auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
|
|
|
|
// Create a container with a simple command.
|
|
WSLCContainerLauncher launcher("debian:latest", "test-container", {"/bin/echo", "OK"});
|
|
launcher.AddVolume(hostFolder.wstring(), "/volume", false);
|
|
|
|
// Add a volume with an invalid (non-existing) host path
|
|
launcher.AddVolume(L"does-not-exist", "/volume-invalid", false);
|
|
|
|
auto [result, container] = launcher.LaunchNoThrow(*session);
|
|
VERIFY_FAILED(result);
|
|
|
|
// Verify that the first volume was mounted before the error occurred, then unmounted after failure.
|
|
ExpectMount(session.get(), "/mnt/wslc/test-container/volumes/0", {});
|
|
}
|
|
|
|
TEST_METHOD(ContainerVolumeUnmountAllFoldersOnError)
|
|
{
|
|
ValidateContainerVolumeUnmountAllFoldersOnError(false);
|
|
}
|
|
|
|
TEST_METHOD(ContainerVolumeUnmountAllFoldersOnErrorVirtioFs)
|
|
{
|
|
ValidateContainerVolumeUnmountAllFoldersOnError(true);
|
|
}
|
|
|
|
TEST_METHOD(LineBasedReader)
|
|
{
|
|
auto runTest = [](bool Crlf, const std::string& Data, const std::vector<std::string>& ExpectedLines) {
|
|
auto [readPipe, writePipe] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
|
|
std::vector<std::string> lines;
|
|
auto onData = [&](const gsl::span<char>& data) { lines.emplace_back(data.data(), data.size()); };
|
|
|
|
wsl::windows::common::relay::MultiHandleWait io;
|
|
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::LineBasedReadHandle>(std::move(readPipe), std::move(onData), Crlf));
|
|
|
|
std::vector<char> buffer{Data.begin(), Data.end()};
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::WriteHandle>(std::move(writePipe), buffer));
|
|
|
|
io.Run({});
|
|
|
|
for (size_t i = 0; i < lines.size(); i++)
|
|
{
|
|
if (i >= ExpectedLines.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs'. Line %zu is missing. Expected: '%hs'",
|
|
EscapeString(Data).c_str(),
|
|
i,
|
|
EscapeString(ExpectedLines[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
else if (ExpectedLines[i] != lines[i])
|
|
{
|
|
LogError(
|
|
"Input: '%hs'. Line %zu does not match expected value. Expected: '%hs', Actual: '%hs'",
|
|
EscapeString(Data).c_str(),
|
|
i,
|
|
EscapeString(ExpectedLines[i]).c_str(),
|
|
EscapeString(lines[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
if (ExpectedLines.size() != lines.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs', Number of lines do not match. Expected: %zu, Actual: %zu",
|
|
EscapeString(Data).c_str(),
|
|
ExpectedLines.size(),
|
|
lines.size());
|
|
VERIFY_FAIL();
|
|
}
|
|
};
|
|
|
|
runTest(false, "foo\nbar", {"foo", "bar"});
|
|
runTest(false, "foo", {"foo"});
|
|
runTest(false, "\n", {});
|
|
runTest(false, "\n\n", {});
|
|
runTest(false, "\n\r\n", {"\r"});
|
|
runTest(false, "\n\nfoo\nbar", {"foo", "bar"});
|
|
runTest(false, "foo\r\nbar", {"foo\r", "bar"});
|
|
runTest(true, "foo\nbar", {"foo\nbar"});
|
|
runTest(true, "foo\r\nbar", {"foo", "bar"});
|
|
runTest(true, "foo\rbar\nbaz", {"foo\rbar\nbaz"});
|
|
runTest(true, "\r", {"\r"});
|
|
}
|
|
|
|
TEST_METHOD(HTTPChunkReader)
|
|
{
|
|
auto runTest = [](const std::string& Data, const std::vector<std::string>& ExpectedChunk) {
|
|
auto [readPipe, writePipe] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
|
|
std::vector<std::string> chunks;
|
|
auto onData = [&](const gsl::span<char>& data) { chunks.emplace_back(data.data(), data.size()); };
|
|
|
|
wsl::windows::common::relay::MultiHandleWait io;
|
|
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::HTTPChunkBasedReadHandle>(std::move(readPipe), std::move(onData)));
|
|
|
|
std::vector<char> buffer{Data.begin(), Data.end()};
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::WriteHandle>(std::move(writePipe), buffer));
|
|
|
|
io.Run({});
|
|
|
|
for (size_t i = 0; i < ExpectedChunk.size(); i++)
|
|
{
|
|
if (i >= chunks.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs': Chunk %zu is missing. Expected: '%hs'",
|
|
EscapeString(Data).c_str(),
|
|
i,
|
|
EscapeString(ExpectedChunk[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
else if (ExpectedChunk[i] != chunks[i])
|
|
{
|
|
LogError(
|
|
|
|
"Input: '%hs': Chunk %zu does not match expected value. Expected: '%hs', Actual: '%hs'",
|
|
EscapeString(Data).c_str(),
|
|
i,
|
|
EscapeString(ExpectedChunk[i]).c_str(),
|
|
EscapeString(chunks[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
if (ExpectedChunk.size() != chunks.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs', Number of chunks do not match. Expected: %zu, Actual: %zu",
|
|
EscapeString(Data).c_str(),
|
|
ExpectedChunk.size(),
|
|
chunks.size());
|
|
VERIFY_FAIL();
|
|
}
|
|
};
|
|
|
|
runTest("3\r\nfoo\r\n3\r\nbar", {"foo", "bar"});
|
|
runTest("3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n", {"foo", "bar"});
|
|
runTest("1\r\na\r\n\r\n", {"a"});
|
|
|
|
runTest("c\r\nlf\nin\r\nchunk\r\n3\r\nEOF", {"lf\nin\r\nchunk", "EOF"});
|
|
runTest("15\r\n\r\nchunkstartingwithlf\r\n3\r\nEOF", {"\r\nchunkstartingwithlf", "EOF"});
|
|
|
|
// Validate that invalid chunk sizes fail
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("Invalid", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("Invalid\r\nInvalid", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("4nolf", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("4\nnocr", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("12\nyeseighteenletters", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("4invalid\nnocr", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("4\rinvalid", {}); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest("4\rinvalid\n", {}); }), E_INVALIDARG);
|
|
}
|
|
|
|
TEST_METHOD(HTTPChunkReaderSplitReads)
|
|
{
|
|
auto runTest = [](const std::vector<std::string>& Data, const std::vector<std::string>& ExpectedChunk) {
|
|
std::vector<std::string> chunks;
|
|
auto onData = [&](const gsl::span<char>& data) { chunks.emplace_back(data.data(), data.size()); };
|
|
|
|
auto reader = std::make_unique<wsl::windows::common::relay::HTTPChunkBasedReadHandle>(
|
|
wsl::windows::common::relay::HandleWrapper{nullptr}, std::move(onData));
|
|
|
|
std::string allData;
|
|
for (const auto& datum : Data)
|
|
{
|
|
size_t currentSize = allData.size();
|
|
allData.append(datum);
|
|
reader->OnRead(gsl::span<char>{&allData[currentSize], datum.size()});
|
|
}
|
|
|
|
// Final 0 byte read
|
|
reader->OnRead(gsl::span<char>{nullptr, static_cast<size_t>(0)});
|
|
|
|
for (size_t i = 0; i < ExpectedChunk.size(); i++)
|
|
{
|
|
if (i >= chunks.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs': Chunk %zu is missing. Expected: '%hs'",
|
|
EscapeString(allData).c_str(),
|
|
i,
|
|
EscapeString(ExpectedChunk[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
else if (ExpectedChunk[i] != chunks[i])
|
|
{
|
|
LogError(
|
|
|
|
"Input: '%hs': Chunk %zu does not match expected value. Expected: '%hs', Actual: '%hs'",
|
|
EscapeString(allData).c_str(),
|
|
i,
|
|
EscapeString(ExpectedChunk[i]).c_str(),
|
|
EscapeString(chunks[i]).c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
if (ExpectedChunk.size() != chunks.size())
|
|
{
|
|
LogError(
|
|
"Input: '%hs', Number of chunks do not match. Expected: %zu, Actual: %zu",
|
|
EscapeString(allData).c_str(),
|
|
ExpectedChunk.size(),
|
|
chunks.size());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
LogInfo("HTTPChunkReaderSplitReads success. Input: %hs", EscapeString(allData).c_str());
|
|
};
|
|
|
|
runTest({"3\r\nfo", "o\r\n3\r\nbar"}, {"foo", "bar"});
|
|
runTest({"1\r\n", "a\r\n\r\n"}, {"a"});
|
|
|
|
runTest({"c\r\nlf\n", "in\r\nchunk\r\n3\r\nEOF"}, {"lf\nin\r\nchunk", "EOF"});
|
|
runTest({"15\r\n\r\nchunkstartingwithlf\r\n", "3\r\nEOF"}, {"\r\nchunkstartingwithlf", "EOF"});
|
|
|
|
runTest({"3", "\r\nfoo\r\n3\r\nbar"}, {"foo", "bar"});
|
|
runTest({"3\r\nfoo\r\n3\r\nbar\r\n0", "\r\n\r\n"}, {"foo", "bar"});
|
|
}
|
|
|
|
TEST_METHOD(WriteHandleContent)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Validate that writing to a pipe works as expected.
|
|
{
|
|
const std::string expectedData = "Pipe-test";
|
|
std::vector<char> writeBuffer{expectedData.begin(), expectedData.end()};
|
|
|
|
auto [readPipe, writePipe] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
|
|
std::string readData;
|
|
wsl::windows::common::relay::MultiHandleWait io;
|
|
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::ReadHandle>(std::move(readPipe), [&](const gsl::span<char>& buffer) {
|
|
if (!buffer.empty())
|
|
{
|
|
readData.append(buffer.data(), buffer.size());
|
|
}
|
|
}));
|
|
|
|
io.AddHandle(std::make_unique<WriteHandle>(std::move(writePipe), writeBuffer));
|
|
|
|
io.Run({});
|
|
|
|
VERIFY_ARE_EQUAL(expectedData, readData);
|
|
}
|
|
|
|
// Validate that writing to files work as expected.
|
|
// Use a large buffer to make sure that overlapped writes correctly handle offsets.
|
|
{
|
|
constexpr size_t fileSize = 50 * 1024 * 1024;
|
|
|
|
std::vector<char> writeBuffer(fileSize);
|
|
for (size_t i = 0; i < fileSize; i++)
|
|
{
|
|
writeBuffer[i] = static_cast<char>(i % 251);
|
|
}
|
|
|
|
auto outputFile = wil::open_or_create_file(L"write-handle-test", GENERIC_WRITE | GENERIC_READ, 0, nullptr);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
outputFile.reset();
|
|
std::filesystem::remove("write-handle-test");
|
|
});
|
|
|
|
wsl::windows::common::relay::MultiHandleWait io;
|
|
io.AddHandle(std::make_unique<WriteHandle>(outputFile.get(), writeBuffer));
|
|
io.Run({});
|
|
|
|
VERIFY_ARE_NOT_EQUAL(SetFilePointer(outputFile.get(), 0, nullptr, FILE_BEGIN), INVALID_SET_FILE_POINTER);
|
|
|
|
LARGE_INTEGER size{};
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(GetFileSizeEx(outputFile.get(), &size));
|
|
VERIFY_ARE_EQUAL(static_cast<long long>(fileSize), size.QuadPart);
|
|
|
|
std::vector<char> readBuffer(fileSize);
|
|
DWORD bytesRead = 0;
|
|
VERIFY_IS_TRUE(ReadFile(outputFile.get(), readBuffer.data(), static_cast<DWORD>(fileSize), &bytesRead, nullptr));
|
|
VERIFY_ARE_EQUAL(static_cast<DWORD>(fileSize), bytesRead);
|
|
VERIFY_IS_TRUE(readBuffer == writeBuffer);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(DockerIORelay)
|
|
{
|
|
using namespace wsl::windows::common::relay;
|
|
|
|
auto runTest = [](const std::vector<char>& Input, const std::string& ExpectedStdout, const std::string& ExpectedStderr) {
|
|
auto [readPipe, writePipe] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
|
|
auto [stdoutRead, stdoutWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
auto [stderrRead, stderrWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false);
|
|
|
|
MultiHandleWait io;
|
|
|
|
std::string readStdout;
|
|
std::string readStderr;
|
|
|
|
io.AddHandle(std::make_unique<DockerIORelayHandle>(
|
|
std::move(readPipe), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw));
|
|
io.AddHandle(std::make_unique<WriteHandle>(std::move(writePipe), Input));
|
|
|
|
io.AddHandle(std::make_unique<ReadHandle>(
|
|
std::move(stdoutRead), [&](const auto& buffer) { readStdout.append(buffer.data(), buffer.size()); }));
|
|
|
|
io.AddHandle(std::make_unique<ReadHandle>(
|
|
std::move(stderrRead), [&](const auto& buffer) { readStderr.append(buffer.data(), buffer.size()); }));
|
|
|
|
io.Run({});
|
|
|
|
VERIFY_ARE_EQUAL(ExpectedStdout, readStdout);
|
|
VERIFY_ARE_EQUAL(ExpectedStderr, readStderr);
|
|
};
|
|
|
|
auto insert = [](std::vector<char>& buffer, auto fd, const std::string& content) {
|
|
DockerIORelayHandle::MultiplexedHeader header;
|
|
header.Fd = fd;
|
|
header.Length = htonl(static_cast<uint32_t>(content.size()));
|
|
|
|
buffer.insert(buffer.end(), (char*)&header, ((char*)&header) + sizeof(header));
|
|
buffer.insert(buffer.end(), content.begin(), content.end());
|
|
};
|
|
|
|
{
|
|
std::vector<char> input;
|
|
insert(input, 1, "foo");
|
|
insert(input, 1, "bar");
|
|
insert(input, 2, "stderr");
|
|
insert(input, 2, "stderrAgain");
|
|
insert(input, 1, "stdOutAgain");
|
|
|
|
runTest(input, "foobarstdOutAgain", "stderrstderrAgain");
|
|
}
|
|
|
|
{
|
|
std::vector<char> input;
|
|
insert(input, 0, "foo");
|
|
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest(input, "", ""); }), E_INVALIDARG);
|
|
}
|
|
|
|
{
|
|
std::vector<char> input;
|
|
insert(input, 12, "foo");
|
|
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([&]() { runTest(input, "", ""); }), E_INVALIDARG);
|
|
}
|
|
|
|
// Validate that behavior is correct if a read spans across multiple streams.
|
|
{
|
|
std::vector<char> input;
|
|
|
|
std::string largeStdout(LX_RELAY_BUFFER_SIZE + 150, 'a');
|
|
std::string largeStderr(LX_RELAY_BUFFER_SIZE + 12, 'b');
|
|
insert(input, 1, largeStdout);
|
|
insert(input, 2, largeStderr);
|
|
insert(input, 1, "regularStdout");
|
|
|
|
runTest(input, largeStdout + "regularStdout", largeStderr);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerRecoveryFromStorage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto restore = ResetTestSession(); // Required to access the storage folder.
|
|
|
|
std::string containerName = "test-container";
|
|
ULONGLONG originalStateChangedAt{};
|
|
ULONGLONG originalCreatedAt{};
|
|
|
|
// Phase 1: Create session and container, then stop the container
|
|
{
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test", true));
|
|
|
|
// Create and start a container
|
|
WSLCContainerLauncher launcher("debian:latest", containerName.c_str(), {"/bin/echo", "OK"});
|
|
|
|
auto container = launcher.Launch(*session);
|
|
container.SetDeleteOnClose(false);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
// Stop the container so it can be recovered and deleted later
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Capture StateChangedAt and CreatedAt before the session is destroyed.
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 1);
|
|
originalStateChangedAt = containers[0].StateChangedAt;
|
|
originalCreatedAt = containers[0].CreatedAt;
|
|
VERIFY_IS_TRUE(originalStateChangedAt > 0);
|
|
VERIFY_IS_TRUE(originalCreatedAt > 0);
|
|
}
|
|
|
|
// Phase 2: Create new session from same storage, recover and delete container
|
|
{
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test", true));
|
|
|
|
auto container = OpenContainer(session.get(), containerName);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
|
|
// Verify that StateChangedAt was correctly restored from the Docker timestamp.
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 1);
|
|
VERIFY_ARE_EQUAL(containers[0].StateChangedAt, originalStateChangedAt);
|
|
VERIFY_ARE_EQUAL(containers[0].CreatedAt, originalCreatedAt);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
|
|
// Verify container is no longer accessible
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(session->OpenContainer(containerName.c_str(), ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Phase 3: Create new session from same storage, verify the container is not listed.
|
|
{
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test", true));
|
|
|
|
// Verify container is no longer accessible
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(session->OpenContainer(containerName.c_str(), ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerVolumeAndPortRecoveryFromStorage)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto restore = ResetTestSession();
|
|
|
|
std::string containerName = "test-recovery-volumes-ports";
|
|
|
|
auto hostFolder = std::filesystem::current_path() / "test-recovery-volume";
|
|
std::filesystem::create_directories(hostFolder);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(hostFolder, ec);
|
|
});
|
|
|
|
// Create a test file in the host folder
|
|
std::ofstream testFile(hostFolder / "test.txt");
|
|
testFile << "recovery-test-content";
|
|
testFile.close();
|
|
|
|
// Create session and container with volumes and ports (but don't start it)
|
|
{
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test-vp", true, WSLCNetworkingModeNAT));
|
|
|
|
WSLCContainerLauncher launcher(
|
|
"python:3.12-alpine", containerName, {"python3", "-m", "http.server", "--directory", "/volume"}, {"PYTHONUNBUFFERED=1"}, WSLCContainerNetworkTypeBridged);
|
|
|
|
launcher.AddPort(1250, 8000, AF_INET);
|
|
launcher.AddVolume(hostFolder.wstring(), "/volume", false);
|
|
|
|
// Create container but don't start it
|
|
auto container = launcher.Create(*session);
|
|
container.SetDeleteOnClose(false);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
|
|
}
|
|
|
|
// Recover the container in a new session, start it and verify volume and port mapping works.
|
|
{
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test-vp", true, WSLCNetworkingModeNAT));
|
|
auto container = OpenContainer(session.get(), containerName);
|
|
container.SetDeleteOnClose(false);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, nullptr));
|
|
|
|
auto initProcess = container.GetInitProcess();
|
|
WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on 0.0.0.0 port 8000");
|
|
|
|
// A 200 response also indicates the test file is available so volume was mounted correctly.
|
|
ExpectHttpResponse(L"http://127.0.0.1:1250/test.txt", 200);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
|
|
}
|
|
|
|
// Delete the host folder to simulate volume folder being missing on recovery
|
|
cleanup.reset();
|
|
|
|
// Create a new session - this should succeed even though the volume folder is gone
|
|
auto session = CreateSession(GetDefaultSessionSettings(L"recovery-test-vp", true, WSLCNetworkingModeNAT));
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = session->OpenContainer(containerName.c_str(), &container);
|
|
|
|
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
TEST_METHOD(ContainerRecoveryFromStorageInvalidMetadata)
|
|
{
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
RunCommand(m_defaultSession.get(), {"/usr/bin/docker", "container", "rm", "-f", "test-invalid-metadata"});
|
|
});
|
|
|
|
{
|
|
// Create a docker container that has no metadata.
|
|
auto result = RunCommand(
|
|
m_defaultSession.get(),
|
|
{"/usr/bin/docker", "container", "create", "--name", "test-invalid-metadata", "debian:latest"});
|
|
VERIFY_ARE_EQUAL(result.Code, 0L);
|
|
}
|
|
|
|
{
|
|
ResetTestSession();
|
|
// Try to open the container - this should fail due to missing metadata.
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = m_defaultSession->OpenContainer("test-invalid-metadata", &container);
|
|
VERIFY_ARE_EQUAL(hr, E_UNEXPECTED);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(SessionManagement)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto manager = OpenSessionManager();
|
|
|
|
auto expectSessions = [&](const std::vector<std::wstring>& expectedSessions) {
|
|
wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
|
|
VERIFY_SUCCEEDED(manager->ListSessions(&sessions, sessions.size_address<ULONG>()));
|
|
|
|
std::set<std::wstring> displayNames;
|
|
for (const auto& e : sessions)
|
|
{
|
|
auto [_, inserted] = displayNames.insert(e.DisplayName);
|
|
|
|
VERIFY_IS_TRUE(inserted);
|
|
}
|
|
|
|
for (const auto& e : expectedSessions)
|
|
{
|
|
auto it = displayNames.find(e);
|
|
if (it == displayNames.end())
|
|
{
|
|
LogError("Session not found: %ls", e.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
|
|
displayNames.erase(it);
|
|
}
|
|
|
|
for (const auto& e : displayNames)
|
|
{
|
|
LogError("Unexpected session found: %ls", e.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
};
|
|
|
|
auto create = [this](LPCWSTR Name, WSLCSessionFlags Flags) {
|
|
return CreateSession(GetDefaultSessionSettings(Name), Flags);
|
|
};
|
|
|
|
// Validate that non-persistent sessions are dropped when released
|
|
{
|
|
auto session1 = create(L"session-1", WSLCSessionFlagsNone);
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
|
|
session1.reset();
|
|
expectSessions({c_testSessionName});
|
|
}
|
|
|
|
// Validate that persistent sessions are only dropped when explicitly terminated.
|
|
{
|
|
auto session1 = create(L"session-1", WSLCSessionFlagsPersistent);
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
|
|
session1.reset();
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
session1 = create(L"session-1", WSLCSessionFlagsOpenExisting);
|
|
|
|
VERIFY_SUCCEEDED(session1->Terminate());
|
|
session1.reset();
|
|
expectSessions({c_testSessionName});
|
|
}
|
|
|
|
// Validate that sessions can be reopened by name.
|
|
{
|
|
auto session1 = create(L"session-1", WSLCSessionFlagsPersistent);
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
|
|
session1.reset();
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
|
|
auto session1Copy =
|
|
create(L"session-1", static_cast<WSLCSessionFlags>(WSLCSessionFlagsPersistent | WSLCSessionFlagsOpenExisting));
|
|
|
|
expectSessions({L"session-1", c_testSessionName});
|
|
|
|
// Verify that name conflicts are correctly handled.
|
|
auto settings = GetDefaultSessionSettings(L"session-1");
|
|
|
|
wil::com_ptr<IWSLCSession> session;
|
|
VERIFY_ARE_EQUAL(manager->CreateSession(&settings, WSLCSessionFlagsPersistent, &session), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
|
|
VERIFY_SUCCEEDED(session1Copy->Terminate());
|
|
WSLCSessionState state{};
|
|
VERIFY_SUCCEEDED(session1Copy->GetState(&state));
|
|
VERIFY_ARE_EQUAL(state, WSLCSessionStateTerminated);
|
|
expectSessions({c_testSessionName});
|
|
|
|
// Validate that a new session is created if WSLCSessionFlagsOpenExisting is set and no match is found.
|
|
auto session2 = create(L"session-2", static_cast<WSLCSessionFlags>(WSLCSessionFlagsOpenExisting));
|
|
}
|
|
|
|
// Validate that elevated session can't be opened by non-elevated tokens
|
|
{
|
|
auto elevatedSession = create(L"elevated-session", WSLCSessionFlagsNone);
|
|
|
|
auto nonElevatedToken = GetNonElevatedToken(TokenImpersonation);
|
|
auto revert = wil::impersonate_token(nonElevatedToken.get());
|
|
auto nonElevatedSession = create(L"non-elevated-session", WSLCSessionFlagsNone);
|
|
|
|
// Validate that non-elevated tokens can't open an elevated session.
|
|
wil::com_ptr<IWSLCSession> openedSession;
|
|
ULONG elevatedId{};
|
|
VERIFY_SUCCEEDED(elevatedSession->GetId(&elevatedId));
|
|
VERIFY_ARE_EQUAL(manager->OpenSession(elevatedId, &openedSession), HRESULT_FROM_WIN32(ERROR_ELEVATION_REQUIRED));
|
|
VERIFY_IS_FALSE(!!openedSession);
|
|
|
|
// Validate that non-elevated tokens can open non-elevated sessions.
|
|
ULONG nonElevatedId{};
|
|
VERIFY_SUCCEEDED(nonElevatedSession->GetId(&nonElevatedId));
|
|
VERIFY_SUCCEEDED(manager->OpenSession(nonElevatedId, &openedSession));
|
|
VERIFY_IS_TRUE(!!openedSession);
|
|
}
|
|
}
|
|
|
|
static void ValidateHandleOutput(HANDLE handle, const std::string& expectedOutput)
|
|
{
|
|
VERIFY_ARE_EQUAL(EscapeString(expectedOutput), EscapeString(ReadToString(handle)));
|
|
}
|
|
|
|
TEST_METHOD(ContainerLogs)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectLogs = [](auto& container,
|
|
const std::string& expectedStdout,
|
|
const std::optional<std::string>& expectedStderr,
|
|
WSLCLogsFlags Flags = WSLCLogsFlagsNone,
|
|
ULONGLONG Tail = 0,
|
|
ULONGLONG Since = 0,
|
|
ULONGLONG Until = 0) {
|
|
COMOutputHandle stdoutHandle;
|
|
COMOutputHandle stderrHandle;
|
|
VERIFY_SUCCEEDED(container.Logs(Flags, &stdoutHandle, &stderrHandle, Since, Until, Tail));
|
|
|
|
ValidateHandleOutput(stdoutHandle.Get(), expectedStdout);
|
|
|
|
if (expectedStderr.has_value())
|
|
{
|
|
ValidateHandleOutput(stderrHandle.Get(), expectedStderr.value());
|
|
}
|
|
};
|
|
|
|
// Test a simple scenario.
|
|
{
|
|
// Create a container with a simple command.
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "logs-test-1", {"/bin/bash", "-c", "echo stdout && (echo stderr >& 2)"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "stdout\n"}, {2, "stderr\n"}});
|
|
|
|
expectLogs(container.Get(), "stdout\n", "stderr\n");
|
|
|
|
// validate that logs can be queried multiple times.
|
|
expectLogs(container.Get(), "stdout\n", "stderr\n");
|
|
}
|
|
|
|
// Validate that tail works.
|
|
{
|
|
// Create a container with a simple command.
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "logs-test-2", {"/bin/bash", "-c", "echo -en 'line1\\nline2\\nline3\\nline4'"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "line1\nline2\nline3\nline4"}});
|
|
|
|
expectLogs(container.Get(), "line1\nline2\nline3\nline4", "");
|
|
expectLogs(container.Get(), "line4", "", WSLCLogsFlagsNone, 1);
|
|
expectLogs(container.Get(), "line3\nline4", "", WSLCLogsFlagsNone, 2);
|
|
expectLogs(container.Get(), "line1\nline2\nline3\nline4", "", WSLCLogsFlagsNone, 4);
|
|
}
|
|
|
|
// Validate that timestamps are correctly returned.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "logs-test-3", {"/bin/bash", "-c", "echo -n OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "OK"}});
|
|
|
|
COMOutputHandle stdoutHandle{};
|
|
COMOutputHandle stderrHandle{};
|
|
VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsTimestamps, &stdoutHandle, &stderrHandle, 0, 0, 0));
|
|
|
|
auto output = ReadToString(stdoutHandle.Get());
|
|
VerifyPatternMatch(output, "20*-*-* OK"); // Timestamp is in ISO 8601 format
|
|
}
|
|
|
|
// Validate that 'since' and 'until' work as expected.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "logs-test-4", {"/bin/bash", "-c", "echo -n OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "OK"}});
|
|
|
|
// Testing would with more granularity would be difficult, but these flags are just forwarded to docker,
|
|
// so validate that they're wired correctly.
|
|
|
|
auto now = time(nullptr);
|
|
expectLogs(container.Get(), "OK", "", WSLCLogsFlagsNone, 0, now - 3600);
|
|
expectLogs(container.Get(), "", "", WSLCLogsFlagsNone, 0, now + 3600);
|
|
|
|
expectLogs(container.Get(), "", "", WSLCLogsFlagsNone, 0, 0, now - 3600);
|
|
expectLogs(container.Get(), "OK", "", WSLCLogsFlagsNone, 0, 0, now + 3600);
|
|
}
|
|
|
|
// Validate that logs work for TTY processes
|
|
{
|
|
WSLCContainerLauncher launcher(
|
|
"debian:latest", "logs-test-5", {"/bin/bash", "-c", "stat -f /dev/stdin | grep -io 'Type:.*$'"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
ValidateHandleOutput(initProcess.GetStdHandle(WSLCFDTty).get(), "Type: devpts\r\n");
|
|
VERIFY_ARE_EQUAL(initProcess.Wait(), 0);
|
|
|
|
expectLogs(container.Get(), "Type: devpts\r\n", {});
|
|
|
|
// Validate that logs can queried multiple times.
|
|
expectLogs(container.Get(), "Type: devpts\r\n", {});
|
|
}
|
|
|
|
// Validate that the 'follow' flag works as expected.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "logs-test-6", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
// Without 'follow', logs return immediately.
|
|
expectLogs(container.Get(), "", "");
|
|
|
|
// Create a 'follow' logs call.
|
|
COMOutputHandle stdoutHandle{};
|
|
COMOutputHandle stderrHandle{};
|
|
VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsFollow, &stdoutHandle, &stderrHandle, 0, 0, 0));
|
|
|
|
PartialHandleRead reader(stdoutHandle.Get());
|
|
|
|
auto containerStdin = initProcess.GetStdHandle(0);
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(containerStdin.get(), "line1\n", 6, nullptr, nullptr));
|
|
|
|
reader.Expect("line1\n");
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(containerStdin.get(), "line2\n", 6, nullptr, nullptr));
|
|
reader.Expect("line1\nline2\n");
|
|
|
|
containerStdin.reset();
|
|
reader.ExpectClosed();
|
|
|
|
expectLogs(container.Get(), "line1\nline2\n", "");
|
|
expectLogs(container.Get(), "line1\nline2\n", "", WSLCLogsFlagsFollow);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerLabels)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Docker labels do not have a size limit, so test with a very large label value to validate that the API can handle it.
|
|
std::map<std::string, std::string> labels = {{"key1", "value1"}, {"key2", std::string(10000, 'a')}};
|
|
|
|
// Test valid labels
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-labels", {"echo", "OK"});
|
|
|
|
for (const auto& [key, value] : labels)
|
|
{
|
|
launcher.AddLabel(key, value);
|
|
}
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(labels, container.Labels());
|
|
|
|
// Keep the container alive after the handle is dropped so we can validate labels are persisted across sessions.
|
|
container.SetDeleteOnClose(false);
|
|
}
|
|
|
|
{
|
|
// Restarting the test session will force the container to be reloaded from storage.
|
|
ResetTestSession();
|
|
|
|
// Validate that labels are correctly loaded.
|
|
auto container = OpenContainer(m_defaultSession.get(), "test-labels");
|
|
VERIFY_ARE_EQUAL(labels, container.Labels());
|
|
}
|
|
|
|
// Test nullptr key
|
|
{
|
|
WSLCLabel label{.Key = nullptr, .Value = "value"};
|
|
|
|
WSLCContainerOptions options{};
|
|
options.Image = "debian:latest";
|
|
options.Name = "test-labels-nullptr-key";
|
|
options.Labels = &label;
|
|
options.LabelsCount = 1;
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = m_defaultSession->CreateContainer(&options, &container);
|
|
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
|
|
}
|
|
|
|
// Test nullptr value
|
|
{
|
|
WSLCLabel label{.Key = "key", .Value = nullptr};
|
|
|
|
WSLCContainerOptions options{};
|
|
options.Image = "debian:latest";
|
|
options.Name = "test-labels-nullptr-value";
|
|
options.Labels = &label;
|
|
options.LabelsCount = 1;
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = m_defaultSession->CreateContainer(&options, &container);
|
|
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
|
|
}
|
|
|
|
// Test duplicate keys
|
|
{
|
|
std::vector<WSLCLabel> labels;
|
|
labels.push_back({.Key = "key", .Value = "value"});
|
|
labels.push_back({.Key = "key", .Value = "value2"});
|
|
|
|
WSLCContainerOptions options{};
|
|
options.Image = "debian:latest";
|
|
options.Name = "test-labels-duplicate-keys";
|
|
options.Labels = labels.data();
|
|
options.LabelsCount = static_cast<ULONG>(labels.size());
|
|
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
auto hr = m_defaultSession->CreateContainer(&options, &container);
|
|
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
}
|
|
|
|
// Test wslc metadata key conflict
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest");
|
|
launcher.AddLabel("com.microsoft.wsl.container.metadata", "value");
|
|
|
|
auto [hr, container] = launcher.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerAttach)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Validate attach behavior in a non-tty process.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "attach-test-1", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(result);
|
|
|
|
// Verify that attaching to a created container fails.
|
|
COMOutputHandle stdinHandle{};
|
|
COMOutputHandle stdoutHandle{};
|
|
COMOutputHandle stderrHandle{};
|
|
VERIFY_ARE_EQUAL(container->Get().Attach(nullptr, &stdinHandle, &stdoutHandle, &stderrHandle), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Start the container.
|
|
VERIFY_SUCCEEDED(container->Get().Start(WSLCContainerStartFlagsAttach, nullptr));
|
|
|
|
// Verify that trying to attach with null handles fails.
|
|
VERIFY_ARE_EQUAL(container->Get().Attach(nullptr, nullptr, nullptr, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
|
|
|
|
// Get its original std handles.
|
|
auto process = container->GetInitProcess();
|
|
auto originalStdin = process.GetStdHandle(0);
|
|
auto originalStdout = process.GetStdHandle(1);
|
|
|
|
// Attach to the container with separate handles.
|
|
stdinHandle.Reset();
|
|
stdoutHandle.Reset();
|
|
stderrHandle.Reset();
|
|
VERIFY_SUCCEEDED(container->Get().Attach(nullptr, &stdinHandle, &stdoutHandle, &stderrHandle));
|
|
|
|
PartialHandleRead originalReader(originalStdout.get());
|
|
PartialHandleRead attachedReader(stdoutHandle.Get());
|
|
|
|
// Write content on the original stdin.
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(originalStdin.get(), "line1\n", 6, nullptr, nullptr));
|
|
|
|
// Content should be relayed on both stdouts.
|
|
originalReader.Expect("line1\n");
|
|
attachedReader.Expect("line1\n");
|
|
|
|
// Write content on the attached stdin.
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(stdinHandle.Get(), "line2\n", 6, nullptr, nullptr));
|
|
|
|
// Content should be relayed on both stdouts.
|
|
originalReader.Expect("line1\nline2\n");
|
|
attachedReader.Expect("line1\nline2\n");
|
|
|
|
// Close the original stdin.
|
|
originalStdin.reset();
|
|
|
|
// Expect both readers to be closed.
|
|
originalReader.ExpectClosed();
|
|
attachedReader.ExpectClosed();
|
|
|
|
process.Wait();
|
|
|
|
stdinHandle.Reset();
|
|
stdoutHandle.Reset();
|
|
stderrHandle.Reset();
|
|
|
|
// Validate that attaching to an exited container fails.
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateExited);
|
|
stdinHandle.Reset();
|
|
stdoutHandle.Reset();
|
|
stderrHandle.Reset();
|
|
VERIFY_ARE_EQUAL(container->Get().Attach(nullptr, &stdinHandle, &stdoutHandle, &stderrHandle), HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
|
|
// Validate that attaching to a deleted container fails.
|
|
VERIFY_SUCCEEDED(container->Get().Delete(WSLCDeleteFlagsNone));
|
|
stdinHandle.Reset();
|
|
stdoutHandle.Reset();
|
|
stderrHandle.Reset();
|
|
|
|
VERIFY_ARE_EQUAL(container->Get().Attach(nullptr, &stdinHandle, &stdoutHandle, &stderrHandle), RPC_E_DISCONNECTED);
|
|
|
|
container->SetDeleteOnClose(false);
|
|
}
|
|
|
|
// Validate that closing an attached stdin terminates the container.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "attach-test-2", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
auto process = container.GetInitProcess();
|
|
auto originalStdin = process.GetStdHandle(0);
|
|
auto originalStdout = process.GetStdHandle(1);
|
|
|
|
COMOutputHandle attachedStdin;
|
|
COMOutputHandle attachedStdout;
|
|
COMOutputHandle attachedStderr;
|
|
VERIFY_SUCCEEDED(container.Get().Attach(nullptr, &attachedStdin, &attachedStdout, &attachedStderr));
|
|
|
|
PartialHandleRead originalReader(originalStdout.get());
|
|
PartialHandleRead attachedReader(attachedStdout.Get());
|
|
|
|
attachedStdin.Reset();
|
|
|
|
// Expect both readers to be closed.
|
|
originalReader.ExpectClosed();
|
|
attachedReader.ExpectClosed();
|
|
}
|
|
|
|
// Validate behavior for tty containers
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "attach-test-3", {"/bin/bash"}, {}, {}, WSLCProcessFlagsTty | WSLCProcessFlagsStdin);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
auto originalTty = process.GetStdHandle(WSLCFDTty);
|
|
|
|
COMOutputHandle attachedTty{};
|
|
COMOutputHandle dummyHandle1{};
|
|
COMOutputHandle dummyHandle2{};
|
|
VERIFY_SUCCEEDED(container.Get().Attach(nullptr, &attachedTty, &dummyHandle1, &dummyHandle2));
|
|
|
|
PartialHandleRead originalReader(originalTty.get());
|
|
PartialHandleRead attachedReader(attachedTty.Get());
|
|
|
|
// Read the prompt from the original tty (hardcoded bytes since behavior is constant).
|
|
auto prompt = originalReader.ReadBytes(13);
|
|
VerifyPatternMatch(prompt, "*root@*");
|
|
|
|
// Resize the tty to force the prompt to redraw.
|
|
process.Get().ResizeTty(61, 81);
|
|
|
|
auto attachedPrompt = attachedReader.ReadBytes(13);
|
|
VerifyPatternMatch(attachedPrompt, "*root@*");
|
|
|
|
// Close the tty.
|
|
originalTty.reset();
|
|
attachedTty.Reset();
|
|
|
|
originalReader.ExpectClosed();
|
|
attachedReader.ExpectClosed();
|
|
}
|
|
|
|
// Validate that containers can be started in detached mode and attached to later.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "attach-test-4", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
|
|
|
|
auto initProcess = container.GetInitProcess();
|
|
WSLCHandle dummy{};
|
|
VERIFY_ARE_EQUAL(initProcess.Get().GetStdHandle(WSLCFDStdin, &dummy), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
VERIFY_ARE_EQUAL(initProcess.Get().GetStdHandle(WSLCFDStdout, &dummy), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
VERIFY_ARE_EQUAL(initProcess.Get().GetStdHandle(WSLCFDStderr, &dummy), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
|
|
|
|
// Verify that the container can be attached to.
|
|
COMOutputHandle attachedStdin{};
|
|
COMOutputHandle attachedStdout{};
|
|
COMOutputHandle attachedStderr{};
|
|
VERIFY_SUCCEEDED(container.Get().Attach(nullptr, &attachedStdin, &attachedStdout, &attachedStderr));
|
|
|
|
PartialHandleRead attachedReader(attachedStdout.Get());
|
|
|
|
// Write content on the attached stdin.
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(attachedStdin.Get(), "OK\n", 3, nullptr, nullptr));
|
|
attachedStdin.Reset();
|
|
|
|
attachedReader.Expect("OK\n");
|
|
attachedReader.ExpectClosed();
|
|
VERIFY_ARE_EQUAL(initProcess.Wait(), 0);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(InvalidNames)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectInvalidArg = [&](const std::string& name) {
|
|
wil::com_ptr<IWSLCContainer> container;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(name.c_str(), &container), E_INVALIDARG);
|
|
VERIFY_IS_NULL(container.get());
|
|
|
|
ValidateCOMErrorMessage(std::format(L"Invalid name: '{}'", name));
|
|
};
|
|
|
|
expectInvalidArg("container with spaces");
|
|
expectInvalidArg("?foo");
|
|
expectInvalidArg("?foo&bar");
|
|
expectInvalidArg("/url/path");
|
|
expectInvalidArg("");
|
|
expectInvalidArg("\\escaped\n\\chars");
|
|
|
|
std::string longName(WSLC_MAX_CONTAINER_NAME_LENGTH + 1, 'a');
|
|
expectInvalidArg(longName);
|
|
|
|
auto expectInvalidPull = [&](const char* name) {
|
|
VERIFY_ARE_EQUAL(m_defaultSession->PullImage(name, nullptr, nullptr), E_INVALIDARG);
|
|
|
|
auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
|
|
VERIFY_IS_TRUE(comError.has_value());
|
|
|
|
VERIFY_ARE_EQUAL(comError->Message.get(), std::format(L"Invalid image: '{}'", name));
|
|
};
|
|
|
|
expectInvalidPull("?foo&bar/url\n:name");
|
|
expectInvalidPull("?:&");
|
|
expectInvalidPull("/:/");
|
|
expectInvalidPull("\n: ");
|
|
expectInvalidPull("invalid\nrepo:valid-image");
|
|
expectInvalidPull("bad!repo:valid-image");
|
|
expectInvalidPull("repo:badimage!name");
|
|
expectInvalidPull("bad+image");
|
|
}
|
|
|
|
TEST_METHOD(PageReporting)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
SKIP_TEST_ARM64();
|
|
|
|
// Determine expected page reporting order based on Windows version.
|
|
// On Germanium or later: 5 (128k), otherwise: 9 (2MB).
|
|
const auto windowsVersion = wsl::windows::common::helpers::GetWindowsVersion();
|
|
int expectedOrder = (windowsVersion.BuildNumber >= wsl::windows::common::helpers::WindowsBuildNumbers::Germanium) ? 5 : 9;
|
|
|
|
// Read the actual value from sysfs and verify it matches.
|
|
auto result =
|
|
ExpectCommandResult(m_defaultSession.get(), {"/bin/cat", "/sys/module/page_reporting/parameters/page_reporting_order"}, 0);
|
|
|
|
VERIFY_ARE_EQUAL(result.Output[1], std::format("{}\n", expectedOrder));
|
|
}
|
|
|
|
TEST_METHOD(ContainerAutoRemove)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Test that a container with the Rm flag is automatically deleted on Stop().
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-auto-remove", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Test that a container with the Rm flag is automatically deleted when the init process is killed.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-auto-remove", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
// Prevent container from being deleted when handle is closed so we can verify auto-remove behavior.
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(process.Get().Signal(WSLCSignalSIGKILL));
|
|
process.Wait();
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Test that a container with the Rm flag is automatically deleted when the container is killed.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-auto-remove-kill", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
// Prevent container from being deleted when handle is closed so we can verify auto-remove behavior.
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
|
|
process.Wait();
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove-kill", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Test that the container autoremove flag is applied when the container exits on its own.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-hostname", {"/bin/sh", "-c", "echo foo"});
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
process.Wait();
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
}
|
|
|
|
// Test that the Rm flag is persisted across wslc sessions.
|
|
{
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-auto-remove", {"/bin/cat"}, {}, {}, WSLCProcessFlagsStdin);
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
container.SetDeleteOnClose(false);
|
|
|
|
ResetTestSession();
|
|
}
|
|
|
|
auto container = OpenContainer(m_defaultSession.get(), "test-auto-remove");
|
|
auto id = container.Id();
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
// verifyContainerDeleted("test-auto-remove");
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(id.c_str(), ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 0);
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerAutoRemoveReadStdout)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "test-auto-remove-stdout", {"echo", "Hello World"});
|
|
launcher.SetContainerFlags(WSLCContainerFlagsRm);
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
// Wait for the container to exit and verify it gets deleted automatically.
|
|
wsl::shared::retry::RetryWithTimeout<void>(
|
|
[&]() { THROW_WIN32_IF(ERROR_RETRY, container.State() != WslcContainerStateDeleted); },
|
|
std::chrono::milliseconds{100},
|
|
std::chrono::seconds{30});
|
|
|
|
VERIFY_ARE_EQUAL(WslcContainerStateDeleted, container.State());
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
|
|
// Ensure we can still get the init process and read stdout.
|
|
auto process = container.GetInitProcess();
|
|
auto result = process.WaitAndCaptureOutput();
|
|
|
|
VERIFY_ARE_EQUAL(0, result.Code);
|
|
VERIFY_ARE_EQUAL(std::string("Hello World\n"), result.Output[1]);
|
|
|
|
// Validate that the container is not found if we try to open it by name or id, or found in the container list.
|
|
wil::com_ptr<IWSLCContainer> notFound;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer("test-auto-remove-stdout", ¬Found), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
VERIFY_ARE_EQUAL(containers.size(), 0);
|
|
}
|
|
|
|
TEST_METHOD(ContainerNameGeneration)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
{
|
|
// Create a container with a specific name
|
|
auto container = WSLCContainerLauncher("debian:latest", "test-container-name").Create(*m_defaultSession.get());
|
|
|
|
// Validate that the container name is correct.
|
|
VERIFY_ARE_EQUAL(container.Name(), "test-container-name");
|
|
}
|
|
|
|
{
|
|
// Create a container without name.
|
|
auto container = WSLCContainerLauncher("debian:latest").Create(*m_defaultSession.get());
|
|
|
|
// Validate that the service generates a name for the container.
|
|
VERIFY_ARE_NOT_EQUAL(container.Name(), "");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(DeferredPortAndVolumeMappingOnStart)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Verify port mapping.
|
|
// Two containers created with the same host port, only the first Start() succeeds.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "deferred-port", {"sleep", "99999"}, {}, WSLCContainerNetworkTypeBridged);
|
|
launcher.AddPort(1240, 8000, AF_INET);
|
|
|
|
// Both Create() calls should succeed because ports are not reserved until Start().
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
|
|
|
|
launcher.SetName("deferred-port-2");
|
|
auto container2 = launcher.Create(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container2.State(), WslcContainerStateCreated);
|
|
|
|
// Start container — should succeed.
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
|
|
|
|
// Start container 2 — should fail because the host port is already reserved by container 1.
|
|
VERIFY_ARE_EQUAL(container2.Get().Start(WSLCContainerStartFlagsNone, nullptr), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS));
|
|
VERIFY_ARE_EQUAL(container2.State(), WslcContainerStateCreated);
|
|
}
|
|
|
|
// Verify mount volume is deferred to Start()
|
|
{
|
|
auto hostFolder = std::filesystem::current_path() / "test-deferred-volume";
|
|
std::filesystem::create_directories(hostFolder);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(hostFolder, ec);
|
|
});
|
|
|
|
auto getMountCount = [&]() {
|
|
auto result = RunCommand(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep -c '^/mnt/'"});
|
|
return std::stoi(result.Output[1]);
|
|
};
|
|
|
|
auto baselineMountCount = getMountCount();
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "deferred-volume", {"sleep", "99999"}, {}, WSLCContainerNetworkTypeHost);
|
|
launcher.AddVolume(hostFolder.wstring(), "/deferred-volume", false);
|
|
|
|
// Create the container — volume should NOT be mounted yet.
|
|
auto [result, container] = launcher.CreateNoThrow(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(result);
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateCreated);
|
|
VERIFY_ARE_EQUAL(getMountCount(), baselineMountCount);
|
|
|
|
// Start the container — volume should now be mounted.
|
|
VERIFY_SUCCEEDED(container->Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
VERIFY_ARE_EQUAL(container->State(), WslcContainerStateRunning);
|
|
VERIFY_ARE_EQUAL(getMountCount(), baselineMountCount + 1);
|
|
|
|
// Verify the volume is unmounted after container is stopped.
|
|
VERIFY_SUCCEEDED(container->Get().Stop(WSLCSignalSIGKILL, 0));
|
|
VERIFY_ARE_EQUAL(getMountCount(), baselineMountCount);
|
|
}
|
|
}
|
|
|
|
// This test case validates that multiple operations can happen in parallel in the same session.
|
|
TEST_METHOD(ParallelSessionOperations)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Start a blocking export
|
|
BlockingOperation operation([&](HANDLE handle) {
|
|
return m_defaultSession->SaveImage(ToCOMInputHandle(handle), "debian:latest", nullptr, nullptr);
|
|
});
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operation.Complete(); });
|
|
|
|
// Validate that various operations can be done while the export is in progress.
|
|
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> containers;
|
|
wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
|
|
VERIFY_SUCCEEDED(
|
|
m_defaultSession->ListContainers(&containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
|
|
|
|
if (containers.size() > 0)
|
|
{
|
|
LogError("Unexpected container found: %hs", containers[0].Name);
|
|
VERIFY_FAIL();
|
|
}
|
|
}
|
|
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-parallel-operation", {"echo", "OK"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto process = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
auto containerRef = OpenContainer(m_defaultSession.get(), "test-parallel-operation");
|
|
}
|
|
|
|
{
|
|
wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
|
|
VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, &images, images.size_address<ULONG>()));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ParallelContainerOperations)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "test-parallel-container-operations", {"echo", "OK"});
|
|
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
auto process = container.GetInitProcess();
|
|
ValidateProcessOutput(process, {{1, "OK\n"}});
|
|
|
|
// Start a blocking export
|
|
BlockingOperation operation([&](HANDLE handle) { return container.Get().Export(ToCOMInputHandle(handle)); });
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operation.Complete(); });
|
|
|
|
// Validate that various operations can be done while the export is in progress.
|
|
{
|
|
VERIFY_ARE_EQUAL(container.GetInitProcess().Wait(), 0);
|
|
}
|
|
|
|
{
|
|
VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
|
|
}
|
|
|
|
{
|
|
COMOutputHandle stdoutHandle;
|
|
COMOutputHandle stderrHandle;
|
|
VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutHandle, &stderrHandle, 0, 0, false));
|
|
|
|
ValidateHandleOutput(stdoutHandle.Get(), "OK\n");
|
|
}
|
|
|
|
{
|
|
VERIFY_ARE_EQUAL(container.Inspect().State.Status, "exited");
|
|
}
|
|
|
|
{
|
|
VERIFY_ARE_EQUAL(container.Labels().size(), 0);
|
|
}
|
|
|
|
{
|
|
// Validate that another export can run.
|
|
BlockingOperation secondExport([&](HANDLE handle) { return container.Get().Export(ToCOMInputHandle(handle)); });
|
|
secondExport.Complete();
|
|
}
|
|
|
|
{
|
|
// Exec() fails because the container is not running. This call just validates that Exec() doesn't get stuck.
|
|
auto [result, _] = WSLCProcessLauncher({}, {"echo", "OK"}).LaunchNoThrow(container.Get());
|
|
VERIFY_ARE_EQUAL(result, HRESULT_FROM_WIN32(ERROR_INVALID_STATE));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(SessionTerminationDuringSave)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Validate that SaveImage is aborted when the session terminates.
|
|
// Use overlapped write pipe so the server-side WriteFile doesn't block synchronously.
|
|
BlockingOperation operation(
|
|
[&](HANDLE handle) { return m_defaultSession->SaveImage(ToCOMInputHandle(handle), "debian:latest", nullptr, nullptr); }, E_ABORT, true, true);
|
|
|
|
// Terminate the session.
|
|
VERIFY_SUCCEEDED(m_defaultSession->Terminate());
|
|
operation.Complete();
|
|
auto restore = ResetTestSession();
|
|
}
|
|
|
|
TEST_METHOD(SessionTerminationDuringExport)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
// Validate that container Export is aborted when the session terminates.
|
|
WSLCContainerLauncher launcher("debian:latest", "test-export-session-terminate", {"echo", "OK"});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
VERIFY_ARE_EQUAL(container.GetInitProcess().Wait(), 0);
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
PruneResult result;
|
|
LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result));
|
|
});
|
|
|
|
// Use overlapped write pipe so the server-side WriteFile doesn't block synchronously.
|
|
BlockingOperation operation([&](HANDLE handle) { return container.Get().Export(ToCOMInputHandle(handle)); }, E_ABORT, true, true);
|
|
|
|
// Avoid attempting container delete on scope exit after intentional session termination;
|
|
// rely on the prune scope-exit above to clean up instead.
|
|
container.SetDeleteOnClose(false);
|
|
|
|
// Terminate the session.
|
|
VERIFY_SUCCEEDED(m_defaultSession->Terminate());
|
|
operation.Complete();
|
|
auto restore = ResetTestSession();
|
|
}
|
|
|
|
TEST_METHOD(InteractiveDetach)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto validateDetaches = [](HANDLE TtyIn, HANDLE TtyOut, const std::vector<char>& Input) {
|
|
VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(TtyIn, Input.data(), static_cast<DWORD>(Input.size()), nullptr, nullptr));
|
|
|
|
std::string output;
|
|
auto onRead = [&](const gsl::span<char>& data) { output.append(data.data(), data.size()); };
|
|
|
|
wsl::windows::common::relay::MultiHandleWait io;
|
|
io.AddHandle(std::make_unique<wsl::windows::common::relay::ReadHandle>(TtyOut, std::move(onRead)));
|
|
|
|
io.Run(60s);
|
|
|
|
// N.B. In the case of exec, the output can either be 'read escape sequence' or 'exec attach failed [...]' based on timing.
|
|
std::set<std::string> expectedOutputs{
|
|
"", "\r\n", "exec attach failed: error on attach stdin: read escape sequence\r\n", "read escape sequence\r\n"};
|
|
|
|
if (expectedOutputs.find(output) == expectedOutputs.end())
|
|
{
|
|
LogError("Unexpected output: %hs", output.c_str());
|
|
VERIFY_FAIL();
|
|
}
|
|
};
|
|
|
|
auto runDetachTest = [&](LPCSTR DetachKeys, const std::vector<char>& DetachSequence) {
|
|
WSLCContainerLauncher launcher("debian:latest", "test-detach", {"sleep", "9999999"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
|
|
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsAttach, DetachKeys));
|
|
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
// Validate detaching from a started container with the attach flag.
|
|
{
|
|
auto tty = initProcess.GetStdHandle(WSLCFDTty);
|
|
validateDetaches(tty.get(), tty.get(), DetachSequence);
|
|
}
|
|
|
|
// Validate detaching from an attached tty.
|
|
{
|
|
COMOutputHandle ttyHandle{};
|
|
COMOutputHandle unusedHandle1{};
|
|
COMOutputHandle unusedHandle2{};
|
|
VERIFY_SUCCEEDED(container.Get().Attach(DetachKeys, &ttyHandle, &unusedHandle1, &unusedHandle2));
|
|
|
|
validateDetaches(ttyHandle.Get(), ttyHandle.Get(), DetachSequence);
|
|
}
|
|
|
|
// Validate detaching from an exec'd process.
|
|
{
|
|
WSLCProcessLauncher processLauncher({}, {"sleep", "9999999"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
|
|
|
|
if (DetachKeys != nullptr)
|
|
{
|
|
processLauncher.SetDetachKeys(DetachKeys);
|
|
}
|
|
|
|
auto process = processLauncher.Launch(container.Get());
|
|
auto tty = process.GetStdHandle(WSLCFDTty);
|
|
|
|
validateDetaches(tty.get(), tty.get(), DetachSequence);
|
|
}
|
|
};
|
|
|
|
{
|
|
// Validate that by default ttys can be detached via ctrlp-ctrlq.
|
|
runDetachTest(nullptr, {0x10, 0x11});
|
|
|
|
// Validate other detach keys.
|
|
runDetachTest("ctrl-a", {0x1});
|
|
runDetachTest("a,b,c,d,ctrl-z", {'a', 'b', 'c', 'd', 0x1a});
|
|
}
|
|
|
|
{
|
|
// Validate that invalid detach keys fail with the appropriate error.
|
|
// N.B. Docker doesn't set an error message for this specific case.
|
|
WSLCContainerLauncher launcher("debian:latest", "test-detach", {"cat"}, {}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
|
|
auto container = launcher.Create(*m_defaultSession);
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsAttach, "invalid"), E_INVALIDARG);
|
|
|
|
VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr));
|
|
|
|
COMOutputHandle unusedHandle{};
|
|
VERIFY_ARE_EQUAL(container.Get().Attach("invalid", &unusedHandle, &unusedHandle, &unusedHandle), E_INVALIDARG);
|
|
|
|
WSLCProcessLauncher processLauncher({}, {"cat"}, {}, WSLCProcessFlagsStdin | WSLCProcessFlagsTty);
|
|
processLauncher.SetDetachKeys("invalid");
|
|
|
|
// N.B. Docker returns HTTP 500 if the detach keys are invalid, but unlike other cases there's a proper error message.
|
|
auto [result, _] = processLauncher.LaunchNoThrow(container.Get());
|
|
VERIFY_ARE_EQUAL(result, E_FAIL);
|
|
|
|
ValidateCOMErrorMessage(L"Invalid escape keys (invalid) provided");
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ContainerPrune)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
auto expectPrune = [this](
|
|
const std::vector<std::string>& expectedIds = {},
|
|
const std::map<std::string, std::pair<const char*, bool>>& labels = {},
|
|
uint64_t until = 0,
|
|
const std::source_location& source = std::source_location::current()) {
|
|
PruneResult result;
|
|
|
|
std::vector<WSLCContainerPruneFilter> labelsFilter;
|
|
for (const auto& e : labels)
|
|
{
|
|
labelsFilter.push_back({e.first.c_str(), e.second.first, e.second.second});
|
|
}
|
|
|
|
VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(
|
|
labels.empty() ? nullptr : labelsFilter.data(), static_cast<DWORD>(labelsFilter.size()), until, &result.result));
|
|
|
|
std::vector<std::string> prunedContainers;
|
|
for (size_t i = 0; i < result.result.ContainersCount; i++)
|
|
{
|
|
prunedContainers.push_back(result.result.Containers[i]);
|
|
}
|
|
|
|
VerifyAreEqualUnordered(expectedIds, prunedContainers, source);
|
|
};
|
|
|
|
auto RunAndWait = [&](auto& launcher) {
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "OK\n"}});
|
|
|
|
return container;
|
|
};
|
|
|
|
// Validate that a prune without any container returns nothing.
|
|
{
|
|
expectPrune({});
|
|
}
|
|
|
|
{
|
|
// Validate that prune doesn't remove running containers.
|
|
WSLCContainerLauncher launcher("debian:latest", "test-prune", {"sleep", "9999999"}, {}, {});
|
|
auto container = launcher.Launch(*m_defaultSession);
|
|
|
|
expectPrune({});
|
|
|
|
// Validate that prune removes stopped containers.
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
auto containerId = container.Id();
|
|
expectPrune({containerId});
|
|
|
|
// Validate that the container can't be opened anymore.
|
|
wil::com_ptr<IWSLCContainer> dummy;
|
|
VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(containerId.c_str(), &dummy), HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
|
|
|
|
VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsNone), RPC_E_DISCONNECTED);
|
|
}
|
|
|
|
// Validate that label filters work.
|
|
{
|
|
WSLCContainerLauncher testPrune1Launcher("debian:latest", "test-prune-1", {"echo", "OK"}, {}, {});
|
|
testPrune1Launcher.AddLabel("key", "value");
|
|
|
|
auto testPrune1 = RunAndWait(testPrune1Launcher);
|
|
|
|
WSLCContainerLauncher testPrune2Launcher("debian:latest", "test-prune-2", {"echo", "OK"}, {}, {});
|
|
testPrune2Launcher.AddLabel("key", "anotherValue");
|
|
|
|
auto testPrune2 = RunAndWait(testPrune2Launcher);
|
|
|
|
WSLCContainerLauncher testPrune3Launcher("debian:latest", "test-prune-3", {"echo", "OK"}, {}, {});
|
|
testPrune3Launcher.AddLabel("anotherKey", "value");
|
|
auto testPrune3 = RunAndWait(testPrune3Launcher);
|
|
|
|
WSLCContainerLauncher testPrune4Launcher("debian:latest", "test-prune-4", {"echo", "OK"}, {}, {});
|
|
auto testPrune4 = RunAndWait(testPrune4Launcher);
|
|
|
|
// Expect testPrune1 to be selected via key=value.
|
|
expectPrune({testPrune1.Id()}, {{"key", {"value", true}}});
|
|
|
|
// Expect testPrune2 to be selected via key being present.
|
|
expectPrune({testPrune2.Id()}, {{"key", {nullptr, true}}});
|
|
|
|
// Prune by absence of 'anotherKey' label.
|
|
expectPrune({testPrune4.Id()}, {{"anotherKey", {nullptr, false}}});
|
|
|
|
// Prune by label inequality.
|
|
expectPrune({testPrune3.Id()}, {{"anotherKey", {"someValue", false}}});
|
|
}
|
|
|
|
// Validate that the 'until' filter works.
|
|
{
|
|
WSLCContainerLauncher launcher("debian:latest", "test-prune-until", {"echo", "OK"}, {}, {});
|
|
|
|
auto container = RunAndWait(launcher);
|
|
|
|
auto now = time(nullptr);
|
|
|
|
expectPrune({}, {}, now - 3600);
|
|
expectPrune({container.Id()}, {}, now + 3600);
|
|
}
|
|
|
|
// Validate error paths.
|
|
{
|
|
WSLCContainerPruneFilter filter{.Key = nullptr, .Value = nullptr, .Present = false};
|
|
PruneResult result;
|
|
|
|
VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, 0, &result.result), E_POINTER);
|
|
VERIFY_ARE_EQUAL(m_defaultSession->PruneContainers(&filter, 1, 0, nullptr), HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER));
|
|
}
|
|
}
|
|
|
|
TEST_METHOD(ImageParsing)
|
|
{
|
|
using wsl::windows::common::wslutil::ParseImage;
|
|
|
|
auto ValidateImageParsing = [](const std::string& input, const std::string& expectedRepo, const std::optional<std::string>& expectedTag) {
|
|
auto [repo, tag] = ParseImage(input);
|
|
VERIFY_ARE_EQUAL(repo, expectedRepo);
|
|
VERIFY_ARE_EQUAL(tag.value_or("<empty>"), expectedTag.value_or("<empty>"));
|
|
};
|
|
|
|
ValidateImageParsing("ubuntu:22.04", "ubuntu", "22.04");
|
|
ValidateImageParsing("ubuntu", "ubuntu", {});
|
|
ValidateImageParsing("library/ubuntu:latest", "library/ubuntu", "latest");
|
|
ValidateImageParsing("myregistry.io:5000/myimage:v1", "myregistry.io:5000/myimage", "v1");
|
|
ValidateImageParsing("myregistry.io:5000/myimage", "myregistry.io:5000/myimage", {});
|
|
|
|
ValidateImageParsing(
|
|
"registry.example.com:8080/org/project/image:stable", "registry.example.com:8080/org/project/image", "stable");
|
|
|
|
ValidateImageParsing("localhost:5000/myimage:latest", "localhost:5000/myimage", "latest");
|
|
ValidateImageParsing("ghcr.io/owner/repo:sha-abc123", "ghcr.io/owner/repo", "sha-abc123");
|
|
|
|
ValidateImageParsing(
|
|
"ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
|
|
"ubuntu",
|
|
"sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
|
|
|
|
// Validate that the digest takes precedence over the tag.
|
|
ValidateImageParsing(
|
|
"ubuntu:latest@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
|
|
"ubuntu",
|
|
"sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
|
|
|
|
ValidateImageParsing(
|
|
"myregistry.io:5000/myimage@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
|
|
"myregistry.io:5000/myimage",
|
|
"sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
|
|
|
|
ValidateImageParsing(
|
|
"ubuntu:22.04@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
|
|
"ubuntu",
|
|
"sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30");
|
|
|
|
ValidateImageParsing("pytorch/pytorch", "pytorch/pytorch", {});
|
|
|
|
// Invalid inputs
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(""); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":debian:latest"); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage("debian:latest@"); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(""); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":"); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage("a:"); }), E_INVALIDARG);
|
|
VERIFY_ARE_EQUAL(wil::ResultFromException([]() { ParseImage(":b"); }), E_INVALIDARG);
|
|
}
|
|
|
|
TEST_METHOD(RepoParsing)
|
|
{
|
|
using wsl::windows::common::wslutil::NormalizeRepo;
|
|
|
|
auto ValidateRepoParsing = [](const std::string& input, const std::string& expectedServer, const std::string& expectedPath) {
|
|
auto [server, path] = NormalizeRepo(input);
|
|
VERIFY_ARE_EQUAL(server, expectedServer);
|
|
VERIFY_ARE_EQUAL(path, expectedPath);
|
|
};
|
|
|
|
ValidateRepoParsing("ubuntu", "docker.io", "library/ubuntu");
|
|
ValidateRepoParsing("docker.io/ubuntu", "docker.io", "library/ubuntu");
|
|
ValidateRepoParsing("index.docker.io/ubuntu", "docker.io", "library/ubuntu");
|
|
ValidateRepoParsing("index.docker.io/library/ubuntu", "docker.io", "library/ubuntu");
|
|
ValidateRepoParsing("docker.io/library/ubuntu", "docker.io", "library/ubuntu");
|
|
ValidateRepoParsing("microsoft.com/ubuntu", "microsoft.com", "ubuntu");
|
|
ValidateRepoParsing("microsoft.com:80/ubuntu", "microsoft.com:80", "ubuntu");
|
|
ValidateRepoParsing("microsoft.com:80/ubuntu/foo/bar", "microsoft.com:80", "ubuntu/foo/bar");
|
|
ValidateRepoParsing("127.0.0.1:80/ubuntu/foo/bar", "127.0.0.1:80", "ubuntu/foo/bar");
|
|
ValidateRepoParsing("pytorch/pytorch", "docker.io", "pytorch/pytorch");
|
|
ValidateRepoParsing("2001:0db8:85a3:0000:0000:8a2e:0370:7334/path", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "path");
|
|
ValidateRepoParsing(
|
|
"2001:0db8:85a3:0000:0000:8a2e:0370:7334:80/path", "2001:0db8:85a3:0000:0000:8a2e:0370:7334:80", "path");
|
|
}
|
|
|
|
TEST_METHOD(ElevatedTokenCanOpenNonElevatedHandles)
|
|
{
|
|
WSL2_TEST_ONLY();
|
|
|
|
wil::com_ptr<IWSLCSession> nonElevatedSession;
|
|
|
|
{
|
|
auto nonElevatedToken = GetNonElevatedToken(TokenImpersonation);
|
|
auto revert = wil::impersonate_token(nonElevatedToken.get());
|
|
|
|
nonElevatedSession = CreateSession(GetDefaultSessionSettings(L"non-elevated-session"), WSLCSessionFlagsNone);
|
|
LoadTestImage("debian:latest", nonElevatedSession.get());
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "test-non-elevated-handles-1", {"echo", "OK"});
|
|
auto container = launcher.Launch(*nonElevatedSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
ValidateProcessOutput(initProcess, {{1, "OK\n"}});
|
|
}
|
|
|
|
WSLCContainerLauncher launcher("debian:latest", "test-non-elevated-handles-2", {"echo", "OK"});
|
|
auto container = launcher.Launch(*nonElevatedSession);
|
|
auto initProcess = container.GetInitProcess();
|
|
|
|
ValidateProcessOutput(initProcess, {{1, "OK\n"}});
|
|
}
|
|
};
|