diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs index 3574339966880d..f6fb59906e32c2 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs @@ -12,6 +12,8 @@ internal static partial class Sys internal enum PipeFlags { O_CLOEXEC = 0x0010, + O_NONBLOCK_READ = 0x0400, + O_NONBLOCK_WRITE = 0x0800, } /// 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/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs new file mode 100644 index 00000000000000..91af8b6f52da39 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs @@ -0,0 +1,22 @@ +// 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; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32, EntryPoint = "CreateNamedPipeW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + internal static partial SafeFileHandle CreateNamedPipeFileHandle( + string pipeName, + int openMode, + int pipeMode, + int maxInstances, + int outBufferSize, + int inBufferSize, + int defaultTimeout, + ref SECURITY_ATTRIBUTES securityAttributes); + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index f89dbf1fab2d31..1a4cb17a51ccb4 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -105,8 +105,6 @@ Link="Common\Interop\Windows\Kernel32\Interop.GetProcessTimes.cs" /> - - - + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs index cebd7469d43667..8a7e2b914c4850 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs @@ -428,10 +428,10 @@ private void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out IntPtr private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) { // See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points: - // * The handles are duplicated as non-inheritable before they are passed to CreateProcess so - // that the child process can not close them + // * The handles are duplicated as inheritable before they are passed to CreateProcess so + // that the child process can use them // * CreateProcess allows you to redirect all or none of the standard IO handles, so we use - // GetStdHandle for the handles that are not being redirected + // Console.OpenStandard*Handle for the handles that are not being redirected var commandLine = new ValueStringBuilder(stackalloc char[256]); BuildCommandLine(startInfo, ref commandLine); @@ -468,7 +468,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) } else { - childInputPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_INPUT_HANDLE), false); + childInputPipeHandle = Console.OpenStandardInputHandle(); } if (startInfo.RedirectStandardOutput) @@ -477,7 +477,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) } else { - childOutputPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_OUTPUT_HANDLE), false); + childOutputPipeHandle = Console.OpenStandardOutputHandle(); } if (startInfo.RedirectStandardError) @@ -486,7 +486,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo) } else { - childErrorPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_ERROR_HANDLE), false); + childErrorPipeHandle = Console.OpenStandardErrorHandle(); } startupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle(); @@ -800,15 +800,6 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true } } - private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, ref Interop.Kernel32.SECURITY_ATTRIBUTES lpPipeAttributes, int nSize) - { - bool ret = Interop.Kernel32.CreatePipe(out hReadPipe, out hWritePipe, ref lpPipeAttributes, nSize); - if (!ret || hReadPipe.IsInvalid || hWritePipe.IsInvalid) - { - throw new Win32Exception(); - } - } - // Using synchronous Anonymous pipes for process input/output redirection means we would end up // wasting a worker threadpool thread per pipe instance. Overlapped pipe IO is desirable, since // it will take advantage of the NT IO completion port infrastructure. But we can't really use @@ -818,47 +809,31 @@ private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPip // for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously! private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs) { - Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributesParent = default; - securityAttributesParent.bInheritHandle = Interop.BOOL.TRUE; + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); - SafeFileHandle? hTmp = null; - try - { - if (parentInputs) - { - CreatePipeWithSecurityAttributes(out childHandle, out hTmp, ref securityAttributesParent, 0); - } - else - { - CreatePipeWithSecurityAttributes(out hTmp, - out childHandle, - ref securityAttributesParent, - 0); - } - // Duplicate the parent handle to be non-inheritable so that the child process - // doesn't have access. This is done for correctness sake, exact reason is unclear. - // One potential theory is that child process can do something brain dead like - // closing the parent end of the pipe and there by getting into a blocking situation - // as parent will not be draining the pipe at the other end anymore. - IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); - if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, - hTmp, - currentProcHandle, - out parentHandle, - 0, - false, - Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) - { - throw new Win32Exception(); - } - } - finally + // parentInputs=true: parent writes to pipe, child reads (stdin redirect). + // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect). + parentHandle = parentInputs ? writeHandle : readHandle; + SafeFileHandle hTmpChild = parentInputs ? readHandle : writeHandle; + + // Duplicate the child handle to be inheritable so that the child process + // has access. The original non-inheritable handle is closed afterwards. + IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess(); + if (!Interop.Kernel32.DuplicateHandle(currentProcHandle, + hTmpChild, + currentProcHandle, + out childHandle, + 0, + bInheritHandle: true, + Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS)) { - if (hTmp != null && !hTmp.IsInvalid) - { - hTmp.Dispose(); - } + int lastError = Marshal.GetLastWin32Error(); + parentHandle.Dispose(); + hTmpChild.Dispose(); + throw new Win32Exception(lastError); } + + hTmpChild.Dispose(); } private static string GetEnvironmentVariablesBlock(DictionaryWrapper sd) diff --git a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs index 867e12206528de..dfe4408e39b800 100644 --- a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs +++ b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; using Xunit; namespace System.IO.Pipes.Tests @@ -782,6 +783,29 @@ protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream } } + public class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeStreamConformanceTests + { + protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream Client) CreateServerAndClientStreams() + { + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); + + SafePipeHandle readPipeHandle = TransferOwnershipToPipeHandle(readHandle); + SafePipeHandle writePipeHandle = TransferOwnershipToPipeHandle(writeHandle); + + AnonymousPipeServerStream server = new(PipeDirection.Out, serverSafePipeHandle: writePipeHandle, clientSafePipeHandle: readPipeHandle); + AnonymousPipeClientStream client = new(PipeDirection.In, server.ClientSafePipeHandle); + return (server, client); + } + + private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle) + { + SafePipeHandle pipeHandle = new(handle.DangerousGetHandle(), ownsHandle: true); + handle.SetHandleAsInvalid(); + handle.Dispose(); + return pipeHandle; + } + } + public abstract class NamedPipeTest_ServerOut_ClientIn : NamedPipeStreamConformanceTests { protected override NamedPipeServerStream CreateServerStream(string pipeName, int maxInstances = 1) => diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs index ce6254df95fed6..038863b0a7f728 100644 --- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs +++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs @@ -49,7 +49,9 @@ public SendPacketsElement(FileStream fileStream, long offset, int count, bool en { ArgumentNullException.ThrowIfNull(fileStream); - if (!fileStream.IsAsync) + // Async IO for regular files is only supported on Windows. On Unix, FileStream.IsAsync is always + // false for regular files, because Unix does not support O_NONBLOCK for regular files. + if (!fileStream.IsAsync && OperatingSystem.IsWindows()) { throw new ArgumentException(SR.net_sockets_sendpackelement_FileStreamMustBeAsync, nameof(fileStream)); } diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs index 1c791e67c99ac2..59bb692db2c5bd 100644 --- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs +++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs @@ -714,6 +714,7 @@ public void FileStreamCtorNegCount_ArgumentOutOfRangeException() } [Fact] + [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files [ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)] public void FileStreamCtorSynchronous_ArgumentException() { diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 0ace31cf5ebb3b..725ef15dffd72e 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -38,8 +38,9 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid || AppContextConfigHelper.GetBooleanConfig("System.IO.DisableFileLocking", "DOTNET_SYSTEM_IO_DISABLEFILELOCKING", defaultValue: false); // not using bool? as it's not thread safe - private volatile NullableBool _canSeek = NullableBool.Undefined; - private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined; + private volatile NullableBool _canSeek /* = NullableBool.Undefined */; + private volatile NullableBool _supportsRandomAccess /* = NullableBool.Undefined */; + private volatile NullableBool _isAsync /* = NullableBool.Undefined */; private bool _deleteOnClose; private bool _isLocked; @@ -53,7 +54,25 @@ private SafeFileHandle(bool ownsHandle) SetHandle(new IntPtr(-1)); } - public bool IsAsync { get; private set; } + public bool IsAsync + { + get + { + NullableBool isAsync = _isAsync; + if (isAsync == NullableBool.Undefined && !IsClosed) + { + if (Interop.Sys.Fcntl.GetIsNonBlocking(this, out bool isNonBlocking) != 0) + { + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), Path); + } + + _isAsync = isAsync = isNonBlocking ? NullableBool.True : NullableBool.False; + } + + return isAsync == NullableBool.True; + } + private set => _isAsync = value ? NullableBool.True : NullableBool.False; + } internal bool CanSeek => !IsClosed && GetCanSeek(); @@ -161,6 +180,49 @@ public override bool IsInvalid } } + public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite) + { + // Allocate the handles first, so in case of OOM we don't leak any handles. + SafeFileHandle tempReadHandle = new(); + SafeFileHandle tempWriteHandle = new(); + + Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC; + if (asyncRead) + { + flags |= Interop.Sys.PipeFlags.O_NONBLOCK_READ; + } + + if (asyncWrite) + { + flags |= Interop.Sys.PipeFlags.O_NONBLOCK_WRITE; + } + + int readFd, writeFd; + unsafe + { + int* fds = stackalloc int[2]; + if (Interop.Sys.Pipe(fds, flags) != 0) + { + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + tempReadHandle.Dispose(); + tempWriteHandle.Dispose(); + throw Interop.GetExceptionForIoErrno(error); + } + + readFd = fds[Interop.Sys.ReadEndOfPipe]; + writeFd = fds[Interop.Sys.WriteEndOfPipe]; + } + + tempReadHandle.SetHandle(readFd); + tempReadHandle.IsAsync = asyncRead; + + tempWriteHandle.SetHandle(writeFd); + tempWriteHandle.IsAsync = asyncWrite; + + readHandle = tempReadHandle; + writeHandle = tempWriteHandle; + } + // Specialized Open that returns the file length and permissions of the opened file. // This information is retrieved from the 'stat' syscall that must be performed to ensure the path is not a directory. internal static SafeFileHandle OpenReadOnly(string fullPath, FileOptions options, out long fileLength, out UnixFileMode filePermissions) @@ -293,7 +355,7 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo } // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. - // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true + // - Asynchronous: Unix does not support O_NONBLOCK for regular files, only for pipes and sockets. // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose // - Encrypted: No equivalent on Unix and is ignored // - RandomAccess: Implemented after open if posix_fadvise is available @@ -346,7 +408,7 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share filePermissions = ((UnixFileMode)status.Mode) & PermissionMask; } - IsAsync = (options & FileOptions.Asynchronous) != 0; + IsAsync = false; // Unix does not support O_NONBLOCK for regular files. // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive // lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory, diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 5589d4b9b74b54..3d5f8d22db02ba 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -21,6 +22,74 @@ public SafeFileHandle() : base(true) { } + public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite) + { + Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default; + SafeFileHandle? tempReadHandle; + SafeFileHandle? tempWriteHandle; + + // When neither end is async, use the simple CreatePipe API + if (!asyncRead && !asyncWrite) + { + bool ret = Interop.Kernel32.CreatePipe(out tempReadHandle, out tempWriteHandle, ref securityAttributes, 0); + if (!ret) + { + throw new Win32Exception(); + } + + Debug.Assert(!tempReadHandle.IsInvalid); + Debug.Assert(!tempWriteHandle.IsInvalid); + + tempReadHandle._fileOptions = FileOptions.None; + tempWriteHandle._fileOptions = FileOptions.None; + } + else + { + // When one or both ends are async, use named pipes to support async I/O. + string pipeName = $@"\\.\pipe\dotnet_{Guid.NewGuid():N}"; + + // Security: we don't need to specify a security descriptor, because + // we allow only for 1 instance of the pipe and immediately open the write end, + // so there is no time window for another process to open the pipe with different permissions. + // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk. + + // Determine the open mode for the read end + int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND | + Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name + + if (asyncRead) + { + openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O + } + + const int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes + + // We could consider specifying a larger buffer size. + tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes); + + try + { + if (tempReadHandle.IsInvalid) + { + throw new Win32Exception(); + } + + tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None; + FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None; + tempWriteHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0); + } + catch + { + tempReadHandle.Dispose(); + + throw; + } + } + + readHandle = tempReadHandle; + writeHandle = tempWriteHandle; + } + public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0; internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0; diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 0a13a420cff8e0..81401661763eac 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -10,6 +10,24 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid private string? _path; private volatile int _cachedFileType = -1; + /// + /// Creates an anonymous pipe. + /// + /// When this method returns, contains the read end of the pipe. + /// When this method returns, contains the write end of the pipe. + /// to enable asynchronous IO for the read end of the pipe; otherwise, . + /// to enable asynchronous IO for the write end of the pipe; otherwise, . + /// + /// + /// The created handles are not inheritable by design to avoid accidental handle leaks to child processes. + /// + /// + /// On Windows, async handles are created with the FILE_FLAG_OVERLAPPED flag. + /// On Unix, async handles are created with the O_NONBLOCK flag. + /// + /// + public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false); + /// /// Creates a around a file handle. /// 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 f040846c83487b..c1b6c5cd7c0c7b 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 @@ -1809,6 +1809,15 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile.cs + + Common\Interop\Windows\Kernel32\Interop.PipeOptions.cs + + + Common\Interop\Windows\Kernel32\Interop.CreatePipe_SafeFileHandle.cs + + + Common\Interop\Windows\Kernel32\Interop.CreateNamedPipe_SafeFileHandle.cs + Common\Interop\Windows\Kernel32\Interop.EventWaitHandle.cs @@ -2410,6 +2419,9 @@ Common\Interop\Unix\System.Native\Interop.Close.cs + + Common\Interop\Unix\System.Native\Interop.Fcntl.cs + Common\Interop\Unix\System.Native\Interop.CopyFile.cs @@ -2434,6 +2446,9 @@ Common\Interop\Unix\System.Native\Interop.GetCpuUtilization.cs + + Common\Interop\Unix\System.Native\Interop.Pipe.cs + Common\Interop\Unix\System.Native\Interop.GetCwd.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 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 CreateConnectedStreamsAsync() protected override bool SupportsConcurrentBidirectionalUse => false; } + public abstract class AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeFileStreamConnectedConformanceTests + { + protected abstract bool AsyncReads { get; } + protected abstract bool AsyncWrites { get; } + + protected override Task CreateConnectedStreamsAsync() + { + SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncReads, asyncWrite: AsyncWrites); + + FileStream writeStream = new(writeHandle, FileAccess.Write); + FileStream readStream = new(readHandle, FileAccess.Read); + + return Task.FromResult((writeStream, readStream)); + } + } + + public class AnonymousPipeFileStreamConnectedConformanceTests_SyncRead_SyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe + { + protected override bool AsyncReads => false; + protected override bool AsyncWrites => false; + } + + public class AnonymousPipeFileStreamConnectedConformanceTests_AsyncRead_SyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe + { + protected override bool AsyncReads => true; + protected override bool AsyncWrites => false; + } + + public class AnonymousPipeFileStreamConnectedConformanceTests_SyncRead_AsyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe + { + protected override bool AsyncReads => false; + protected override bool AsyncWrites => true; + } + + public class AnonymousPipeFileStreamConnectedConformanceTests_AsyncRead_AsyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe + { + protected override bool AsyncReads => true; + protected override bool AsyncWrites => true; + } + public class NamedPipeFileStreamConnectedConformanceTests : ConnectedStreamConformanceTests { protected override async Task CreateConnectedStreamsAsync() diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs index 5155fa41c490bd..3c13c634b5b0df 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs @@ -192,7 +192,7 @@ static void Validate(FileStream fs, string expectedPath, bool expectedAsync, boo using (fs) { Assert.Equal(expectedPath, fs.Name); - Assert.Equal(expectedAsync, fs.IsAsync); + Assert.Equal(expectedAsync && IsAsyncIoSupportedForRegularFiles, fs.IsAsync); Assert.Equal(expectedCanRead, fs.CanRead); Assert.Equal(expectedCanWrite, fs.CanWrite); } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs index e22428c4b39780..30e21b98de7510 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs @@ -15,7 +15,7 @@ public void IsAsyncConstructorArg() { using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true)) { - Assert.True(fs.IsAsync); + Assert.Equal(IsAsyncIoSupportedForRegularFiles, fs.IsAsync); } using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false)) @@ -29,7 +29,7 @@ public void FileOptionsAsynchronousConstructorArg() { using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.Asynchronous)) { - Assert.True(fs.IsAsync); + Assert.Equal(IsAsyncIoSupportedForRegularFiles, fs.IsAsync); } using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.None)) @@ -44,7 +44,7 @@ public void AsyncDiscoveredFromHandle() using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true)) using (FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite)) { - Assert.True(fsh.IsAsync); + Assert.Equal(IsAsyncIoSupportedForRegularFiles, fsh.IsAsync); } using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false)) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs index 665bf08dbb3892..256129d8c3a7e0 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs @@ -40,7 +40,7 @@ public async Task UnmatchedAsyncIsAllowed(bool isAsync) using (FileStream newFs = CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync)) { // Verify that the new FileStream uses handle's IsAsync, not the parameter - Assert.Equal(isAsync, newFs.IsAsync); + Assert.Equal(IsAsyncIoSupportedForRegularFiles && isAsync, newFs.IsAsync); // Perform async write, seek to beginning, and async read to verify functionality byte[] writeBuffer = new byte[] { 1, 2, 3, 4, 5 }; diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs index ee4036a312fc5a..0ab7a860d9d9cd 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs @@ -24,7 +24,7 @@ public void ValidUseAsync(bool isAsync) { using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, isAsync)) { - Assert.Equal(isAsync, fs.IsAsync); + Assert.Equal(IsAsyncIoSupportedForRegularFiles && isAsync, fs.IsAsync); } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs index 54a7d3043472ba..9fc1a850666f24 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs @@ -49,7 +49,7 @@ public void ValidFileOptions(FileOptions option) using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, option)) { - Assert.Equal((option & FileOptions.Asynchronous) != 0, fs.IsAsync); + Assert.Equal((option & FileOptions.Asynchronous) != 0 && IsAsyncIoSupportedForRegularFiles, fs.IsAsync); // make sure we can write, seek, and read data with this option set fs.Write(data, 0, data.Length); diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs index b2db26f0b50c4f..88e75df34c7333 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs @@ -13,6 +13,8 @@ public abstract partial class FileSystemTest : FileCleanupTestBase public static bool ReservedDeviceNamesAreBlocked => PlatformDetection.IsWindows && !PlatformDetection.IsWindows10OrLater; + public static bool IsAsyncIoSupportedForRegularFiles => PlatformDetection.IsWindows; + public static TheoryData PathsWithInvalidColons = TestData.PathsWithInvalidColons; public static TheoryData PathsWithInvalidCharacters = TestData.PathsWithInvalidCharacters; public static TheoryData TrailingCharacters = TestData.TrailingCharacters; 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 31aa7d87f97c27..cd488c01c2c339 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -569,32 +569,32 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags) errno = ENOTSUP; return -1; #else // TARGET_WASM - switch (flags) + if ((flags & ~(PAL_O_CLOEXEC | PAL_O_NONBLOCK_READ | PAL_O_NONBLOCK_WRITE)) != 0) + { + assert_msg(false, "Unknown pipe flag", (int)flags); + errno = EINVAL; + return -1; + } + + int32_t pipeFlags = 0; + if ((flags & PAL_O_CLOEXEC) != 0) { - case 0: - break; - case PAL_O_CLOEXEC: #if HAVE_O_CLOEXEC - flags = O_CLOEXEC; + pipeFlags = O_CLOEXEC; #endif - break; - default: - assert_msg(false, "Unknown pipe flag", (int)flags); - errno = EINVAL; - return -1; } int32_t result; #if HAVE_PIPE2 // If pipe2 is available, use it. This will handle O_CLOEXEC if it was set. - while ((result = pipe2(pipeFds, flags)) < 0 && errno == EINTR); + while ((result = pipe2(pipeFds, pipeFlags)) < 0 && errno == EINTR); #elif HAVE_PIPE // Otherwise, use pipe. while ((result = pipe(pipeFds)) < 0 && errno == EINTR); // Then, if O_CLOEXEC was specified, use fcntl to configure the file descriptors appropriately. #if HAVE_O_CLOEXEC - if ((flags & O_CLOEXEC) != 0 && result == 0) + if ((pipeFlags & O_CLOEXEC) != 0 && result == 0) #else if ((flags & PAL_O_CLOEXEC) != 0 && result == 0) #endif @@ -616,6 +616,28 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags) #else /* HAVE_PIPE */ result = -1; #endif /* HAVE_PIPE */ + + if (result == 0 && ((flags & (PAL_O_NONBLOCK_READ | PAL_O_NONBLOCK_WRITE)) != 0)) + { + if ((flags & PAL_O_NONBLOCK_READ) != 0) + { + result = SystemNative_FcntlSetIsNonBlocking((intptr_t)pipeFds[0], 1); + } + + if (result == 0 && (flags & PAL_O_NONBLOCK_WRITE) != 0) + { + result = SystemNative_FcntlSetIsNonBlocking((intptr_t)pipeFds[1], 1); + } + + if (result != 0) + { + int tmpErrno = errno; + close(pipeFds[0]); + close(pipeFds[1]); + errno = tmpErrno; + } + } + return result; #endif // TARGET_WASM } @@ -1210,6 +1232,67 @@ 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) +{ + while (1) + { + int32_t result = Common_Read(fd, buffer, bufferSize); + if (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) + { + return result; + } + + // 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; + } + } +} + +int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize) +{ + while (1) + { + int32_t result = Common_Write(fd, buffer, bufferSize); + if (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) + { + return result; + } + + // 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; + } + } +} + 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 3d94e28bd1113d..bc6c108828d256 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -180,6 +180,8 @@ enum PAL_O_TRUNC = 0x0080, // Truncate file to length 0 if it already exists PAL_O_SYNC = 0x0100, // Block writes call will block until physically written PAL_O_NOFOLLOW = 0x0200, // Fails to open the target if it's a symlink, parent symlinks are allowed + PAL_O_NONBLOCK_READ = 0x0400, // Set O_NONBLOCK on the read end of a pipe + PAL_O_NONBLOCK_WRITE = 0x0800, // Set O_NONBLOCK on the write end of a pipe }; /** @@ -439,12 +441,11 @@ PALEXPORT int32_t SystemNative_CloseDir(DIR* dir); /** * Creates a pipe. Implemented as shim to pipe(2) or pipe2(2) if available. - * Flags are ignored if pipe2 is not available. * * Returns 0 for success, -1 for failure. Sets errno on failure. */ PALEXPORT int32_t SystemNative_Pipe(int32_t pipefd[2], // [out] pipefds[0] gets read end, pipefd[1] gets write end. - int32_t flags); // 0 for defaults or PAL_O_CLOEXEC for close-on-exec + int32_t flags); // 0 for defaults. Use PAL_O_CLOEXEC, PAL_O_NONBLOCK_READ, and PAL_O_NONBLOCK_WRITE for additional behavior. // NOTE: Rather than a general fcntl shim, we opt to export separate functions // for each command. This allows use to have strongly typed arguments and saves @@ -704,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. @@ -739,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. *