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 f65aaf042bf891..367e90da48fd56 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,7 +38,6 @@ 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 NullableBool _canSeek /* = NullableBool.Undefined */; private NullableBool _supportsRandomAccess /* = NullableBool.Undefined */; private NullableBool _isAsync /* = NullableBool.Undefined */; private bool _deleteOnClose; @@ -74,8 +73,6 @@ public bool IsAsync private set => _isAsync = value ? NullableBool.True : NullableBool.False; } - internal bool CanSeek => !IsClosed && GetCanSeek(); - internal bool SupportsRandomAccess { get @@ -83,7 +80,7 @@ internal bool SupportsRandomAccess NullableBool supportsRandomAccess = _supportsRandomAccess; if (supportsRandomAccess == NullableBool.Undefined) { - _supportsRandomAccess = supportsRandomAccess = GetCanSeek() ? NullableBool.True : NullableBool.False; + _supportsRandomAccess = supportsRandomAccess = CanSeek ? NullableBool.True : NullableBool.False; } return supportsRandomAccess == NullableBool.True; @@ -545,19 +542,7 @@ private void FStatCheckIO(string path, ref Interop.Sys.FileStatus status, ref bo } } - private bool GetCanSeek() - { - Debug.Assert(!IsClosed); - Debug.Assert(!IsInvalid); - - NullableBool canSeek = _canSeek; - if (canSeek == NullableBool.Undefined) - { - _canSeek = canSeek = Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0 ? NullableBool.True : NullableBool.False; - } - - return canSeek == NullableBool.True; - } + private bool GetCanSeekCore() => Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0; internal FileHandleType GetFileTypeCore() { 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 5e99be778d7a57..dc26da4fc4d300 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 @@ -108,8 +108,6 @@ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, ou internal bool IsNoBuffering => (GetFileOptions() & FileStreamHelpers.NoBuffering) != 0; - internal bool CanSeek => !IsClosed && Type == FileHandleType.RegularFile; - internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } internal bool TryGetCachedLength(out long cachedLength) @@ -329,6 +327,20 @@ internal unsafe FileOptions GetFileOptions() return _fileOptions = result; } + // We need to figure out whether given file is seekable or not. + // The options that were considered: + // 1. Using SetFilePointerEx to move by 0 from current position: + // - It can succeed for non-seekable files (pipes, sockets) opened for sync IO, + // - It can fail for non-seekable files (pipes, sockets) opened for async IO. + // 2. Using Type property which internally uses GetFileType and then GetNamedPipeInfo to distinguish pipes from sockets. + // GetNamedPipeInfo requires GENERIC_READ or GENERIC_WRITE and FILE_READ_ATTRIBUTES access. + // There are valid scenarios like CreateNamedPipeW(PIPE_ACCESS_OUTBOUND) where GetNamedPipeInfo fails with ERROR_ACCESS_DENIED. + // 3. Switching from GetNamedPipeInfo to getsockopt to distinguish pipes from sockets. It would add a dependency on ws2_32.dll, which is not desirable. + // That is why we use the cached file type (if available), and if not available, we use GetFileType to determine whether the file is seekable or not. + private bool GetCanSeekCore() => _cachedFileType != -1 + ? (FileHandleType)_cachedFileType == FileHandleType.RegularFile + : Interop.Kernel32.GetFileType(this) == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + internal FileHandleType GetFileTypeCore() { Debug.Assert(Path is null || _fileOptions != UninitializedOptions, "When Path is set, _fileOptions are also provided."); 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 2cd5123f739c88..f33aec21932067 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 @@ -9,6 +9,8 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { private string? _path; private int _cachedFileType = -1; + // not using bool? as it's not thread safe + private NullableBool _canSeek /* = NullableBool.Undefined */; /// /// Creates an anonymous pipe. @@ -40,6 +42,25 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand internal string? Path => _path; + internal bool CanSeek + { + get + { + if (IsClosed) + { + return false; + } + + NullableBool canSeek = _canSeek; + if (canSeek == NullableBool.Undefined) + { + _canSeek = canSeek = GetCanSeekCore() ? NullableBool.True : NullableBool.False; + } + + return canSeek == NullableBool.True; + } + } + /// /// Gets the type of the file that this handle represents. /// diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs index 90ad33818e2f1d..0c3191757d7cb9 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -59,6 +59,34 @@ public async Task GetFileType_NamedPipe() Assert.Equal(FileHandleType.Pipe, clientHandle.Type); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void FileStream_WriteOnlyNamedPipe_CanSeek_IsAsync(bool isAsync) + { + int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_OUTBOUND; + if (isAsync) + { + openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; + } + + Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = Interop.Kernel32.SECURITY_ATTRIBUTES.Create(); + using SafeFileHandle handle = Interop.Kernel32.CreateNamedPipeFileHandle( + $@"\\.\pipe\{Guid.NewGuid():N}", + openMode, + pipeMode: (int)Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE, + maxInstances: 1, + outBufferSize: 0, + inBufferSize: 0, + defaultTimeout: 0, + ref securityAttributes); + Assert.False(handle.IsInvalid); + + using FileStream stream = new(handle, FileAccess.Write, bufferSize: 4096, isAsync); + Assert.False(stream.CanSeek); + Assert.Equal(isAsync, stream.IsAsync); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] public void GetFileType_ConsoleInput() { 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 94332ce80eae1a..92c3ef09d8c301 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 @@ -134,12 +134,14 @@ + +