-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
FileStream and PipeStream code reuse for async File IO on Windows #81960
Conversation
…PipeStream in case of pipes
…nceledException when user obtains handle and calls CancelIoEx (this is what ReadAsync and WriteAsync do)
Tagging subscribers to this area: @dotnet/area-system-io Issue Detailsnull
|
5bbfd49
to
6d74c6a
Compare
6d74c6a
to
e76bd6e
Compare
// For invalid handles, detect the error and mark our handle | ||
// as invalid to give slightly better error messages. Also | ||
// help ensure we avoid handle recycling bugs. | ||
_fileHandle!.SetHandleAsInvalid(); |
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.
in case Dispose
happened after the operation was started, only PipeStream
was so far disposing the handle:
_handle!.SetHandleAsInvalid(); |
RandomAccess
was doing that only when the handle was disposed before the operation was started:
runtime/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs
Line 272 in 84c2ff1
errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); |
_threadPoolBinding = threadPoolBinding; | ||
_canSeek = canSeek; | ||
_source.RunContinuationsAsynchronously = true; | ||
_preAllocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null); |
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.
so far SafeFileHandle
was using the UnsafeCreate
(not touching the execution context):
Line 66 in 5a62a79
_preallocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null); |
while PipeStream
used the "safe" ctor:
runtime/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.ValueTaskSource.cs
Line 34 in 5a62a79
_preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, null); |
We should unify that and I am not sure which option to choose. If the safe ctor should be the default we need to add tests for this scenario (currently all are passing)
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.
Do we run any user code as part of s_ioCallback? I believe the answer is "no", in which case it can be UnsafeCreate.
{ | ||
case Interop.Errors.ERROR_SUCCESS: | ||
case Interop.Errors.ERROR_PIPE_CONNECTED: // special case for when the client has already connected (used only by ConnectionValueTaskSource) | ||
case Interop.Errors.ERROR_BROKEN_PIPE when _isRead: // write to broken pipe should throw, read should return 0 |
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.
special handling these failure for only the read operations was so far done only by the pipes:
runtime/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.ValueTaskSource.cs
Lines 167 to 176 in 5a62a79
if (!_isWrite) | |
{ | |
bool messageCompletion = true; | |
switch (errorCode) | |
{ | |
case Interop.Errors.ERROR_BROKEN_PIPE: | |
case Interop.Errors.ERROR_PIPE_NOT_CONNECTED: | |
case Interop.Errors.ERROR_NO_DATA: | |
errorCode = 0; |
I've missed it in #77543
case Interop.Errors.ERROR_NO_DATA: | ||
// The handle was closed | ||
case Interop.Errors.ERROR_INVALID_HANDLE: | ||
_pipeStream.State = PipeState.Broken; |
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 need of updating this state is one of the reasons why relaxing RandomAccess
requirements to support non-seekable files was no the way to go to share the code between FileStream
and PipeStream
. We could ofc just await
the operations and call last error, but it would not be performant.
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 need of updating this state is one of the reasons why relaxing RandomAccess requirements to support non-seekable files was no the way to go to share the code between FileStream and PipeStream
Can you elaborate? I'd really like to get to a place where PipeStream doesn't need its own implementation and can just use RandomAccess.
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 some errors, PipeStream
modifies it's state before throwing an exception or returning result. This state is publicly exposed (IsMessageComplete
for example).
runtime/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.ValueTaskSource.cs
Lines 165 to 185 in 5a89bbe
private protected override void CompleteCore(uint errorCode, uint numBytes) | |
{ | |
if (!_isWrite) | |
{ | |
bool messageCompletion = true; | |
switch (errorCode) | |
{ | |
case Interop.Errors.ERROR_BROKEN_PIPE: | |
case Interop.Errors.ERROR_PIPE_NOT_CONNECTED: | |
case Interop.Errors.ERROR_NO_DATA: | |
errorCode = 0; | |
break; | |
case Interop.Errors.ERROR_MORE_DATA: | |
errorCode = 0; | |
messageCompletion = false; | |
break; | |
} | |
_pipeStream.UpdateMessageCompletion(messageCompletion); |
In case of IsMessageComplete
we would need to have sth like this:
public class PipeStream
{
public ValueTask<int> ReadAsync(Buffer, CancellationToken)
{
int result = await RandomAccess.ReadAsync(!Handle, Buffer, CancellationToken);
if (LastError() == ERROR_MORE_DATA)
{
UpdateMessageCompletion(messageCompletion)
}
return result;
}
}
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 problem is that SafePipeStream
binds a handle to the thread pool and currently there are no public APIs that would allow for creating SafeFileHandle
out of SafePipeHandle
.
Most likely both SafePipeHandle
and SafeFileHandle
would need to expose ThreadPoolBoundHandle
properties (Windows-only, low level, easy to mess up things):
Line 31 in 5a89bbe
internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } |
runtime/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Windows.cs
Lines 14 to 17 in 5a89bbe
public abstract partial class PipeStream : Stream | |
{ | |
internal const bool CheckOperationsRequiresSetHandle = true; | |
internal ThreadPoolBoundHandle? _threadPoolBinding; |
and add ctors that accept a handle and ThreadPoolBoundHandle
@@ -278,7 +278,7 @@ public async Task CancelTokenOn_ServerWaitForConnectionAsyncWithOuterCancellatio | |||
Task waitForConnectionTask = server.WaitForConnectionAsync(cts.Token); | |||
|
|||
Assert.True(InteropTest.CancelIoEx(server.SafePipeHandle), "Outer cancellation failed"); | |||
await Assert.ThrowsAsync<IOException>(() => waitForConnectionTask); | |||
await Assert.ThrowsAsync<OperationCanceledException>(() => waitForConnectionTask); |
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.
in theory this is a breaking change. I wanted to align WaitForConnectionAsync
with ReadAsync
and WriteAsync
.
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.
in theory this is a breaking change
We've gone to some lengths to maintain this behavior from .NET Framework. If we want to take a breaking change around it, I'm ok with that, but I don't have a great sense of how impactful that will be.
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 don't have a great sense of how impactful that will be
I hope that calling CancelIoEx
rather than passing cancellable token to WaitForConnectionAsync
is rare. I don't have strong opinion about it.
@@ -502,8 +502,6 @@ public async Task ReadAsync_DisconnectDuringRead_Returns0() | |||
} | |||
|
|||
[PlatformSpecific(TestPlatforms.Windows)] // Unix named pipes are on sockets, where small writes with an empty buffer will succeed immediately | |||
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] |
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 was just a copy paste error (the test is marked as [PlatformSpecific(TestPlatforms.Windows)]
so we don't need to skip it on other platforms)
/azp list |
This comment was marked as resolved.
This comment was marked as resolved.
/azp run runtime-libraries-coreclr outerloop-windows |
Azure Pipelines successfully started running 1 pipeline(s). |
@adamsitnik I'm going to close this PR since it's gone stale and we're not actively driving it forward. We can resume this effort later in the release cycle if/as time permits. |
No description provided.