未验证 提交 6bcc3e09 编写于 作者: A Adam Sitnik 提交者: GitHub

unify error handling for FileStream and Pipe Streams (#77543)

上级 1b27edad
......@@ -22,6 +22,7 @@
<Compile Include="NamedPipeTests\NamedPipeTest.Specific.cs" />
<Compile Include="PipeStream.ProtectedMethods.Tests.cs" />
<Compile Include="PipeStreamConformanceTests.cs" />
<Compile Include="UnifiedErrorHandlingTests.cs" />
<Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs" Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Xunit;
namespace System.IO.Pipes.Tests
{
public static class UnifiedErrorHandlingTests
{
[Fact]
public static void When_AnonymousPipeServer_IsClosed_AnonymousPipeClient_ReadReturnsZero()
=> DiposeServerAndVerifyClientBehaviour(
GetAnonymousPipeStreams(PipeDirection.Out, PipeDirection.In),
AssertZeroByteRead);
[Fact]
public static void When_AnonymousPipeServer_IsClosed_FileStreamClient_ReadReturnsZero()
=> DiposeServerAndVerifyClientBehaviour(
GetAnonymousPipeServerAndFileStreamClient(PipeDirection.Out, FileAccess.Read),
AssertZeroByteRead);
[Fact]
public static void When_AnonymousPipeServer_IsClosed_AnonymousPipeClient_WriteThrows()
=> DiposeServerAndVerifyClientBehaviour(
GetAnonymousPipeStreams(PipeDirection.In, PipeDirection.Out),
AssertWriteThrows);
[Fact]
public static void When_AnonymousPipeServer_IsClosed_FileStreamClient_WriteThrows()
=> DiposeServerAndVerifyClientBehaviour(
GetAnonymousPipeServerAndFileStreamClient(PipeDirection.In, FileAccess.Write),
AssertWriteThrows);
[Fact]
public static Task When_AnonymousPipeServer_IsClosed_AnonymousPipeClient_ReadAsyncReturnsZero()
=> DiposeServerAndVerifyClientBehaviourAsync(
GetAnonymousPipeStreams(PipeDirection.Out, PipeDirection.In),
AssertZeroByteReadAsync);
[Fact]
public static Task When_AnonymousPipeServer_IsClosed_FileStreamClient_ReadAsyncReturnsZero()
=> DiposeServerAndVerifyClientBehaviourAsync(
GetAnonymousPipeServerAndFileStreamClient(PipeDirection.Out, FileAccess.Read),
AssertZeroByteReadAsync);
[Fact]
public static Task When_AnonymousPipeServer_IsClosed_AnonymousPipeClient_WriteAsyncThrows()
=> DiposeServerAndVerifyClientBehaviourAsync(
GetAnonymousPipeStreams(PipeDirection.In, PipeDirection.Out),
AssertWriteAsyncThrows);
[Fact]
public static Task When_AnonymousPipeServer_IsClosed_FileStreamClient_WriteAsyncThrows()
=> DiposeServerAndVerifyClientBehaviourAsync(
GetAnonymousPipeServerAndFileStreamClient(PipeDirection.In, FileAccess.Write),
AssertWriteAsyncThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_NamedPipeClient_ReadReturnsZero(bool asyncHandles)
=> DiposeServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.Out, PipeDirection.In),
AssertZeroByteRead);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_FileStreamClient_ReadReturnsZero(bool asyncHandles)
=> DiposeServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.Out, FileAccess.Read),
AssertZeroByteRead);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_NamedPipeClient_WriteThrows(bool asyncHandles)
=> DiposeServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.In, PipeDirection.Out),
AssertWriteThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_FileStreamClient_WriteThrows(bool asyncHandles)
=> DiposeServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.In, FileAccess.Write),
AssertWriteThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_NamedPipeClient_ReadAsyncReturnsZero(bool asyncHandles)
=> await DiposeServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.Out, PipeDirection.In),
AssertZeroByteReadAsync);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_FileStreamClient_ReadAsyncReturnsZero(bool asyncHandles)
=> await DiposeServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.Out, FileAccess.Read),
AssertZeroByteReadAsync);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_NamedPipeClient_WriteAsyncThrows(bool asyncHandles)
=> await DiposeServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.In, PipeDirection.Out),
AssertWriteAsyncThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServer_IsClosed_FileStreamClient_WriteAsyncThrows(bool asyncHandles)
=> await DiposeServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.In, FileAccess.Write),
AssertWriteAsyncThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsNamedPipeClient_ReadReturnsZero(bool asyncHandles)
=> DisconnectServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.Out, PipeDirection.In),
AssertZeroByteRead);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsFileStreamClient_ReadReturnsZero(bool asyncHandles)
=> DisconnectServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.Out, FileAccess.Read),
AssertZeroByteRead);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsNamedPipeClient_WriteThrows(bool asyncHandles)
=> DisconnectServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.In, PipeDirection.Out),
AssertWriteThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsFileStreamClient_WriteThrows(bool asyncHandles)
=> DisconnectServerAndVerifyClientBehaviour(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.In, FileAccess.Write),
AssertWriteThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsNamedPipeClient_ReadAsyncReturnsZero(bool asyncHandles)
=> await DisconnectServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.Out, PipeDirection.In),
AssertZeroByteReadAsync);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsFileStreamClient_ReadAsyncReturnsZero(bool asyncHandles)
=> await DisconnectServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.Out, FileAccess.Read),
AssertZeroByteReadAsync);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsNamedPipeClient_WriteAsyncThrows(bool asyncHandles)
=> await DisconnectServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeStreams(asyncHandles, PipeDirection.In, PipeDirection.Out),
AssertWriteAsyncThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")]
[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS, "iOS/tvOS blocks binding to UNIX sockets")]
public static async Task When_NamedPipeServerDisconnectsFileStreamClient_WriteAsyncThrows(bool asyncHandles)
=> await DisconnectServerAndVerifyClientBehaviourAsync(
await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.In, FileAccess.Write),
AssertWriteAsyncThrows);
[Theory]
[InlineData(true)]
[InlineData(false)]
[PlatformSpecific(TestPlatforms.Windows)]
public static Task When_NamedPipeServer_IsClosed_CopyToAsyncJustFinishesWithoutThrowing(bool asyncHandles)
=> TestCopyToAsync(asyncHandles, dispose: true);
[Theory]
[InlineData(true)]
[InlineData(false)]
[PlatformSpecific(TestPlatforms.Windows)]
public static Task When_NamedPipeServerDisconnectsCopyToAsyncJustFinishesWithoutThrowing(bool asyncHandles)
=> TestCopyToAsync(asyncHandles, dispose: false);
private static async Task TestCopyToAsync(bool asyncHandles, bool dispose)
{
(NamedPipeServerStream server, FileStream client) = await GetConnectedNamedPipeServerAndFileStreamClientStreams(asyncHandles, PipeDirection.Out, FileAccess.Read);
using (server)
using (client)
using (MemoryStream destination = new())
{
if (dispose)
{
await server.DisposeAsync();
}
else
{
server.Disconnect();
}
// At this moment, the client handle is opened, it does not know yet that server has disconnected/closed the handle
Assert.True(client.CanRead);
await client.CopyToAsync(destination);
Assert.Equal(0, destination.Length);
Assert.Equal(0, destination.Position);
}
}
private static (AnonymousPipeServerStream server, AnonymousPipeClientStream client) GetAnonymousPipeStreams(
PipeDirection serverDirection, PipeDirection clientDirection)
{
AnonymousPipeServerStream server = new(serverDirection);
AnonymousPipeClientStream client = new(clientDirection, server.ClientSafePipeHandle);
return (server, client);
}
private static (AnonymousPipeServerStream server, FileStream client) GetAnonymousPipeServerAndFileStreamClient(
PipeDirection serverDirection, FileAccess clientAccess)
{
AnonymousPipeServerStream server = new(serverDirection);
FileStream client = new(new SafeFileHandle(nint.Parse(server.GetClientHandleAsString()), ownsHandle: true), clientAccess, 0);
return (server, client);
}
private static async Task<(NamedPipeServerStream server, NamedPipeClientStream client)> GetConnectedNamedPipeStreams(
bool asyncHandles, PipeDirection serverDirection, PipeDirection clientDirection)
{
PipeOptions options = asyncHandles ? PipeOptions.Asynchronous : PipeOptions.None;
string pipeName = PipeStreamConformanceTests.GetUniquePipeName();
NamedPipeServerStream server = new(pipeName, serverDirection, 1, PipeTransmissionMode.Byte, options);
NamedPipeClientStream client = new(".", pipeName, clientDirection, options);
await Task.WhenAll(client.ConnectAsync(), server.WaitForConnectionAsync());
return (server, client);
}
private static async Task<(NamedPipeServerStream server, FileStream client)> GetConnectedNamedPipeServerAndFileStreamClientStreams(
bool asyncHandles, PipeDirection serverDirection, FileAccess clientAccess)
{
if (OperatingSystem.IsWindows())
{
PipeOptions pipeOptions = asyncHandles ? PipeOptions.Asynchronous : PipeOptions.None;
FileOptions fileOptions = asyncHandles ? FileOptions.Asynchronous : FileOptions.None;
string pipeName = PipeStreamConformanceTests.GetUniquePipeName();
NamedPipeServerStream server = new(pipeName, serverDirection, 1, PipeTransmissionMode.Byte, pipeOptions);
FileStream client = new($@"\\.\pipe\{pipeName}", FileMode.Open, clientAccess, FileShare.None, 0, fileOptions);
await server.WaitForConnectionAsync();
return (server, client);
}
else
{
(NamedPipeServerStream server, NamedPipeClientStream namedPipeClient) = await GetConnectedNamedPipeStreams(
asyncHandles, serverDirection, clientAccess == FileAccess.Read ? PipeDirection.In : PipeDirection.Out);
FileStream fileStreamClient = new(
new SafeFileHandle(namedPipeClient.SafePipeHandle.DangerousGetHandle(), ownsHandle: true),
clientAccess, bufferSize: 0, isAsync: false);
namedPipeClient.SafePipeHandle.SetHandleAsInvalid();
return (server, fileStreamClient);
}
}
private static void DiposeServerAndVerifyClientBehaviour((Stream server, Stream client) connectedStreams, Action<Stream> assertMethod)
{
connectedStreams.server.Dispose();
assertMethod(connectedStreams.client);
}
private static void DisconnectServerAndVerifyClientBehaviour((NamedPipeServerStream server, Stream client) connectedStreams, Action<Stream> assertMethod)
{
connectedStreams.server.Disconnect();
assertMethod(connectedStreams.client);
}
private static async Task DiposeServerAndVerifyClientBehaviourAsync((Stream server, Stream client) connectedStreams, Func<Stream, Task> assertMethod)
{
await connectedStreams.server.DisposeAsync();
await assertMethod(connectedStreams.client);
}
private static async Task DisconnectServerAndVerifyClientBehaviourAsync((NamedPipeServerStream server, Stream client) connectedStreams, Func<Stream, Task> assertMethod)
{
connectedStreams.server.Disconnect();
await assertMethod(connectedStreams.client);
}
private static void AssertZeroByteRead(Stream client)
{
using (client)
{
Assert.Equal(0, client.Read(new byte[100]));
Assert.Equal(0, client.Read(new byte[100], 0, 100));
}
}
private static void AssertWriteThrows(Stream client)
{
using (client)
{
Assert.Throws<IOException>(() => client.Write(new byte[100], 0, 100));
Assert.Throws<IOException>(() => client.Write(new byte[100]));
}
}
private static async Task AssertZeroByteReadAsync(Stream client)
{
using (client)
{
Assert.Equal(0, await client.ReadAsync(new byte[100]));
Assert.Equal(0, await client.ReadAsync(new byte[100], 0, 100));
}
}
private static async Task AssertWriteAsyncThrows(Stream client)
{
using (client)
{
await Assert.ThrowsAsync<IOException>(() => client.WriteAsync(new byte[100], 0, 100));
await Assert.ThrowsAsync<IOException>(() => client.WriteAsync(new byte[100]).AsTask());
}
}
}
}
......@@ -55,19 +55,15 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span<byte> buffer
}
int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
switch (errorCode)
return errorCode switch
{
case Interop.Errors.ERROR_HANDLE_EOF:
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile#synchronization-and-file-position :
// "If lpOverlapped is not NULL, then when a synchronous read operation reaches the end of a file,
// ReadFile returns FALSE and GetLastError returns ERROR_HANDLE_EOF"
return numBytesRead;
case Interop.Errors.ERROR_BROKEN_PIPE: // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.
case Interop.Errors.ERROR_INVALID_PARAMETER when IsEndOfFileForNoBuffering(handle, fileOffset):
return 0;
default:
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
}
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile#synchronization-and-file-position:
// "If lpOverlapped is not NULL, then when a synchronous read operation reaches the end of a file,
// ReadFile returns FALSE and GetLastError returns ERROR_HANDLE_EOF"
Interop.Errors.ERROR_HANDLE_EOF => numBytesRead,
_ when IsEndOfFile(errorCode, handle, fileOffset) => 0,
_ => throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path)
};
}
}
......@@ -113,20 +109,16 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b
resetEvent.ReleaseRefCount(overlapped);
}
switch (errorCode)
if (IsEndOfFile(errorCode, handle, fileOffset))
{
case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file)
case Interop.Errors.ERROR_BROKEN_PIPE:
case Interop.Errors.ERROR_INVALID_PARAMETER when IsEndOfFileForNoBuffering(handle, fileOffset):
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
overlapped->InternalLow = IntPtr.Zero;
return 0;
default:
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
overlapped->InternalLow = IntPtr.Zero;
return 0;
}
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
}
}
finally
......@@ -163,13 +155,7 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan<by
}
int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
switch (errorCode)
{
case Interop.Errors.ERROR_NO_DATA: // EOF on a pipe
return;
default:
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
}
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
}
}
......@@ -222,10 +208,6 @@ private static unsafe void WriteSyncUsingAsyncHandle(SafeFileHandle handle, Read
switch (errorCode)
{
case Interop.Errors.ERROR_NO_DATA:
// For pipes, ERROR_NO_DATA is not an error, but the pipe is closing.
return;
case Interop.Errors.ERROR_INVALID_PARAMETER:
// ERROR_INVALID_PARAMETER may be returned for writes
// where the position is too large or for synchronous writes
......@@ -288,28 +270,27 @@ private static unsafe void WriteSyncUsingAsyncHandle(SafeFileHandle handle, Read
{
// The operation failed, or it's pending.
errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
switch (errorCode)
{
case Interop.Errors.ERROR_IO_PENDING:
// Common case: IO was initiated, completion will be handled by callback.
// Register for cancellation now that the operation has been initiated.
vts.RegisterForCancellation(cancellationToken);
break;
case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file)
case Interop.Errors.ERROR_BROKEN_PIPE:
case Interop.Errors.ERROR_INVALID_PARAMETER when IsEndOfFileForNoBuffering(handle, fileOffset):
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
nativeOverlapped->InternalLow = IntPtr.Zero;
vts.Dispose();
return (null, 0);
default:
// Error. Callback will not be called.
vts.Dispose();
return (null, errorCode);
if (errorCode == Interop.Errors.ERROR_IO_PENDING)
{
// Common case: IO was initiated, completion will be handled by callback.
// Register for cancellation now that the operation has been initiated.
vts.RegisterForCancellation(cancellationToken);
}
else if (IsEndOfFile(errorCode, handle, fileOffset))
{
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
nativeOverlapped->InternalLow = IntPtr.Zero;
vts.Dispose();
return (null, 0);
}
else
{
// Error. Callback will not be called.
vts.Dispose();
return (null, errorCode);
}
}
}
......@@ -378,9 +359,6 @@ private static unsafe void WriteSyncUsingAsyncHandle(SafeFileHandle handle, Read
// Register for cancellation now that the operation has been initiated.
vts.RegisterForCancellation(cancellationToken);
break;
case Interop.Errors.ERROR_NO_DATA: // EOF on a pipe. IO callback will not be called.
vts.Dispose();
return (null, 0);
default:
// Error. Callback will not be invoked.
vts.Dispose();
......@@ -587,27 +565,27 @@ private static unsafe ValueTask<int> ReadFileScatterAsync(SafeFileHandle handle,
{
// The operation failed, or it's pending.
int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
switch (errorCode)
{
case Interop.Errors.ERROR_IO_PENDING:
// Common case: IO was initiated, completion will be handled by callback.
// Register for cancellation now that the operation has been initiated.
vts.RegisterForCancellation(cancellationToken);
break;
case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file)
case Interop.Errors.ERROR_BROKEN_PIPE:
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
nativeOverlapped->InternalLow = IntPtr.Zero;
vts.Dispose();
return ValueTask.FromResult(0);
default:
// Error. Callback will not be called.
vts.Dispose();
return ValueTask.FromException<int>(Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path));
if (errorCode == Interop.Errors.ERROR_IO_PENDING)
{
// Common case: IO was initiated, completion will be handled by callback.
// Register for cancellation now that the operation has been initiated.
vts.RegisterForCancellation(cancellationToken);
}
else if (IsEndOfFile(errorCode, handle, fileOffset))
{
// EOF on a pipe. Callback will not be called.
// We clear the overlapped status bit for this special case (failure
// to do so looks like we are freeing a pending overlapped later).
nativeOverlapped->InternalLow = IntPtr.Zero;
vts.Dispose();
return ValueTask.FromResult(0);
}
else
{
// Error. Callback will not be called.
vts.Dispose();
return ValueTask.FromException<int>(Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path));
}
}
}
......@@ -695,9 +673,7 @@ private static unsafe ValueTask WriteFileGatherAsync(SafeFileHandle handle, IntP
{
// Error. Callback will not be invoked.
vts.Dispose();
return errorCode == Interop.Errors.ERROR_NO_DATA // EOF on a pipe. IO callback will not be called.
? ValueTask.CompletedTask
: ValueTask.FromException(SafeFileHandle.OverlappedValueTaskSource.GetIOError(errorCode, path: null));
return ValueTask.FromException(SafeFileHandle.OverlappedValueTaskSource.GetIOError(errorCode, path: null));
}
}
}
......@@ -771,6 +747,20 @@ static unsafe void Callback(uint errorCode, uint numBytes, NativeOverlapped* pOv
}
}
internal static bool IsEndOfFile(int errorCode, SafeFileHandle handle, long fileOffset)
{
switch (errorCode)
{
case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file)
case Interop.Errors.ERROR_BROKEN_PIPE: // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.
case Interop.Errors.ERROR_PIPE_NOT_CONNECTED: // Named pipe server has disconnected, return 0 to match NamedPipeClientStream behaviour
case Interop.Errors.ERROR_INVALID_PARAMETER when IsEndOfFileForNoBuffering(handle, fileOffset):
return true;
default:
return false;
}
}
// From https://docs.microsoft.com/en-us/windows/win32/fileio/file-buffering:
// "File access sizes, including the optional file offset in the OVERLAPPED structure,
// if specified, must be for a number of bytes that is an integer multiple of the volume sector size."
......
......@@ -225,40 +225,34 @@ internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, bool canS
}
// If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation.
if (!synchronousSuccess)
if (!synchronousSuccess && errorCode != Interop.Errors.ERROR_IO_PENDING)
{
switch (errorCode)
if (!RandomAccess.IsEndOfFile(errorCode, handle, readAwaitable._position))
{
case Interop.Errors.ERROR_IO_PENDING:
// Async operation in progress.
break;
case Interop.Errors.ERROR_BROKEN_PIPE:
case Interop.Errors.ERROR_HANDLE_EOF:
// We're at or past the end of the file, and the overlapped callback
// won't be raised in these cases. Mark it as completed so that the await
// below will see it as such.
readAwaitable.MarkCompleted();
break;
default:
// Everything else is an error (and there won't be a callback).
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
}
// We're at or past the end of the file, and the overlapped callback
// won't be raised in these cases. Mark it as completed so that the await
// below will see it as such.
readAwaitable.MarkCompleted();
}
// Wait for the async operation (which may or may not have already completed), then throw if it failed.
await readAwaitable;
switch (readAwaitable._errorCode)
if (readAwaitable._errorCode != Interop.Errors.ERROR_SUCCESS)
{
case 0: // success
break;
case Interop.Errors.ERROR_BROKEN_PIPE: // logically success with 0 bytes read (write end of pipe closed)
case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file)
Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}");
break;
case Interop.Errors.ERROR_OPERATION_ABORTED: // canceled
if (readAwaitable._errorCode == Interop.Errors.ERROR_OPERATION_ABORTED)
{
throw new OperationCanceledException(cancellationToken.IsCancellationRequested ? cancellationToken : new CancellationToken(true));
default: // error
}
else if (!RandomAccess.IsEndOfFile((int)readAwaitable._errorCode, handle, readAwaitable._position))
{
throw Win32Marshal.GetExceptionForWin32Error((int)readAwaitable._errorCode, handle.Path);
}
Debug.Assert(readAwaitable._numBytes == 0, $"Expected 0 bytes read, got {readAwaitable._numBytes}");
}
// Successful operation. If we got zero bytes, we're done: exit the read/write loop.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册