Skip to content

Commit bf7f3c8

Browse files
committed
Add NamedPipeChannel support with client and server channel implementations - Implement `NamedPipeClientChannel` and `NamedPipeServerChannel` for inter-process communication via named pipes. - Add `SimpleNamedPipeServer` and `SimpleNamedPipeConnection` utility classes for server-side management of named pipe connections. - Update `ServerConfig` and `ClientConfig` to include `ChannelConnectionName` property for configurable pipe names. - Update solution and project files to include the new `CoreRemoting.Channels.NamedPipe` project.
1 parent bbe5ce2 commit bf7f3c8

7 files changed

Lines changed: 764 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>10.0</LangVersion>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\CoreRemoting\CoreRemoting.csproj" />
10+
</ItemGroup>
11+
12+
</Project>
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Pipes;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using CoreRemoting.Threading;
7+
8+
namespace CoreRemoting.Channels.NamedPipe;
9+
10+
/// <summary>
11+
/// Client side Named Pipe channel implementation.
12+
/// </summary>
13+
public class NamedPipeClientChannel : IClientChannel, IRawMessageTransport
14+
{
15+
private NamedPipeClientStream _pipeClient;
16+
private IRemotingClient _remotingClient;
17+
private readonly CancellationTokenSource _cancellationTokenSource;
18+
private Task _receivingTask;
19+
private bool _isConnected;
20+
private readonly AsyncLock _disposeLock;
21+
22+
/// <summary>
23+
/// Creates a new instance of the NamedPipeClientChannel class.
24+
/// </summary>
25+
public NamedPipeClientChannel()
26+
{
27+
_cancellationTokenSource = new CancellationTokenSource();
28+
_disposeLock = new AsyncLock();
29+
}
30+
31+
/// <summary>
32+
/// Event: Fires when a message is received from server.
33+
/// </summary>
34+
public event Action<byte[]> ReceiveMessage;
35+
36+
/// <summary>
37+
/// Event: Fires when an error occurred.
38+
/// </summary>
39+
public event Action<string, Exception> ErrorOccured;
40+
41+
/// <inheritdoc />
42+
public event Action Disconnected;
43+
44+
/// <summary>
45+
/// Initializes the channel.
46+
/// </summary>
47+
/// <param name="client">CoreRemoting client</param>
48+
public void Init(IRemotingClient client)
49+
{
50+
_remotingClient = client ?? throw new ArgumentNullException(nameof(client));
51+
}
52+
53+
/// <summary>
54+
/// Establish a connection with the server.
55+
/// </summary>
56+
public async Task ConnectAsync()
57+
{
58+
if (_remotingClient == null)
59+
throw new InvalidOperationException("Channel is not initialized.");
60+
61+
if (_isConnected)
62+
return;
63+
64+
try
65+
{
66+
var pipeName = GetPipeName();
67+
68+
_pipeClient = new NamedPipeClientStream(
69+
".",
70+
pipeName,
71+
PipeDirection.InOut,
72+
PipeOptions.Asynchronous);
73+
74+
// Connect with timeout from client configuration
75+
var connectionTimeoutMs = _remotingClient.Config?.ConnectionTimeout > 0
76+
? _remotingClient.Config.ConnectionTimeout * 1000
77+
: -1; // Infinite timeout if 0 is specified
78+
await _pipeClient.ConnectAsync(connectionTimeoutMs);
79+
80+
if (!_pipeClient.IsConnected)
81+
throw new InvalidOperationException("Failed to establish named pipe connection.");
82+
83+
_isConnected = true;
84+
85+
// Start receiving messages after connection is established
86+
_receivingTask = Task.Run(ReceiveMessagesAsync, _cancellationTokenSource.Token);
87+
88+
// Send handshake message immediately after connection
89+
await SendHandshakeAsync();
90+
}
91+
catch (Exception ex)
92+
{
93+
LastException = new NetworkException($"Failed to connect to named pipe server: {ex.Message}", ex);
94+
ErrorOccured?.Invoke($"Connection error: {ex.Message}", ex);
95+
throw;
96+
}
97+
}
98+
99+
private async Task SendHandshakeAsync()
100+
{
101+
try
102+
{
103+
// Send initial empty message to trigger server handshake response
104+
await SendMessageAsync(new byte[0]);
105+
}
106+
catch (Exception ex)
107+
{
108+
// Log handshake error but don't fail connection
109+
Console.Error.WriteLine($"Handshake failed: {ex.Message}");
110+
}
111+
}
112+
113+
private async Task ReceiveMessagesAsync()
114+
{
115+
try
116+
{
117+
// Process messages while connected
118+
while (!_cancellationTokenSource.Token.IsCancellationRequested && _isConnected && _pipeClient.IsConnected)
119+
{
120+
var messageData = await ReadMessageAsync();
121+
if (messageData == null)
122+
break;
123+
124+
// Check if this is the first message (handshake response)
125+
if (messageData.Length > 0)
126+
{
127+
// This is a real message, not handshake
128+
ReceiveMessage?.Invoke(messageData);
129+
}
130+
// If message is empty, it's the handshake completion
131+
// CoreRemoting will handle this internally
132+
}
133+
}
134+
catch (OperationCanceledException)
135+
{
136+
// Expected when disconnecting
137+
}
138+
catch (Exception ex)
139+
{
140+
if (_isConnected)
141+
{
142+
LastException = new NetworkException($"Error receiving message: {ex.Message}", ex);
143+
ErrorOccured?.Invoke($"Receive error: {ex.Message}", ex);
144+
}
145+
}
146+
finally
147+
{
148+
if (_isConnected)
149+
{
150+
_isConnected = false;
151+
Disconnected?.Invoke();
152+
}
153+
}
154+
}
155+
156+
private async Task<byte[]> ReadMessageAsync()
157+
{
158+
if (_pipeClient == null || !_pipeClient.IsConnected)
159+
return null;
160+
161+
try
162+
{
163+
// Read message length (4 bytes)
164+
var lengthBuffer = new byte[4];
165+
var bytesRead = await _pipeClient.ReadAsync(lengthBuffer, 0, 4, _cancellationTokenSource.Token);
166+
167+
if (bytesRead != 4)
168+
return null;
169+
170+
var messageLength = BitConverter.ToInt32(lengthBuffer, 0);
171+
172+
if (messageLength < 0 || messageLength > 1024 * 1024) // 1MB max message size
173+
throw new InvalidOperationException($"Invalid message length: {messageLength}");
174+
175+
// Read message content
176+
var messageBuffer = new byte[messageLength];
177+
var totalBytesRead = 0;
178+
179+
while (totalBytesRead < messageLength)
180+
{
181+
// Check connection before each read
182+
if (!_pipeClient.IsConnected)
183+
return null;
184+
185+
var bytesToRead = messageLength - totalBytesRead;
186+
var read = await _pipeClient.ReadAsync(messageBuffer, totalBytesRead, bytesToRead, _cancellationTokenSource.Token);
187+
188+
if (read == 0)
189+
return null;
190+
191+
totalBytesRead += read;
192+
}
193+
194+
return messageBuffer;
195+
}
196+
catch (OperationCanceledException)
197+
{
198+
return null;
199+
}
200+
catch (IOException ioEx) when (ioEx.Message.Contains("Broken pipe") || ioEx.Message.Contains("Connection"))
201+
{
202+
// Normal pipe disconnection scenario
203+
return null;
204+
}
205+
catch (Exception ex)
206+
{
207+
throw new NetworkException($"Failed to read message: {ex.Message}", ex);
208+
}
209+
}
210+
211+
/// <summary>
212+
/// Closes the connection.
213+
/// </summary>
214+
public async Task DisconnectAsync()
215+
{
216+
using (await _disposeLock)
217+
{
218+
if (!_isConnected)
219+
return;
220+
221+
_isConnected = false;
222+
_cancellationTokenSource.Cancel();
223+
224+
try
225+
{
226+
_receivingTask?.Wait(TimeSpan.FromSeconds(5));
227+
}
228+
catch (AggregateException)
229+
{
230+
// Expected when cancelling
231+
}
232+
233+
try
234+
{
235+
if (_pipeClient != null)
236+
{
237+
if (_pipeClient.IsConnected)
238+
{
239+
_pipeClient.Close();
240+
}
241+
_pipeClient.Dispose();
242+
_pipeClient = null;
243+
}
244+
}
245+
catch
246+
{
247+
// Ignore disposal errors
248+
}
249+
}
250+
}
251+
252+
/// <summary>
253+
/// Gets whether the connection is established or not.
254+
/// </summary>
255+
public bool IsConnected => _isConnected && _pipeClient?.IsConnected == true;
256+
257+
/// <summary>
258+
/// Gets the raw message transport component for this connection.
259+
/// </summary>
260+
public IRawMessageTransport RawMessageTransport => this;
261+
262+
/// <summary>
263+
/// Sends a message to the server.
264+
/// </summary>
265+
/// <param name="rawMessage">Raw message data</param>
266+
public async Task<bool> SendMessageAsync(byte[] rawMessage)
267+
{
268+
if (!_isConnected || _pipeClient == null || rawMessage == null)
269+
return false;
270+
271+
try
272+
{
273+
// Check if stream is still connected
274+
if (!_pipeClient.IsConnected)
275+
return false;
276+
277+
// Send message length (4 bytes)
278+
var lengthBuffer = BitConverter.GetBytes(rawMessage.Length);
279+
await _pipeClient.WriteAsync(lengthBuffer, 0, 4, _cancellationTokenSource.Token);
280+
281+
// Send message content
282+
await _pipeClient.WriteAsync(rawMessage, 0, rawMessage.Length, _cancellationTokenSource.Token);
283+
await _pipeClient.FlushAsync(_cancellationTokenSource.Token);
284+
285+
return true;
286+
}
287+
catch (IOException ioEx) when (ioEx.Message.Contains("Broken pipe") || ioEx.Message.Contains("Connection"))
288+
{
289+
// Normal pipe disconnection scenario
290+
return false;
291+
}
292+
catch (Exception ex)
293+
{
294+
LastException = new NetworkException($"Failed to send message: {ex.Message}", ex);
295+
ErrorOccured?.Invoke($"Send error: {ex.Message}", ex);
296+
return false;
297+
}
298+
}
299+
300+
/// <summary>
301+
/// Gets or sets the last exception.
302+
/// </summary>
303+
public NetworkException LastException { get; set; }
304+
305+
private string GetPipeName()
306+
{
307+
// Use configured pipe name or default
308+
return _remotingClient.Config?.ChannelConnectionName ?? "CoreRemoting";
309+
}
310+
311+
/// <summary>
312+
/// Disconnect and free managed resources.
313+
/// </summary>
314+
public async ValueTask DisposeAsync()
315+
{
316+
await DisconnectAsync();
317+
_cancellationTokenSource.Dispose();
318+
_disposeLock.Dispose();
319+
}
320+
}

0 commit comments

Comments
 (0)