Description
Background and motivation
Cannot set ReadMode
after creating a NamedPipeClientStream
instance. This is due to not exposing the NamedPipeClientStream
constructor which accepts a PipeAccessRights
.
API Proposal
namespace System.IO.Pipes;
// existing type
public sealed partial class NamedPipeClientStream : PipeStream
{
// new constructor
[SupportedOSPlatform("windows")]
public NamedPipeClientStream(String serverName, String pipeName, PipeAccessRights desiredAccessRights,
PipeOptions options, TokenImpersonationLevel impersonationLevel, HandleInheritability inheritability)
}
// existing type moved from System.IO.Pipes.AccessControl assembly.
[System.FlagsAttribute]
public enum PipeAccessRights
{
ReadData = 1,
WriteData = 2,
CreateNewInstance = 4,
ReadExtendedAttributes = 8,
WriteExtendedAttributes = 16,
ReadAttributes = 128,
WriteAttributes = 256,
Write = 274,
Delete = 65536,
ReadPermissions = 131072,
Read = 131209,
ReadWrite = 131483,
ChangePermissions = 262144,
TakeOwnership = 524288,
Synchronize = 1048576,
FullControl = 2032031,
AccessSystemSecurity = 16777216,
}
API Usage
var pipeOut = new NamedPipeServerStream("SomeNamedPipe",
PipeDirection.Out,
1,
PipeTransmissionMode.Message);
var pipeIn = new NamedPipeClientStream(".",
"SomeNamedPipe",
PipeAccessRights.ReadData | PipeAccessRights.WriteAttributes,
PipeOptions.None,
System.Security.Principal.TokenImpersonationLevel.None,
System.IO.HandleInheritability.None);
pipeIn.Connect();
pipeIn.ReadMode = PipeTransmissionMode.Message;
Alternative Designs
We could expose this with a Create
factory method instead, as was done for NamedPipeServerStream
However that's not necessary and probably a misnomer since this new constructor isn't really about ACLs, but instead access rights on the handle. Contrast that to those existing factory methods which take PipeSecurity
which is an ACLs concept. Splitting hairs, perhaps, since the API is related to access and is likely not supported on non-Windows, but I think we can do better here by exposing the exact missing API so we should.
Risks
Moving types around carries with it some risk. Luckily all the assemblies here are in the shared framework (since 6.0) and referenced as a set so it's less of a risk,
Original issue
Consider this code:
var pipeOut = new NamedPipeServerStream("SomeNamedPipe",
PipeDirection.Out,
1,
PipeTransmissionMode.Message);
var pipeIn = new NamedPipeClientStream(".",
"SomeNamedPipe",
PipeDirection.In);
pipeIn.Connect();
pipeIn.ReadMode = PipeTransmissionMode.Message; // <-- exception thrown here
In .NET Framework, there used to be a constructor on the client side that accepted PipeAccessRights
, which would solve the issue, and would allow the user to change ReadMode
later:
var pipeIn =
new NamedPipeClientStream(".",
"SomeNamedPipe",
PipeAccessRights.ReadData | PipeAccessRights.WriteAttributes,
PipeOptions.None,
System.Security.Principal.TokenImpersonationLevel.None,
System.IO.HandleInheritability.None);
Unfortunately, we haven't ported that constructor to .NET Core+.
The underlying problem is that, according to the documentation of the Windows method CreateNamedPipe
, we need to pass the FILE_WRITE_ATTRIBUTES
access right when constructing the pipe handle, to be able to modify it after creation. This is explained in the remarks: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea#remarks
The .NET Framework solution worked because it would set FILE_WRITE_ATTRIBUTES
when using the constructor that takes a PipeAccessRights
argument.
Unfortunately, the only workaround available in .NET Core+ is to:
- Manually construct the
SafePipeHandle
, making sure to passFILE_WRITE_ATTRIBUTES
to the P/Invoke, which is not straightforward. - Create the client stream instance using the constructor that takes the pipe handle, while also making sure to pass
true
to the argument that indicates that the pipe should get connected. - Set the
ReadMode
.
I wrote a working example of this workaround for .NET 6.0 and published it here: https://github.com/carlossanlop/experiments/tree/NamedPipeClientStream_ReadMode
But we should consider solving it in runtime.
Note that the workaround provided above had to call an internal constructor from PipeSecurity
using reflection. It's another argumemt supporting the idea that we should provide the solution ourselves.