Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
901ec06
Implement Stop Container
Dec 9, 2025
be9cdce
Add TODO for testing Stop() on a container in 'Created' state
Dec 9, 2025
d1aab64
Update src/windows/wslaservice/exe/WSLAContainer.cpp
ptrivedi Dec 9, 2025
63ec07c
Update src/windows/wslaservice/exe/WSLAContainer.cpp
ptrivedi Dec 9, 2025
38d53f6
Update src/windows/wslaservice/exe/WSLAContainer.cpp
ptrivedi Dec 9, 2025
c614d36
Fix timeout, state, etc.
Dec 9, 2025
d7f3e27
Change to using a recursive_mutex for now
Dec 9, 2025
ae4c079
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/stop-cont
ptrivedi Dec 9, 2025
938f482
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/stop-cont
ptrivedi Dec 10, 2025
bcbd3b0
Merge branch 'wsl-for-apps' into user/ptrivedi/stop-cont
Dec 10, 2025
0643572
Fix merge issues and change to recursive_mutex after merge
Dec 10, 2025
9fcb838
Merge branch 'user/ptrivedi/stop-cont' of https://github.com/microsof…
Dec 10, 2025
7286ff9
Implement container network mode
Dec 12, 2025
e0153c3
Change container network type to enum
Dec 12, 2025
f2dd958
Merge branch 'wsl-for-apps' into user/ptrivedi/cont-network-mode
Dec 12, 2025
d2b4155
Merge branch 'feature/wsl-for-apps' into user/ptrivedi/cont-network-mode
ptrivedi Dec 15, 2025
40007d7
Implement code review feedback
Dec 15, 2025
c19e851
Merge branch 'user/ptrivedi/cont-network-mode' of https://github.com/…
Dec 15, 2025
592439c
Fix merge issue: Pass container network type to WSLAContainerLauncher
Dec 15, 2025
4cf5c11
Incorporate copilot code review suggestion
Dec 15, 2025
606dc8d
Fix formatting and change hresult return to E_INVALIDARG for invalid
Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/windows/common/WSLAContainerLauncher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ WSLAContainerLauncher::WSLAContainerLauncher(
const std::string& EntryPoint,
const std::vector<std::string>& Arguments,
const std::vector<std::string>& Environment,
WSLA_CONTAINER_NETWORK_TYPE containerNetworkType,
ProcessFlags Flags) :
WSLAProcessLauncher(EntryPoint, Arguments, Environment, Flags), m_image(Image), m_name(Name)
WSLAProcessLauncher(EntryPoint, Arguments, Environment, Flags), m_image(Image), m_name(Name), m_containerNetworkType(containerNetworkType)
{
}

Expand All @@ -60,6 +61,7 @@ std::pair<HRESULT, std::optional<RunningWSLAContainer>> WSLAContainerLauncher::L
options.Name = m_name.c_str();
auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions();
options.InitProcessOptions = processOptions;
options.ContainerNetwork.ContainerNetworkType = m_containerNetworkType;

if (m_executable.empty())
{
Expand Down
2 changes: 2 additions & 0 deletions src/windows/common/WSLAContainerLauncher.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class WSLAContainerLauncher : private WSLAProcessLauncher
const std::string& EntryPoint = "",
const std::vector<std::string>& Arguments = {},
const std::vector<std::string>& Environment = {},
WSLA_CONTAINER_NETWORK_TYPE containerNetworkType = WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags Flags = ProcessFlags::Stdout | ProcessFlags::Stderr);

void AddVolume(const std::string& HostPath, const std::string& ContainerPath, bool ReadOnly);
Expand All @@ -56,5 +57,6 @@ class WSLAContainerLauncher : private WSLAProcessLauncher
private:
std::string m_image;
std::string m_name;
WSLA_CONTAINER_NETWORK_TYPE m_containerNetworkType;
};
} // namespace wsl::windows::common
40 changes: 32 additions & 8 deletions src/windows/wslaservice/exe/WSLAContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ Module Name:

using wsl::windows::service::wsla::WSLAContainer;

// Constants for required default arguments for "nerdctl run..."
static std::vector<std::string> defaultNerdctlRunArgs{//"--pull=never", // TODO: Uncomment once PullImage() is implemented.
"--net=host", // TODO: default for now, change later
"--ulimit",
"nofile=65536:65536"};
// Constants for required default arguments for "nerdctl create..."
static std::vector<std::string> defaultNerdctlCreateArgs{//"--pull=never", // TODO: Uncomment once PullImage() is implemented.
"--ulimit",
"nofile=65536:65536"};

WSLAContainer::WSLAContainer(WSLAVirtualMachine* parentVM, const WSLA_CONTAINER_OPTIONS& Options, std::string&& Id, ContainerEventTracker& tracker) :
m_parentVM(parentVM), m_name(Options.Name), m_image(Options.Image), m_id(std::move(Id))
Expand Down Expand Up @@ -102,7 +101,7 @@ void WSLAContainer::OnEvent(ContainerEvent event)
HRESULT WSLAContainer::Stop(int Signal, ULONG TimeoutMs)
try
{
std::lock_guard<std::recursive_mutex> lock(m_lock);
std::lock_guard lock(m_lock);

if (State() == WslaContainerStateExited)
{
Expand Down Expand Up @@ -324,6 +323,31 @@ std::vector<std::string> WSLAContainer::PrepareNerdctlCreateCommand(const WSLA_C
args.push_back("create");
args.push_back("--name");
args.push_back(options.Name);

switch (options.ContainerNetwork.ContainerNetworkType)
{
case WSLA_CONTAINER_NETWORK_HOST:
args.push_back("--net=host");
break;
case WSLA_CONTAINER_NETWORK_NONE:
args.push_back("--net=none");
break;
case WSLA_CONTAINER_NETWORK_BRIDGE:
args.push_back("--net=bridge");
break;
// TODO: uncomment and implement when we have custom networks
// case WSLA_CONTAINER_NETWORK_CUSTOM:
// args.push_back(std::format("--net={}", options.ContainerNetwork.ContainerNetworkName));
// break;
default:
THROW_HR_MSG(
E_INVALIDARG,
"No such network: type: %i, name: %hs",
options.ContainerNetwork.ContainerNetworkType,
options.ContainerNetwork.ContainerNetworkName);
break;
}

if (options.ShmSize > 0)
{
args.push_back(std::format("--shm-size={}m", options.ShmSize));
Expand All @@ -335,7 +359,7 @@ std::vector<std::string> WSLAContainer::PrepareNerdctlCreateCommand(const WSLA_C
args.push_back("all");
}

args.insert(args.end(), defaultNerdctlRunArgs.begin(), defaultNerdctlRunArgs.end());
args.insert(args.end(), defaultNerdctlCreateArgs.begin(), defaultNerdctlCreateArgs.end());
args.insert(args.end(), inputOptions.begin(), inputOptions.end());

if (options.InitProcessOptions.Executable != nullptr)
Expand Down Expand Up @@ -383,4 +407,4 @@ std::optional<std::string> WSLAContainer::GetNerdctlStatus()

// N.B. nerdctl inspect can return with exit code 0 and no output. Return an empty optional if that happens.
return status.empty() ? std::optional<std::string>{} : status;
}
}
15 changes: 15 additions & 0 deletions src/windows/wslaservice/inc/wslaservice.idl
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ struct WSLA_PORT_MAPPING
USHORT ContainerPort;
};

enum WSLA_CONTAINER_NETWORK_TYPE
{
WSLA_CONTAINER_NETWORK_BRIDGE = 0,
WSLA_CONTAINER_NETWORK_HOST = 1,
WSLA_CONTAINER_NETWORK_NONE = 2,
// WSLA_CONTAINER_NETWORK_CUSTOM = 3 // TODO: Implement when implementing custom networks
};
struct WSLA_CONTAINER_NETWORK
{
// TODO: Change default to bridge when implemented
enum WSLA_CONTAINER_NETWORK_TYPE ContainerNetworkType;
LPCSTR ContainerNetworkName;
};

enum WSLA_CONTAINER_FLAGS
{
WSLA_CONTAINER_FLAG_ENABLE_GPU = 1
Expand All @@ -154,6 +168,7 @@ struct WSLA_CONTAINER_OPTIONS
// TODO: List specific GPU devices.
// TODO: Flags on wether the caller wants to override entrypoint, args, or both.
ULONGLONG ShmSize;
struct WSLA_CONTAINER_NETWORK ContainerNetwork;
};

enum WSLA_CONTAINER_STATE
Expand Down
197 changes: 190 additions & 7 deletions test/windows/WSLATests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,13 @@ class WSLATests

{
WSLAContainerLauncher launcher(
"debian:latest", "test-default-entrypoint", "/bin/cat", {}, {}, ProcessFlags::Stdin | ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-default-entrypoint",
"/bin/cat",
{},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdin | ProcessFlags::Stdout | ProcessFlags::Stderr);

// For now, validate that trying to use stdin without a tty returns the appropriate error.
auto result = wil::ResultFromException([&]() { auto container = launcher.Launch(*session); });
Expand Down Expand Up @@ -1268,7 +1274,13 @@ class WSLATests

// Create a stuck container.
WSLAContainerLauncher launcher(
"debian:latest", "test-container-1", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-container-1",
"sleep",
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);

Expand Down Expand Up @@ -1312,7 +1324,13 @@ class WSLATests
{
// Create a container
WSLAContainerLauncher launcher(
"debian:latest", "test-container-2", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-container-2",
"sleep",
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);

Expand Down Expand Up @@ -1346,7 +1364,13 @@ class WSLATests
// Validate that container names are unique.
{
WSLAContainerLauncher launcher(
"debian:latest", "test-unique-name", "sleep", {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-unique-name",
"sleep",
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning);
Expand Down Expand Up @@ -1396,14 +1420,167 @@ class WSLATests

// Verify that the same name can be reused now that the container is deleted.
WSLAContainerLauncher otherLauncher(
"debian:latest", "test-unique-name", "echo", {"OK"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-unique-name",
"echo",
{"OK"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto result = otherLauncher.Launch(*session).GetInitProcess().WaitAndCaptureOutput();
VERIFY_ARE_EQUAL(result.Output[1], "OK\n");
VERIFY_ARE_EQUAL(result.Code, 0);
}
}

TEST_METHOD(ContainerNetwork)
{
WSL2_TEST_ONLY();
SKIP_TEST_ARM64();

auto storagePath = std::filesystem::current_path() / "test-storage";

auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
std::error_code error;

std::filesystem::remove_all(storagePath, error);
if (error)
{
LogError("Failed to cleanup storage path %ws: %hs", storagePath.c_str(), error.message().c_str());
}
});

auto settings = GetDefaultSessionSettings();
settings.NetworkingMode = WSLANetworkingModeNAT;
settings.StoragePath = storagePath.c_str();
settings.MaximumStorageSizeMb = 1024;

auto session = CreateSession(settings);

auto expectContainerList = [&](const std::vector<std::tuple<std::string, std::string, WSLA_CONTAINER_STATE>>& expectedContainers) {
wil::unique_cotaskmem_array_ptr<WSLA_CONTAINER> containers;

VERIFY_SUCCEEDED(session->ListContainers(&containers, containers.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 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
{
WSLAContainerLauncher launcher(
"debian:latest",
"test-network",
{},
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_HOST,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning);
auto result = ExpectCommandResult(
session.get(),
{"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"},
0);
VERIFY_ARE_EQUAL(result.Output[1], "'[\"host\"]'\n");

VERIFY_SUCCEEDED(container.Get().Stop(15, 50000));

expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}});

// Verify that the container is in exited state.
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited);

// Verify that deleting a container stopped via Stop() works.
VERIFY_SUCCEEDED(container.Get().Delete());

expectContainerList({});
}

{
WSLAContainerLauncher launcher(
"debian:latest",
"test-network",
{},
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning);
auto result = ExpectCommandResult(
session.get(),
{"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"},
0);
VERIFY_ARE_EQUAL(result.Output[1], "'[\"none\"]'\n");

VERIFY_SUCCEEDED(container.Get().Stop(15, 50000));

expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}});

// Verify that the container is in exited state.
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited);

// Verify that deleting a container stopped via Stop() works.
VERIFY_SUCCEEDED(container.Get().Delete());

expectContainerList({});
}

{
WSLAContainerLauncher launcher(
"debian:latest",
"test-network",
{},
{"sleep", "99999"},
{},
(WSLA_CONTAINER_NETWORK_TYPE)6, // WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto retVal = launcher.LaunchNoThrow(*session);
VERIFY_ARE_EQUAL(retVal.first, E_INVALIDARG);
}

// Test bridge when ready
/*
{
WSLAContainerLauncher launcher(
"debian:latest", "test-network", {}, {"sleep", "99999"}, {}, WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_BRIDGE, ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning);
auto result = ExpectCommandResult(
session.get(),
{"/usr/bin/nerdctl", "inspect", "-f", "'{{ index .Config.Labels \"nerdctl/networks\" }}'", "test-network"},
0);
VERIFY_ARE_EQUAL(result.Output[1], "'[\"bridge\"]'\n");

VERIFY_SUCCEEDED(container.Get().Stop(15, 50000));

expectContainerList({{"test-network", "debian:latest", WslaContainerStateExited}});

// Verify that the container is in exited state.
VERIFY_ARE_EQUAL(container.State(), WslaContainerStateExited);

// Verify that deleting a container stopped via Stop() works.
VERIFY_SUCCEEDED(container.Get().Delete());

expectContainerList({});
}
*/
}

TEST_METHOD(Exec)
{
WSL2_TEST_ONLY();
Expand All @@ -1416,7 +1593,13 @@ class WSLATests

// Create a container.
WSLAContainerLauncher launcher(
"debian:latest", "test-container-exec", {}, {"sleep", "99999"}, {}, ProcessFlags::Stdout | ProcessFlags::Stderr);
"debian:latest",
"test-container-exec",
{},
{"sleep", "99999"},
{},
WSLA_CONTAINER_NETWORK_TYPE::WSLA_CONTAINER_NETWORK_NONE,
ProcessFlags::Stdout | ProcessFlags::Stderr);

auto container = launcher.Launch(*session);

Expand Down Expand Up @@ -1480,4 +1663,4 @@ class WSLATests
// TODO: Implement proper handling of executables that don't exist in the container.
}
}
};
};