Skip to content

Commit ca29f93

Browse files
github-actions[bot]David Cantujozkeeadamsitnik
authored
[release/6.0] Fallback to read/write if pread/pwrite fails (#60280)
* Fallback to read/write if pread/pwrite fails * * Cache whether a handle supports random access * Keep avoiding pread/pwrite for unseekable files * Fallback to read/write only for known error ENXIO * Test more device paths and add tests for WriteAll* * Fix CI: skip test when device can't be opened * * Do not use SkipTestException * Exclude EBADF from assertion * use NullableBool in _supportsRandomAccess * Add tests for mkfifo and socketpair * Rename test file * Address feedback in src * Address suggestions * Address suggestions * Update src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs Use Lazy for thread-safety Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com> * Use IEnumerable<string> in Lazy and fix usages Co-authored-by: David Cantu <jozkee@macbook.lan> Co-authored-by: Jozkee <dacantu@microsoft.com> Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
1 parent dcc4be5 commit ca29f93

File tree

4 files changed

+287
-6
lines changed

4 files changed

+287
-6
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.DotNet.XUnitExtensions;
5+
using Microsoft.Win32.SafeHandles;
6+
using System.Collections.Generic;
7+
using System.IO.Pipes;
8+
using System.Runtime.InteropServices;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
using Xunit;
12+
13+
namespace System.IO.Tests
14+
{
15+
[PlatformSpecific(TestPlatforms.AnyUnix)]
16+
public class DevicesPipesAndSockets : FileSystemTest
17+
{
18+
[Theory]
19+
[MemberData(nameof(DevicePath_FileOptions_TestData))]
20+
public void CharacterDevice_FileStream_Write(string devicePath, FileOptions fileOptions)
21+
{
22+
FileStreamOptions options = new() { Options = fileOptions, Access = FileAccess.Write, Share = FileShare.Write };
23+
using FileStream fs = new(devicePath, options);
24+
fs.Write(Encoding.UTF8.GetBytes("foo"));
25+
}
26+
27+
[Theory]
28+
[MemberData(nameof(DevicePath_FileOptions_TestData))]
29+
public async Task CharacterDevice_FileStream_WriteAsync(string devicePath, FileOptions fileOptions)
30+
{
31+
FileStreamOptions options = new() { Options = fileOptions, Access = FileAccess.Write, Share = FileShare.Write };
32+
using FileStream fs = new(devicePath, options);
33+
await fs.WriteAsync(Encoding.UTF8.GetBytes("foo"));
34+
}
35+
36+
[Theory]
37+
[MemberData(nameof(DevicePath_TestData))]
38+
public void CharacterDevice_WriteAllBytes(string devicePath)
39+
{
40+
File.WriteAllBytes(devicePath, Encoding.UTF8.GetBytes("foo"));
41+
}
42+
43+
[Theory]
44+
[MemberData(nameof(DevicePath_TestData))]
45+
public async Task CharacterDevice_WriteAllBytesAsync(string devicePath)
46+
{
47+
await File.WriteAllBytesAsync(devicePath, Encoding.UTF8.GetBytes("foo"));
48+
}
49+
50+
[Theory]
51+
[MemberData(nameof(DevicePath_TestData))]
52+
public void CharacterDevice_WriteAllText(string devicePath)
53+
{
54+
File.WriteAllText(devicePath, "foo");
55+
}
56+
57+
[Theory]
58+
[MemberData(nameof(DevicePath_TestData))]
59+
public async Task CharacterDevice_WriteAllTextAsync(string devicePath)
60+
{
61+
await File.WriteAllTextAsync(devicePath, "foo");
62+
}
63+
64+
[Fact]
65+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
66+
public async Task NamedPipe_ReadWrite()
67+
{
68+
string fifoPath = GetTestFilePath();
69+
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ ));
70+
71+
await Task.WhenAll(
72+
Task.Run(() =>
73+
{
74+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
75+
ReadByte(fs, 42);
76+
}),
77+
Task.Run(() =>
78+
{
79+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read);
80+
WriteByte(fs, 42);
81+
}));
82+
}
83+
84+
[Fact]
85+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
86+
public async Task NamedPipe_ReadWrite_Async()
87+
{
88+
string fifoPath = GetTestFilePath();
89+
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ ));
90+
91+
await Task.WhenAll(
92+
Task.Run(async () => {
93+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
94+
await ReadByteAsync(fs, 42);
95+
}),
96+
Task.Run(async () =>
97+
{
98+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read);
99+
await WriteByteAsync(fs, 42);
100+
}));
101+
}
102+
103+
private const int AF_UNIX = 1;
104+
private const int SOCK_STREAM = 1;
105+
106+
[Fact]
107+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
108+
public unsafe void SocketPair_ReadWrite()
109+
{
110+
int* ptr = stackalloc int[2];
111+
Assert.Equal(0, socketpair(AF_UNIX, SOCK_STREAM, 0, ptr));
112+
113+
using var readFileStream = new FileStream(new SafeFileHandle((IntPtr)ptr[0], ownsHandle: true), FileAccess.Read);
114+
using var writeFileStream = new FileStream(new SafeFileHandle((IntPtr)ptr[1], ownsHandle: true), FileAccess.Write);
115+
116+
Task.WhenAll(
117+
Task.Run(() => ReadByte(readFileStream, 42)),
118+
Task.Run(() => WriteByte(writeFileStream, 42))).GetAwaiter().GetResult();
119+
}
120+
121+
[Fact]
122+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
123+
public void SocketPair_ReadWrite_Async()
124+
{
125+
unsafe
126+
{
127+
int* ptr = stackalloc int[2];
128+
Assert.Equal(0, socketpair(AF_UNIX, SOCK_STREAM, 0, ptr));
129+
130+
using var readFileStream = new FileStream(new SafeFileHandle((IntPtr)ptr[0], ownsHandle: true), FileAccess.Read);
131+
using var writeFileStream = new FileStream(new SafeFileHandle((IntPtr)ptr[1], ownsHandle: true), FileAccess.Write);
132+
133+
Task.WhenAll(
134+
ReadByteAsync(readFileStream, 42),
135+
WriteByteAsync(writeFileStream, 42)).GetAwaiter().GetResult();
136+
}
137+
}
138+
139+
private static void ReadByte(FileStream fs, byte expected)
140+
{
141+
var buffer = new byte[1];
142+
Assert.Equal(1, fs.Read(buffer));
143+
Assert.Equal(expected, buffer[0]);
144+
}
145+
146+
private static void WriteByte(FileStream fs, byte value)
147+
{
148+
fs.Write(new byte[] { value });
149+
fs.Flush();
150+
}
151+
152+
private static async Task ReadByteAsync(FileStream fs, byte expected)
153+
{
154+
var buffer = new byte[1];
155+
Assert.Equal(1, await fs.ReadAsync(buffer));
156+
Assert.Equal(expected, buffer[0]);
157+
}
158+
159+
private static async Task WriteByteAsync(FileStream fs, byte value)
160+
{
161+
await fs.WriteAsync(new byte[] { value });
162+
await fs.FlushAsync();
163+
}
164+
165+
private static Lazy<IEnumerable<string>> AvailableDevicePaths = new Lazy<IEnumerable<string>>(() =>
166+
{
167+
List<string> paths = new();
168+
FileStreamOptions options = new() { Access = FileAccess.Write, Share = FileShare.Write };
169+
170+
foreach (string devicePath in new[] { "/dev/tty", "/dev/console", "/dev/null", "/dev/zero" })
171+
{
172+
if (!File.Exists(devicePath))
173+
{
174+
continue;
175+
}
176+
177+
try
178+
{
179+
File.Open(devicePath, options).Dispose();
180+
}
181+
catch (Exception ex)
182+
{
183+
if (ex is IOException || ex is UnauthorizedAccessException)
184+
{
185+
continue;
186+
}
187+
188+
throw;
189+
}
190+
191+
paths.Add(devicePath);
192+
}
193+
194+
return paths;
195+
});
196+
197+
public static IEnumerable<object[]> DevicePath_FileOptions_TestData()
198+
{
199+
foreach (string devicePath in AvailableDevicePaths.Value)
200+
{
201+
foreach (FileOptions options in new[] { FileOptions.None, FileOptions.Asynchronous })
202+
{
203+
yield return new object[] { devicePath, options};
204+
}
205+
}
206+
}
207+
208+
public static IEnumerable<object[]> DevicePath_TestData()
209+
{
210+
foreach (string devicePath in AvailableDevicePaths.Value)
211+
{
212+
yield return new object[] { devicePath };
213+
}
214+
}
215+
216+
[DllImport("libc")]
217+
private static unsafe extern int socketpair(int domain, int type, int protocol, int* ptr);
218+
}
219+
}

src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
<Compile Include="FileInfo\GetSetAttributes.cs" />
119119
<Compile Include="FileInfo\Length.cs" />
120120
<Compile Include="FileInfo\Open.cs" />
121+
<Compile Include="FileStream\DevicesPipesAndSockets.cs" />
121122
<Compile Include="FileStream\FlushAsync.cs" />
122123
<Compile Include="FileStream\FileStreamConformanceTests.cs" />
123124
<Compile Include="FileStream\SafeFileHandle.cs" />

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
1616

1717
// not using bool? as it's not thread safe
1818
private volatile NullableBool _canSeek = NullableBool.Undefined;
19+
private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined;
1920
private bool _deleteOnClose;
2021
private bool _isLocked;
2122

@@ -33,6 +34,25 @@ private SafeFileHandle(bool ownsHandle)
3334

3435
internal bool CanSeek => !IsClosed && GetCanSeek();
3536

37+
internal bool SupportsRandomAccess
38+
{
39+
get
40+
{
41+
NullableBool supportsRandomAccess = _supportsRandomAccess;
42+
if (supportsRandomAccess == NullableBool.Undefined)
43+
{
44+
_supportsRandomAccess = supportsRandomAccess = GetCanSeek() ? NullableBool.True : NullableBool.False;
45+
}
46+
47+
return supportsRandomAccess == NullableBool.True;
48+
}
49+
set
50+
{
51+
Debug.Assert(value == false); // We should only use the setter to disable random access.
52+
_supportsRandomAccess = value ? NullableBool.True : NullableBool.False;
53+
}
54+
}
55+
3656
internal ThreadPoolBoundHandle? ThreadPoolBinding => null;
3757

3858
internal void EnsureThreadPoolBindingInitialized() { /* nop */ }

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,30 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer
3333
// The Windows implementation uses ReadFile, which ignores the offset if the handle
3434
// isn't seekable. We do the same manually with PRead vs Read, in order to enable
3535
// the function to be used by FileStream for all the same situations.
36-
int result = handle.CanSeek ?
37-
Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset) :
38-
Interop.Sys.Read(handle, bufPtr, buffer.Length);
36+
int result;
37+
if (handle.SupportsRandomAccess)
38+
{
39+
// Try pread for seekable files.
40+
result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
41+
if (result == -1)
42+
{
43+
// We need to fallback to the non-offset version for certain file types
44+
// e.g: character devices (such as /dev/tty), pipes, and sockets.
45+
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
46+
47+
if (errorInfo.Error == Interop.Error.ENXIO ||
48+
errorInfo.Error == Interop.Error.ESPIPE)
49+
{
50+
handle.SupportsRandomAccess = false;
51+
result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
52+
}
53+
}
54+
}
55+
else
56+
{
57+
result = Interop.Sys.Read(handle, bufPtr, buffer.Length);
58+
}
59+
3960
FileStreamHelpers.CheckFileCall(result, handle.Path);
4061
return result;
4162
}
@@ -90,9 +111,29 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<by
90111
// The Windows implementation uses WriteFile, which ignores the offset if the handle
91112
// isn't seekable. We do the same manually with PWrite vs Write, in order to enable
92113
// the function to be used by FileStream for all the same situations.
93-
int bytesWritten = handle.CanSeek ?
94-
Interop.Sys.PWrite(handle, bufPtr, GetNumberOfBytesToWrite(buffer.Length), fileOffset) :
95-
Interop.Sys.Write(handle, bufPtr, GetNumberOfBytesToWrite(buffer.Length));
114+
int bytesToWrite = GetNumberOfBytesToWrite(buffer.Length);
115+
int bytesWritten;
116+
if (handle.SupportsRandomAccess)
117+
{
118+
bytesWritten = Interop.Sys.PWrite(handle, bufPtr, bytesToWrite, fileOffset);
119+
if (bytesWritten == -1)
120+
{
121+
// We need to fallback to the non-offset version for certain file types
122+
// e.g: character devices (such as /dev/tty), pipes, and sockets.
123+
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
124+
125+
if (errorInfo.Error == Interop.Error.ENXIO ||
126+
errorInfo.Error == Interop.Error.ESPIPE)
127+
{
128+
handle.SupportsRandomAccess = false;
129+
bytesWritten = Interop.Sys.Write(handle, bufPtr, bytesToWrite);
130+
}
131+
}
132+
}
133+
else
134+
{
135+
bytesWritten = Interop.Sys.Write(handle, bufPtr, bytesToWrite);
136+
}
96137

97138
FileStreamHelpers.CheckFileCall(bytesWritten, handle.Path);
98139
if (bytesWritten == buffer.Length)

0 commit comments

Comments
 (0)