Skip to content

Instantly share code, notes, and snippets.

@ghorsington
Created February 27, 2019 20:41
Show Gist options
  • Select an option

  • Save ghorsington/b612e1fa0ce9c62c850612060dfee063 to your computer and use it in GitHub Desktop.

Select an option

Save ghorsington/b612e1fa0ce9c62c850612060dfee063 to your computer and use it in GitHub Desktop.
A simple ghetto named pipe implementation for win32
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
namespace GhettoPipes
{
static class Win32
{
public const uint GENERIC_READ = 0x80000000;
public const uint GENERIC_WRITE = 0x40000000;
public const int INVALID_HANDLE_VALUE = -1;
public const uint OPEN_EXISTING = 3;
public const uint PIPE_WAIT = 0x00000000;
public const uint PIPE_TYPE_BYTE = 0x00000000;
public const uint PIPE_UNLIMITED_INSTANCES = 255;
public const uint NMPWAIT_WAIT_FOREVER = 0xffffffff;
public const ulong ERROR_PIPE_CONNECTED = 535;
[DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)]
public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode,
IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateNamedPipe(string lpName, // pipe name
uint dwOpenMode, // pipe open mode
uint dwPipeMode, // pipe-specific modes
uint nMaxInstances, // maximum number of instances
uint nOutBufferSize, // output buffer size
uint nInBufferSize, // input buffer size
uint nDefaultTimeOut, // time-out interval
IntPtr pipeSecurityDescriptor // SD
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DisconnectNamedPipe(IntPtr hHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ConnectNamedPipe(IntPtr hHandle, // handle to named pipe
IntPtr lpOverlapped // overlapped structure
);
[DllImport("kernel32.dll", EntryPoint = "PeekNamedPipe", SetLastError = true)]
public static extern bool PeekNamedPipe(IntPtr handle, byte[] buffer, uint nBufferSize, ref uint bytesRead,
ref uint bytesAvail, ref uint bytesLeft);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadFile(IntPtr handle, byte[] buffer, uint toRead, ref uint read,
IntPtr lpOverLapped);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteFile(IntPtr handle, byte[] buffer, uint count, ref uint written,
IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FlushFileBuffers(IntPtr handle);
}
/// <summary>
/// Original version from https://www.pinvoke.net/default.aspx/kernel32/CreateNamedPipe.html
///
/// NamedPipeStream is an odd little hybrid between the client and server ends of a NamedPipe,
/// and a System.IO.Stream subclass. Basically, it simplifies the unnecessarily horrific process
/// of implementing named pipe support in .Net. (If you doubt this, try it the hard way... we'll wait.)
/// Usage idiom:
/// Server side
/// -----------
/// 1. Call NamedPipeStream.Create, specify inbound, outbound, or both
/// 2. Call Listen(). This will block until a client connects. Sorry, the alternatives
/// are ugly. Use a thread.
/// 3. Call DataAvailable() in a loop with Read(), Write, ReadLine(), etc. until IsConnected turns false.
/// 4. Call Listen() again to wait for the next client.
/// Client side
/// -----------
/// 1. Call Open()
/// 2. Call DataAvailable(), Read(), Write(), etc. until you're done,
/// then call Close();
/// And yes, you can attach TextReader and TextWriter instances to this stream.
/// Server side caveat:
/// The idiom described above only supports a single client at a time. If you need
/// to support multiple clients, multiple calls to Create()/Listen() in separate threads is the
/// recommended approach.
/// There is a test driver class at the end of this file which can be cannibalized for sample usage code.
/// </summary>
public class NamedPipeStream : Stream
{
/// <summary>
/// Type of the pipe
/// </summary>
public enum PeerType
{
/// <summary>
/// The pipe is a client pipe.
/// </summary>
Client = 0,
/// <summary>
/// The pipe is a server pipe.
/// </summary>
Server = 1
}
/// <summary>
/// Available server modes
/// </summary>
public enum ServerMode
{
/// <summary>
/// Allow only receiving data.
/// </summary>
InboundOnly = 0x00000001,
/// <summary>
/// Allow only send data.
/// </summary>
OutboundOnly = 0x00000002,
/// <summary>
/// Allow to both send and receive data.
/// </summary>
Bidirectional = InboundOnly + OutboundOnly
}
readonly PeerType peerType;
IntPtr handle;
FileAccess mode;
protected NamedPipeStream()
{
handle = IntPtr.Zero;
mode = 0;
peerType = PeerType.Server;
}
/// <summary>
/// Constructs a client named pipe
/// </summary>
/// <param name="pipeName">Full name of the pipe</param>
/// <param name="mode">Pipe access mode</param>
public NamedPipeStream(string pipeName, FileAccess mode)
{
handle = IntPtr.Zero;
peerType = PeerType.Client;
Open(pipeName, mode);
}
/// <summary>
/// Initializes a managed named pipe around the unmanaged pipe handle.
/// </summary>
/// <param name="handle">Pointer to the pipe handle.</param>
/// <param name="mode">Pipe access mode</param>
public NamedPipeStream(IntPtr handle, FileAccess mode)
{
this.handle = handle;
this.mode = mode;
peerType = PeerType.Client;
}
/// <summary>
/// Returns true if client is connected. Should only be called after Listen() succeeds.
/// </summary>
/// <returns></returns>
public bool IsConnected
{
get
{
if (peerType != PeerType.Server) throw new Exception("IsConnected() is only for server-side streams");
if (Win32.ConnectNamedPipe(handle, IntPtr.Zero)) return false;
return (uint) Marshal.GetLastWin32Error() == Win32.ERROR_PIPE_CONNECTED;
}
}
/// <summary>
/// Returns true if there is data available to read.
/// </summary>
public bool DataAvailable
{
get
{
uint bytesRead = 0, avail = 0, thismsg = 0;
var result = Win32.PeekNamedPipe(handle, null, 0, ref bytesRead, ref avail, ref thismsg);
return result && avail > 0;
}
}
/// <inheritdoc />
public override bool CanRead => (mode & FileAccess.Read) > 0;
/// <inheritdoc />
public override bool CanWrite => (mode & FileAccess.Write) > 0;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override long Length => throw new NotSupportedException("NamedPipeStream does not support seeking");
/// <inheritdoc />
public override long Position
{
get => throw new NotSupportedException("NamedPipeStream does not support seeking");
set { }
}
/// <summary>
/// Opens the client side of a pipe.
/// </summary>
/// <param name="pipeName">Full path to the pipe, e.g. '\\.\pipe\mypipe'</param>
/// <param name="accessMode">Read,Write, or ReadWrite - must be compatible with server-side creation mode</param>
public void Open(string pipeName, FileAccess accessMode)
{
uint pipeMode = 0;
if ((accessMode & FileAccess.Read) > 0) pipeMode |= Win32.GENERIC_READ;
if ((accessMode & FileAccess.Write) > 0) pipeMode |= Win32.GENERIC_WRITE;
var pipeHandle = Win32.CreateFile(pipeName, pipeMode, 0, IntPtr.Zero, Win32.OPEN_EXISTING, 0, IntPtr.Zero);
if (pipeHandle.ToInt32() == Win32.INVALID_HANDLE_VALUE)
{
var err = Marshal.GetLastWin32Error();
throw new Win32Exception(err,
$"NamedPipeStream.Open failed, win32 error code {err}, pipe name '{pipeName}' ");
}
mode = accessMode;
handle = pipeHandle;
}
/// <summary>
/// Create a server named pipe instance.
/// </summary>
/// <param name="pipeName">Local name (the part after \\.\pipe\)</param>
/// <param name="mode">Server mode of the pipe</param>
public static NamedPipeStream Create(string pipeName, ServerMode mode)
{
var name = $@"\\.\pipe\{pipeName}";
var handle = Win32.CreateNamedPipe(name, (uint) mode, Win32.PIPE_TYPE_BYTE | Win32.PIPE_WAIT,
Win32.PIPE_UNLIMITED_INSTANCES, 0, // outBuffer,
1024, // inBuffer,
Win32.NMPWAIT_WAIT_FOREVER, IntPtr.Zero);
if (handle.ToInt32() == Win32.INVALID_HANDLE_VALUE)
throw new Win32Exception(
$"Error creating named pipe {name}. Internal error: {Marshal.GetLastWin32Error()}");
// Set members persistently...
var self = new NamedPipeStream {handle = handle};
switch (mode)
{
case ServerMode.InboundOnly:
self.mode = FileAccess.Read;
break;
case ServerMode.OutboundOnly:
self.mode = FileAccess.Write;
break;
case ServerMode.Bidirectional:
self.mode = FileAccess.ReadWrite;
break;
default:
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
}
return self;
}
/// <summary>
/// Server only: block until client connects
/// </summary>
/// <returns></returns>
public bool Listen()
{
if (peerType != PeerType.Server) throw new Exception("Listen() is only for server-side streams");
Win32.DisconnectNamedPipe(handle);
if (Win32.ConnectNamedPipe(handle, IntPtr.Zero)) return true;
return (uint) Marshal.GetLastWin32Error() == Win32.ERROR_PIPE_CONNECTED;
}
/// <summary>
/// Server only: disconnect the pipe. For most applications, you should just call Listen()
/// instead, which automatically does a disconnect of any old connection.
/// </summary>
public void Disconnect()
{
if (peerType != PeerType.Server) throw new Exception("Disconnect() is only for server-side streams");
Win32.DisconnectNamedPipe(handle);
}
/// <inheritdoc />
public override void Flush()
{
if (handle == IntPtr.Zero)
throw new ObjectDisposedException("NamedPipeStream", "The stream has already been closed");
Win32.FlushFileBuffers(handle);
}
/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer), "The buffer to read into cannot be null");
if (buffer.Length < offset + count)
throw new ArgumentException("Buffer is not large enough to hold requested data", nameof(buffer));
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset cannot be negative");
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), count, "Count cannot be negative");
if (!CanRead) throw new NotSupportedException("The stream does not support reading");
if (handle == IntPtr.Zero)
throw new ObjectDisposedException("NamedPipeStream", "The stream has already been closed");
// first read the data into an internal buffer since ReadFile cannot read into a buf at
// a specified offset
uint read = 0;
var buf = buffer;
if (offset != 0) buf = new byte[count];
var f = Win32.ReadFile(handle, buf, (uint) count, ref read, IntPtr.Zero);
if (!f) throw new Win32Exception(Marshal.GetLastWin32Error(), "ReadFile failed");
if (offset == 0) return (int) read;
for (var x = 0; x < read; x++) buffer[offset + x] = buf[x];
return (int) read;
}
/// <inheritdoc />
public override void Close()
{
Win32.CloseHandle(handle);
handle = IntPtr.Zero;
}
/// <inheritdoc />
public override void SetLength(long length)
{
throw new NotSupportedException("NamedPipeStream doesn't support SetLength");
}
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer), "The buffer to write into cannot be null");
if (buffer.Length < offset + count)
throw new ArgumentException("Buffer does not contain amount of requested data", nameof(buffer));
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset cannot be negative");
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), count, "Count cannot be negative");
if (!CanWrite) throw new NotSupportedException("The stream does not support writing");
if (handle == IntPtr.Zero)
throw new ObjectDisposedException("NamedPipeStream", "The stream has already been closed");
// copy data to internal buffer to allow writing from a specified offset
if (offset != 0)
{
var buf = new byte[count];
for (var x = 0; x < count; x++) buf[x] = buffer[offset + x];
buffer = buf;
}
uint written = 0;
var result = Win32.WriteFile(handle, buffer, (uint) count, ref written, IntPtr.Zero);
if (!result)
{
var err = Marshal.GetLastWin32Error();
throw new Win32Exception(err, "Writing to the stream failed");
}
if (written < count) throw new IOException("Unable to write entire buffer to stream");
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException("NamedPipeStream doesn't support seeking");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment