diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs new file mode 100644 index 00000000000000..10d21b40d22c20 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ReadV.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadV", SetLastError = true)] + internal static unsafe partial long ReadV(SafeHandle fd, IOVector* vectors, int vectorCount); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs new file mode 100644 index 00000000000000..e890545f14ba39 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.WriteV.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WriteV", SetLastError = true)] + internal static unsafe partial long WriteV(SafeHandle fd, IOVector* vectors, int vectorCount); + } +} diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj index 341c9947b9c829..34c35a2f62cd64 100644 --- a/src/libraries/System.Console/src/System.Console.csproj +++ b/src/libraries/System.Console/src/System.Console.csproj @@ -81,16 +81,8 @@ Link="Common\Interop\Unix\Interop.StdinReady.cs" /> - - - - - - - - buffer) => _useReadLine ? ConsolePal.StdInReader.ReadLine(buffer) : #endif - ConsolePal.Read(_handle, buffer); + RandomAccess.Read(_handle, buffer, fileOffset: 0); public override void Write(ReadOnlySpan buffer) => ConsolePal.WriteFromConsoleStream(_handle, buffer); diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 57d4ef11fd62e3..8845acada13e2a 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -939,20 +939,6 @@ private static unsafe void EnsureInitializedCore() } } - /// Reads data from the file descriptor into the buffer. - /// The file descriptor. - /// The buffer to read into. - /// The number of bytes read, or an exception if there's an error. - private static unsafe int Read(SafeFileHandle fd, Span buffer) - { - fixed (byte* bufPtr = buffer) - { - int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length)); - Debug.Assert(result <= buffer.Length); - return result; - } - } - internal static void WriteToTerminal(ReadOnlySpan buffer, SafeFileHandle? handle = null, bool mayChangeCursorPosition = true) { handle ??= s_terminalHandle; @@ -978,65 +964,35 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp /// The file descriptor. /// The buffer from which to write data. /// Writing this buffer may change the cursor position. - private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) + private static void Write(SafeFileHandle fd, ReadOnlySpan buffer, bool mayChangeCursorPosition = true) { - fixed (byte* p = buffer) - { - byte* bufPtr = p; - int count = buffer.Length; - while (count > 0) - { - int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; + int cursorVersion = mayChangeCursorPosition ? Volatile.Read(ref s_cursorVersion) : -1; - int bytesWritten = Interop.Sys.Write(fd, bufPtr, count); - if (bytesWritten < 0) - { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (errorInfo.Error == Interop.Error.EPIPE) - { - // Broken pipe... likely due to being redirected to a program - // that ended, so simply pretend we were successful. - return; - } - else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK - { - // May happen if the file handle is configured as non-blocking. - // In that case, we need to wait to be able to write and then - // try again. We poll, but don't actually care about the result, - // only the blocking behavior, and thus ignore any poll errors - // and loop around to do another write (which may correctly fail - // if something else has gone wrong). - Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered); - continue; - } - else - { - // Something else... fail. - throw Interop.GetExceptionForIoErrno(errorInfo); - } - } - else - { - if (mayChangeCursorPosition) - { - UpdatedCachedCursorPosition(bufPtr, bytesWritten, cursorVersion); - } - } + try + { + RandomAccess.Write(fd, buffer, fileOffset: 0); + } + catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) + { + // Broken pipe... likely due to being redirected to a program + // that ended, so simply pretend we were successful. + return; + } - count -= bytesWritten; - bufPtr += bytesWritten; - } + if (mayChangeCursorPosition) + { + UpdatedCachedCursorPosition(buffer, cursorVersion); } } - private static unsafe void UpdatedCachedCursorPosition(byte* bufPtr, int count, int cursorVersion) + private static void UpdatedCachedCursorPosition(ReadOnlySpan buffer, int cursorVersion) { lock (Console.Out) { int left, top; if (cursorVersion != s_cursorVersion || // the cursor was changed during the write by another operation !TryGetCachedCursorPosition(out left, out top) || // we don't have a cursor position - count > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect + buffer.Length > InteractiveBufferSize) // limit the amount of bytes we are willing to inspect { InvalidateCachedCursorPosition(); return; @@ -1044,9 +1000,8 @@ private static unsafe void UpdatedCachedCursorPosition(byte* bufPtr, int count, GetWindowSize(out int width, out int height); - for (int i = 0; i < count; i++) + foreach (byte c in buffer) { - byte c = bufPtr[i]; if (c < 127 && c >= 32) // ASCII/UTF-8 characters that take up a single position { left++; diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs index 70aec828947c03..53d20868d0d028 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs @@ -244,20 +244,6 @@ internal static void EnsureConsoleInitialized() { } - /// Reads data from the file descriptor into the buffer. - /// The file descriptor. - /// The buffer to read into. - /// The number of bytes read, or an exception if there's an error. - private static unsafe int Read(SafeFileHandle fd, Span buffer) - { - fixed (byte* bufPtr = buffer) - { - int result = Interop.CheckIo(Interop.Sys.Read(fd, bufPtr, buffer.Length)); - Debug.Assert(result <= buffer.Length); - return result; - } - } - internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySpan buffer) { EnsureConsoleInitialized(); @@ -271,45 +257,16 @@ internal static unsafe void WriteFromConsoleStream(SafeFileHandle fd, ReadOnlySp /// Writes data from the buffer into the file descriptor. /// The file descriptor. /// The buffer from which to write data. - private static unsafe void Write(SafeFileHandle fd, ReadOnlySpan buffer) + private static void Write(SafeFileHandle fd, ReadOnlySpan buffer) { - fixed (byte* p = buffer) + try + { + RandomAccess.Write(fd, buffer, fileOffset: 0); + } + catch (IOException ex) when (Interop.Sys.ConvertErrorPlatformToPal(ex.HResult) == Interop.Error.EPIPE) { - byte* bufPtr = p; - int count = buffer.Length; - while (count > 0) - { - int bytesWritten = Interop.Sys.Write(fd, bufPtr, count); - if (bytesWritten < 0) - { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (errorInfo.Error == Interop.Error.EPIPE) - { - // Broken pipe... likely due to being redirected to a program - // that ended, so simply pretend we were successful. - return; - } - else if (errorInfo.Error == Interop.Error.EAGAIN) // aka EWOULDBLOCK - { - // May happen if the file handle is configured as non-blocking. - // In that case, we need to wait to be able to write and then - // try again. We poll, but don't actually care about the result, - // only the blocking behavior, and thus ignore any poll errors - // and loop around to do another write (which may correctly fail - // if something else has gone wrong). - Interop.Sys.Poll(fd, Interop.PollEvents.POLLOUT, Timeout.Infinite, out Interop.PollEvents triggered); - continue; - } - else - { - // Something else... fail. - throw Interop.GetExceptionForIoErrno(errorInfo); - } - } - - count -= bytesWritten; - bufPtr += bytesWritten; - } + // Broken pipe... likely due to being redirected to a program + // that ended, so simply pretend we were successful. } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs index 8ef602113be272..22db2706974389 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs @@ -350,7 +350,7 @@ public void WaitForPeerProcess() child2.StartInfo.RedirectStandardOutput = true; child2.Start(); char[] output = new char[6]; - child2.StandardOutput.Read(output, 0, output.Length); + child2.StandardOutput.ReadBlock(output, 0, output.Length); Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer child1.Kill(); @@ -380,7 +380,7 @@ public async Task WaitAsyncForPeerProcess() child2.StartInfo.RedirectStandardOutput = true; child2.Start(); char[] output = new char[6]; - child2.StandardOutput.Read(output, 0, output.Length); + child2.StandardOutput.ReadBlock(output, 0, output.Length); Assert.Equal("Signal", new string(output)); // wait for the signal before killing the peer child1.Kill(); diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index ece38286e289a0..7315408e420ca9 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2558,6 +2558,9 @@ Common\Interop\Unix\System.Native\Interop.PRead.cs + + Common\Interop\Unix\System.Native\Interop.ReadV.cs + Common\Interop\Unix\System.Native\Interop.PReadV.cs @@ -2570,6 +2573,9 @@ Common\Interop\Unix\System.Native\Interop.Read.cs + + Common\Interop\Unix\System.Native\Interop.WriteV.cs + Common\Interop\Unix\System.Native\Interop.ReadDir.cs 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 1fb4140feac36c..cf2e56021b15a9 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 @@ -29,7 +29,7 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer // The Windows implementation uses ReadFile, which ignores the offset if the handle // 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; + int result = -1; if (handle.IsAsync) { result = Interop.Sys.ReadFromNonblocking(handle, bufPtr, buffer.Length); @@ -38,21 +38,14 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer { // Try pread for seekable files. result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset); - if (result == -1) - { - // We need to fallback to the non-offset version for certain file types - // e.g: character devices (such as /dev/tty), pipes, and sockets. - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (errorInfo.Error == Interop.Error.ENXIO || - errorInfo.Error == Interop.Error.ESPIPE) - { - handle.SupportsRandomAccess = false; - result = Interop.Sys.Read(handle, bufPtr, buffer.Length); - } + if (result == -1 && ShouldFallBackToNonOffsetSyscall(Interop.Sys.GetLastErrorInfo())) + { + handle.SupportsRandomAccess = false; // Fall through to non-offset Read below. } } - else + + if (!handle.IsAsync && !handle.SupportsRandomAccess) { result = Interop.Sys.Read(handle, bufPtr, buffer.Length); } @@ -67,7 +60,7 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly MemoryHandle[] handles = new MemoryHandle[buffers.Count]; Span vectors = buffers.Count <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : new Interop.Sys.IOVector[buffers.Count]; - long result; + long result = -1; try { int buffersCount = buffers.Count; @@ -81,7 +74,20 @@ internal static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnly fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors)) { - result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset); + if (handle.SupportsRandomAccess) + { + result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset); + + if (result == -1 && ShouldFallBackToNonOffsetSyscall(Interop.Sys.GetLastErrorInfo())) + { + handle.SupportsRandomAccess = false; // Fall through to non-offset ReadV below. + } + } + + if (!handle.SupportsRandomAccess) + { + result = Interop.Sys.ReadV(handle, pinnedVectors, buffers.Count); + } } } finally @@ -111,7 +117,7 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan 0) { - long bytesWritten; + long bytesWritten = -1; Span left = vectors.Slice(buffersOffset); fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(left)) { - bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset); + if (handle.SupportsRandomAccess) + { + bytesWritten = Interop.Sys.PWriteV(handle, pinnedVectors, left.Length, fileOffset); + + if (bytesWritten == -1 && ShouldFallBackToNonOffsetSyscall(Interop.Sys.GetLastErrorInfo())) + { + handle.SupportsRandomAccess = false; // Fall through to non-offset WriteV below. + } + } + + if (!handle.SupportsRandomAccess) + { + bytesWritten = Interop.Sys.WriteV(handle, pinnedVectors, left.Length); + } } FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path); @@ -253,5 +265,12 @@ internal static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemo private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset, CancellationToken cancellationToken) => handle.GetThreadPoolValueTaskSource().QueueWriteGather(buffers, fileOffset, cancellationToken); + + /// + /// Checks the last error after a failed pread/pwrite/preadv/pwritev call + /// and returns true if the error indicates a non-seekable file type (ENXIO or ESPIPE). + /// + private static bool ShouldFallBackToNonOffsetSyscall(Interop.ErrorInfo lastError) + => lastError.Error == Interop.Error.ENXIO || lastError.Error == Interop.Error.ESPIPE; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs index 166d2faf3fbb14..154e8b4e5f8892 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs @@ -22,7 +22,7 @@ public static partial class RandomAccess /// The file does not support seeking (pipe or socket). public static long GetLength(SafeFileHandle handle) { - ValidateInput(handle, fileOffset: 0); + ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: false); return handle.GetFileLength(); } @@ -39,7 +39,7 @@ public static long GetLength(SafeFileHandle handle) /// is negative. public static void SetLength(SafeFileHandle handle, long length) { - ValidateInput(handle, fileOffset: 0); + ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: false); if (length < 0) { @@ -54,12 +54,12 @@ public static void SetLength(SafeFileHandle handle, long length) /// /// The file handle. /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -76,12 +76,12 @@ public static int Read(SafeFileHandle handle, Span buffer, long fileOffset /// /// The file handle. /// A list of memory buffers. When this method returns, the contents of the buffers are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -99,13 +99,13 @@ public static long Read(SafeFileHandle handle, IReadOnlyList> buffe /// /// The file handle. /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -127,13 +127,13 @@ public static ValueTask ReadAsync(SafeFileHandle handle, Memory buffe /// /// The file handle. /// A list of memory buffers. When this method returns, the contents of these buffers are replaced by the bytes read from the file. - /// The file position to read from. + /// The file position to read from. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for reading. /// An I/O error occurred. @@ -156,11 +156,11 @@ public static ValueTask ReadAsync(SafeFileHandle handle, IReadOnlyList /// The file handle. /// A region of memory. This method copies the contents of this region to the file. - /// The file position to write to. + /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -177,11 +177,11 @@ public static void Write(SafeFileHandle handle, ReadOnlySpan buffer, long /// /// The file handle. /// A list of memory buffers. This method copies the contents of these buffers to the file. - /// The file position to write to. + /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -199,13 +199,13 @@ public static void Write(SafeFileHandle handle, IReadOnlyList /// The file handle. /// A region of memory. This method copies the contents of this region to the file. - /// The file position to write to. + /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// A task representing the asynchronous completion of the write operation. /// is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -227,13 +227,13 @@ public static ValueTask WriteAsync(SafeFileHandle handle, ReadOnlyMemory b /// /// The file handle. /// A list of memory buffers. This method copies the contents of these buffers to the file. - /// The file position to write to. + /// The file position to write to. In .NET 11 and later versions, for a file that does not support seeking (pipe or socket), it's ignored. /// The token to monitor for cancellation requests. The default value is . /// A task representing the asynchronous completion of the write operation. /// or is . /// is invalid. /// The file is closed. - /// The file does not support seeking (pipe or socket). + /// In .NET 10 and earlier versions, the file does not support seeking (pipe or socket). /// is negative. /// was not opened for writing. /// An I/O error occurred. @@ -276,12 +276,12 @@ public static void FlushToDisk(SafeFileHandle handle) // Unix does NOT support unseekable handles however, the code that ultimately runs on Unix when we // call FileStreamHelpers.FlushToDisk() later below, will silently ignore those errors, effectively // making FlushToDisk() a no-op on Unix when used with unseekable handles. - ValidateInput(handle, fileOffset: 0, allowUnseekableHandles: true); + ValidateInput(handle, fileOffset: 0); FileStreamHelpers.FlushToDisk(handle); } - private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool allowUnseekableHandles = false) + private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool allowUnseekableHandles = true) { if (handle is null) { @@ -291,18 +291,13 @@ private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool a { ThrowHelper.ThrowArgumentException_InvalidHandle(nameof(handle)); } - else if (!handle.CanSeek) + else if (handle.IsClosed) { - // CanSeek calls IsClosed, we don't want to call it twice for valid handles - if (handle.IsClosed) - { - ThrowHelper.ThrowObjectDisposedException_FileClosed(); - } - - if (!allowUnseekableHandles) - { - ThrowHelper.ThrowNotSupportedException_UnseekableStream(); - } + ThrowHelper.ThrowObjectDisposedException_FileClosed(); + } + else if (!allowUnseekableHandles && !handle.CanSeek) + { + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); } else if (fileOffset < 0) { diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs index 9506d954d28374..3a0a23020d7bc8 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/Base.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.IO.Pipes; using System.Threading; using Microsoft.Win32.SafeHandles; using Xunit; @@ -15,8 +14,6 @@ public abstract class RandomAccess_Base : FileSystemTest protected virtual bool UsesOffsets => true; - protected virtual bool ThrowsForUnseekableFile => true; - public static IEnumerable GetSyncAsyncOptions() { yield return new object[] { FileOptions.None }; @@ -50,20 +47,6 @@ public void ThrowsObjectDisposedExceptionForDisposedHandle() Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); } - [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")] - public void ThrowsNotSupportedExceptionForUnseekableFile() - { - if (ThrowsForUnseekableFile) - { - using (var server = new AnonymousPipeServerStream(PipeDirection.Out)) - using (SafeFileHandle handle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false)) - { - Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); - } - } - } - [Theory] [MemberData(nameof(GetSyncAsyncOptions))] public void ThrowsArgumentOutOfRangeExceptionForNegativeFileOffset(FileOptions options) @@ -77,7 +60,7 @@ public void ThrowsArgumentOutOfRangeExceptionForNegativeFileOffset(FileOptions o } } - protected static CancellationTokenSource GetCancelledTokenSource() + internal static CancellationTokenSource GetCancelledTokenSource() { CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs index e821ad654e6b5d..10fa74f1b56291 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/FlushToDisk.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.IO.Pipes; using System.Security.Cryptography; using Microsoft.Win32.SafeHandles; using Xunit; @@ -24,8 +23,6 @@ public partial class RandomAccess_FlushToDisk : RandomAccess_Base protected override bool UsesOffsets => false; - protected override bool ThrowsForUnseekableFile => false; - protected override long MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) { RandomAccess.FlushToDisk(handle); @@ -84,18 +81,20 @@ public void CanFlushWithoutWriting() } [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")] + [SkipOnPlatform(TestPlatforms.Browser, "pipes aren't supported on browser")] public void CanFlushUnseekableFile() { - using (var server = new AnonymousPipeServerStream(PipeDirection.Out)) - using (SafeFileHandle handle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false)) + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); + + using (readHandle) + using (writeHandle) { // Flushing a non-seekable handle (in this case, a pipe handle) should work without throwing an // exception. On Windows, the FlushFileBuffers() function DOES work with non-seekable handles // (e.g. pipe handles) and that is what we are testing here. The fsync() function on Unix does // NOT support non-seekable handles but no exception is thrown on Unix either because we silently // ignore the errors effectively making the call below a no-op. - RandomAccess.FlushToDisk(handle); + RandomAccess.FlushToDisk(writeHandle); } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs new file mode 100644 index 00000000000000..95d7a5b34e75b9 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable.cs @@ -0,0 +1,329 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + // This class uses SafeFileHandle.CreateAnonymousPipe to create non-seekable file handles. + // On Windows, anonymous pipes are just named pipes. + // By default, all named pipes are created with blocking-wait mode enabled (PIPE_WAIT). + // With a blocking-wait handle (it's orthogonal FILE_FLAG_OVERLAPPED), the write operation + // cannot succeed until sufficient space is created in the buffer by reading from the other end of the pipe. + // It means that even small write operations may not complete until the corresponding + // read operations are issued on the other end of the pipe. + // That is why this class issues async reads before synchronous writes and async writes before synchronous reads. + // Source: https://learn.microsoft.com/windows/win32/ipc/named-pipe-type-read-and-wait-modes + [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")] + public class RandomAccess_NonSeekable : FileSystemTest + { + private const int VectorCount = 10; + private const int BufferSize = 3; + private const int VectorsByteCount = VectorCount * BufferSize; + + protected virtual bool AsyncHandles => false; + + private (SafeFileHandle readHandle, SafeFileHandle writeHandle) GetAnonymousPipeHandles() + { + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, + asyncRead: AsyncHandles, asyncWrite: AsyncHandles); + return (readHandle, writeHandle); + } + + [Fact] + public void ThrowsTaskAlreadyCanceledForCancelledTokenAsync() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + CancellationTokenSource cts = RandomAccess_Base.GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + AssertCanceled(RandomAccess.ReadAsync(readHandle, new byte[1], 0, token).AsTask(), token); + AssertCanceled(RandomAccess.WriteAsync(writeHandle, new byte[1], 0, token).AsTask(), token); + AssertCanceled(RandomAccess.ReadAsync(readHandle, GenerateVectors(1, 1), 0, token).AsTask(), token); + AssertCanceled(RandomAccess.WriteAsync(writeHandle, GenerateReadOnlyVectors(1, 1), 0, token).AsTask(), token); + } + + static void AssertCanceled(Task task, CancellationToken token) + { + Assert.True(task.IsCanceled); + TaskCanceledException ex = Assert.ThrowsAsync(() => task).Result; + Assert.Equal(token, ex.CancellationToken); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadToAnEmptyBufferReturnsZeroWhenDataIsAvailable(bool asyncRead) + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize); + byte[] readBuffer = new byte[writeBuffer.Length]; + + ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0); + + Assert.Equal(0, asyncRead + ? await RandomAccess.ReadAsync(readHandle, Array.Empty(), fileOffset: 0) + : RandomAccess.Read(readHandle, Array.Empty(), fileOffset: 0)); + + if (asyncRead) + { + await ReadExactlyAsync(readHandle, readBuffer, writeBuffer.Length); + } + else + { + ReadExactly(readHandle, readBuffer, writeBuffer.Length); + } + + await writeTask; + AssertExtensions.SequenceEqual(writeBuffer, readBuffer); + } + } + + [Fact] + public async Task CanReadToStackAllocatedMemory() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize); + + ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0); + + ReadToStackAllocatedBuffer(readHandle, writeBuffer); + + await writeTask; + } + + void ReadToStackAllocatedBuffer(SafeFileHandle handle, byte[] writeBuffer) + { + Span readBuffer = stackalloc byte[writeBuffer.Length]; + ReadExactly(handle, readBuffer, writeBuffer.Length); + AssertExtensions.SequenceEqual((ReadOnlySpan)writeBuffer, readBuffer); + } + } + + [Fact] + public async Task CanWriteFromStackAllocatedMemory() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize); + byte[] readBuffer = new byte[writeBuffer.Length]; + + Task readTask = ReadExactlyAsync(readHandle, readBuffer, writeBuffer.Length); + + WriteFromStackAllocatedBuffer(writeHandle, writeBuffer); + + await readTask; + AssertExtensions.SequenceEqual(writeBuffer, readBuffer); + } + + void WriteFromStackAllocatedBuffer(SafeFileHandle handle, byte[] array) + { + Span buffer = stackalloc byte[array.Length]; + array.CopyTo(buffer); + RandomAccess.Write(handle, buffer, fileOffset: 0); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task FileOffsetsAreIgnored(bool asyncWrite) + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize); + byte[] readBuffer = new byte[writeBuffer.Length]; + + ValueTask readTask = RandomAccess.ReadAsync(readHandle, readBuffer, fileOffset: 456); + + if (asyncWrite) + { + await RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 123); + } + else + { + RandomAccess.Write(writeHandle, writeBuffer, fileOffset: 123); + } + + int readFromOffset456 = await readTask; + Assert.InRange(readFromOffset456, 1, writeBuffer.Length); + AssertExtensions.SequenceEqual(writeBuffer.AsSpan(0, readFromOffset456), readBuffer.AsSpan(0, readFromOffset456)); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task PartialReadsAreSupported(bool useAsync) + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + byte[] writeBuffer = RandomNumberGenerator.GetBytes(BufferSize); + byte[] readBuffer = new byte[BufferSize]; + + ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 0); + + for (int i = 0; i < BufferSize; i++) + { + if (useAsync) + { + Assert.Equal(1, await RandomAccess.ReadAsync(readHandle, readBuffer.AsMemory(i, 1), fileOffset: 0)); + } + else + { + Assert.Equal(1, RandomAccess.Read(readHandle, readBuffer.AsSpan(i, 1), fileOffset: 0)); + } + } + + await writeTask; + Assert.Equal(writeBuffer, readBuffer); + } + } + + [Fact] + public async Task MultipleBuffersAreSupported_AsyncWrite_SyncReads() + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + ReadOnlyMemory[] writeVectors = GenerateReadOnlyVectors(VectorCount, BufferSize); + byte[] readBuffer = new byte[VectorsByteCount]; + + ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeVectors, fileOffset: 123); + + int totalBytesRead = 0; + int bytesRead; + do + { + bytesRead = RandomAccess.Read(readHandle, readBuffer.AsSpan(totalBytesRead), fileOffset: 456); + Assert.InRange(bytesRead, 0, VectorsByteCount - totalBytesRead); + totalBytesRead += bytesRead; + } while (totalBytesRead != VectorsByteCount && bytesRead > 0); + + await writeTask; + AssertExtensions.SequenceEqual( + writeVectors.SelectMany(vector => vector.ToArray()).ToArray().AsSpan(0, totalBytesRead), + readBuffer.AsSpan(0, totalBytesRead)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleBuffersAreSupported_AsyncWrite_ThenRead(bool asyncRead) + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + ReadOnlyMemory[] writeVectors = GenerateReadOnlyVectors(VectorCount, BufferSize); + Memory[] readVectors = GenerateVectors(VectorCount, BufferSize); + + ValueTask writeTask = RandomAccess.WriteAsync(writeHandle, writeVectors, fileOffset: 123); + + long bytesRead = asyncRead + ? await RandomAccess.ReadAsync(readHandle, readVectors, fileOffset: 456) + : RandomAccess.Read(readHandle, readVectors, fileOffset: 456); + + await writeTask; + Assert.InRange(bytesRead, 1, VectorsByteCount); + AssertEqual(writeVectors, readVectors, (int)bytesRead); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleBuffersAreSupported_AsyncRead_ThenWrite(bool asyncWrite) + { + (SafeFileHandle readHandle, SafeFileHandle writeHandle) = GetAnonymousPipeHandles(); + + using (readHandle) + using (writeHandle) + { + Memory[] readVectors = GenerateVectors(VectorCount, BufferSize); + byte[] writeBuffer = RandomNumberGenerator.GetBytes(VectorsByteCount); + + ValueTask readTask = RandomAccess.ReadAsync(readHandle, readVectors, fileOffset: 456); + + if (asyncWrite) + { + await RandomAccess.WriteAsync(writeHandle, writeBuffer, fileOffset: 123); + } + else + { + RandomAccess.Write(writeHandle, writeBuffer, fileOffset: 123); + } + + int bytesRead = (int)await readTask; + Assert.InRange(bytesRead, 1, VectorsByteCount); + Assert.Equal(writeBuffer.Take(bytesRead).ToArray(), readVectors.SelectMany(vector => vector.ToArray()).Take(bytesRead).ToArray()); + } + } + + private static ReadOnlyMemory[] GenerateReadOnlyVectors(int vectorCount, int bufferSize) + => Enumerable.Range(0, vectorCount).Select(_ => new ReadOnlyMemory(RandomNumberGenerator.GetBytes(bufferSize))).ToArray(); + + private static Memory[] GenerateVectors(int vectorCount, int bufferSize) + => Enumerable.Range(0, vectorCount).Select(_ => new Memory(RandomNumberGenerator.GetBytes(bufferSize))).ToArray(); + + private static void ReadExactly(SafeFileHandle readHandle, Span buffer, int expectedByteCount) + { + int totalBytesRead = 0; + int bytesRead; + do + { + bytesRead = RandomAccess.Read(readHandle, buffer.Slice(totalBytesRead), fileOffset: 0); + Assert.InRange(bytesRead, 0, expectedByteCount - totalBytesRead); + totalBytesRead += bytesRead; + } while (totalBytesRead != expectedByteCount && bytesRead > 0); + } + + private static async Task ReadExactlyAsync(SafeFileHandle readHandle, byte[] buffer, int expectedByteCount) + { + int totalBytesRead = 0; + int bytesRead; + do + { + bytesRead = await RandomAccess.ReadAsync(readHandle, buffer.AsMemory(totalBytesRead), fileOffset: 0); + Assert.InRange(bytesRead, 0, expectedByteCount - totalBytesRead); + totalBytesRead += bytesRead; + } while (totalBytesRead != expectedByteCount && bytesRead > 0); + } + + private static void AssertEqual(ReadOnlyMemory[] readOnlyVectors, Memory[] writableVectors, int byteCount) + => AssertExtensions.SequenceEqual( + readOnlyVectors.SelectMany(vector => vector.ToArray()).Take(byteCount).ToArray(), + writableVectors.SelectMany(vector => vector.ToArray()).Take(byteCount).ToArray()); + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs new file mode 100644 index 00000000000000..611760e361b17d --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/RandomAccess/NonSeekable_AsyncHandles.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_NonSeekable_AsyncHandles : RandomAccess_NonSeekable + { + protected override bool AsyncHandles => true; + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] + [InlineData(FileAccess.Read)] + [InlineData(FileAccess.Write)] + public async Task CancellationIsSupported(FileAccess access) + { + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, + asyncRead: true, asyncWrite: true); + + using (readHandle) + using (writeHandle) + { + SafeFileHandle handle = access == FileAccess.Read ? readHandle : writeHandle; + + Assert.True(handle.IsAsync); + + CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(250)); + CancellationToken token = cts.Token; + byte[] buffer = new byte[1024 * 1024]; // use a large buffer to ensure the async pipe write is pending + + OperationCanceledException ex = await Assert.ThrowsAsync( + () => access == FileAccess.Write + ? RandomAccess.WriteAsync(handle, buffer, 0, token).AsTask() + : RandomAccess.ReadAsync(handle, buffer, 0, token).AsTask()); + + Assert.Equal(token, ex.CancellationToken); + } + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index a81429675984db..e25561d68230a8 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -85,6 +85,8 @@ + + diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 458980b89bd75e..ada6d523e8a51b 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -278,6 +278,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_PWrite) DllImportEntry(SystemNative_PReadV) DllImportEntry(SystemNative_PWriteV) + DllImportEntry(SystemNative_ReadV) + DllImportEntry(SystemNative_WriteV) DllImportEntry(SystemNative_CreateThread) DllImportEntry(SystemNative_EnablePosixSignalHandling) DllImportEntry(SystemNative_DisablePosixSignalHandling) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index cd488c01c2c339..8d25d7b370dbf0 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1949,7 +1949,6 @@ int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64 return (int32_t)count; } -#if (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM) static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount) { #if defined(IOV_MAX) @@ -1994,7 +1993,45 @@ static int GetAllowedVectorCount(IOVector* vectors, int32_t vectorCount) return allowedCount; } -#endif // (HAVE_PREADV || HAVE_PWRITEV) && !defined(TARGET_WASM) + +int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount) +{ + assert(vectors != NULL); + assert(vectorCount >= 0); + + int fileDescriptor = ToFileDescriptor(fd); + int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount); + + while (1) + { + int64_t count; + while ((count = readv(fileDescriptor, (struct iovec*)vectors, allowedVectorCount)) < 0 && errno == EINTR); + + if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) + { + assert(count >= -1); + return count; + } + + // 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 = fileDescriptor, .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; + } + } +} int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset) { @@ -2037,6 +2074,46 @@ int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, return count; } +int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount) +{ + assert(vectors != NULL); + assert(vectorCount >= 0); + + int fileDescriptor = ToFileDescriptor(fd); + int allowedVectorCount = GetAllowedVectorCount(vectors, vectorCount); + + while (1) + { + int64_t count; + while ((count = writev(fileDescriptor, (struct iovec*)vectors, allowedVectorCount)) < 0 && errno == EINTR); + + if (count != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) + { + assert(count >= -1); + return count; + } + + // 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 = fileDescriptor, .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; + } + } +} + int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset) { assert(vectors != NULL); diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index bc6c108828d256..b27b45659b0a2e 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -875,13 +875,27 @@ PALEXPORT int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferS /** * Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor at specified offset. * - * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set. + * Returns the number of bytes read on success; otherwise, -1 is returned and errno is set. */ PALEXPORT int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset); /** * Writes the number of bytes specified in the buffers into the specified, opened file descriptor at specified offset. * - * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set. + * Returns the number of bytes written on success; otherwise, -1 is returned and errno is set. */ PALEXPORT int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset); + +/** + * Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor. + * + * Returns the number of bytes read on success; otherwise, -1 is returned and errno is set. + */ +PALEXPORT int64_t SystemNative_ReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount); + +/** + * Writes the number of bytes specified in the buffers into the specified, opened file descriptor + * + * Returns the number of bytes written on success; otherwise, -1 is returned and errno is set. + */ +PALEXPORT int64_t SystemNative_WriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount);