Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/windows/service/inc/wslaservice.idl
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ typedef struct _WSLAContainerEntry
ULONGLONG StateChangedAt;
ULONGLONG CreatedAt;
WSLAContainerState State;
[unique, size_is(PortsCount)] WSLAPortMapping* Ports;
ULONG PortsCount;
} WSLAContainerEntry;

typedef enum _WSLAProcessState
Expand Down
5 changes: 5 additions & 0 deletions src/windows/wslasession/WSLAContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ const std::string& WSLAContainerImpl::Name() const noexcept
return m_name;
}

const std::vector<ContainerPortMapping>& WSLAContainerImpl::GetPorts() const noexcept
{
return m_mappedPorts;
}

void WSLAContainerImpl::GetStateChangedAt(ULONGLONG* Result)
{
auto lock = m_lock.lock_shared();
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslasession/WSLAContainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class WSLAContainerImpl
const std::string& Image() const noexcept;
const std::string& Name() const noexcept;
WSLAContainerState State() const noexcept;
const std::vector<ContainerPortMapping>& GetPorts() const noexcept;

__requires_lock_held(m_lock) void Transition(WSLAContainerState State) noexcept;

Expand Down
47 changes: 47 additions & 0 deletions src/windows/wslasession/WSLASession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,17 @@ try
auto output = wil::make_unique_cotaskmem<WSLAContainerEntry[]>(m_containers.size());

size_t index = 0;
auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
for (size_t i = 0; i < index; ++i)
{
for (ULONG j = 0; j < output[i].PortsCount; ++j)
{
CoTaskMemFree(const_cast<LPSTR>(output[i].Ports[j].BindingAddress));
}
CoTaskMemFree(output[i].Ports);
}
});

for (const auto& e : m_containers)
{
THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, e->Image().c_str()) != 0);
Expand All @@ -1172,9 +1183,45 @@ try
e->GetState(&output[index].State);
e->GetStateChangedAt(&output[index].StateChangedAt);
e->GetCreatedAt(&output[index].CreatedAt);

const auto& ports = e->GetPorts();
if (!ports.empty())
{
auto portsArray = wil::make_unique_cotaskmem<::WSLAPortMapping[]>(ports.size());
size_t portsBuilt = 0;
auto portsCleanup = wil::scope_exit([&]() {
for (size_t j = 0; j < portsBuilt; ++j)
{
CoTaskMemFree(const_cast<LPSTR>(portsArray[j].BindingAddress));
}
});

for (size_t i = 0; i < ports.size(); ++i)
{
portsArray[i].HostPort = ports[i].VmMapping.HostPort();
portsArray[i].ContainerPort = ports[i].ContainerPort;
portsArray[i].Family = ports[i].VmMapping.BindAddress.si_family;
portsArray[i].Protocol = ports[i].VmMapping.Protocol;
portsArray[i].BindingAddress =
wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(ports[i].VmMapping.BindingAddressString().c_str())
.release();
portsBuilt++;
}

output[index].Ports = portsArray.release();
output[index].PortsCount = static_cast<ULONG>(ports.size());
portsCleanup.release();
}
else
{
output[index].Ports = nullptr;
output[index].PortsCount = 0;
}

index++;
}

errorCleanup.release();
*Count = static_cast<ULONG>(m_containers.size());
*Containers = output.release();
return S_OK;
Expand Down
13 changes: 12 additions & 1 deletion src/windows/wslc/services/ContainerModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ struct KillContainerOptions
int Signal = WSLASignalSIGKILL;
};

struct PortInformation
{
uint16_t HostPort{};
uint16_t ContainerPort{};
int Family{}; // AF_INET or AF_INET6
int Protocol{}; // IP protocol number (e.g., IPPROTO_TCP or IPPROTO_UDP)

NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(PortInformation, HostPort, ContainerPort, Family, Protocol);
};

struct ContainerInformation
{
std::string Id;
Expand All @@ -65,8 +75,9 @@ struct ContainerInformation
WSLAContainerState State;
ULONGLONG StateChangedAt{};
ULONGLONG CreatedAt{};
std::vector<PortInformation> Ports;

NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt);
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(ContainerInformation, Id, Name, Image, State, StateChangedAt, CreatedAt, Ports);
};

struct PublishPort
Expand Down
46 changes: 46 additions & 0 deletions src/windows/wslc/services/ContainerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,33 @@ std::wstring ContainerService::ContainerStateToString(WSLAContainerState state,
return std::format(L"{} {}", stateString, FormatRelativeTime(stateChangedAt));
}

std::wstring ContainerService::FormatPorts(const std::vector<PortInformation>& ports)
{
if (ports.empty())
{
return L"";
}

std::wstring result;
for (size_t i = 0; i < ports.size(); ++i)
{
const auto& port = ports[i];

// AF_INET = 2, AF_INET6 = 23
std::wstring hostIp = (port.Family == AF_INET6) ? L"[::]" : L"0.0.0.0";
std::wstring protocol = (port.Protocol == IPPROTO_UDP) ? L"udp" : L"tcp";

if (i > 0)
{
result += L", ";
}

result += std::format(L"{}:{}->{}/{}", hostIp, port.HostPort, port.ContainerPort, protocol);
Comment on lines +261 to +270
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormatPorts() hardcodes the host IP to 0.0.0.0 / [::] based only on address family and treats any non-UDP protocol as TCP. Since the service already provides a per-mapping BindingAddress, this output can be incorrect (and misleading) for localhost-bound mappings (e.g., 127.0.0.1 / ::1) and for any future protocols. Use the actual binding address from the model (format IPv6 with brackets) and handle unexpected protocol values explicitly (e.g., show the numeric protocol).

Copilot uses AI. Check for mistakes.
}

return result;
}

int ContainerService::Run(Session& session, const std::string& image, ContainerOptions runOptions, IProgressCallback* callback)
{
// Create the container
Expand Down Expand Up @@ -323,6 +350,25 @@ std::vector<ContainerInformation> ContainerService::List(Session& session)
entry.Id = current.Id;
entry.StateChangedAt = current.StateChangedAt;
entry.CreatedAt = current.CreatedAt;

// Extract ports
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend putting port extraction/processing into a common method, we probably want to use the same for argument validation.

for (ULONG i = 0; i < current.PortsCount; ++i)
{
PortInformation port;
port.HostPort = current.Ports[i].HostPort;
port.ContainerPort = current.Ports[i].ContainerPort;
port.Family = current.Ports[i].Family;
port.Protocol = static_cast<int>(current.Ports[i].Protocol);
entry.Ports.push_back(port);
}

// Free nested ports array
for (ULONG i = 0; i < current.PortsCount; ++i)
{
CoTaskMemFree(const_cast<LPSTR>(current.Ports[i].BindingAddress));
}
CoTaskMemFree(current.Ports);

result.emplace_back(std::move(entry));
}

Expand Down
1 change: 1 addition & 0 deletions src/windows/wslc/services/ContainerService.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct ContainerService
{
static std::wstring ContainerStateToString(WSLAContainerState state, ULONGLONG stateChangedAt = 0);
static std::wstring FormatRelativeTime(ULONGLONG timestamp);
static std::wstring FormatPorts(const std::vector<models::PortInformation>& ports);
static int Attach(models::Session& session, const std::string& id);
static int Run(models::Session& session, const std::string& image, models::ContainerOptions options, IProgressCallback* callback);
static models::CreateContainerResult Create(models::Session& session, const std::string& image, models::ContainerOptions options, IProgressCallback* callback);
Expand Down
3 changes: 2 additions & 1 deletion src/windows/wslc/tasks/ContainerTasks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ void ListContainers(CLIExecutionContext& context)
}
case FormatType::Table:
{
utils::TablePrinter tablePrinter({L"ID", L"NAME", L"IMAGE", L"CREATED", L"STATUS"});
utils::TablePrinter tablePrinter({L"ID", L"NAME", L"IMAGE", L"CREATED", L"STATUS", L"PORTS"});
for (const auto& container : containers)
{
tablePrinter.AddRow({
Expand All @@ -148,6 +148,7 @@ void ListContainers(CLIExecutionContext& context)
MultiByteToWide(container.Image),
ContainerService::FormatRelativeTime(container.CreatedAt),
ContainerService::ContainerStateToString(container.State, container.StateChangedAt),
ContainerService::FormatPorts(container.Ports),
});
}

Expand Down
41 changes: 41 additions & 0 deletions test/windows/WSLATests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3143,6 +3143,7 @@ class WSLATests
VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLA_CONTAINER_ID_LENGTH);
VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
VERIFY_ARE_EQUAL(0, containers[i].PortsCount);
}
};

Expand Down Expand Up @@ -3410,6 +3411,7 @@ class WSLATests
VERIFY_ARE_EQUAL(strlen(containers[i].Id), WSLA_CONTAINER_ID_LENGTH);
VERIFY_IS_TRUE(containers[i].StateChangedAt > 0);
VERIFY_IS_TRUE(containers[i].CreatedAt > 0);
VERIFY_ARE_EQUAL(0, containers[i].PortsCount);
}
};

Expand Down Expand Up @@ -3853,6 +3855,45 @@ class WSLATests

ExpectHttpResponse(L"http://[::1]:1234", 200);

// Verify that ListContainers returns the port data.
{
wil::unique_cotaskmem_array_ptr<WSLAContainerEntry> containers;
VERIFY_SUCCEEDED(session.ListContainers(&containers, containers.size_address<ULONG>()));

auto freeNestedPorts = wil::scope_exit([&]() noexcept {
for (const auto& entry : containers)
{
for (ULONG j = 0; j < entry.PortsCount; ++j)
{
CoTaskMemFree(const_cast<LPSTR>(entry.Ports[j].BindingAddress));
}
CoTaskMemFree(entry.Ports);
}
});

bool found = false;
for (const auto& entry : containers)
{
if (std::string(entry.Name) == "test-ports")
{
found = true;
VERIFY_IS_NOT_NULL(entry.Ports);
VERIFY_ARE_EQUAL(2, entry.PortsCount);
VERIFY_ARE_EQUAL(1234, entry.Ports[0].HostPort);
VERIFY_ARE_EQUAL(8000, entry.Ports[0].ContainerPort);
VERIFY_ARE_EQUAL(AF_INET, entry.Ports[0].Family);
VERIFY_ARE_EQUAL(1234, entry.Ports[1].HostPort);
VERIFY_ARE_EQUAL(8000, entry.Ports[1].ContainerPort);
VERIFY_ARE_EQUAL(AF_INET6, entry.Ports[1].Family);
VERIFY_ARE_EQUAL(IPPROTO_TCP, entry.Ports[0].Protocol);
VERIFY_ARE_EQUAL(IPPROTO_TCP, entry.Ports[1].Protocol);
break;
}
}

VERIFY_IS_TRUE(found);
}

// Validate that the port cannot be reused while the container is running.
WSLAContainerLauncher subLauncher(
"python:3.12-alpine", "test-ports-2", {"python3", "-m", "http.server"}, {"PYTHONUNBUFFERED=1"}, containerNetworkType);
Expand Down
Loading