mirror of
https://github.com/aphrody-labs/WSL.git
synced 2026-10-14 21:59:15 +00:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1042 lines
43 KiB
C++
1042 lines
43 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
PluginTests.cpp
|
|
|
|
Abstract:
|
|
|
|
This file contains test cases for the plugin API.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "Common.h"
|
|
#include "registry.hpp"
|
|
#include "PluginTests.h"
|
|
#include "wslc.h"
|
|
#include "WSLCContainerLauncher.h"
|
|
#include "WSLCProcessLauncher.h"
|
|
#include "wslc/e2e/WSLCE2EHelpers.h"
|
|
|
|
using namespace wsl::windows::common::registry;
|
|
using WSLCE2ETests::StartLocalRegistry;
|
|
|
|
extern std::wstring g_testDistroPath;
|
|
extern std::wstring g_testDataPath;
|
|
|
|
class PluginTests
|
|
{
|
|
std::wstring logFile;
|
|
bool m_initialized = false;
|
|
std::wstring pluginDll;
|
|
std::optional<WslConfigChange> config;
|
|
|
|
WSL_TEST_CLASS(PluginTests)
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
|
|
m_initialized = true;
|
|
logFile = wil::GetCurrentDirectoryW<std::wstring>() + L"\\plugin-logs.txt";
|
|
|
|
const auto currentDll = std::filesystem::path(wil::GetModuleFileNameW<std::wstring>(wil::GetModuleInstanceHandle()));
|
|
const auto pluginDllPath = currentDll.parent_path() / L"testplugin.dll";
|
|
if (!std::filesystem::exists(pluginDllPath))
|
|
{
|
|
const std::wstring message = L"Plugin not found in: " + pluginDllPath.wstring();
|
|
VERIFY_FAIL(message.c_str());
|
|
return false;
|
|
}
|
|
|
|
pluginDll = pluginDllPath.wstring();
|
|
|
|
// Disable VM timeouts during the plugin tests
|
|
config.emplace(LxssGenerateTestConfig({.vmIdleTimeout = -1}));
|
|
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
|
|
auto key = OpenLxssMachineKey(KEY_ALL_ACCESS);
|
|
DeleteKey(key.get(), L"Test");
|
|
|
|
key = OpenKey(key.get(), L"Plugins", KEY_SET_VALUE);
|
|
DeleteValue(key.get(), L"TestPlugin");
|
|
|
|
RestartWslService();
|
|
|
|
std::wifstream file(logFile);
|
|
const auto fileContent = std::wstring{std::istreambuf_iterator<wchar_t>(file), {}};
|
|
LogInfo("Logfile: %ls", fileContent.c_str());
|
|
file.close();
|
|
|
|
StopWslService();
|
|
if (!DeleteFile(logFile.c_str()))
|
|
{
|
|
VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError());
|
|
}
|
|
|
|
if (m_initialized)
|
|
{
|
|
LxsstuUninitialize(FALSE);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ConfigurePlugin(PluginTestType testCase, LPCWSTR mountFolder = L"") const
|
|
{
|
|
StopWslService();
|
|
if (!DeleteFile(logFile.c_str()))
|
|
{
|
|
VERIFY_ARE_EQUAL(ERROR_FILE_NOT_FOUND, GetLastError());
|
|
}
|
|
|
|
const auto testKey = OpenTestRegistryKey(KEY_SET_VALUE);
|
|
WriteDword(testKey.get(), nullptr, c_testType, static_cast<DWORD>(testCase));
|
|
WriteString(testKey.get(), nullptr, c_logFile, logFile.c_str());
|
|
WriteString(testKey.get(), nullptr, c_mountFolder, mountFolder);
|
|
|
|
const auto lxssKey =
|
|
CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0);
|
|
WriteString(lxssKey.get(), nullptr, L"TestPlugin", pluginDll.c_str());
|
|
|
|
RestartWslService();
|
|
}
|
|
|
|
static void StartWsl(int expectedExitCode, const std::wstring& expectedOutput = {})
|
|
{
|
|
auto [output, error] = LxsstuLaunchWslAndCaptureOutput(L"echo -n OK", expectedExitCode);
|
|
if (expectedExitCode == 0)
|
|
{
|
|
VERIFY_ARE_EQUAL(output, L"OK");
|
|
}
|
|
else
|
|
{
|
|
VERIFY_ARE_EQUAL(output, expectedOutput);
|
|
}
|
|
}
|
|
|
|
void ValidateLogFile(LPCWSTR expected) const
|
|
{
|
|
StopWslService();
|
|
|
|
std::wifstream file(logFile);
|
|
auto fileContent = std::wstring{std::istreambuf_iterator<wchar_t>(file), {}};
|
|
LogInfo("Logfile: %ls", fileContent.c_str());
|
|
|
|
auto fileLines = wsl::shared::string::Split<wchar_t>(fileContent, '\n');
|
|
auto expectedLines = wsl::shared::string::Split<wchar_t>(expected, '\n');
|
|
|
|
for (size_t i = 0; i < std::max(fileLines.size(), expectedLines.size()); i++)
|
|
{
|
|
if (i >= fileLines.size())
|
|
{
|
|
std::wstring message = L"Line is expected but not in log file: " + expectedLines[i];
|
|
VERIFY_FAIL(message.c_str());
|
|
}
|
|
else if (i >= expectedLines.size())
|
|
{
|
|
std::wstring message = L"Line is in file but not expected: " + fileLines[i];
|
|
VERIFY_FAIL(message.c_str());
|
|
}
|
|
|
|
const auto& expected = expectedLines[i];
|
|
const auto& actual = fileLines[i];
|
|
if (!PathMatchSpec(actual.c_str(), expected.c_str()))
|
|
{
|
|
LogInfo("Plugin log: %ls", fileContent.c_str());
|
|
std::wstring message = L"Line (" + actual + L") didn't match pattern: " + expected;
|
|
VERIFY_FAIL(message.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
WSL2_TEST_METHOD(Success)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(MountFolderAccess)
|
|
{
|
|
const auto testFolder = std::filesystem::current_path() / "deny-write";
|
|
std::filesystem::remove_all(testFolder);
|
|
VERIFY_IS_TRUE(std::filesystem::create_directory(testFolder));
|
|
VERIFY_IS_TRUE(std::filesystem::create_directory(testFolder / "allowed"));
|
|
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
|
|
|
|
const auto user = wil::get_token_information<TOKEN_USER>();
|
|
EXPLICIT_ACCESSW access{};
|
|
access.grfAccessPermissions = FILE_ADD_FILE;
|
|
access.grfAccessMode = DENY_ACCESS;
|
|
access.grfInheritance = NO_INHERITANCE;
|
|
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
|
access.Trustee.ptstrName = static_cast<LPWSTR>(user->User.Sid);
|
|
|
|
PACL originalAcl = nullptr;
|
|
wil::unique_hlocal originalDescriptor;
|
|
THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW(
|
|
testFolder.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &originalAcl, nullptr, &originalDescriptor));
|
|
|
|
auto restoreAcl = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
|
|
THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
|
|
const_cast<LPWSTR>(testFolder.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, originalAcl, nullptr));
|
|
});
|
|
|
|
wsl::windows::common::security::unique_acl deniedAcl;
|
|
THROW_IF_WIN32_ERROR(SetEntriesInAclW(1, &access, originalAcl, &deniedAcl));
|
|
THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW(
|
|
const_cast<LPWSTR>(testFolder.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, deniedAcl.get(), nullptr));
|
|
|
|
const auto testFile = testFolder / L"plugin-test.txt";
|
|
wil::unique_hfile deniedFile{CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
|
VERIFY_IS_TRUE(!deniedFile);
|
|
VERIFY_ARE_EQUAL(ERROR_ACCESS_DENIED, GetLastError());
|
|
|
|
ConfigurePlugin(PluginTestType::MountFolderAccess, testFolder.c_str());
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=25
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
*Permission denied
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
StartWsl(0);
|
|
VERIFY_IS_TRUE(std::filesystem::exists(testFolder / "allowed" / "plugin-allowed.txt"));
|
|
VERIFY_IS_FALSE(std::filesystem::exists(testFile));
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CustomKernelOverriddenByPolicy)
|
|
{
|
|
RegistryKeyChange policy(
|
|
HKEY_LOCAL_MACHINE, wsl::windows::policies::c_registryKey, wsl::windows::policies::c_allowCustomKernelUserSetting, static_cast<DWORD>(0));
|
|
|
|
WslConfigChange config(LxssGenerateTestConfig({.kernel = L"kernel-that-doesn't-exist"}));
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(DuplicatedPlugin)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
|
|
// Register the same plugin dll twice. Validate that it's only called once.
|
|
const auto key =
|
|
CreateKey(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss\\Plugins", KEY_SET_VALUE, nullptr, 0);
|
|
WriteString(key.get(), nullptr, L"TestPlugin-duplicated", pluginDll.c_str());
|
|
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { DeleteValue(key.get(), L"TestPlugin-duplicated"); });
|
|
RestartWslService();
|
|
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CustomKernel)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=1)
|
|
OnVmStarted: E_ACCESSDENIED
|
|
VM Stopping)";
|
|
|
|
#ifdef WSL_KERNEL_PATH
|
|
|
|
std::wstring kernelPath = WIDEN(WSL_KERNEL_PATH);
|
|
|
|
#else
|
|
|
|
auto kernelPath = wsl::windows::common::wslutil::GetMsiPackagePath().value_or(L"");
|
|
VERIFY_IS_FALSE(kernelPath.empty());
|
|
kernelPath += L"\\tools\\kernel";
|
|
|
|
#endif
|
|
|
|
WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernel = kernelPath}));
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_ACCESSDENIED"));
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(CustomKernelCommandLine)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=2)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernelCommandLine = L"custom"}));
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(DistroIdStaysTheSame)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=10
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
OnDistroStarted: received same GUID
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::SameDistroId);
|
|
StartWsl(0);
|
|
WslShutdown();
|
|
StartWsl(0);
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(InitPidIsDifferent)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=14
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Init's pid is different (* ! = *)
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::InitPidIsDifferent);
|
|
StartWsl(0);
|
|
TerminateDistribution();
|
|
StartWsl(0);
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(PluginUpdateRequired)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=9
|
|
OnLoad: WSL_E_PLUGINREQUIRESUPDATE)";
|
|
|
|
ConfigurePlugin(PluginTestType::PluginRequiresUpdate);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"The plugin 'TestPlugin' requires a newer version of WSL. Please run: wsl.exe --update",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/WSL_E_PLUGIN_REQUIRES_UPDATE"));
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(APIErrors)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=7
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
API error tests passed
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::ApiErrors);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL1_TEST_METHOD(SuccessWSL1)
|
|
{
|
|
constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=1)";
|
|
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(LoadFailureFatalWSL2)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=2
|
|
OnLoad: E_UNEXPECTED)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToLoad);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL1_TEST_METHOD(LoadFailureNonFatalWSL1)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=2
|
|
OnLoad: E_UNEXPECTED)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToLoad);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(VmStartFailure)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=3
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
OnVmStarted: E_UNEXPECTED
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToStartVm);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(VmStartFailureWithPluginErrorTwice)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=13
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
OnVmStarted: E_UNEXPECTED
|
|
VM Stopping
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
OnVmStarted: E_UNEXPECTED
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToStartVmWithPluginErrorMessage);
|
|
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
|
|
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'Plugin error message'",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_UNEXPECTED"));
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(VmStopFailure)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=5
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping
|
|
OnVmStopping: E_UNEXPECTED)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToStopVm);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(DistributionStartFailure)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=4
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
OnDistroStarted: E_UNEXPECTED
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToStartDistro);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'", L"Wsl/Service/CreateInstance/Plugin/E_UNEXPECTED"));
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(DistributionStopFailure)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=6
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
OnDistroStopping: E_UNEXPECTED
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::FailToStopDistro);
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ErrorMessageStartVm)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=11
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
OnVmStarted: E_FAIL
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::ErrorMessageStartVm);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartVm plugin error message'",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/E_FAIL"));
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ErrorMessageStartDistro)
|
|
{
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=12
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
OnDistroStarted: E_FAIL
|
|
VM Stopping)";
|
|
|
|
ConfigurePlugin(PluginTestType::ErrorMessageStartDistro);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'. Error message: 'StartDistro plugin error message'",
|
|
L"Wsl/Service/CreateInstance/Plugin/E_FAIL"));
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(RegisterSuccess)
|
|
{
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ImportInplaceSuccess)
|
|
{
|
|
ConfigurePlugin(PluginTestType::Success);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
|
|
WslShutdown();
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--export plugin-test-distro plugin-test-distro.vhdx --format vhd"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import-in-place plugin-test-distro-vhd plugin-test-distro.vhdx"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro-vhd"), 0L);
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=1
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
VM Stopping
|
|
Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Folder mounted (* -> /test-plugin)
|
|
Process created
|
|
Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13
|
|
Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(RegisterUnregisterFail)
|
|
{
|
|
ConfigurePlugin(PluginTestType::FailToRegisterUnregisterDistro);
|
|
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--import plugin-test-distro . \"" + g_testDistroPath + L"\" --version 2"), 0L);
|
|
VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--unregister plugin-test-distro"), 0L);
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=15
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
OnDistributionRegistered: E_UNEXPECTED
|
|
Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13
|
|
OnDistributionUnregistered: E_UNEXPECTED
|
|
VM Stopping)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(ExecuteDistroCommand)
|
|
{
|
|
ConfigurePlugin(PluginTestType::RunDistroCommand);
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=16
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Process created
|
|
Failed process launch returned: -2147467259
|
|
Invalid distro launch returned: -2147220717
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(PluginToken)
|
|
{
|
|
ConfigurePlugin(PluginTestType::GetUsername);
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=17
|
|
VM created (settings->CustomConfigurationFlags=0)
|
|
Username: *
|
|
Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13
|
|
Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13
|
|
VM Stopping)";
|
|
|
|
StartWsl(0);
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
static wil::com_ptr<IWSLCSessionManager> OpenWslcSessionManager()
|
|
{
|
|
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;
|
|
}
|
|
|
|
static wil::com_ptr<IWSLCSession> CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone, LPCWSTR StoragePath = nullptr)
|
|
{
|
|
WSLCSessionSettings settings{};
|
|
settings.DisplayName = Name;
|
|
settings.CpuCount = 4;
|
|
settings.MemoryMb = 4096;
|
|
settings.BootTimeoutMs = 30 * 1000;
|
|
settings.NetworkingMode = NetworkingMode;
|
|
settings.StoragePath = StoragePath;
|
|
settings.MaximumStorageSizeMb = 1024 * 20; // 20GB, only used when StoragePath is set.
|
|
|
|
auto manager = OpenWslcSessionManager();
|
|
wil::com_ptr<IWSLCSession> session;
|
|
VERIFY_SUCCEEDED(manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session));
|
|
wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
|
|
|
|
WSLCSessionState state{};
|
|
VERIFY_SUCCEEDED(session->GetState(&state));
|
|
VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
|
|
|
|
return session;
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslcSuccess)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcSuccess);
|
|
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-test");
|
|
|
|
LoadTestImage(*session, "debian:latest");
|
|
|
|
// Create a container that will have a stuck process so it's still in a running state when the callback is made.
|
|
wsl::windows::common::WSLCContainerLauncher launcher(
|
|
"debian:latest", "wslc-plugin-container", {"/bin/sh", "-c", "sleep 120"});
|
|
|
|
auto container = launcher.Launch(*session, WSLCContainerStartFlagsAttach);
|
|
VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
|
|
|
|
// Delete the image so we get an ImageDeleted notification before the session goes away.
|
|
WSLCDeleteImageOptions options{.Image = "debian:latest", .Flags = WSLCDeleteImageFlagsForce};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(session->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
}
|
|
|
|
const auto ExpectedOutput = std::format(
|
|
LR"(Plugin loaded. TestMode=18
|
|
WSLC Session created, name=plugin-wslc-test, id=*, pid=*, token=set, sid=set
|
|
Command: 'echo -n stdout-ok && echo -n stderr-ok >&2', status=0, stdout: stdout-ok, stderr: stderr-ok
|
|
Command: 'cat', status=0, stdout: stdin-ok, stderr:
|
|
Command: 'exit 12', status=12, stdout: , stderr:
|
|
Command: 'echo -n $ENV', status=0, stdout: env-ok, stderr:
|
|
WSLCCreateProcess(does-not-exist): {:x}, errno=2
|
|
WSLCProcessGetFd(999): {}
|
|
WSLCProcessGetExitCode(<running>): {}
|
|
WSLC RW folder mounted at: /mnt/wsl-plugin/plugin-rw-test
|
|
Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-test.txt', status=0, stdout: Windows-content, stderr:
|
|
Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-denied.txt', status=1, stdout: , stderr: *
|
|
WSLC RO folder mounted at: /mnt/wsl-plugin/plugin-ro-test
|
|
Command: 'echo fail > /mnt/wsl-plugin/plugin-ro-test/should-not-exist.txt', status=1, stdout: , stderr: *
|
|
WSLCMountFolder(nonexistent): {}
|
|
WSLCMountFolder(relative): {}
|
|
Test completed
|
|
WSLC Image created, session=*, id=sha256:*, name=debian:latest
|
|
WSLC Container started, session=*, id=*, name=/wslc-plugin-container, image=debian:latest, state=*
|
|
WSLC Container stopping, session=*, id=*
|
|
WSLC Image deleted, session=*, id=*
|
|
WSLC Session stopping, name=plugin-wslc-test, id=*)",
|
|
static_cast<uint32_t>(E_FAIL),
|
|
E_INVALIDARG,
|
|
HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
|
|
HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
|
|
E_INVALIDARG);
|
|
|
|
ValidateLogFile(ExpectedOutput.c_str());
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslcPullImageNotification)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcImagePull);
|
|
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-pull-test", WSLCNetworkingModeConsomme);
|
|
|
|
// Load the registry and debian images.
|
|
LoadTestImage(*session, "debian:latest");
|
|
|
|
// Start a local registry container.
|
|
auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
|
|
|
|
// Tag debian:latest for the local registry and push it.
|
|
auto registryImage = std::format("{}/debian:latest", registryAddress);
|
|
auto registryRepo = std::format("{}/debian", registryAddress);
|
|
WSLCTagImageOptions tagOptions{};
|
|
tagOptions.Image = "debian:latest";
|
|
tagOptions.Repo = registryRepo.c_str();
|
|
tagOptions.Tag = "latest";
|
|
VERIFY_SUCCEEDED(session->TagImage(&tagOptions));
|
|
|
|
auto emptyAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
|
|
VERIFY_SUCCEEDED(session->PushImage(registryImage.c_str(), emptyAuth.c_str(), FALSE, nullptr, nullptr));
|
|
|
|
// Delete the local tagged copy so PullImage actually downloads it.
|
|
WSLCDeleteImageOptions deleteOpts{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
|
|
VERIFY_SUCCEEDED(session->DeleteImage(&deleteOpts, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
|
|
|
|
// Pull the image back — this should trigger the ImageCreated plugin callback.
|
|
VERIFY_SUCCEEDED(session->PullImage(registryImage.c_str(), nullptr, FALSE, nullptr, nullptr));
|
|
|
|
// Publish two distinct images into one repository, one of them under two tags. An --all-tags
|
|
// pull reports a digest per tag, so this covers both that every image the pull created is
|
|
// notified and that two tags resolving to one image are notified once.
|
|
const auto versionedRepo = std::format("{}/debian-versioned", registryAddress);
|
|
auto publish = [&](LPCSTR image, LPCSTR tag) {
|
|
const auto reference = std::format("{}:{}", versionedRepo, tag);
|
|
tagOptions.Image = image;
|
|
tagOptions.Repo = versionedRepo.c_str();
|
|
tagOptions.Tag = tag;
|
|
VERIFY_SUCCEEDED(session->TagImage(&tagOptions));
|
|
VERIFY_SUCCEEDED(session->PushImage(reference.c_str(), emptyAuth.c_str(), FALSE, nullptr, nullptr));
|
|
|
|
// Drop the local tag so the pull is the only thing that can report it.
|
|
WSLCDeleteImageOptions deleteOptions{.Image = reference.c_str(), .Flags = WSLCDeleteImageFlagsNone};
|
|
wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deleted;
|
|
VERIFY_SUCCEEDED(session->DeleteImage(&deleteOptions, deleted.addressof(), deleted.size_address<ULONG>()));
|
|
};
|
|
|
|
publish("debian:latest", "v1");
|
|
publish("debian:latest", "v2");
|
|
publish("wslc-registry:latest", "v3");
|
|
|
|
VERIFY_SUCCEEDED(session->PullImage(versionedRepo.c_str(), nullptr, TRUE, nullptr, nullptr));
|
|
}
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=21
|
|
WSLC Session created, name=plugin-wslc-pull-test, id=*, pid=*, token=set, sid=set
|
|
WSLC Image created, session=*, id=sha256:*, name=debian:latest
|
|
WSLC Image created, session=*, id=sha256:*, name=wslc-registry:latest
|
|
WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running
|
|
WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest
|
|
WSLC Image created, session=*, id=sha256:*, name=*
|
|
WSLC Image created, session=*, id=sha256:*, name=*
|
|
WSLC Container stopping, session=*, id=*
|
|
WSLC Session stopping, name=plugin-wslc-pull-test, id=*)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslcSessionRejected)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcSessionRejected);
|
|
|
|
WSLCSessionSettings settings{};
|
|
settings.DisplayName = L"plugin-wslc-rejected";
|
|
settings.CpuCount = 4;
|
|
settings.MemoryMb = 2048;
|
|
settings.BootTimeoutMs = 30 * 1000;
|
|
settings.MaximumStorageSizeMb = 1024 * 20;
|
|
settings.NetworkingMode = WSLCNetworkingModeNone;
|
|
|
|
auto manager = OpenWslcSessionManager();
|
|
wil::com_ptr<IWSLCSession> session;
|
|
const auto hr = manager->CreateSession(&settings, WSLCSessionFlagsNone, nullptr, &session);
|
|
ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
|
|
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=19
|
|
WSLC Session created, name=plugin-wslc-rejected, id=*, pid=*, token=set, sid=set
|
|
OnWslcSessionCreated: ERROR_ACCESS_DENIED)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslcContainerRejected)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcContainerRejected);
|
|
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-container-rejected");
|
|
|
|
LoadTestImage(*session, "debian:latest");
|
|
|
|
wsl::windows::common::WSLCContainerLauncher launcher(
|
|
"debian:latest", "wslc-plugin-rejected-container", {"/bin/sh", "-c", "echo nope"});
|
|
|
|
auto [hr, container] = launcher.LaunchNoThrow(*session, WSLCContainerStartFlagsAttach);
|
|
ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
|
|
VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
|
|
}
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=20
|
|
WSLC Session created, name=plugin-wslc-container-rejected, id=*, pid=*, token=set, sid=set
|
|
WSLC Image created, session=*, id=sha256:*, name=debian:latest
|
|
WSLC Container started, session=*, id=*, name=*, image=debian:latest, state=*
|
|
OnWslcContainerStarted: ERROR_ACCESS_DENIED
|
|
WSLC Session stopping, name=plugin-wslc-container-rejected, id=*)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
// Validates the VM-lifecycle hooks: OnWslcVmStarted fires each time the VM is (re)created and
|
|
// OnWslcVmStopping each time it is torn down, decoupled from the once-per-session hooks. Also
|
|
// proves the started hook can call back into the session (WSLCCreateProcess) without deadlocking.
|
|
WSL2_TEST_METHOD(WslcVmRestart)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcVmRestart);
|
|
|
|
// Idle termination only tears down storage-backed sessions (tmpfs state is unrecoverable), so
|
|
// this restart lifecycle test needs a dedicated persistent storage directory.
|
|
const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-restart";
|
|
std::error_code storageError;
|
|
std::filesystem::remove_all(storageDir, storageError);
|
|
std::filesystem::create_directories(storageDir);
|
|
auto storageCleanup = wil::scope_exit([&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(storageDir, ec);
|
|
});
|
|
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-vm-restart", WSLCNetworkingModeNone, storageDir.c_str());
|
|
|
|
// First operation brings the VM up -> OnWslcVmStarted (which reentrantly runs a process).
|
|
{
|
|
wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
|
|
auto process = launcher.Launch(*session);
|
|
}
|
|
|
|
// Force idle teardown of the running VM -> OnWslcVmStopping.
|
|
BOOL wasAlreadyIdle = TRUE;
|
|
VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
|
|
VERIFY_IS_FALSE(wasAlreadyIdle);
|
|
|
|
// Next operation lazily restarts the VM -> OnWslcVmStarted fires again.
|
|
{
|
|
wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
|
|
auto process = launcher.Launch(*session);
|
|
}
|
|
|
|
// Session teardown tears the second VM down -> OnWslcVmStopping, then OnWslcSessionStopping.
|
|
}
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=22
|
|
WSLC Session created, name=plugin-wslc-vm-restart, id=*, pid=*, token=set, sid=set
|
|
WSLC VM started, session=*
|
|
WSLC VM started reentrant WSLCCreateProcess: ok
|
|
WSLC VM started mount+unmount: ok
|
|
WSLC VM stopping, session=*
|
|
WSLC VM stopping reentrant WSLCCreateProcess: ok
|
|
WSLC VM stopping mount+unmount: ok
|
|
WSLC VM started, session=*
|
|
WSLC VM started reentrant WSLCCreateProcess: ok
|
|
WSLC VM started mount+unmount: ok
|
|
WSLC VM stopping, session=*
|
|
WSLC VM stopping reentrant WSLCCreateProcess: failed
|
|
WSLC VM stopping mount+unmount: skipped
|
|
WSLC Session stopping, name=plugin-wslc-vm-restart, id=*)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
// Validates that an announced VM teardown always happens. The plugin leaves a process running when
|
|
// OnWslcVmStopping returns -- which under the previous design made the runtime abandon the
|
|
// teardown -- and starts a call from a thread of its own during the notification window. The VM
|
|
// must stop anyway, the leaked process must die with it, and the windowed call must be served by
|
|
// the stopping VM rather than blocking on the teardown it cannot influence.
|
|
WSL2_TEST_METHOD(WslcVmStopCommitted)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcVmStopCommitted);
|
|
|
|
// Idle termination only tears down storage-backed sessions (see WslcVmRestart).
|
|
const auto storageDir = std::filesystem::current_path() / "test-storage-wslc-vm-stop-committed";
|
|
std::error_code storageError;
|
|
std::filesystem::remove_all(storageDir, storageError);
|
|
std::filesystem::create_directories(storageDir);
|
|
auto storageCleanup = wil::scope_exit([&]() {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(storageDir, ec);
|
|
});
|
|
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-vm-stop-committed", WSLCNetworkingModeNone, storageDir.c_str());
|
|
|
|
// Bring the VM up -> OnWslcVmStarted.
|
|
{
|
|
wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
|
|
auto process = launcher.Launch(*session);
|
|
}
|
|
|
|
// The teardown is announced and then carried out, even though the plugin's callback left a
|
|
// process running. The session is genuinely idle afterwards.
|
|
BOOL wasAlreadyIdle = TRUE;
|
|
VERIFY_SUCCEEDED(session->TriggerIdleTermination(&wasAlreadyIdle));
|
|
VERIFY_IS_FALSE(wasAlreadyIdle);
|
|
|
|
// The VM is gone, so this starts a fresh one -> a second OnWslcVmStarted.
|
|
{
|
|
wsl::windows::common::WSLCProcessLauncher launcher("/bin/sleep", {"/bin/sleep", "60"});
|
|
auto process = launcher.Launch(*session);
|
|
}
|
|
}
|
|
|
|
// "leaked process died: yes" is the assertion that the announced stop actually happened: the
|
|
// process the callback left running was killed by the teardown rather than keeping the VM
|
|
// alive. "stop-window caller: ok" shows a plugin call from another thread was served by the
|
|
// stopping VM instead of deadlocking against the teardown that callback was holding up. Both
|
|
// are reported when that thread is joined, at session teardown.
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=23
|
|
WSLC Session created, name=plugin-wslc-vm-stop-committed, id=*, pid=*, token=set, sid=set
|
|
WSLC VM started, session=*
|
|
WSLC VM stopping, session=*
|
|
WSLC VM stopping leaked process: ok
|
|
WSLC VM started, session=*
|
|
WSLC stop-window caller: ok
|
|
WSLC leaked process died: yes
|
|
WSLC Session stopping, name=plugin-wslc-vm-stop-committed, id=*)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
WSL2_TEST_METHOD(WslcVmNeverStarted)
|
|
{
|
|
ConfigurePlugin(PluginTestType::WslcVmNeverStarted);
|
|
|
|
// A session whose VM is never needed. VM bring-up is lazy, so creating and destroying the
|
|
// session must not produce either VM notification: OnWslcVmStopping is documented to fire
|
|
// exactly once per OnWslcVmStarted, and a stop for a VM that never existed would break the
|
|
// pairing every plugin relies on to track VM lifetime. The plugin also issues a call from
|
|
// OnWslcSessionCreated, which must be rejected rather than bring a VM up.
|
|
{
|
|
auto session = CreateWslcSession(L"plugin-wslc-vm-never-started");
|
|
VERIFY_IS_NOT_NULL(session.get());
|
|
}
|
|
|
|
constexpr auto ExpectedOutput =
|
|
LR"(Plugin loaded. TestMode=24
|
|
WSLC Session created, name=plugin-wslc-vm-never-started, id=*, pid=*, token=set, sid=set
|
|
WSLC no-vm caller: rejected
|
|
WSLC Session stopping, name=plugin-wslc-vm-never-started, id=*)";
|
|
|
|
ValidateLogFile(ExpectedOutput);
|
|
}
|
|
|
|
// This test must run last so it doesn't break test cases that depends on plugin signature.
|
|
WSL2_TEST_METHOD(InvalidPluginSignature)
|
|
{
|
|
if constexpr (!wsl::shared::OfficialBuild)
|
|
{
|
|
LogSkipped("This test only applies to signed builds");
|
|
return;
|
|
}
|
|
|
|
StopWslService();
|
|
|
|
// Append one byte at the end of the plugin dll to break its signature
|
|
wil::unique_handle plugin{CreateFile(pluginDll.c_str(), FILE_APPEND_DATA, 0, nullptr, OPEN_EXISTING, 0, nullptr)};
|
|
VERIFY_IS_TRUE(plugin.is_valid());
|
|
|
|
char c{};
|
|
VERIFY_IS_TRUE(WriteFile(plugin.get(), &c, sizeof(c), nullptr, nullptr));
|
|
plugin.reset();
|
|
|
|
ConfigurePlugin(PluginTestType::ErrorMessageStartDistro);
|
|
StartWsl(
|
|
-1,
|
|
FormatErrorMessage(
|
|
L"A fatal error was returned by plugin 'TestPlugin'",
|
|
L"Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE"));
|
|
}
|
|
};
|