From 831916aebeae29bcb75d79feee22fa24ae2131af Mon Sep 17 00:00:00 2001 From: wfurt Date: Mon, 15 Jul 2024 12:04:41 -0700 Subject: [PATCH 1/8] fix broken Select with error list on macOS --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 37 +++++++-- .../tests/FunctionalTests/SelectTest.cs | 75 +++++++++++++++++++ 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index 26ee3ccb885b9..2658d2339a667 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -19,6 +19,8 @@ internal static partial class SocketPal public static readonly int MaximumAddressSize = Interop.Sys.GetMaximumAddressSize(); private static readonly bool SupportsDualModeIPv4PacketInfo = GetPlatformSupportsDualModeIPv4PacketInfo(); + private static readonly bool PollNeedsErrorListFixup = OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS(); + // IovStackThreshold matches Linux's UIO_FASTIOV, which is the number of 'struct iovec' // that get stackalloced in the Linux kernel. private const int IovStackThreshold = 8; @@ -1817,20 +1819,22 @@ private static unsafe SocketError SelectViaPoll( Debug.Assert(eventsLength == checkReadInitialCount + checkWriteInitialCount + checkErrorInitialCount, "Invalid eventsLength"); int offset = 0; int refsAdded = 0; + int readRefs; try { // In case we can't increase the reference count for each Socket, // we'll unref refAdded Sockets in the finally block ordered: [checkRead, checkWrite, checkError]. AddToPollArray(events, eventsLength, checkRead, ref offset, Interop.PollEvents.POLLIN | Interop.PollEvents.POLLHUP, ref refsAdded); + readRefs = refsAdded; AddToPollArray(events, eventsLength, checkWrite, ref offset, Interop.PollEvents.POLLOUT, ref refsAdded); - AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded); - Debug.Assert(offset == eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); - Debug.Assert(refsAdded == eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); + AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded, PollNeedsErrorListFixup ? readRefs : 0); + Debug.Assert(offset <= eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); + Debug.Assert(refsAdded <= eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); // Do the poll uint triggered = 0; int milliseconds = microseconds == -1 ? -1 : microseconds / 1000; - Interop.Error err = Interop.Sys.Poll(events, (uint)eventsLength, milliseconds, &triggered); + Interop.Error err = Interop.Sys.Poll(events, (uint)refsAdded, milliseconds, &triggered); if (err != Interop.Error.SUCCESS) { return GetSocketErrorForErrorCode(err); @@ -1867,7 +1871,7 @@ private static unsafe SocketError SelectViaPoll( } } - private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, IList? socketList, ref int arrOffset, Interop.PollEvents events, ref int refsAdded) + private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, IList? socketList, ref int arrOffset, Interop.PollEvents events, ref int refsAdded, int readCount = 0) { if (socketList == null) return; @@ -1887,6 +1891,29 @@ private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, bool success = false; socket.InternalSafeHandle.DangerousAddRef(ref success); int fd = (int)socket.InternalSafeHandle.DangerousGetHandle(); + + if (readCount > 0) + { + // some platfoms like macOS do not like if there is duplication between real and error list. + // To fix that we will search read list and if macthing descriptor exiost we will add events flags + // instead of adding new entry to error list. + int readIndex = 0; + while (readIndex < readCount) + { + if (arr[readIndex].FileDescriptor == fd) + { + arr[i].Events |= events; + socket.InternalSafeHandle.DangerousRelease(); + break; + } + readIndex++; + } + if (readIndex != readCount) + { + continue; + } + } + arr[arrOffset++] = new Interop.PollEvent { Events = events, FileDescriptor = fd }; refsAdded++; } diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs index 0b49749e09aee..c24a7bac45a5a 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs @@ -78,6 +78,81 @@ public void Select_ReadWrite_AllReady(int reads, int writes) } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Select_ReadError_Success(bool dispose) + { + using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + using Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + sender.Connect(listener.LocalEndPoint); + using Socket receiver = listener.Accept(); + + if (dispose) + { + sender.Dispose(); + } + else + { + sender.Send(new byte[] { 1 }); + } + + var readList = new List { receiver }; + var errorList = new List { receiver }; + Socket.Select(readList, null, errorList, -1); + if (dispose) + { + Assert.True(readList.Count == 1 || errorList.Count == 1); + } + else + { + Assert.Equal(1, readList.Count); + Assert.Equal(0, errorList.Count); + } + } + + [Fact] + public void Select_WriteError_Success() + { + using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + using Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + sender.Connect(listener.LocalEndPoint); + using Socket receiver = listener.Accept(); + + var writeList = new List { receiver }; + var errorList = new List { receiver }; + Socket.Select(null, writeList, errorList, -1); + Assert.Equal(1, writeList.Count); + Assert.Equal(0, errorList.Count); + } + + [Fact] + public void Select_ReadWriteError_Success() + { + using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + using Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Unspecified); + + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + sender.Connect(listener.LocalEndPoint); + using Socket receiver = listener.Accept(); + + sender.Send(new byte[] { 1 }); + var readList = new List { receiver }; + var writeList = new List { receiver }; + var errorList = new List { receiver }; + Socket.Select(readList, writeList, errorList, -1); + Assert.Equal(1, readList.Count); + Assert.Equal(1, writeList.Count); + Assert.Equal(0, errorList.Count); + } + [Theory] [InlineData(2, 0)] [InlineData(2, 1)] From 3afc585ced5f098cd274661c9908158ef88b368c Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 17 Jul 2024 15:10:19 -0700 Subject: [PATCH 2/8] update --- .../Unix/System.Native/Interop.Select.cs | 14 ++ .../src/System.Net.Sockets.csproj | 2 + .../src/System/Net/Sockets/SocketPal.Unix.cs | 121 +++++++++++++++++- .../tests/FunctionalTests/SelectTest.cs | 32 ++++- src/native/libs/System.Native/entrypoints.c | 1 + .../libs/System.Native/pal_networking.c | 98 +++++++++++++- .../libs/System.Native/pal_networking.h | 2 + 7 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.Select.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Select.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Select.cs new file mode 100644 index 0000000000000..245e41d81138b --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Select.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Select")] + internal static unsafe partial Error Select(Span readFDs, int readFDsLength, Span writeFDs, int writeFDsLength, Span checkError, int checkErrorLength, int timeout, int maxFd, out int triggered); + } +} diff --git a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj index 5198e80906163..e7a4cfbcce3c9 100644 --- a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj +++ b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj @@ -267,6 +267,8 @@ Link="Common\Interop\Unix\System.Native\Interop.ReceiveMessage.cs" /> + readFDs = checkRead?.Count > 20 ? new int[checkRead.Count] : checkRead?.Count > 0 ? stackalloc int[checkRead.Count] : Span.Empty; + Span writeFDs = checkWrite?.Count > 20 ? new int[checkWrite.Count] : checkWrite?.Count > 0 ? stackalloc int[checkWrite.Count] : Span.Empty;; + Span errorFDs = checkError?.Count > 20 ? new int[checkError.Count] : checkError?.Count > 0 ? stackalloc int[checkError.Count] : Span.Empty; + + int refsAdded = 0; + int maxFd = 0; + try + { + AddDesriptors(ref readFDs, checkRead, ref refsAdded, ref maxFd); + AddDesriptors(ref writeFDs, checkWrite, ref refsAdded, ref maxFd); + AddDesriptors(ref errorFDs, checkError, ref refsAdded, ref maxFd); + + int triggered = 0; + Interop.Error err = Interop.Sys.Select(readFDs, readFDs.Length, writeFDs, writeFDs.Length, errorFDs, errorFDs.Length, microseconds, maxFd, out triggered); + if (err != Interop.Error.SUCCESS) + { + return GetSocketErrorForErrorCode(err); + } + + if (triggered == 0) + { + Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); + + checkRead?.Clear(); + checkWrite?.Clear(); + checkError?.Clear(); + } + else + { + Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); + + FilterSelectList(checkRead, readFDs); + FilterSelectList(checkWrite, writeFDs); + FilterSelectList(checkError, errorFDs); + } + } + finally + { + // This order matches with the AddToPollArray calls + // to release only the handles that were ref'd. + Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); + Debug.Assert(refsAdded == 0); + } + + return (SocketError)0; + } + + private static void AddDesriptors(ref Span buffer, IList? socketList, ref int refsAdded, ref int maxFd) + { + if (socketList == null || socketList.Count == 0 ) + { + return; + } + + Debug.Assert(buffer.Length == socketList.Count); + for (int i = 0; i < socketList.Count; i++) + { + Socket? socket = socketList[i] as Socket; + if (socket == null) + { + throw new ArgumentException(SR.Format(SR.net_sockets_select, socket?.GetType().FullName ?? "null", typeof(Socket).FullName), nameof(socketList)); + } + + if (socket.Handle > maxFd) + { + maxFd = (int)socket.Handle; + } + + bool success = false; + socket.InternalSafeHandle.DangerousAddRef(ref success); + buffer[i] = (int)socket.InternalSafeHandle.DangerousGetHandle(); + + refsAdded++; + } + } + + private static void FilterSelectList(IList? socketList, Span results) + { + if (socketList == null) + return; + + // The Select API requires leaving in the input lists only those sockets that were ready. As such, we need to loop + // through each poll event, and for each that wasn't ready, remove the corresponding Socket from its list. Technically + // this is O(n^2), due to removing from the list requiring shifting down all elements after it. However, this doesn't + // happen with the most common cases. If very few sockets were ready, then as we iterate from the end of the list, each + // removal will typically be O(1) rather than O(n). If most sockets were ready, then we only need to remove a few, in + // which case we're only doing a small number of O(n) shifts. It's only for the intermediate case, where a non-trivial + // number of sockets are ready and a non-trivial number of sockets are not ready that we end up paying the most. We could + // avoid these costs by, for example, allocating a side list that we fill with the sockets that should remain, clearing + // the original list, and then populating the original list with the contents of the side list. That of course has its + // own costs, and so for now we do the "simple" thing. This can be changed in the future as needed. + + for (int i = socketList.Count - 1; i >= 0; --i) + { + if (results[i] == 0) + { + socketList.RemoveAt(i); + } + } + } + private static unsafe SocketError SelectViaPoll( IList? checkRead, int checkReadInitialCount, IList? checkWrite, int checkWriteInitialCount, @@ -1819,15 +1932,15 @@ private static unsafe SocketError SelectViaPoll( Debug.Assert(eventsLength == checkReadInitialCount + checkWriteInitialCount + checkErrorInitialCount, "Invalid eventsLength"); int offset = 0; int refsAdded = 0; - int readRefs; try { // In case we can't increase the reference count for each Socket, // we'll unref refAdded Sockets in the finally block ordered: [checkRead, checkWrite, checkError]. AddToPollArray(events, eventsLength, checkRead, ref offset, Interop.PollEvents.POLLIN | Interop.PollEvents.POLLHUP, ref refsAdded); - readRefs = refsAdded; AddToPollArray(events, eventsLength, checkWrite, ref offset, Interop.PollEvents.POLLOUT, ref refsAdded); - AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded, PollNeedsErrorListFixup ? readRefs : 0); + AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded); + + // Console.WriteLine("Fixup ??? {0}", PollNeedsErrorListFixup); Debug.Assert(offset <= eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); Debug.Assert(refsAdded <= eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs index c24a7bac45a5a..b5ede7ae30830 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -21,7 +21,7 @@ public SelectTest(ITestOutputHelper output) } private const int SmallTimeoutMicroseconds = 10 * 1000; - private const int FailTimeoutMicroseconds = 30 * 1000 * 1000; + internal const int FailTimeoutMicroseconds = 30 * 1000 * 1000; [SkipOnPlatform(TestPlatforms.OSX, "typical OSX install has very low max open file descriptors value")] [Theory] @@ -144,6 +144,7 @@ public void Select_ReadWriteError_Success() using Socket receiver = listener.Accept(); sender.Send(new byte[] { 1 }); + receiver.Poll(FailTimeoutMicroseconds, SelectMode.SelectRead); var readList = new List { receiver }; var writeList = new List { receiver }; var errorList = new List { receiver }; @@ -184,7 +185,6 @@ public void Select_SocketAlreadyClosed_AllSocketsClosableAfterException(int sock } } - [SkipOnPlatform(TestPlatforms.OSX, "typical OSX install has very low max open file descriptors value")] [Fact] [ActiveIssue("https://github.com/dotnet/runtime/issues/51392", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)] public void Select_ReadError_NoneReady_ManySockets() @@ -320,7 +320,7 @@ public void Poll_ReadReady_LongTimeouts(int microsecondsTimeout) } } - private static KeyValuePair CreateConnectedSockets() + internal static KeyValuePair CreateConnectedSockets() { using (Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { @@ -417,5 +417,29 @@ private static void DoAccept(Socket listenSocket, int connectionsToAccept) } } } + + [ConditionalFact] + public void Slect_LargeNumber_Succcess() + { + const int MaxSockets = 1025; + KeyValuePair[] socketPairs; + try + { + // we try to shoot for more socket than FD_SETSIZE (that is typically 1024 + socketPairs = Enumerable.Range(0, MaxSockets).Select(_ => SelectTest.CreateConnectedSockets()).ToArray(); + } + catch + { + throw new SkipTestException("Unable to open large count number of socket"); + } + + var readList = new List(socketPairs.Select(p => p.Key).ToArray()); + + // Try to write and read on last sockets + (Socket reader, Socket writer) = socketPairs[MaxSockets - 1]; + writer.Send(new byte[1]); + Socket.Select(readList, null, null, SelectTest.FailTimeoutMicroseconds); + Assert.Equal(1, readList.Count); + } } } diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 51c761109159b..03dd2f65083e0 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -282,6 +282,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_GetGroupName) DllImportEntry(SystemNative_GetUInt64OSThreadId) DllImportEntry(SystemNative_TryGetUInt32OSThreadId) + DllImportEntry(SystemNative_Select) }; EXTERN_C const void* SystemResolveDllImport(const char* name); diff --git a/src/native/libs/System.Native/pal_networking.c b/src/native/libs/System.Native/pal_networking.c index 2916c33206cb0..53e358bdcdf48 100644 --- a/src/native/libs/System.Native/pal_networking.c +++ b/src/native/libs/System.Native/pal_networking.c @@ -1,13 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if defined(__APPLE__) && __APPLE__ +#define _DARWIN_UNLIMITED_SELECT 1 +#endif + #include "pal_config.h" #include "pal_networking.h" #include "pal_safecrt.h" #include "pal_utilities.h" #include #include - #include #include #include @@ -21,6 +24,7 @@ #include #elif HAVE_SYS_POLL_H #include +#include #endif #if HAVE_SYS_PROCINFO_H #include @@ -31,7 +35,6 @@ #include #include #include -#include #include #include #include @@ -2762,6 +2765,97 @@ int32_t SystemNative_GetBytesAvailable(intptr_t socket, int32_t* available) return Error_SUCCESS; } +int32_t SystemNative_Select(int* readFds, int readFdsCount, int* writeFds, int writeFdsCount, int* errorFds, int errorFdsCount, int32_t microseconds, int maxFd, int* triggered) +{ +#ifdef _DARWIN_UNLIMITED_SELECT + fd_set readSet; + fd_set writeSet; + fd_set errorSet; + + fd_set* readSetPtr; + fd_set* writeSetPtr; + fd_set* errorSetPtr; + + if (maxFd < FD_SETSIZE) + { + FD_ZERO(&readSet); + FD_ZERO(&writeSet); + FD_ZERO(&errorSet); + readSetPtr = readFdsCount == 0 ? NULL : &readSet; + writeSetPtr= writeFdsCount == 0 ? NULL : &writeSet; + errorSetPtr = errorFdsCount == 0 ? NULL : &errorSet; + } + else + { + readSetPtr = readFdsCount == 0 ? NULL : calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); + writeSetPtr = writeFdsCount == 0 ? NULL : calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); + errorSetPtr = errorFdsCount == 0 ? NULL :calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); + } + + + struct timeval timeout; + timeout.tv_sec = microseconds / 1000000; + timeout.tv_usec = microseconds % 1000000; + + int fd; + for (int i = 0 ; i < readFdsCount; i++) + { + fd = *(readFds + i); + __DARWIN_FD_SET(fd, readSetPtr); + } + for (int i = 0 ; i < writeFdsCount; i++) + { + fd = *(writeFds + i); + __DARWIN_FD_SET(fd, writeSetPtr); + } + for (int i = 0 ; i < errorFdsCount; i++) + { + fd = *(errorFds + i); + __DARWIN_FD_SET(fd, errorSetPtr); + } + + *triggered = select(maxFd + 1, readSetPtr, writeSetPtr, errorSetPtr, microseconds < 0 ? NULL : &timeout); + + if (*triggered < 0) + { + return SystemNative_ConvertErrorPlatformToPal(errno); + } + + for (int i = 0 ; i < readFdsCount; i++) + { + fd = *(readFds + i); + *(readFds + i) = __DARWIN_FD_ISSET(fd, readSetPtr); + } + for (int i = 0 ; i < writeFdsCount; i++) + { + fd = *(writeFds + i); + *(writeFds + i) = __DARWIN_FD_ISSET(fd, writeSetPtr); + } + for (int i = 0 ; i < errorFdsCount; i++) + { + fd = *(errorFds + i); + *(errorFds + i) = __DARWIN_FD_ISSET(fd, errorSetPtr); + } + + if (maxFd >= FD_SETSIZE) + { + free(readSetPtr); + free(writeSetPtr); + free(errorSetPtr); + } + + return Error_SUCCESS; +#else + // avoild unused parameters warnings + (void*)readFds; + (void*)writeFds; + (void*)errorFds; + (void*)triggered; + readFdsCount + writeFdsCount + errorFdsCount + microseconds + maxFd; + return SystemNative_ConvertErrorPlatformToPal(ENOTSUP); +#endif +} + #if HAVE_EPOLL static const size_t SocketEventBufferElementSize = sizeof(struct epoll_event) > sizeof(SocketEvent) ? sizeof(struct epoll_event) : sizeof(SocketEvent); diff --git a/src/native/libs/System.Native/pal_networking.h b/src/native/libs/System.Native/pal_networking.h index f10f490c5db91..a0904f295267d 100644 --- a/src/native/libs/System.Native/pal_networking.h +++ b/src/native/libs/System.Native/pal_networking.h @@ -427,3 +427,5 @@ PALEXPORT int32_t SystemNative_SendFile(intptr_t out_fd, intptr_t in_fd, int64_t PALEXPORT int32_t SystemNative_Disconnect(intptr_t socket); PALEXPORT uint32_t SystemNative_InterfaceNameToIndex(char* interfaceName); + +PALEXPORT int32_t SystemNative_Select(int* readFds, int readFdsCount, int* writeFds, int writeFdsCount, int* errorFds, int errorFdsCount, int32_t microseconds, int32_t maxFd, int* triggered); From 1f8d309818f99fc088f59090bad30e245bcf7a16 Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Wed, 17 Jul 2024 22:41:00 -0700 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Stephen Toub --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 2 +- src/native/libs/System.Native/pal_networking.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index 5303815c7a93c..1abc5e389af53 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -1788,6 +1788,7 @@ public static unsafe SocketError Select(IList? checkRead, IList? checkWrite, ILi { return SelectViaSelect(checkRead, checkWrite, checkError, microseconds); } + const int StackThreshold = 80; // arbitrary limit to avoid too much space on stack if (count < StackThreshold) { @@ -1940,7 +1941,6 @@ private static unsafe SocketError SelectViaPoll( AddToPollArray(events, eventsLength, checkWrite, ref offset, Interop.PollEvents.POLLOUT, ref refsAdded); AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded); - // Console.WriteLine("Fixup ??? {0}", PollNeedsErrorListFixup); Debug.Assert(offset <= eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); Debug.Assert(refsAdded <= eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); diff --git a/src/native/libs/System.Native/pal_networking.c b/src/native/libs/System.Native/pal_networking.c index 53e358bdcdf48..dc727fe546504 100644 --- a/src/native/libs/System.Native/pal_networking.c +++ b/src/native/libs/System.Native/pal_networking.c @@ -2789,7 +2789,7 @@ int32_t SystemNative_Select(int* readFds, int readFdsCount, int* writeFds, int w { readSetPtr = readFdsCount == 0 ? NULL : calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); writeSetPtr = writeFdsCount == 0 ? NULL : calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); - errorSetPtr = errorFdsCount == 0 ? NULL :calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); + errorSetPtr = errorFdsCount == 0 ? NULL : calloc( __DARWIN_howmany(maxFd, __DARWIN_NFDBITS), sizeof(int32_t)); } @@ -2846,7 +2846,7 @@ int32_t SystemNative_Select(int* readFds, int readFdsCount, int* writeFds, int w return Error_SUCCESS; #else - // avoild unused parameters warnings + // avoid unused parameters warnings (void*)readFds; (void*)writeFds; (void*)errorFds; From 8141412734e31edbeebee89c53fc10df6e5b252f Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 17 Jul 2024 22:52:32 -0700 Subject: [PATCH 4/8] feedback --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 50 ++++--------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index 1abc5e389af53..da05879c49fc7 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -1823,9 +1823,9 @@ private static SocketError SelectViaSelect(IList? checkRead, IList? checkWrite, int maxFd = 0; try { - AddDesriptors(ref readFDs, checkRead, ref refsAdded, ref maxFd); - AddDesriptors(ref writeFDs, checkWrite, ref refsAdded, ref maxFd); - AddDesriptors(ref errorFDs, checkError, ref refsAdded, ref maxFd); + AddDesriptors(readFDs, checkRead, ref refsAdded, ref maxFd); + AddDesriptors(writeFDs, checkWrite, ref refsAdded, ref maxFd); + AddDesriptors(errorFDs, checkError, ref refsAdded, ref maxFd); int triggered = 0; Interop.Error err = Interop.Sys.Select(readFDs, readFDs.Length, writeFDs, writeFDs.Length, errorFDs, errorFDs.Length, microseconds, maxFd, out triggered); @@ -1868,7 +1868,7 @@ private static SocketError SelectViaSelect(IList? checkRead, IList? checkWrite, return (SocketError)0; } - private static void AddDesriptors(ref Span buffer, IList? socketList, ref int refsAdded, ref int maxFd) + private static void AddDesriptors(Span buffer, IList? socketList, ref int refsAdded, ref int maxFd) { if (socketList == null || socketList.Count == 0 ) { @@ -1902,16 +1902,7 @@ private static void FilterSelectList(IList? socketList, Span results) if (socketList == null) return; - // The Select API requires leaving in the input lists only those sockets that were ready. As such, we need to loop - // through each poll event, and for each that wasn't ready, remove the corresponding Socket from its list. Technically - // this is O(n^2), due to removing from the list requiring shifting down all elements after it. However, this doesn't - // happen with the most common cases. If very few sockets were ready, then as we iterate from the end of the list, each - // removal will typically be O(1) rather than O(n). If most sockets were ready, then we only need to remove a few, in - // which case we're only doing a small number of O(n) shifts. It's only for the intermediate case, where a non-trivial - // number of sockets are ready and a non-trivial number of sockets are not ready that we end up paying the most. We could - // avoid these costs by, for example, allocating a side list that we fill with the sockets that should remain, clearing - // the original list, and then populating the original list with the contents of the side list. That of course has its - // own costs, and so for now we do the "simple" thing. This can be changed in the future as needed. + // Removing list can be O(n^2), some more tgoughs are written in FilterPollList that does exactly same opeation. for (int i = socketList.Count - 1; i >= 0; --i) { @@ -1940,14 +1931,13 @@ private static unsafe SocketError SelectViaPoll( AddToPollArray(events, eventsLength, checkRead, ref offset, Interop.PollEvents.POLLIN | Interop.PollEvents.POLLHUP, ref refsAdded); AddToPollArray(events, eventsLength, checkWrite, ref offset, Interop.PollEvents.POLLOUT, ref refsAdded); AddToPollArray(events, eventsLength, checkError, ref offset, Interop.PollEvents.POLLPRI, ref refsAdded); - - Debug.Assert(offset <= eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); - Debug.Assert(refsAdded <= eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); + Debug.Assert(offset == eventsLength, $"Invalid adds. offset={offset}, eventsLength={eventsLength}."); + Debug.Assert(refsAdded == eventsLength, $"Invalid ref adds. refsAdded={refsAdded}, eventsLength={eventsLength}."); // Do the poll uint triggered = 0; int milliseconds = microseconds == -1 ? -1 : microseconds / 1000; - Interop.Error err = Interop.Sys.Poll(events, (uint)refsAdded, milliseconds, &triggered); + Interop.Error err = Interop.Sys.Poll(events, (uint)eventsLength, milliseconds, &triggered); if (err != Interop.Error.SUCCESS) { return GetSocketErrorForErrorCode(err); @@ -1984,7 +1974,7 @@ private static unsafe SocketError SelectViaPoll( } } - private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, IList? socketList, ref int arrOffset, Interop.PollEvents events, ref int refsAdded, int readCount = 0) + private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, IList? socketList, ref int arrOffset, Interop.PollEvents events, ref int refsAdded) { if (socketList == null) return; @@ -2005,28 +1995,6 @@ private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, socket.InternalSafeHandle.DangerousAddRef(ref success); int fd = (int)socket.InternalSafeHandle.DangerousGetHandle(); - if (readCount > 0) - { - // some platfoms like macOS do not like if there is duplication between real and error list. - // To fix that we will search read list and if macthing descriptor exiost we will add events flags - // instead of adding new entry to error list. - int readIndex = 0; - while (readIndex < readCount) - { - if (arr[readIndex].FileDescriptor == fd) - { - arr[i].Events |= events; - socket.InternalSafeHandle.DangerousRelease(); - break; - } - readIndex++; - } - if (readIndex != readCount) - { - continue; - } - } - arr[arrOffset++] = new Interop.PollEvent { Events = events, FileDescriptor = fd }; refsAdded++; } From 65effb7cbd86b4696c4fb45fa7b219f727af23ce Mon Sep 17 00:00:00 2001 From: wfurt Date: Wed, 17 Jul 2024 22:59:11 -0700 Subject: [PATCH 5/8] feedback --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index da05879c49fc7..cce94f71bada8 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -1815,9 +1815,9 @@ public static unsafe SocketError Select(IList? checkRead, IList? checkWrite, ILi private static SocketError SelectViaSelect(IList? checkRead, IList? checkWrite, IList? checkError, int microseconds) { - Span readFDs = checkRead?.Count > 20 ? new int[checkRead.Count] : checkRead?.Count > 0 ? stackalloc int[checkRead.Count] : Span.Empty; - Span writeFDs = checkWrite?.Count > 20 ? new int[checkWrite.Count] : checkWrite?.Count > 0 ? stackalloc int[checkWrite.Count] : Span.Empty;; - Span errorFDs = checkError?.Count > 20 ? new int[checkError.Count] : checkError?.Count > 0 ? stackalloc int[checkError.Count] : Span.Empty; + Span readFDs = checkRead?.Count > 20 ? new int[checkRead.Count] : stackalloc int[checkRead?.Count ?? 0]; + Span writeFDs = checkWrite?.Count > 20 ? new int[checkWrite.Count] : stackalloc int[checkWrite?.Count ?? 0]; + Span errorFDs = checkError?.Count > 20 ? new int[checkError.Count] : stackalloc int[checkError?.Count ?? 0]; int refsAdded = 0; int maxFd = 0; @@ -1834,22 +1834,18 @@ private static SocketError SelectViaSelect(IList? checkRead, IList? checkWrite, return GetSocketErrorForErrorCode(err); } + Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); + Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); + if (triggered == 0) { - Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); - Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); - Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); - checkRead?.Clear(); checkWrite?.Clear(); checkError?.Clear(); } else { - Socket.SocketListDangerousReleaseRefs(checkRead, ref refsAdded); - Socket.SocketListDangerousReleaseRefs(checkWrite, ref refsAdded); - Socket.SocketListDangerousReleaseRefs(checkError, ref refsAdded); - FilterSelectList(checkRead, readFDs); FilterSelectList(checkWrite, writeFDs); FilterSelectList(checkError, errorFDs); @@ -1994,7 +1990,6 @@ private static unsafe void AddToPollArray(Interop.PollEvent* arr, int arrLength, bool success = false; socket.InternalSafeHandle.DangerousAddRef(ref success); int fd = (int)socket.InternalSafeHandle.DangerousGetHandle(); - arr[arrOffset++] = new Interop.PollEvent { Events = events, FileDescriptor = fd }; refsAdded++; } From c3fdca58594b943ca02342e03a3e0f55d7c9c4c9 Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Mon, 22 Jul 2024 11:50:43 -0700 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Stephen Toub --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 2 +- .../System.Net.Sockets/tests/FunctionalTests/SelectTest.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index cce94f71bada8..bfeeb193b0675 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -1898,7 +1898,7 @@ private static void FilterSelectList(IList? socketList, Span results) if (socketList == null) return; - // Removing list can be O(n^2), some more tgoughs are written in FilterPollList that does exactly same opeation. + // This loop can be O(n^2) in the unexpected and worst case. Some more thoughts are written in FilterPollList that does exactly same operation. for (int i = socketList.Count - 1; i >= 0; --i) { diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs index b5ede7ae30830..9c97069e3110f 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SelectTest.cs @@ -419,13 +419,13 @@ private static void DoAccept(Socket listenSocket, int connectionsToAccept) } [ConditionalFact] - public void Slect_LargeNumber_Succcess() + public void Select_LargeNumber_Succcess() { const int MaxSockets = 1025; KeyValuePair[] socketPairs; try { - // we try to shoot for more socket than FD_SETSIZE (that is typically 1024 + // we try to shoot for more socket than FD_SETSIZE (that is typically 1024) socketPairs = Enumerable.Range(0, MaxSockets).Select(_ => SelectTest.CreateConnectedSockets()).ToArray(); } catch From f9a0b17713b921560fb78011658973740d894393 Mon Sep 17 00:00:00 2001 From: wfurt Date: Mon, 22 Jul 2024 11:54:36 -0700 Subject: [PATCH 7/8] feedback --- .../src/System/Net/Sockets/SocketPal.Unix.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs index bfeeb193b0675..579c2dea66160 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs @@ -1815,9 +1815,10 @@ public static unsafe SocketError Select(IList? checkRead, IList? checkWrite, ILi private static SocketError SelectViaSelect(IList? checkRead, IList? checkWrite, IList? checkError, int microseconds) { - Span readFDs = checkRead?.Count > 20 ? new int[checkRead.Count] : stackalloc int[checkRead?.Count ?? 0]; - Span writeFDs = checkWrite?.Count > 20 ? new int[checkWrite.Count] : stackalloc int[checkWrite?.Count ?? 0]; - Span errorFDs = checkError?.Count > 20 ? new int[checkError.Count] : stackalloc int[checkError?.Count ?? 0]; + const int MaxStackAllocCount = 20; // this is just arbitrary limit 3x 20 -> 60 e.g. close to 64 we have in some other places + Span readFDs = checkRead?.Count > MaxStackAllocCount ? new int[checkRead.Count] : stackalloc int[checkRead?.Count ?? 0]; + Span writeFDs = checkWrite?.Count > MaxStackAllocCount ? new int[checkWrite.Count] : stackalloc int[checkWrite?.Count ?? 0]; + Span errorFDs = checkError?.Count > MaxStackAllocCount ? new int[checkError.Count] : stackalloc int[checkError?.Count ?? 0]; int refsAdded = 0; int maxFd = 0; From f6e1acc60ce8a57ab127c68cc95a5c6ec7f137b0 Mon Sep 17 00:00:00 2001 From: wfurt Date: Fri, 26 Jul 2024 16:40:04 +0200 Subject: [PATCH 8/8] test --- .../tests/FunctionalTests/SocketOptionNameTest.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs index 8692ecd232954..d6b1caae730f4 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SocketOptionNameTest.cs @@ -236,11 +236,8 @@ public void FailedConnect_GetSocketOption_SocketOptionNameError(bool simpleGet) Assert.ThrowsAny(() => client.Connect(server.LocalEndPoint)); } - // Verify via Select that there's an error - const int FailedTimeout = 10 * 1000 * 1000; // 10 seconds - var errorList = new List { client }; - Socket.Select(null, null, errorList, FailedTimeout); - Assert.Equal(1, errorList.Count); + // Verify via Poll that there's an error + Assert.True(client.Poll(10_000_000, SelectMode.SelectError)); // Get the last error and validate it's what's expected int errorCode;