Skip to content
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

Merged
merged 13 commits into from
Oct 11, 2021
Merged

Conversation

jozkee
Copy link
Member

@jozkee jozkee commented Sep 30, 2021

Fixes the scenario in #59754 for main (7.0).
cc @am11

@jozkee jozkee added this to the 6.0.0 milestone Sep 30, 2021
@jozkee jozkee self-assigned this Sep 30, 2021
@ghost
Copy link

ghost commented Sep 30, 2021

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #59754
cc @am11

Author: Jozkee
Assignees: Jozkee
Labels:

area-System.IO

Milestone: 6.0.0

[PlatformSpecific(TestPlatforms.AnyUnix)]
public class CharacterDevice
{
const string CharacterDevicePath = "/dev/tty";
Copy link
Member Author

@jozkee jozkee Sep 30, 2021

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.

Copy link
Member

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.

Comment on lines 15 to 16
[InlineData(FileOptions.None)]
[InlineData(FileOptions.Asynchronous)]
Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

@tmds tmds Oct 4, 2021

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
Copy link
Member

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?

Copy link
Member Author

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.

David Cantu added 2 commits October 3, 2021 17:15
* Keep avoiding pread/pwrite for unseekable files
* Fallback to read/write only for known error ENXIO
@jozkee jozkee requested a review from adamsitnik October 4, 2021 04:45
* Exclude EBADF from assertion
Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo();
AssertPReadPWriteKnownErrors(errorInfo.Error);

if (errorInfo.Error == Interop.Error.ENXIO)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs ESPIPE also.

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member

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)

Copy link
Member Author

@jozkee jozkee Oct 5, 2021

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.

Copy link
Member

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.

@jozkee jozkee requested a review from tmds October 6, 2021 02:19
@jozkee jozkee requested a review from tmds October 7, 2021 18:07
@jozkee
Copy link
Member Author

jozkee commented Oct 7, 2021

@tmds @stephentoub @adamsitnik @carlossanlop this is ready for review.


return supportsRandomAccess == NullableBool.True;
}
set => _supportsRandomAccess = value ? NullableBool.True : NullableBool.False;
Copy link
Member

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?

Copy link
Member

@tmds tmds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Copy link
Member

@adamsitnik adamsitnik left a 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))]
Copy link
Member

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.

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));
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these consts 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));
}

Copy link
Member

@adamsitnik adamsitnik left a 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

…esAndSockets.cs


Use Lazy for thread-safety

Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>
@jozkee jozkee merged commit 825b264 into dotnet:main Oct 11, 2021
@jozkee jozkee deleted the seekable-dev-tty branch October 11, 2021 21:18
@jozkee
Copy link
Member Author

jozkee commented Oct 12, 2021

/backport to release/6.0

@github-actions
Copy link
Contributor

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;
Copy link
Member

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.

@ghost ghost locked as resolved and limited conversation to collaborators Nov 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants