using System.ComponentModel; using System.IO.Pipes; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Microsoft.Win32.SafeHandles; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol; /// /// Blocking, full-duplex I/O on an overlapped Windows pipe handle that never touches the I/O completion port /// (lmxopcua#789). /// /// /// A .NET pipe opened with is bound to the thread pool's completion /// port, and even its synchronous Read/Write complete through it: every frame costs a completion-port /// packet plus a thread-pool work item, and the pool's spin-waiting between those items was most of the host's CPU /// per evaluation on Windows (+115 points over in process at 10k evals/s). A handle opened for synchronous I/O /// would avoid the port but serialises a read against a write on the same file object, which a full-duplex pipe /// with a blocked reader cannot afford. /// So this stream issues its own overlapped ReadFile/WriteFile on the pipe's handle with an /// OVERLAPPED.hEvent whose low-order bit is set — which tells the kernel not to queue a completion packet /// (documented on GetQueuedCompletionStatus) — and waits on that event on the calling thread. Reads and /// writes stay concurrent (one read and one write at a time, which is how the frame reader and the combining writer /// use a pipe), and a frame wakes the waiting thread straight from the kernel: the Windows twin of the Unix /// blocking socket (). /// Disposing cancels any read or write still waiting (CancelIoEx) and then closes the pipe; the waiter /// sees end of stream (a read) or an . A peer that goes away completes a /// pending read as end of stream and fails a pending write with an , as /// does. /// [SupportedOSPlatform("windows")] public sealed unsafe class WindowsBlockingPipeStream : Stream { private const int ErrorHandleEof = 38; private const int ErrorBrokenPipe = 109; private const int ErrorNoData = 232; private const int ErrorPipeNotConnected = 233; private const int ErrorOperationAborted = 995; private const int ErrorIoPending = 997; private const uint StatusSuccess = 0; private const uint StatusEndOfFile = 0xC0000011; private const uint StatusPipeDisconnected = 0xC00000B0; private const uint StatusPipeClosing = 0xC00000B1; private const uint StatusPipeBroken = 0xC000014B; private const uint StatusCancelled = 0xC0000120; private readonly PipeStream _pipe; private readonly SafePipeHandle _handle; private readonly Operation _read; private readonly Operation _write; /// The owner's reference plus one per operation in progress; the native state is freed when it reaches 0. private int _refs = 1; private int _disposed; /// Wraps a connected pipe; the wrapper owns it from here on (disposing the wrapper closes it). /// A connected pipe opened with . public WindowsBlockingPipeStream(PipeStream pipe) { ArgumentNullException.ThrowIfNull(pipe); if (!pipe.IsAsync) throw new ArgumentException("the pipe must be opened for overlapped (asynchronous) I/O", nameof(pipe)); _pipe = pipe; _handle = pipe.SafePipeHandle; _read = new Operation(); _write = new Operation(); } /// public override bool CanRead => Volatile.Read(ref _disposed) == 0; /// public override bool CanWrite => Volatile.Read(ref _disposed) == 0; /// public override bool CanSeek => false; /// public override long Length => throw new NotSupportedException(); /// public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } /// public override int Read(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); return Read(buffer.AsSpan(offset, count)); } /// /// The bytes read; 0 at end of stream (the peer closed or went away). public override int Read(Span buffer) { if (buffer.IsEmpty) return 0; Enter(); try { fixed (byte* p = buffer) { var ov = _read.Prepare(); var started = ReadFile(_handle, p, buffer.Length, IntPtr.Zero, ov); var (status, bytes) = _read.Finish(started, out var error); if (error != 0) { return error is ErrorBrokenPipe or ErrorPipeNotConnected or ErrorNoData or ErrorHandleEof ? 0 : throw Failure(error, "read"); } return status switch { StatusSuccess => bytes, StatusEndOfFile or StatusPipeBroken or StatusPipeDisconnected or StatusPipeClosing => 0, _ => throw StatusFailure(status, "read"), }; } } finally { Exit(); } } /// public override void Write(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); Write(new ReadOnlySpan(buffer, offset, count)); } /// public override void Write(ReadOnlySpan buffer) { Enter(); try { fixed (byte* start = buffer) { var done = 0; while (done < buffer.Length) { var ov = _write.Prepare(); var started = WriteFile(_handle, start + done, buffer.Length - done, IntPtr.Zero, ov); var (status, bytes) = _write.Finish(started, out var error); if (error != 0) throw Failure(error, "write"); if (status != StatusSuccess) throw StatusFailure(status, "write"); if (bytes <= 0) throw new IOException("pipe write made no progress"); done += bytes; } } } finally { Exit(); } } /// A no-op: a pipe has no write buffer to flush. public override void Flush() { } /// A no-op, completed: a pipe has no write buffer to flush. /// Unused. /// A completed task. public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // The asynchronous surface is refused: Stream's base versions run the blocking call on the pool behind ONE semaphore // shared by reads and writes, which would serialise a write behind a read that is waiting for the peer — the very // deadlock this stream exists to avoid. Callers use the blocking calls on their own threads. /// public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw AsyncRefused(); /// public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => throw AsyncRefused(); /// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw AsyncRefused(); /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw AsyncRefused(); /// public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw AsyncRefused(); /// public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw AsyncRefused(); private static NotSupportedException AsyncRefused() => new("WindowsBlockingPipeStream is blocking only: read and write on dedicated threads (lmxopcua#789)"); /// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// public override void SetLength(long value) => throw new NotSupportedException(); /// protected override void Dispose(bool disposing) { if (Interlocked.Exchange(ref _disposed, 1) == 0) { // Wake a reader or writer still waiting on the kernel before the handle goes: it then sees cancellation // (reported as disposed), and the pipe's own close cannot race an operation that is still being issued. try { var add = false; _handle.DangerousAddRef(ref add); try { _ = CancelIoEx(_handle.DangerousGetHandle(), IntPtr.Zero); } finally { if (add) _handle.DangerousRelease(); } } catch (ObjectDisposedException) { } _pipe.Dispose(); Exit(); } base.Dispose(disposing); } private void Enter() { while (true) { var refs = Volatile.Read(ref _refs); if (refs == 0 || Volatile.Read(ref _disposed) != 0) throw new ObjectDisposedException(nameof(WindowsBlockingPipeStream)); if (Interlocked.CompareExchange(ref _refs, refs + 1, refs) == refs) return; } } private void Exit() { if (Interlocked.Decrement(ref _refs) != 0) return; _read.Free(); _write.Free(); } private Exception Failure(int error, string what) => error == ErrorOperationAborted && Volatile.Read(ref _disposed) != 0 ? new ObjectDisposedException(nameof(WindowsBlockingPipeStream)) : new IOException($"pipe {what} failed: {new Win32Exception(error).Message}", new Win32Exception(error)); private Exception StatusFailure(uint status, string what) => status == StatusCancelled && Volatile.Read(ref _disposed) != 0 ? new ObjectDisposedException(nameof(WindowsBlockingPipeStream)) : new IOException($"pipe {what} failed: NTSTATUS 0x{status:X8}"); /// One direction's native state: its OVERLAPPED and the manual-reset event it completes on. private sealed class Operation { private readonly ManualResetEvent _done = new(false); private readonly NativeOverlapped* _ov = (NativeOverlapped*)NativeMemory.AllocZeroed((nuint)sizeof(NativeOverlapped)); private readonly IntPtr _event; public Operation() { // Low-order bit set: the completion is NOT queued to the completion port the pipe is bound to. _event = _done.SafeWaitHandle.DangerousGetHandle() | 1; } public NativeOverlapped* Prepare() { *_ov = default; _ov->EventHandle = _event; return _ov; } /// Waits for the operation issued and returns its NTSTATUS and byte count. /// What ReadFile / WriteFile returned. /// The Win32 error when the call failed outright (not pending); 0 otherwise. public (uint Status, int Bytes) Finish(bool started, out int error) { error = 0; if (!started) { var last = Marshal.GetLastPInvokeError(); if (last != ErrorIoPending) { error = last; return (0, 0); } // The kernel sets the event when the I/O completes (Read/WriteFile reset it when they start it). _done.WaitOne(); } return ((uint)(long)_ov->InternalLow, (int)(long)_ov->InternalHigh); } public void Free() { NativeMemory.Free(_ov); _done.Dispose(); } } [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ReadFile(SafePipeHandle handle, byte* buffer, int count, IntPtr bytesRead, NativeOverlapped* overlapped); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool WriteFile(SafePipeHandle handle, byte* buffer, int count, IntPtr bytesWritten, NativeOverlapped* overlapped); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CancelIoEx(IntPtr handle, IntPtr overlapped); }