diff --git a/ExampleCLI/ExampleCLI.csproj b/ExampleCLI/ExampleCLI.csproj index 49e786d..ebf7d91 100644 --- a/ExampleCLI/ExampleCLI.csproj +++ b/ExampleCLI/ExampleCLI.csproj @@ -20,7 +20,7 @@ true full false - bin\Debug\ + ..\bin\ DEBUG;TRACE prompt 4 diff --git a/ExampleGUI/ExampleGUI.csproj b/ExampleGUI/ExampleGUI.csproj index 163a9e7..6e22e07 100644 --- a/ExampleGUI/ExampleGUI.csproj +++ b/ExampleGUI/ExampleGUI.csproj @@ -20,7 +20,7 @@ true full false - bin\Debug\ + ..\bin\ DEBUG;TRACE prompt 4 diff --git a/ExampleGUI/FormServer.cs b/ExampleGUI/FormServer.cs index d9f0b9d..044db0e 100644 --- a/ExampleGUI/FormServer.cs +++ b/ExampleGUI/FormServer.cs @@ -70,8 +70,16 @@ private void buttonSend_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(textBoxMessage.Text)) return; - - _server.PushMessage(textBoxMessage.Text); + if (listBoxClients.SelectedItem == null) + { + _server.PushMessage(textBoxMessage.Text); + } + else + { + var clientName = listBoxClients.SelectedItem.ToString(); + _server.PushMessage(textBoxMessage.Text, clientName); + } + textBoxMessage.Text = ""; } } diff --git a/NamedPipeWrapper/NamedPipeClient.cs b/NamedPipeWrapper/NamedPipeClient.cs index 23fca59..36cf074 100644 --- a/NamedPipeWrapper/NamedPipeClient.cs +++ b/NamedPipeWrapper/NamedPipeClient.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Pipes; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using NamedPipeWrapper.IO; @@ -40,6 +42,14 @@ public class NamedPipeClient /// public bool AutoReconnect { get; set; } + /// + /// Gets or sets how long the client waits between a reconnection attempt. + /// Default value is 0. + /// + public int AutoReconnectDelay { get; set; } + + + /// /// Invoked whenever a message is received from the server. /// @@ -77,7 +87,7 @@ public NamedPipeClient(string pipeName) /// Connects to the named pipe server asynchronously. /// This method returns immediately, possibly before the connection has been established. /// - public void Start() + public void Start(bool waitForconnection = false) { _closedExplicitly = false; var worker = new Worker(); @@ -141,6 +151,8 @@ public void WaitForDisconnection(TimeSpan timeout) #region Private methods + + private void ListenSync() { // Get the name of the data pipe that should be used from now on by this NamedPipeClient @@ -170,7 +182,10 @@ private void OnDisconnected(NamedPipeConnection connection) // Reconnect if (AutoReconnect && !_closedExplicitly) + { + Thread.Sleep(AutoReconnectDelay); Start(); + } } private void OnReceiveMessage(NamedPipeConnection connection, TRead message) @@ -202,6 +217,31 @@ private void OnError(Exception exception) static class PipeClientFactory { + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool WaitNamedPipe(string name, int timeout); + + public static bool NamedPipeExists(string pipeName) + { + try + { + bool exists = WaitNamedPipe(pipeName, -1); + if (!exists) + { + int error = Marshal.GetLastWin32Error(); + if (error == 0 || error == 2) + { + return false; + } + } + return true; + } + catch (Exception ex) + { + return false; + } + } + public static PipeStreamWrapper Connect(string pipeName) where TRead : class where TWrite : class @@ -209,10 +249,15 @@ public static PipeStreamWrapper Connect(string pip return new PipeStreamWrapper(CreateAndConnectPipe(pipeName)); } - public static NamedPipeClientStream CreateAndConnectPipe(string pipeName) + public static NamedPipeClientStream CreateAndConnectPipe(string pipeName, int timeout = 10) { + string normalizedPath = Path.GetFullPath(string.Format(@"\\.\pipe\{0}", pipeName)); + while (!NamedPipeExists(normalizedPath)) + { + Thread.Sleep(timeout); + } var pipe = CreatePipe(pipeName); - pipe.Connect(); + pipe.Connect(1000); return pipe; } diff --git a/NamedPipeWrapper/NamedPipeConnection.cs b/NamedPipeWrapper/NamedPipeConnection.cs index efa2c87..af06b15 100644 --- a/NamedPipeWrapper/NamedPipeConnection.cs +++ b/NamedPipeWrapper/NamedPipeConnection.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO.Pipes; using System.Linq; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Threading; @@ -29,6 +30,11 @@ public class NamedPipeConnection /// public readonly string Name; + /// + /// Gets the connection's handle. + /// + public readonly SafeHandle Handle; + /// /// Gets a value indicating whether the pipe is connected or not. /// @@ -60,6 +66,7 @@ internal NamedPipeConnection(int id, string name, PipeStream serverStream) { Id = id; Name = name; + Handle = serverStream.SafePipeHandle; _streamWrapper = new PipeStreamWrapper(serverStream); } diff --git a/NamedPipeWrapper/NamedPipeServer.cs b/NamedPipeWrapper/NamedPipeServer.cs index 7795a2a..23df63f 100644 --- a/NamedPipeWrapper/NamedPipeServer.cs +++ b/NamedPipeWrapper/NamedPipeServer.cs @@ -5,254 +5,371 @@ using System.Text; using NamedPipeWrapper.IO; using NamedPipeWrapper.Threading; +using System.Security.Principal; namespace NamedPipeWrapper { - /// - /// Wraps a and provides multiple simultaneous client connection handling. - /// - /// Reference type to read from and write to the named pipe - public class NamedPipeServer : Server where TReadWrite : class - { - /// - /// Constructs a new NamedPipeServer object that listens for client connections on the given . - /// - /// Name of the pipe to listen on - public NamedPipeServer(string pipeName) : base(pipeName) - { - } - } - - /// - /// Wraps a and provides multiple simultaneous client connection handling. - /// - /// Reference type to read from the named pipe - /// Reference type to write to the named pipe - public class Server - where TRead : class - where TWrite : class - { - /// - /// Invoked whenever a client connects to the server. - /// - public event ConnectionEventHandler ClientConnected; - - /// - /// Invoked whenever a client disconnects from the server. - /// - public event ConnectionEventHandler ClientDisconnected; - - /// - /// Invoked whenever a client sends a message to the server. - /// - public event ConnectionMessageEventHandler ClientMessage; - - /// - /// Invoked whenever an exception is thrown during a read or write operation. - /// - public event PipeExceptionEventHandler Error; - - private readonly string _pipeName; - private readonly List> _connections = new List>(); - - private int _nextPipeId; - - private volatile bool _shouldKeepRunning; - private volatile bool _isRunning; - - /// - /// Constructs a new NamedPipeServer object that listens for client connections on the given . - /// - /// Name of the pipe to listen on - public Server(string pipeName) - { - _pipeName = pipeName; - } - - /// - /// Begins listening for client connections in a separate background thread. - /// This method returns immediately. - /// - public void Start() - { - _shouldKeepRunning = true; - var worker = new Worker(); - worker.Error += OnError; - worker.DoWork(ListenSync); - } - - /// - /// Sends a message to all connected clients asynchronously. - /// This method returns immediately, possibly before the message has been sent to all clients. - /// - /// - public void PushMessage(TWrite message) - { - lock (_connections) - { - foreach (var client in _connections) - { - client.PushMessage(message); - } - } - } - - /// - /// Closes all open client connections and stops listening for new ones. - /// - public void Stop() - { - _shouldKeepRunning = false; - - lock (_connections) - { - foreach (var client in _connections.ToArray()) - { - client.Close(); - } - } - - // If background thread is still listening for a client to connect, - // initiate a dummy connection that will allow the thread to exit. - var dummyClient = new NamedPipeClient(_pipeName); - dummyClient.Start(); - dummyClient.WaitForConnection(TimeSpan.FromSeconds(2)); - dummyClient.Stop(); - dummyClient.WaitForDisconnection(TimeSpan.FromSeconds(2)); - } - - #region Private methods - - private void ListenSync() - { - _isRunning = true; - while (_shouldKeepRunning) - { - WaitForConnection(_pipeName); - } - _isRunning = false; - } - - private void WaitForConnection(string pipeName) - { - NamedPipeServerStream handshakePipe = null; - NamedPipeServerStream dataPipe = null; - NamedPipeConnection connection = null; - - var connectionPipeName = GetNextConnectionPipeName(pipeName); - - try - { - // Send the client the name of the data pipe to use - handshakePipe = PipeServerFactory.CreateAndConnectPipe(pipeName); - var handshakeWrapper = new PipeStreamWrapper(handshakePipe); - handshakeWrapper.WriteObject(connectionPipeName); - handshakeWrapper.WaitForPipeDrain(); - handshakeWrapper.Close(); - - // Wait for the client to connect to the data pipe - dataPipe = PipeServerFactory.CreatePipe(connectionPipeName); - dataPipe.WaitForConnection(); - - // Add the client's connection to the list of connections - connection = ConnectionFactory.CreateConnection(dataPipe); - connection.ReceiveMessage += ClientOnReceiveMessage; - connection.Disconnected += ClientOnDisconnected; - connection.Error += ConnectionOnError; - connection.Open(); - - lock (_connections) - { - _connections.Add(connection); - } - - ClientOnConnected(connection); - } - // Catch the IOException that is raised if the pipe is broken or disconnected. - catch (Exception e) - { - Console.Error.WriteLine("Named pipe is broken or disconnected: {0}", e); - - Cleanup(handshakePipe); - Cleanup(dataPipe); - - ClientOnDisconnected(connection); - } - } - - private void ClientOnConnected(NamedPipeConnection connection) - { - if (ClientConnected != null) - ClientConnected(connection); - } - - private void ClientOnReceiveMessage(NamedPipeConnection connection, TRead message) - { - if (ClientMessage != null) - ClientMessage(connection, message); - } - - private void ClientOnDisconnected(NamedPipeConnection connection) - { - if (connection == null) - return; - - lock (_connections) - { - _connections.Remove(connection); - } - - if (ClientDisconnected != null) - ClientDisconnected(connection); - } - - /// - /// Invoked on the UI thread. - /// - private void ConnectionOnError(NamedPipeConnection connection, Exception exception) - { - OnError(exception); - } - - /// - /// Invoked on the UI thread. - /// - /// - private void OnError(Exception exception) - { - if (Error != null) - Error(exception); - } - - private string GetNextConnectionPipeName(string pipeName) - { - return string.Format("{0}_{1}", pipeName, ++_nextPipeId); - } - - private static void Cleanup(NamedPipeServerStream pipe) - { - if (pipe == null) return; - using (var x = pipe) - { - x.Close(); - } - } - - #endregion - } - - static class PipeServerFactory - { - public static NamedPipeServerStream CreateAndConnectPipe(string pipeName) - { - var pipe = CreatePipe(pipeName); - pipe.WaitForConnection(); - return pipe; - } - - public static NamedPipeServerStream CreatePipe(string pipeName) - { - return new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough); - } - } + /// + /// Wraps a and provides multiple simultaneous client connection handling. + /// + /// Reference type to read from and write to the named pipe + public class NamedPipeServer : Server where TReadWrite : class + { + /// + /// Constructs a new NamedPipeServer object that listens for client connections on the given . + /// + /// Name of the pipe to listen on + public NamedPipeServer(string pipeName) + : base(pipeName) + { + } + + /// + /// Constructs a new NamedPipeServer object that listens for client connections on the given . + /// + /// Name of the pipe to listen on + /// Size of input and output buffer + /// And object that determine the access control and audit security for the pipe + public NamedPipeServer(string pipeName, int bufferSize, PipeSecurity security) + : base(pipeName, bufferSize, security) + { } + } + + /// + /// Wraps a and provides multiple simultaneous client connection handling. + /// + /// Reference type to read from the named pipe + /// Reference type to write to the named pipe + public class Server + where TRead : class + where TWrite : class + { + /// + /// Invoked whenever a client connects to the server. + /// + public event ConnectionEventHandler ClientConnected; + + /// + /// Invoked whenever a client disconnects from the server. + /// + public event ConnectionEventHandler ClientDisconnected; + + /// + /// Invoked whenever a client sends a message to the server. + /// + public event ConnectionMessageEventHandler ClientMessage; + + /// + /// Invoked whenever an exception is thrown during a read or write operation. + /// + public event PipeExceptionEventHandler Error; + + private readonly string _pipeName; + private readonly int _bufferSize; + private readonly PipeSecurity _security; + private readonly List> _connections = new List>(); + + private int _nextPipeId; + + private volatile bool _shouldKeepRunning; + private volatile bool _isRunning; + + /// + /// Constructs a new NamedPipeServer object that listens for client connections on the given . + /// + /// Name of the pipe to listen on + public Server(string pipeName) + { + _pipeName = pipeName; + } + + /// + /// Constructs a new NamedPipeServer object that listens for client connections on the given . + /// + /// Name of the pipe to listen on + /// Size of input and output buffer + /// And object that determine the access control and audit security for the pipe + public Server(string pipeName, int bufferSize, PipeSecurity security) + { + _pipeName = pipeName; + _bufferSize = bufferSize; + _security = security; + } + + /// + /// Begins listening for client connections in a separate background thread. + /// This method returns immediately. + /// + public void Start() + { + _shouldKeepRunning = true; + var worker = new Worker(); + worker.Error += OnError; + worker.DoWork(ListenSync); + } + + /// + /// Sends a message to all connected clients asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + public void PushMessage(TWrite message) + { + lock (_connections) + { + foreach (var client in _connections) + { + client.PushMessage(message); + } + } + } + + /// + /// Sends a message to a specific client asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + /// Specific client ID to send to. + public void PushMessage(TWrite message, int targetId) + { + lock (_connections) + { + // Can we speed this up with Linq or does that add overhead? + foreach (var client in _connections) + { + if (client.Id == targetId) + { + client.PushMessage(message); + break; + } + } + } + } + + /// + /// Sends a message to a specific clients asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + /// A list of client ID's to send to. + public void PushMessage(TWrite message, List targetIds) + { + lock (_connections) + { + // Can we speed this up with Linq or does that add overhead? + foreach (var client in _connections) + { + if (targetIds.Contains(client.Id)) + { + client.PushMessage(message); + } + } + } + } + + + /// + /// Sends a message to a specific clients asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + /// An array of client ID's to send to. + public void PushMessage(TWrite message, int[] targetIds) + { + PushMessage(message, targetIds.ToList()); + } + + /// + /// Sends a message to a specific client asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + /// Specific client name to send to. + public void PushMessage(TWrite message, string targetName) + { + lock (_connections) + { + // Can we speed this up with Linq or does that add overhead? + foreach (var client in _connections) + { + if (client.Name.Equals(targetName)) + { + client.PushMessage(message); + break; + } + } + } + } + /// + /// Sends a message to a specific client asynchronously. + /// This method returns immediately, possibly before the message has been sent to all clients. + /// + /// + /// A list of client names to send to. + public void PushMessage(TWrite message, List targetNames) + { + lock (_connections) + { + foreach (var client in _connections) + { + if (targetNames.Contains(client.Name)) + { + client.PushMessage(message); + } + } + } + } + + + /// + /// Closes all open client connections and stops listening for new ones. + /// + public void Stop() + { + _shouldKeepRunning = false; + + lock (_connections) + { + foreach (var client in _connections.ToArray()) + { + client.Close(); + } + } + + // If background thread is still listening for a client to connect, + // initiate a dummy connection that will allow the thread to exit. + var dummyClient = new NamedPipeClient(_pipeName); + dummyClient.Start(); + dummyClient.WaitForConnection(TimeSpan.FromSeconds(2)); + dummyClient.Stop(); + dummyClient.WaitForDisconnection(TimeSpan.FromSeconds(2)); + } + + #region Private methods + + private void ListenSync() + { + _isRunning = true; + while (_shouldKeepRunning) { WaitForConnection(); } + _isRunning = false; + } + + private void WaitForConnection() + { + NamedPipeServerStream handshakePipe = null; + NamedPipeServerStream dataPipe = null; + NamedPipeConnection connection = null; + + var connectionPipeName = GetNextConnectionPipeName(); + + try + { + // Send the client the name of the data pipe to use + handshakePipe = CreateAndConnectPipe(); + var handshakeWrapper = new PipeStreamWrapper(handshakePipe); + handshakeWrapper.WriteObject(connectionPipeName); + handshakeWrapper.WaitForPipeDrain(); + handshakeWrapper.Close(); + + // Wait for the client to connect to the data pipe + dataPipe = CreatePipe(connectionPipeName); + dataPipe.WaitForConnection(); + + // Add the client's connection to the list of connections + connection = ConnectionFactory.CreateConnection(dataPipe); + connection.ReceiveMessage += ClientOnReceiveMessage; + connection.Disconnected += ClientOnDisconnected; + connection.Error += ConnectionOnError; + connection.Open(); + + lock (_connections) { _connections.Add(connection); } + + ClientOnConnected(connection); + } + // Catch the IOException that is raised if the pipe is broken or disconnected. + catch (Exception e) + { + Console.Error.WriteLine("Named pipe is broken or disconnected: {0}", e); + + Cleanup(handshakePipe); + Cleanup(dataPipe); + + ClientOnDisconnected(connection); + } + } + + private NamedPipeServerStream CreateAndConnectPipe() + { + return _security == null + ? PipeServerFactory.CreateAndConnectPipe(_pipeName) + : PipeServerFactory.CreateAndConnectPipe(_pipeName, _bufferSize, _security); + } + + private NamedPipeServerStream CreatePipe(string connectionPipeName) + { + return _security == null + ? PipeServerFactory.CreatePipe(connectionPipeName) + : PipeServerFactory.CreatePipe(connectionPipeName, _bufferSize, _security); + } + + private void ClientOnConnected(NamedPipeConnection connection) + { + if (ClientConnected != null) + ClientConnected(connection); + } + + private void ClientOnReceiveMessage(NamedPipeConnection connection, TRead message) + { + if (ClientMessage != null) + ClientMessage(connection, message); + } + + private void ClientOnDisconnected(NamedPipeConnection connection) + { + if (connection == null) + return; + + lock (_connections) + { + _connections.Remove(connection); + } + + if (ClientDisconnected != null) + ClientDisconnected(connection); + } + + /// + /// Invoked on the UI thread. + /// + private void ConnectionOnError(NamedPipeConnection connection, Exception exception) + { + OnError(exception); + } + + /// + /// Invoked on the UI thread. + /// + /// + private void OnError(Exception exception) + { + if (Error != null) + Error(exception); + } + + private string GetNextConnectionPipeName() + { + return string.Format("{0}_{1}", _pipeName, ++_nextPipeId); + } + + private static void Cleanup(NamedPipeServerStream pipe) + { + if (pipe == null) return; + using (var x = pipe) + { + x.Close(); + } + } + + #endregion + } } diff --git a/NamedPipeWrapper/NamedPipeWrapper.csproj b/NamedPipeWrapper/NamedPipeWrapper.csproj index e1ecc09..f2927e3 100644 --- a/NamedPipeWrapper/NamedPipeWrapper.csproj +++ b/NamedPipeWrapper/NamedPipeWrapper.csproj @@ -17,7 +17,7 @@ true full false - bin\Debug\ + ..\bin\ DEBUG;TRACE prompt 4 @@ -30,6 +30,7 @@ TRACE prompt 4 + bin\Release\NamedPipeWrapper.xml @@ -42,6 +43,7 @@ + diff --git a/NamedPipeWrapper/PipeServerFactory.cs b/NamedPipeWrapper/PipeServerFactory.cs new file mode 100644 index 0000000..ef55080 --- /dev/null +++ b/NamedPipeWrapper/PipeServerFactory.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Text; + +namespace NamedPipeWrapper +{ + static class PipeServerFactory + { + public static NamedPipeServerStream CreateAndConnectPipe(string pipeName) + { + var pipe = CreatePipe(pipeName); + pipe.WaitForConnection(); + + return pipe; + } + + public static NamedPipeServerStream CreatePipe(string pipeName) + { + return new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough); + } + + public static NamedPipeServerStream CreateAndConnectPipe(string pipeName, int bufferSize, PipeSecurity security) + { + var pipe = CreatePipe(pipeName, bufferSize, security); + pipe.WaitForConnection(); + + return pipe; + } + + public static NamedPipeServerStream CreatePipe(string pipeName, int bufferSize, PipeSecurity security) + { + return new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough, bufferSize, bufferSize, security); + } + } +} diff --git a/NamedPipeWrapper/Properties/AssemblyInfo.cs b/NamedPipeWrapper/Properties/AssemblyInfo.cs index 2dae0fe..775a346 100644 --- a/NamedPipeWrapper/Properties/AssemblyInfo.cs +++ b/NamedPipeWrapper/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.4.0.0")] -[assembly: AssemblyFileVersion("1.4.0.0")] +[assembly: AssemblyVersion("1.5.0.0")] +[assembly: AssemblyFileVersion("1.5.0.0")]