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