diff --git a/src/windows/common/HandleIO.cpp b/src/windows/common/HandleIO.cpp index a49268023..7fe3ba25d 100644 --- a/src/windows/common/HandleIO.cpp +++ b/src/windows/common/HandleIO.cpp @@ -9,6 +9,7 @@ using wsl::windows::common::io::DockerIORelayHandle; using wsl::windows::common::io::EventHandle; using wsl::windows::common::io::HandleWrapper; using wsl::windows::common::io::HTTPChunkBasedReadHandle; +using wsl::windows::common::io::IOCPHandle; using wsl::windows::common::io::IOHandleStatus; using wsl::windows::common::io::LineBasedReadHandle; using wsl::windows::common::io::MultiHandleWait; @@ -61,8 +62,90 @@ void CancelPendingIo(auto Handle, OVERLAPPED& Overlapped) } } +// FileReplaceCompletionInformation (introduced in Windows 8.1) lets us swap or remove +// the I/O completion port association of a kernel file object. Neither the info class +// constant nor the FILE_COMPLETION_INFORMATION layout are exposed via the user-mode +// SDK headers, so they're declared locally. +constexpr auto c_fileReplaceCompletionInformation = static_cast(61); + +struct FILE_COMPLETION_INFORMATION +{ + HANDLE Port; + PVOID Key; +}; + +// NTSTATUS codes - winnt.h only exposes a small subset, the rest are in ntstatus.h +// which conflicts with windows.h. Defined locally to avoid the conflict. +constexpr NTSTATUS c_statusInvalidInfoClass = static_cast(0xC0000003L); +constexpr NTSTATUS c_statusObjectTypeMismatch = static_cast(0xC0000024L); + +void DetachFromIocp(HANDLE Handle) +{ + FILE_COMPLETION_INFORMATION info{}; + IO_STATUS_BLOCK iosb{}; + const NTSTATUS status = NtSetInformationFile(Handle, &iosb, &info, sizeof(info), c_fileReplaceCompletionInformation); + + // Best-effort: failures are expected for handles that were never associated, for + // objects that aren't kernel file objects (events, console handles, ...), and on + // pre-Windows 8.1 systems that don't recognize the info class. The handle is being + // destroyed regardless so there's nothing to recover. + if (!NT_SUCCESS(status)) + { + LOG_HR_IF(HRESULT_FROM_NT(status), status != STATUS_INVALID_PARAMETER && status != c_statusObjectTypeMismatch && status != c_statusInvalidInfoClass); + } +} + } // namespace +void IOCPHandle::Bind(HANDLE Handle, HANDLE Iocp, ULONG_PTR Key) +{ + WI_ASSERT(m_handle == nullptr); + + // Detach the handle from any previous IOCP first. The previous IOCP may have + // been closed (a prior MultiHandleWait that owned it has gone out of scope) or + // may still be live (e.g. a sibling boost::asio::stream::assign just bound it). + // Either way the kernel only ever permits one IOCP+key association per handle, + // so the new association will fail with ERROR_INVALID_PARAMETER unless any + // existing one is cleared first. + DetachFromIocp(Handle); + + if (CreateIoCompletionPort(Handle, Iocp, Key, 0) == nullptr) + { + const auto error = GetLastError(); + if (error == ERROR_INVALID_PARAMETER) + { + // Handle doesn't support overlapped I/O (anonymous pipe, console handle, ...). + // The caller is expected to complete its I/O synchronously inside Schedule(). + // Leave m_handle == nullptr so the destructor is a no-op. + return; + } + + THROW_WIN32(error); + } + + m_handle = Handle; + + // Best-effort: not every device type supports FileCompletionNotificationModes. If + // unsupported, the worst case is a redundant packet for a synchronous success which + // is filtered out in MultiHandleWait::Run. + if (!SetFileCompletionNotificationModes(Handle, FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)) + { + LOG_LAST_ERROR_IF(GetLastError() != ERROR_INVALID_FUNCTION); + } +} + +IOCPHandle::~IOCPHandle() +{ + if (m_handle != nullptr) + { + // MultiHandleWait owns one IOCPHandle per kernel handle, so there is no other + // IOCPHandle to step on. Detach so any external consumer (boost::asio, or a + // later MultiHandleWait reusing the same socket) can rebind to its own IOCP + // without hitting ERROR_INVALID_PARAMETER. + DetachFromIocp(m_handle); + } +} + // HandleWrapper HandleWrapper::HandleWrapper(wil::unique_handle&& handle, std::function&& OnClose) : @@ -184,6 +267,15 @@ EventHandle::EventHandle(HandleWrapper&& Handle, std::function&& OnSigna void EventHandle::Schedule() { State = IOHandleStatus::Pending; + + // If we have been bound to a completion port, (re-)arm the thread pool wait so the + // event signal posts a packet to the IOCP. Otherwise the wait simply transitions to + // Pending state and a synchronous WaitForMultipleObjects-style consumer is expected + // (this branch only matters for direct usage outside of MultiHandleWait::Run). + if (m_threadpoolWait) + { + SetThreadpoolWait(m_threadpoolWait.get(), Handle.Get(), nullptr); + } } void EventHandle::Collect() @@ -192,9 +284,24 @@ void EventHandle::Collect() OnSignalled(); } -HANDLE EventHandle::GetHandle() const +std::vector> EventHandle::Bind(HANDLE Iocp, ULONG_PTR Key) +{ + // Events cannot be associated with an IOCP directly. Arm a thread pool wait that + // posts a packet to the IOCP via PostQueuedCompletionStatus when the event signals, + // then return no kernel handles for MultiHandleWait to bind. + m_iocp = Iocp; + m_completionKey = Key; + + m_threadpoolWait.reset(CreateThreadpoolWait(EventHandle::WaitCallback, this, nullptr)); + THROW_LAST_ERROR_IF(!m_threadpoolWait); + + return {}; +} + +void NTAPI EventHandle::WaitCallback(PTP_CALLBACK_INSTANCE, PVOID Context, PTP_WAIT, TP_WAIT_RESULT) { - return Handle.Get(); + auto* self = static_cast(Context); + LOG_LAST_ERROR_IF(!PostQueuedCompletionStatus(self->m_iocp, 0, self->m_completionKey, nullptr)); } // ReadHandle @@ -289,9 +396,11 @@ void ReadHandle::Collect() } } -HANDLE ReadHandle::GetHandle() const +std::vector> ReadHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - return Event.get(); + UNREFERENCED_PARAMETER(Iocp); + UNREFERENCED_PARAMETER(Key); + return {{Handle.Get(), &Overlapped}}; } // SingleAcceptHandle @@ -352,9 +461,11 @@ void SingleAcceptHandle::Collect() OnAccepted(); } -HANDLE SingleAcceptHandle::GetHandle() const +std::vector> SingleAcceptHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - return Event.get(); + UNREFERENCED_PARAMETER(Iocp); + UNREFERENCED_PARAMETER(Key); + return {{ListenSocket.Get(), &Overlapped}}; } // LineBasedReadHandle @@ -649,9 +760,11 @@ void ReadSocketMessageHandle::Collect() ProcessRecvResult(bytesRead); } -HANDLE ReadSocketMessageHandle::GetHandle() const +std::vector> ReadSocketMessageHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - return Event.get(); + UNREFERENCED_PARAMETER(Iocp); + UNREFERENCED_PARAMETER(Key); + return {{Socket.Get(), &Overlapped}}; } // WriteHandle @@ -740,9 +853,11 @@ void WriteHandle::Push(const gsl::span& Content) State = IOHandleStatus::Standby; } -HANDLE WriteHandle::GetHandle() const +std::vector> WriteHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - return Event.get(); + UNREFERENCED_PARAMETER(Iocp); + UNREFERENCED_PARAMETER(Key); + return {{Handle.Get(), &Overlapped}}; } // DockerIORelayHandle @@ -861,16 +976,17 @@ void DockerIORelayHandle::Collect() } } -HANDLE DockerIORelayHandle::GetHandle() const +std::vector> DockerIORelayHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - if (ActiveHandle != nullptr && ActiveHandle->GetState() == IOHandleStatus::Pending) - { - return ActiveHandle->GetHandle(); - } - else - { - return Read->GetHandle(); - } + // Concatenate every sub-handle's pairs so the parent MultiHandleWait associates + // each underlying kernel handle with its IOCP exactly once and routes any of the + // sub-completions back to this DockerIORelayHandle by OVERLAPPED pointer. + auto handles = Read->Bind(Iocp, Key); + auto stdoutHandles = WriteStdout.Bind(Iocp, Key); + auto stderrHandles = WriteStderr.Bind(Iocp, Key); + handles.insert(handles.end(), stdoutHandles.begin(), stdoutHandles.end()); + handles.insert(handles.end(), stderrHandles.begin(), stderrHandles.end()); + return handles; } void DockerIORelayHandle::ProcessNextHeader() @@ -916,7 +1032,36 @@ void DockerIORelayHandle::OnRead(const gsl::span& Buffer) void MultiHandleWait::AddHandle(std::unique_ptr&& handle, Flags flags) { - m_handles.emplace_back(flags, std::move(handle)); + HandleEntry entry{flags, std::move(handle), {}}; + + // Each leaf reports the kernel handles it operates on along with the OVERLAPPED it + // will use; we associate every unique kernel handle with our IOCP exactly once + // (sharing across leaves is fine since dispatch routes by OVERLAPPED pointer in + // Run()) and remember the OVL list on the entry so completions can be matched back + // to the entry quickly. The completion key is the leaf's address - guaranteed + // unique while the entry exists; only EventHandle uses it (to post via + // PostQueuedCompletionStatus). + const auto bindings = entry.Handle->Bind(m_iocp.get(), reinterpret_cast(entry.Handle.get())); + entry.Overlappeds.reserve(bindings.size()); + for (const auto& [kernelHandle, overlapped] : bindings) + { + auto [it, inserted] = m_iocpBindings.try_emplace(kernelHandle); + if (inserted) + { + try + { + it->second.Bind(kernelHandle, m_iocp.get(), 0); + } + catch (...) + { + m_iocpBindings.erase(it); + throw; + } + } + entry.Overlappeds.push_back(overlapped); + } + + m_handles.push_back(std::move(entry)); } void MultiHandleWait::Cancel() @@ -940,19 +1085,19 @@ bool MultiHandleWait::Run(std::optional Timeout) while (!m_handles.empty() && !m_cancel) { // Schedule IO on each handle until all are either pending, or completed. - for (size_t i = 0; i < m_handles.size() && !m_cancel; i++) + for (auto it = m_handles.begin(); it != m_handles.end() && !m_cancel; ++it) { - while (m_handles[i].second->GetState() == IOHandleStatus::Standby && !m_cancel) + while (it->Handle && it->Handle->GetState() == IOHandleStatus::Standby && !m_cancel) { try { - m_handles[i].second->Schedule(); + it->Handle->Schedule(); } catch (...) { - if (WI_IsFlagSet(m_handles[i].first, Flags::IgnoreErrors)) + if (WI_IsFlagSet(it->Options, Flags::IgnoreErrors)) { - m_handles[i].second.reset(); // Reset the handle so it can be deleted. + it->Handle.reset(); // Reset the handle so it can be deleted. break; } else @@ -967,13 +1112,13 @@ bool MultiHandleWait::Run(std::optional Timeout) bool hasHandleToWaitFor = false; for (auto it = m_handles.begin(); it != m_handles.end();) { - if (!it->second) + if (!it->Handle) { it = m_handles.erase(it); } - else if (it->second->GetState() == IOHandleStatus::Completed) + else if (it->Handle->GetState() == IOHandleStatus::Completed) { - if (WI_IsFlagSet(it->first, Flags::CancelOnCompleted)) + if (WI_IsFlagSet(it->Options, Flags::CancelOnCompleted)) { m_cancel = true; // Cancel the IO if a handle with CancelOnCompleted is in the completed state. } @@ -983,7 +1128,7 @@ bool MultiHandleWait::Run(std::optional Timeout) else { // If only NeedNotComplete handles are left, we want to exit Run. - if (WI_IsFlagClear(it->first, Flags::NeedNotComplete)) + if (WI_IsFlagClear(it->Options, Flags::NeedNotComplete)) { hasHandleToWaitFor = true; } @@ -997,49 +1142,94 @@ bool MultiHandleWait::Run(std::optional Timeout) } // Wait for the next operation to complete. - std::vector waitHandles; - for (const auto& e : m_handles) - { - waitHandles.emplace_back(e.second->GetHandle()); - } - DWORD waitTimeout = INFINITE; if (deadline.has_value()) { - auto miliseconds = + const auto miliseconds = std::chrono::duration_cast(deadline.value() - std::chrono::steady_clock::now()).count(); waitTimeout = static_cast(std::max(0LL, miliseconds)); } - auto result = WaitForMultipleObjects(static_cast(waitHandles.size()), waitHandles.data(), false, waitTimeout); - if (result == WAIT_TIMEOUT) + DWORD bytes = 0; + ULONG_PTR key = 0; + OVERLAPPED* ovl = nullptr; + if (!GetQueuedCompletionStatus(m_iocp.get(), &bytes, &key, &ovl, waitTimeout)) { - THROW_WIN32(ERROR_TIMEOUT); + // GetQueuedCompletionStatus returns FALSE for two distinct conditions: + // + // - ovl == nullptr: no packet was dequeued. This is either a wait timeout + // (treated as success: just exit the loop) or a real failure of the IOCP + // itself (rethrow). + // + // - ovl != nullptr: a completion packet for a failed I/O operation was + // dequeued. key/bytes/ovl are all valid and the failure should be + // dispatched to the owning leaf, whose Collect() reads the actual status + // via GetOverlappedResult and decides how to react (e.g. ReadHandle + // treats ERROR_BROKEN_PIPE/ERROR_HANDLE_EOF as a clean EOF). + const auto error = GetLastError(); + if (ovl == nullptr) + { + if (error == WAIT_TIMEOUT) + { + break; + } + + THROW_WIN32(error); + } } - else if (result >= WAIT_OBJECT_0 && result < WAIT_OBJECT_0 + m_handles.size()) - { - auto index = result - WAIT_OBJECT_0; - try + // Find the entry that owns this completion. When the OS provides an OVERLAPPED + // pointer (any real I/O completion), route by OVL because a single kernel handle + // can only be associated with one IOCP+key pair: when two leaves share the same + // kernel handle (e.g. RelayHandle::Write and DockerHttpResponseHandle wrapping + // the same docker socket), the kernel-level key is identical for every + // completion on that handle. Each leaf's I/O carries its own OVERLAPPED, so the + // pointer is always unambiguous. Fall back to key-based dispatch when OVL is + // nullptr (EventHandle posts via PostQueuedCompletionStatus with no OVL, using + // the leaf address as key). + const auto it = std::find_if(m_handles.begin(), m_handles.end(), [key, ovl](const HandleEntry& entry) { + if (!entry.Handle) { - m_handles[index].second->Collect(); + return false; } - catch (...) + + if (ovl != nullptr) { - if (WI_IsFlagSet(m_handles[index].first, Flags::IgnoreErrors)) - { - m_handles.erase(m_handles.begin() + index); - } - else - { - throw; - } + return std::find(entry.Overlappeds.begin(), entry.Overlappeds.end(), ovl) != entry.Overlappeds.end(); } + + return reinterpret_cast(entry.Handle.get()) == key; + }); + + if (it == m_handles.end() || it->Handle->GetState() != IOHandleStatus::Pending) + { + LOG_HR_MSG( + E_UNEXPECTED, + "Receive IO completion for stale handle. Key: 0x%p, Overlapped: 0x%p, Handle State: %d", + (void*)key, + (void*)ovl, + it != m_handles.end() && it->Handle ? static_cast(it->Handle->GetState()) : -1); + continue; } - else + + try + { + it->Handle->Collect(); + } + catch (...) { - THROW_LAST_ERROR_MSG("Timeout: %lu, Count: %llu", waitTimeout, waitHandles.size()); + if (WI_IsFlagSet(it->Options, Flags::IgnoreErrors)) + { + // Reset the handle (rather than erasing the entry) so we don't invalidate + // any iterator the schedule loop above might be holding. The cleanup pass + // at the top of the next outer iteration removes entries with !Handle. + it->Handle.reset(); + } + else + { + throw; + } } } diff --git a/src/windows/common/HandleIO.h b/src/windows/common/HandleIO.h index d52284acd..8d04af86e 100644 --- a/src/windows/common/HandleIO.h +++ b/src/windows/common/HandleIO.h @@ -58,6 +58,43 @@ class BufferWrapper gsl::span m_unowned; }; +// RAII helper for binding a file/socket to an I/O completion port. Bind detaches +// any prior IOCP association on the handle before installing the new one, so the +// same underlying file/socket can be re-bound to a different IOCP from a later +// MultiHandleWait instance even after the previous instance's IOCP has been +// closed. The destructor likewise detaches the binding so external consumers +// (e.g. boost::asio::stream::assign which calls CreateIoCompletionPort with its +// own IOCP) can take ownership of the kernel handle without hitting +// ERROR_INVALID_PARAMETER. +// +// Detach uses the undocumented NtSetInformationFile + +// FileReplaceCompletionInformation NTAPI (introduced in Windows 8.1). +// +// CreateIoCompletionPort returns ERROR_INVALID_PARAMETER for handles that don't +// support overlapped I/O - anonymous pipes from CreatePipe, console handles from +// GetStdHandle, and a few other device types. Bind silently treats those as a +// no-op; such handles are expected to complete their I/O synchronously inside +// Schedule() so no IOCP packet is required. +// +// IOCPHandle is owned exclusively by MultiHandleWait (via a per-MHW +// std::map) so each kernel handle has at most one binding +// per wait. Two leaves wrapping the same kernel handle therefore share a single +// IOCPHandle and the destructor unconditionally detaches when the wait ends. +class IOCPHandle +{ +public: + IOCPHandle() = default; + ~IOCPHandle(); + + NON_COPYABLE(IOCPHandle) + NON_MOVABLE(IOCPHandle) + + void Bind(HANDLE Handle, HANDLE Iocp, ULONG_PTR Key); + +private: + HANDLE m_handle = nullptr; +}; + class OverlappedIOHandle { public: @@ -68,7 +105,21 @@ class OverlappedIOHandle virtual ~OverlappedIOHandle() = default; virtual void Schedule() = 0; virtual void Collect() = 0; - virtual HANDLE GetHandle() const = 0; + + // Hand the wait's IOCP and the leaf's per-entry completion key to this leaf and + // return every (kernel handle, OVERLAPPED*) pair that should be associated with + // the IOCP. MultiHandleWait calls Bind exactly once per leaf, when the leaf is + // added via AddHandle. + // + // Most leaves ignore Iocp/Key and simply return their own (handle, &overlapped) + // pair. EventHandle wraps an event - which cannot be associated with an IOCP - + // and instead uses Iocp/Key to arm a thread pool wait that posts a completion + // packet via PostQueuedCompletionStatus, so it returns no pairs. + // + // Composite handles forward Bind to every sub-handle and concatenate the + // returned pairs. + virtual std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) = 0; + IOHandleStatus GetState() const; protected: @@ -84,11 +135,16 @@ class EventHandle : public OverlappedIOHandle EventHandle(HandleWrapper&& Handle, std::function&& OnSignalled = []() {}); void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; private: + static void NTAPI WaitCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult); + HandleWrapper Handle; std::function OnSignalled; + HANDLE m_iocp{}; + ULONG_PTR m_completionKey{}; + wil::unique_threadpool_wait m_threadpoolWait; }; class ReadHandle : public OverlappedIOHandle @@ -102,7 +158,7 @@ class ReadHandle : public OverlappedIOHandle void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; private: HandleWrapper Handle; @@ -124,7 +180,7 @@ class SingleAcceptHandle : public OverlappedIOHandle void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; private: HandleWrapper ListenSocket; @@ -181,7 +237,7 @@ class ReadSocketMessageHandle : public OverlappedIOHandle void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; private: void ScheduleRecv(); @@ -208,7 +264,7 @@ class WriteHandle : public OverlappedIOHandle ~WriteHandle(); void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; void Push(const gsl::span& Buffer); private: @@ -286,17 +342,15 @@ class RelayHandle : public OverlappedIOHandle } } - HANDLE GetHandle() const override + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override { - if (Read.GetState() == IOHandleStatus::Pending) - { - return Read.GetHandle(); - } - else - { - WI_ASSERT(Write.GetState() == IOHandleStatus::Pending); - return Write.GetHandle(); - } + // Concatenate the children's (handle, overlapped) pairs so the parent + // MultiHandleWait associates both kernel handles with its IOCP and routes + // each completion to this entry by OVERLAPPED pointer. + auto handles = Read.Bind(Iocp, Key); + auto writeHandles = Write.Bind(Iocp, Key); + handles.insert(handles.end(), writeHandles.begin(), writeHandles.end()); + return handles; } private: @@ -325,7 +379,7 @@ class DockerIORelayHandle : public OverlappedIOHandle DockerIORelayHandle(HandleWrapper&& Input, HandleWrapper&& Stdout, HandleWrapper&& Stderr, Format ReadFormat); void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; #pragma pack(push, 1) struct MultiplexedHeader @@ -364,14 +418,35 @@ class MultiHandleWait NeedNotComplete = 4, }; - MultiHandleWait() = default; + MultiHandleWait() + { + m_iocp.reset(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0)); + THROW_LAST_ERROR_IF(!m_iocp); + } void AddHandle(std::unique_ptr&& handle, Flags flags = Flags::None); bool Run(std::optional Timeout); void Cancel(); private: - std::vector>> m_handles; + struct HandleEntry + { + Flags Options; + std::unique_ptr Handle; + std::vector Overlappeds; + }; + + + wil::unique_handle m_iocp; + // std::list (rather than std::vector) so iterators held by the schedule loop + // survive erasures performed by the cleanup pass and processPacket. + std::list m_handles; + // One IOCPHandle per unique kernel handle this MultiHandleWait operates on. + // Owns the IOCP association lifetime: when m_iocpBindings is destroyed (before + // m_handles), each IOCPHandle's destructor detaches its kernel handle so + // external consumers (e.g. boost::asio::stream::assign, or a later + // MultiHandleWait reusing the same socket) can rebind freely. + std::map m_iocpBindings; bool m_cancel = false; }; diff --git a/src/windows/common/precomp.h b/src/windows/common/precomp.h index cbdaedd94..644d18789 100644 --- a/src/windows/common/precomp.h +++ b/src/windows/common/precomp.h @@ -72,6 +72,7 @@ Module Name: #include #include #include +#include #include #include #include diff --git a/src/windows/wslcsession/PortRelayHandle.cpp b/src/windows/wslcsession/PortRelayHandle.cpp index 29911495d..a73021447 100644 --- a/src/windows/wslcsession/PortRelayHandle.cpp +++ b/src/windows/wslcsession/PortRelayHandle.cpp @@ -90,9 +90,11 @@ void PortRelayAcceptHandle::Collect() State = io::IOHandleStatus::Standby; } -HANDLE PortRelayAcceptHandle::GetHandle() const +std::vector> PortRelayAcceptHandle::Bind(HANDLE Iocp, ULONG_PTR Key) { - return Event.get(); + UNREFERENCED_PARAMETER(Iocp); + UNREFERENCED_PARAMETER(Key); + return {{reinterpret_cast(ListenSocket.get()), &Overlapped}}; } void PortRelayAcceptHandle::LaunchRelay(wil::unique_socket&& AcceptedSocket) diff --git a/src/windows/wslcsession/PortRelayHandle.h b/src/windows/wslcsession/PortRelayHandle.h index 58ace5edf..428f65b95 100644 --- a/src/windows/wslcsession/PortRelayHandle.h +++ b/src/windows/wslcsession/PortRelayHandle.h @@ -32,7 +32,7 @@ class PortRelayAcceptHandle : public common::io::OverlappedIOHandle void Schedule() override; void Collect() override; - HANDLE GetHandle() const override; + std::vector> Bind(HANDLE Iocp, ULONG_PTR Key) override; private: void LaunchRelay(wil::unique_socket&& AcceptedSocket); diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp index 186029dbb..cfd8e547e 100644 --- a/test/windows/InstallerTests.cpp +++ b/test/windows/InstallerTests.cpp @@ -379,7 +379,7 @@ class InstallerTests if (wsl::shared::string::EndsWith(installerFile, L".msi")) { PrepareForMsiOperation(); - CallMsiExec(std::format(L"/qn /norestart /i {} /L*V {}", installerFile, GenerateMsiLogPath())); + CallMsiExec(std::format(L"/qn /norestart /i \"{}\" /L*V \"{}\"", installerFile, GenerateMsiLogPath())); } else { diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 3a94a7683..7a83dd7f3 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -6828,6 +6828,70 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", } } + TEST_METHOD(MultiHandleWaitMoreThan64Handles) + { + // Validate that MultiHandleWait can drive more than MAXIMUM_WAIT_OBJECTS (64) + // pending handles in parallel. The implementation uses an I/O completion port to + // bypass the WaitForMultipleObjects limit; before the IOCP rewrite this would + // have asserted or failed inside Run. + constexpr size_t handleCount = 65; + static_assert(handleCount > MAXIMUM_WAIT_OBJECTS, "Test must exceed the WaitForMultipleObjects limit"); + + std::vector writeEnds; + writeEnds.reserve(handleCount); + + std::vector received(handleCount); + std::vector sawEof(handleCount, false); + + wsl::windows::common::io::MultiHandleWait io; + + for (size_t i = 0; i < handleCount; ++i) + { + auto [readPipe, writePipe] = wsl::windows::common::wslutil::OpenAnonymousPipe(16 * 1024, true, false); + writeEnds.emplace_back(std::move(writePipe)); + + io.AddHandle(std::make_unique( + std::move(readPipe), [&received, &sawEof, i](const gsl::span& buffer) { + if (buffer.empty()) + { + sawEof[i] = true; + } + else + { + received[i].append(buffer.data(), buffer.size()); + } + })); + } + + // Background producer: write a unique payload to each pipe and close it. Doing + // this from a separate thread guarantees that the reads queued by Run have to + // wait asynchronously through the completion port. + std::thread producer([&]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + for (size_t i = 0; i < handleCount; ++i) + { + const std::string payload = std::format("handle-{}", i); + DWORD written = 0; + if (!WriteFile(writeEnds[i].get(), payload.data(), static_cast(payload.size()), &written, nullptr)) + { + LOG_LAST_ERROR(); + } + writeEnds[i].reset(); + } + }); + + auto producerCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { producer.join(); }); + + io.Run(std::chrono::seconds(60)); + + for (size_t i = 0; i < handleCount; ++i) + { + const std::string expected = std::format("handle-{}", i); + VERIFY_ARE_EQUAL(expected, received[i]); + VERIFY_IS_TRUE(sawEof[i]); + } + } + TEST_METHOD(SocketChannel) { // Read exactly `size` bytes from a raw socket into the destination buffer.