Add FileHandleType enum and SafeFileHandle.Type property#124561
Add FileHandleType enum and SafeFileHandle.Type property#124561
Conversation
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
|
Tagging subscribers to this area: @dotnet/area-system-io |
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
Show resolved
Hide resolved
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot address my feedback
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
Outdated
Show resolved
Hide resolved
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
Outdated
Show resolved
Hide resolved
…API in CanSeek Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
...libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
Outdated
Show resolved
Hide resolved
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
LGTM assuming the tests are going to pass
|
@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR |
There was a problem hiding this comment.
Pull request overview
Adds a new System.IO.FileHandleType enum and a SafeFileHandle.Type property to expose (and cache) the OS-level handle kind so callers can distinguish files/pipes/sockets/devices/directories/symlinks without repeated native queries.
Changes:
- Introduces
System.IO.FileHandleTypeand exposes it viaSafeFileHandle.Type(with caching and disposed-handle validation). - Implements platform-specific detection (
GetFileTypeCore) on Windows (Win32 APIs) and Unix (fstat), with Unix additionally priming the cache duringInit(). - Adds new cross-platform + platform-specific tests for the new API surface.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj | Includes new SafeFileHandle Type tests and required interop sources per platform. |
| src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs | Adds OS-agnostic tests for regular files, null device, anonymous pipes, sockets, caching, and disposed behavior. |
| src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs | Adds Windows-specific tests for directories, named pipes, console input, and symlinks. |
| src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs | Adds Unix-specific tests for directories, FIFOs, symlinks (via O_NOFOLLOW), and block devices. |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Updates public ref surface: adds SafeFileHandle.Type and FileHandleType enum. |
| src/libraries/System.Private.CoreLib/src/System/IO/FileHandleType.cs | Implements the new System.IO.FileHandleType enum in CoreLib. |
| src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems | Ensures new enum and GetNamedPipeInfo interop compile into CoreLib. |
| src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | Adds _cachedFileType field and public Type property with centralized caching. |
| src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | Implements Windows GetFileTypeCore, removes old cached _fileType, and updates CanSeek to use Type. |
| src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | Primes cached file type during Init() and adds GetFileTypeCore + Unix type mapping. |
| src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs | Generalizes interop signature to accept SafeHandle so it can be used from SafeFileHandle. |
| // If GetNamedPipeInfo fails, it's likely a socket | ||
| return System.IO.FileHandleType.Socket; |
There was a problem hiding this comment.
GetPipeOrSocketType treats any GetNamedPipeInfo failure as a socket. GetNamedPipeInfo can fail for reasons other than “this is a socket” (e.g., access denied, invalid parameters/handle state), which would misclassify non-socket handles as Socket. Consider checking Marshal.GetLastPInvokeError() and only returning Socket for the expected “not a pipe” error; otherwise return Unknown (or propagate) to avoid false positives.
| // If GetNamedPipeInfo fails, it's likely a socket | |
| return System.IO.FileHandleType.Socket; | |
| // GetNamedPipeInfo failed; inspect the error to determine if this is actually a socket | |
| const int ERROR_INVALID_HANDLE = 6; | |
| int error = Marshal.GetLastPInvokeError(); | |
| if (error == ERROR_INVALID_HANDLE) | |
| { | |
| return System.IO.FileHandleType.Socket; | |
| } | |
| return System.IO.FileHandleType.Unknown; |
There was a problem hiding this comment.
Good point, but we should throw Win32Exception(error) rather than return System.IO.FileHandleType.Unknown in such case.
There was a problem hiding this comment.
Addressed in commit b025b70:
- Added error code check in
GetPipeOrSocketType() - Only treats
ERROR_INVALID_HANDLEas Socket - Throws
Win32Exception(error)for unexpected errors
Also fixed:
- Reparse point handling now checks tag (only
IO_REPARSE_TAG_SYMLINKreturns SymbolicLink) - Unix exception includes Path for better diagnostics
- Test nullable and platform check fixes
| // Check if it's a reparse point (symbolic link) using GetFileInformationByHandleEx | ||
| Interop.Kernel32.FILE_BASIC_INFO basicInfo; | ||
| if (Interop.Kernel32.GetFileInformationByHandleEx(this, Interop.Kernel32.FileBasicInfo, &basicInfo, (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO))) | ||
| { | ||
| if ((basicInfo.FileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0) | ||
| { | ||
| return System.IO.FileHandleType.SymbolicLink; | ||
| } | ||
| } |
There was a problem hiding this comment.
GetDiskBasedType currently returns FileHandleType.SymbolicLink for any handle with FILE_ATTRIBUTE_REPARSE_POINT. Not all reparse points are symbolic links (e.g., mount points/junctions, OneDrive placeholders), so this can misreport the handle type. Consider querying the reparse tag (e.g., via GetFileInformationByHandleEx with an attribute/tag info class or FSCTL_GET_REPARSE_POINT) and only mapping IO_REPARSE_TAG_SYMLINK to SymbolicLink; otherwise return Unknown/RegularFile/Directory as appropriate.
There was a problem hiding this comment.
Addressed in commit b025b70. Now checks reparse tag using GetFileInformationByHandleEx with FileAttributeTagInfo (35). Only IO_REPARSE_TAG_SYMLINK returns SymbolicLink; other reparse points return RegularFile/Directory.
| int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status); | ||
| if (result != 0) | ||
| { | ||
| throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo()); |
There was a problem hiding this comment.
GetFileTypeCore throws using Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo()), which omits the handle’s Path from the exception. Other fstat-based paths in this type include the path for better diagnostics (e.g., FStatCheckIO / FileStreamHelpers.CheckFileCall). Consider using the overload that accepts Path when available so failures from the new public Type property are actionable.
| throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo()); | |
| Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); | |
| throw Interop.GetExceptionForIoErrno(error, Path); |
There was a problem hiding this comment.
Addressed in commit b025b70. Changed to Interop.GetExceptionForIoErrno(error, Path) to include the path in the exception message.
| string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" }; | ||
|
|
||
| string blockDevice = null; | ||
| foreach (string device in possibleBlockDevices) | ||
| { | ||
| if (File.Exists(device)) | ||
| { | ||
| blockDevice = device; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (blockDevice == null) | ||
| { |
There was a problem hiding this comment.
Nullable annotations: string blockDevice = null; will trigger nullable warnings (and this repo treats warnings as errors in tests). Use string? blockDevice = null; and prefer is null for the null check to avoid build breaks.
There was a problem hiding this comment.
Addressed in commit b025b70. Changed to string? blockDevice = null; and is null check.
| await readerTask; | ||
| } | ||
|
|
||
| [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] |
There was a problem hiding this comment.
GetFileType_SymbolicLink is in an AnyUnix-only test file, but it’s guarded by IsNotWindowsNanoServer, which is Windows-specific and effectively redundant here. Consider removing this condition or replacing it with a Unix-relevant capability check to keep the test intent clear.
| [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] | |
| [Fact] |
There was a problem hiding this comment.
Addressed in commit b025b70. Removed the redundant IsNotWindowsNanoServer condition from the Unix-only test file.
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
🤖 Copilot Code ReviewHolistic AssessmentMotivation: Adds public API ( Approach: Implementation reuses existing platform syscalls and follows established caching patterns. Platform separation is clean. Interop change ( Summary: Detailed Findings❌ API Approval RequiredPer dotnet/runtime policy: "We do not accept PRs for unapproved APIs." This PR adds public Required: Provide
|
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Description
Adds
System.IO.FileHandleTypeenum andSafeFileHandle.Typeproperty for efficient runtime detection of file handle types without additional syscalls.Changes
API Surface
System.IO.FileHandleTypeenum:Unknown,RegularFile,Pipe,Socket,CharacterDevice,Directory,SymbolicLink,BlockDeviceSafeFileHandle.Typeproperty:FileHandleTypeObjectDisposedExceptionwhen handle is closedvolatile int _cachedFileType)Implementation
Shared (
SafeFileHandle.cs):Typeproperty: validates handle state, manages caching, delegates toGetFileTypeCore()volatile int _cachedFileType = -1(uncached sentinel)GetFileTypeCore()→ store and returnWindows (
SafeFileHandle.Windows.cs):GetFileTypeCore(): mapsGetFileType()result via switch expressionFILE_TYPE_CHAR→CharacterDeviceFILE_TYPE_PIPE→ callsGetNamedPipeInfo; success →Pipe,ERROR_INVALID_HANDLE→Socket, other errors → throwsWin32ExceptionFILE_TYPE_DISK→ checksFILE_ATTRIBUTE_DIRECTORY, then queries reparse tag viaGetFileInformationByHandleExwithFileAttributeTagInfo- onlyIO_REPARSE_TAG_SYMLINKreturnsSymbolicLink(junctions, mount points, etc. returnRegularFile)CanSeekupdated to checkType == RegularFile(more precise than previousFILE_TYPE_DISKcheck - excludes directories and symlinks)Unix (
SafeFileHandle.Unix.cs):GetFileTypeCore(): callsFStat, mapsS_IFMTmode bits to enum valuesInit()opportunistically caches file type whenFStatalready performed (line 342)FStatfailure for better diagnosticsInterop (
Interop.GetNamedPipeInfo.cs):SafePipeHandletoSafeHandle(CoreLib cannot referenceSafePipeHandle)System.Private.CoreLib.Shared.projitemsfor Windows buildsExample Usage
Testing
Three platform-organized test files with comprehensive coverage:
GetFileType.cs(OS-agnostic): Regular files, null device (File.OpenNullHandle()), anonymous pipes, sockets, closed handle exception, caching behavior. Browser skips for pipes/sockets.GetFileType.Windows.cs: Directories (viaCreateFile+FILE_FLAG_BACKUP_SEMANTICS), named pipes (async coordination), console handles (Console.OpenStandardInputHandle()), symlinks (FILE_FLAG_OPEN_REPARSE_POINT). UsesunsafeforCreateFilecalls with null security attributes.GetFileType.Unix.cs: Directories, named pipes (mkfifo+ async/await pattern), symlinks (O_NOFOLLOW), block devices (conditional on privileged process, tries common device paths). Fixed nullable annotations (string? blockDevice).All Interop dependencies properly included in platform-specific ItemGroups in
.csproj. All 9,195 tests pass with 0 failures.💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.