-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fallback to read/write if pread/pwrite fails #59846
Conversation
Tagging subscribers to this area: @dotnet/area-system-io |
[PlatformSpecific(TestPlatforms.AnyUnix)] | ||
public class CharacterDevice | ||
{ | ||
const string CharacterDevicePath = "/dev/tty"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the reason of getting ENXIO was because we were using a character device. I tried to repro with /dev/null
and /dev/zero
but I couldn't so I don't know what's special about this vs the other two character devices.
Also, this prints in console what you write to it, not sure if that may be an issue for testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was the issue with /dev/tty
(aka text/input environment) and /dev/console
(aka physical terminal).
/dev/null
has special handling https://man7.org/linux/man-pages/man4/null.4.html, I think that's why it bails early as a no-op.
src/libraries/System.IO.FileSystem/tests/FileStream/CharacterDevice.cs
Outdated
Show resolved
Hide resolved
[InlineData(FileOptions.None)] | ||
[InlineData(FileOptions.Asynchronous)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we pass devicePath
as param and test /dev/tty
, /dev/console
and /dev/null
? They are available in test environments AFAICT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, I also included /dev/zero just in case.
int result = handle.CanSeek ? | ||
Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset) : | ||
Interop.Sys.Read(handle, bufPtr, buffer.Length); | ||
int result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue we're facing is that CanSeek returns true but PRead fails. However, is there ever a situation where CanSeek would be false but PRead at arbitrary offsets would succeed? Seems like we should continue using Read always if CanSeek is false; otherwise we're going to be paying for a PRead that's always going to fail.
Similar question/comment for the write path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
CanSeek
was added to the handle pread/pwrite support. I think we should replace it by something like MaybeCanRandomAccess
which optimistically defaults to true
, and gets set to false
in the error path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an almost-approved api proposal to expose SafeFileHandle.CanSeek #58370 so I don't think we should remove it.
I added another field that caches "supports random access" and its initial value is set to CanSeek.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we should continue using Read always if CanSeek is false
Done, for read and for write as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an almost-approved api proposal to expose SafeFileHandle.CanSeek #58370 so I don't think we should remove it.
I've added a comment to that proposal: #58370 (comment).
I think we should remove CanSeek
in this PR or when implementing #58381.
It makes sense to cache it as part of FileStream
then.
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
namespace System.IO.Tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be part of the ctor_*
tests that gets run from different APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was hard to figure out exacty how much is covered by those clases and I didn't want to end up with a ton of duplication (caused by the inheritance).
I would prefer to wait for @adamsitnik to come back to refactor the tests since he is more familiar with them.
src/libraries/System.IO.FileSystem/tests/FileStream/CharacterDevice.cs
Outdated
Show resolved
Hide resolved
* Keep avoiding pread/pwrite for unseekable files * Fallback to read/write only for known error ENXIO
* Exclude EBADF from assertion
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
Outdated
Show resolved
Hide resolved
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); | ||
AssertPReadPWriteKnownErrors(errorInfo.Error); | ||
|
||
if (errorInfo.Error == Interop.Error.ENXIO) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs ESPIPE
also.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you describe in which case we can get ESPIPE?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the path you pass in is a pipe/socket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for example, EPIPE when using pwrite on socket:
socketpair(AF_UNIX, SOCK_STREAM, 0, [3, 4]) = 0
pwrite64(3, "hello", 5, 0) = -1 ESPIPE (Illegal seek)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CanSeek reports false
for a FileStream opening a socket so in theory we will never call pwrite for that case (unless there's another distro that unexpectedly reports true).
I'm trying to add a unit test that covers this.
At least I can see that there is no issue with macOS in such case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As part of #58381, we'll be able to remove the syscall that is made for CanSeek
, which will require adding ESPIPE
here.
It's safer to add it, and there is no real cost to it.
src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.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/System/IO/RandomAccess.Unix.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Outdated
Show resolved
Hide resolved
@tmds @stephentoub @adamsitnik @carlossanlop this is ready for review. |
|
||
return supportsRandomAccess == NullableBool.True; | ||
} | ||
set => _supportsRandomAccess = value ? NullableBool.True : NullableBool.False; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This setter should only be used to set to false
. Maybe we should add an Assert
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests are excellent, the implementation is valid but I wonder if we could simplify it (please see my comment). Thank you @jozkee !
public class DevicesPipesAndSockets : FileSystemTest | ||
{ | ||
[Theory] | ||
[MemberData(nameof(DevicePath_FileOptions_TestData))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an opinion: when reviewing this file, the first thing I had to do was to scroll down and find DevicePath_FileOptions_TestData
. If it was at the top, I would not need to do that.
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Show resolved
Hide resolved
const int AF_UNIX = 1; | ||
const int SOCK_STREAM = 1; | ||
int* ptr = stackalloc int[2]; | ||
Assert.Equal(0, socketpair(AF_UNIX, SOCK_STREAM, 0, ptr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another great idea to test non-seekable files! 👍
public void SocketPair_ReadWrite_Async() | ||
{ | ||
const int AF_UNIX = 1; | ||
const int SOCK_STREAM = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these const
s are used in more than one method, they could be declared in the type or you could move the socketpair-related logic to a separate method:
private static unsafe (FileStream read, FileStream write) CreateSocketPair()
{
int* ptr = stackalloc int[2];
Assert.Equal(0, socketpair(1, 1, 0, ptr));
return (new FileStream(new SafeFileHandle((IntPtr)ptr[0], ownsHandle: true), FileAccess.Read),
new FileStream(new SafeFileHandle((IntPtr)ptr[1], ownsHandle: true), FileAccess.Write));
}
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you @jozkee
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Outdated
Show resolved
Hide resolved
…esAndSockets.cs Use Lazy for thread-safety Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
src/libraries/System.IO.FileSystem/tests/FileStream/DevicesPipesAndSockets.cs
Outdated
Show resolved
Hide resolved
/backport to release/6.0 |
Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/1331368776 |
set | ||
{ | ||
Debug.Assert(value == false); // We should only use the setter to disable random access. | ||
_supportsRandomAccess = value ? NullableBool.True : NullableBool.False; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth adding a comment here about benign race conditions. It's possible two threads could interleave in such a way that one thread fails an I/O and sets _supportsRandomAccess to false and then another thread that was in the process of initializing _supportsRandomAccess overwrites it with true based on CanSeek. It's currently benign, however, because it'll get set to false again the next time I/O fails.
Fixes the scenario in #59754 for main (7.0).
cc @am11