From 34fe11419c8c89f443a057a9c4b462bc4598b293 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:03:27 +0000 Subject: [PATCH 1/3] Initial plan From 854cb2d25ba03dfdb2de248265e78756fefe172d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:38:17 +0000 Subject: [PATCH 2/3] Implement async handle support on Unix for FileStream Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Unix/System.Native/Interop.Read.cs | 3 + .../Unix/System.Native/Interop.Write.cs | 3 + .../src/System/IO/FileStream.cs | 4 -- .../src/System/IO/RandomAccess.Unix.cs | 12 +++- .../File/OpenHandle.cs | 38 ++++++++---- src/native/libs/System.Native/entrypoints.c | 2 + src/native/libs/System.Native/pal_io.c | 59 +++++++++++++++++++ src/native/libs/System.Native/pal_io.h | 16 +++++ 8 files changed, 120 insertions(+), 17 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs index 76f21fc80496e0..66d3913cdc7ae9 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs @@ -19,5 +19,8 @@ internal static partial class Sys /// [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Read", SetLastError = true)] internal static unsafe partial int Read(SafeHandle fd, byte* buffer, int count); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadFromNonblocking", SetLastError = true)] + internal static unsafe partial int ReadFromNonblocking(SafeHandle fd, byte* buffer, int count); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs index 749b34b2e0ca72..059debf52c7684 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs @@ -22,5 +22,8 @@ internal static partial class Sys [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Write", SetLastError = true)] internal static unsafe partial int Write(IntPtr fd, byte* buffer, int bufferSize); + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WriteToNonblocking", SetLastError = true)] + internal static unsafe partial int WriteToNonblocking(SafeHandle fd, byte* buffer, int bufferSize); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index 9404c2c2d45b27..50f15b4165c117 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -82,10 +82,6 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } - else if (!OperatingSystem.IsWindows() && handle.IsAsync) - { - throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle)); - } } public FileStream(SafeFileHandle handle, FileAccess access) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs index 147e3815812d0c..1fb4140feac36c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -30,7 +30,11 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer // isn't seekable. We do the same manually with PRead vs Read, in order to enable // the function to be used by FileStream for all the same situations. int result; - if (handle.SupportsRandomAccess) + if (handle.IsAsync) + { + result = Interop.Sys.ReadFromNonblocking(handle, bufPtr, buffer.Length); + } + else if (handle.SupportsRandomAccess) { // Try pread for seekable files. result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset); @@ -108,7 +112,11 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan("handle", () => new FileStream(readHandle, FileAccess.ReadWrite)); - AssertExtensions.Throws("handle", () => new FileStream(writeHandle, FileAccess.ReadWrite)); + readTask = readStream.ReadExactlyAsync(buffer).AsTask(); + writeTask = writeStream.WriteAsync(message, 0, message.Length); + await Task.WhenAll(readTask, writeTask); + Assert.Equal(message, buffer); } } diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 428c06603470a8..458980b89bd75e 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -104,11 +104,13 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_PosixFAdvise) DllImportEntry(SystemNative_FAllocate) DllImportEntry(SystemNative_Read) + DllImportEntry(SystemNative_ReadFromNonblocking) DllImportEntry(SystemNative_ReadLink) DllImportEntry(SystemNative_Rename) DllImportEntry(SystemNative_RmDir) DllImportEntry(SystemNative_Sync) DllImportEntry(SystemNative_Write) + DllImportEntry(SystemNative_WriteToNonblocking) DllImportEntry(SystemNative_CopyFile) DllImportEntry(SystemNative_INotifyInit) DllImportEntry(SystemNative_INotifyAddWatch) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index 8495923fd42388..e04f95fb08e4b7 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1232,6 +1232,65 @@ int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize) return Common_Read(fd, buffer, bufferSize); } +int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t bufferSize) +{ + int32_t result = Common_Read(fd, buffer, bufferSize); + if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + { + // The fd is non-blocking and no data is available yet. + // Block (on a thread pool thread) until data arrives or the pipe/socket is closed. + PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLIN, .TriggeredEvents = 0 }; + uint32_t triggered = 0; + int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered); + if (pollResult != Error_SUCCESS) + { + errno = ConvertErrorPalToPlatform(pollResult); + return -1; + } + + if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 && + (pollEvent.TriggeredEvents & PAL_POLLIN) == 0) + { + // The pipe/socket was closed with no data available (EOF). + return 0; + } + + result = Common_Read(fd, buffer, bufferSize); + } + + return result; +} + +int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize) +{ + int32_t result = Common_Write(fd, buffer, bufferSize); + if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + { + // The fd is non-blocking and the write buffer is full. + // Block (on a thread pool thread) until space is available or the pipe/socket is closed. + PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLOUT, .TriggeredEvents = 0 }; + uint32_t triggered = 0; + int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered); + if (pollResult != Error_SUCCESS) + { + errno = ConvertErrorPalToPlatform(pollResult); + return -1; + } + + if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 && + (pollEvent.TriggeredEvents & PAL_POLLOUT) == 0) + { + // The pipe/socket was closed. + errno = EPIPE; + return -1; + } + + result = Common_Write(fd, buffer, bufferSize); + } + + return result; +} + int32_t SystemNative_ReadLink(const char* path, char* buffer, int32_t bufferSize) { assert(buffer != NULL || bufferSize == 0); diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index 8c66bcef207227..bc6c108828d256 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -705,6 +705,14 @@ PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t le */ PALEXPORT int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize); +/** + * Reads the number of bytes specified into the provided buffer from the specified, opened non-blocking file descriptor. + * If no data is currently available, polls the file descriptor until data arrives or the pipe/socket is closed. + * + * Returns the number of bytes read on success; 0 on EOF; otherwise, -1 is returned and errno is set. + */ +PALEXPORT int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t bufferSize); + /** * Takes a path to a symbolic link and attempts to place the link target path into the buffer. If the buffer is too * small, the path will be truncated. No matter what, the buffer will not be null terminated. @@ -740,6 +748,14 @@ PALEXPORT void SystemNative_Sync(void); */ PALEXPORT int32_t SystemNative_Write(intptr_t fd, const void* buffer, int32_t bufferSize); +/** + * Writes the specified buffer to the provided open non-blocking file descriptor. + * If the write buffer is currently full, polls the file descriptor until space is available or the pipe/socket is closed. + * + * Returns the number of bytes written on success; otherwise, returns -1 and sets errno. + */ +PALEXPORT int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize); + /** * Copies all data from the source file descriptor to the destination file descriptor. * From 0f8f1817c24d3e11bf6c972ad2e07920c1e0c8e0 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 10 Mar 2026 16:15:01 +0100 Subject: [PATCH 3/3] Apply suggestions from code review --- .../tests/System.IO.FileSystem.Tests/File/OpenHandle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs index 30f206415f96f6..51886b46b9d428 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs @@ -189,7 +189,7 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_FileStream_SetsIsAsy using (FileStream writeStream = new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite)) { Task writeTask = writeStream.WriteAsync(message, 0, message.Length); - Task readTask = readStream.ReadAsync(buffer, 0, buffer.Length); + Task readTask = readStream.ReadExactlyAsync(buffer).AsTask(); await Task.WhenAll(writeTask, readTask); Assert.Equal(message, buffer);