AsyncSocketListener.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #pragma warning disable IDE0005 // Using directive is unnecessary; IntegrationTests use implicit usings
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System.Threading;
  7. #pragma warning restore IDE0005
  8. namespace Renci.SshNet.Tests.Common
  9. {
  10. public class AsyncSocketListener : IDisposable
  11. {
  12. private readonly IPEndPoint _endPoint;
  13. private readonly ManualResetEvent _acceptCallbackDone;
  14. private readonly List<Socket> _connectedClients;
  15. private readonly object _syncLock;
  16. private Socket _listener;
  17. private Thread _receiveThread;
  18. private bool _started;
  19. private string _stackTrace;
  20. public delegate void BytesReceivedHandler(byte[] bytesReceived, Socket socket);
  21. public delegate void ConnectedHandler(Socket socket);
  22. public event BytesReceivedHandler BytesReceived;
  23. public event ConnectedHandler Connected;
  24. public event ConnectedHandler Disconnected;
  25. public AsyncSocketListener(IPEndPoint endPoint)
  26. {
  27. _endPoint = endPoint;
  28. _acceptCallbackDone = new ManualResetEvent(false);
  29. _connectedClients = new List<Socket>();
  30. _syncLock = new object();
  31. ShutdownRemoteCommunicationSocket = true;
  32. }
  33. /// <summary>
  34. /// Gets a value indicating whether the <see cref="Socket.Shutdown(SocketShutdown)"/> is invoked on the <see cref="Socket"/>
  35. /// that is used to handle the communication with the remote host, when the remote host has closed the connection.
  36. /// </summary>
  37. /// <value>
  38. /// <see langword="true"/> to invoke <see cref="Socket.Shutdown(SocketShutdown)"/> on the <see cref="Socket"/> that is used
  39. /// to handle the communication with the remote host, when the remote host has closed the connection; otherwise,
  40. /// <see langword="false"/>. The default is <see langword="true"/>.
  41. /// </value>
  42. public bool ShutdownRemoteCommunicationSocket { get; set; }
  43. public void Start()
  44. {
  45. _listener = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  46. _listener.Bind(_endPoint);
  47. _listener.Listen(1);
  48. _started = true;
  49. _receiveThread = new Thread(StartListener);
  50. _receiveThread.Start(_listener);
  51. _stackTrace = Environment.StackTrace;
  52. }
  53. public void Stop()
  54. {
  55. _started = false;
  56. lock (_syncLock)
  57. {
  58. foreach (var connectedClient in _connectedClients)
  59. {
  60. try
  61. {
  62. connectedClient.Shutdown(SocketShutdown.Send);
  63. }
  64. catch (Exception ex)
  65. {
  66. Console.Error.WriteLine("[{0}] Failure shutting down socket: {1}",
  67. typeof(AsyncSocketListener).FullName,
  68. ex);
  69. }
  70. DrainSocket(connectedClient);
  71. connectedClient.Dispose();
  72. }
  73. _connectedClients.Clear();
  74. }
  75. _listener?.Dispose();
  76. if (_receiveThread != null)
  77. {
  78. _receiveThread.Join();
  79. _receiveThread = null;
  80. }
  81. }
  82. public void Dispose()
  83. {
  84. Stop();
  85. GC.SuppressFinalize(this);
  86. }
  87. private void StartListener(object state)
  88. {
  89. try
  90. {
  91. var listener = (Socket)state;
  92. while (_started)
  93. {
  94. _ = _acceptCallbackDone.Reset();
  95. _ = listener.BeginAccept(AcceptCallback, listener);
  96. _ = _acceptCallbackDone.WaitOne();
  97. }
  98. }
  99. catch (Exception ex)
  100. {
  101. // On .NET framework when Thread throws an exception then unit tests
  102. // were executed without any problem.
  103. // On new .NET exceptions from Thread breaks unit tests session.
  104. Console.Error.WriteLine("[{0}] Failure in StartListener: {1}",
  105. typeof(AsyncSocketListener).FullName,
  106. ex);
  107. }
  108. }
  109. private void AcceptCallback(IAsyncResult ar)
  110. {
  111. // Signal the main thread to continue
  112. _ = _acceptCallbackDone.Set();
  113. // Get the socket that listens for inbound connections
  114. var listener = (Socket)ar.AsyncState;
  115. // Get the socket that handles the client request
  116. Socket handler;
  117. try
  118. {
  119. handler = listener.EndAccept(ar);
  120. }
  121. catch (SocketException ex)
  122. {
  123. // The listener is stopped through a Dispose() call, which in turn causes
  124. // Socket.EndAccept(...) to throw a SocketException or
  125. // ObjectDisposedException
  126. //
  127. // Since we consider such an exception normal when the listener is being
  128. // stopped, we only write a message to stderr if the listener is considered
  129. // to be up and running
  130. if (_started)
  131. {
  132. Console.Error.WriteLine("[{0}] Failure accepting new connection: {1}",
  133. typeof(AsyncSocketListener).FullName,
  134. ex);
  135. }
  136. return;
  137. }
  138. catch (ObjectDisposedException ex)
  139. {
  140. // The listener is stopped through a Dispose() call, which in turn causes
  141. // Socket.EndAccept(IAsyncResult) to throw a SocketException or
  142. // ObjectDisposedException
  143. //
  144. // Since we consider such an exception normal when the listener is being
  145. // stopped, we only write a message to stderr if the listener is considered
  146. // to be up and running
  147. if (_started)
  148. {
  149. Console.Error.WriteLine("[{0}] Failure accepting new connection: {1}",
  150. typeof(AsyncSocketListener).FullName,
  151. ex);
  152. }
  153. return;
  154. }
  155. // Signal new connection
  156. SignalConnected(handler);
  157. lock (_syncLock)
  158. {
  159. // Register client socket
  160. _connectedClients.Add(handler);
  161. }
  162. var state = new SocketStateObject(handler);
  163. try
  164. {
  165. _ = handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state);
  166. }
  167. catch (SocketException ex)
  168. {
  169. // The listener is stopped through a Dispose() call, which in turn causes
  170. // Socket.BeginReceive(...) to throw a SocketException or
  171. // ObjectDisposedException
  172. //
  173. // Since we consider such an exception normal when the listener is being
  174. // stopped, we only write a message to stderr if the listener is considered
  175. // to be up and running
  176. if (_started)
  177. {
  178. Console.Error.WriteLine("[{0}] Failure receiving new data: {1}",
  179. typeof(AsyncSocketListener).FullName,
  180. ex);
  181. }
  182. }
  183. catch (ObjectDisposedException ex)
  184. {
  185. // The listener is stopped through a Dispose() call, which in turn causes
  186. // Socket.BeginReceive(...) to throw a SocketException or
  187. // ObjectDisposedException
  188. //
  189. // Since we consider such an exception normal when the listener is being
  190. // stopped, we only write a message to stderr if the listener is considered
  191. // to be up and running
  192. if (_started)
  193. {
  194. Console.Error.WriteLine("[{0}] Failure receiving new data: {1}",
  195. typeof(AsyncSocketListener).FullName,
  196. ex);
  197. }
  198. }
  199. }
  200. private void ReadCallback(IAsyncResult ar)
  201. {
  202. // Retrieve the state object and the handler socket
  203. // from the asynchronous state object
  204. var state = (SocketStateObject)ar.AsyncState;
  205. var handler = state.Socket;
  206. int bytesRead;
  207. try
  208. {
  209. // Read data from the client socket.
  210. bytesRead = handler.EndReceive(ar, out var errorCode);
  211. if (errorCode != SocketError.Success)
  212. {
  213. bytesRead = 0;
  214. }
  215. }
  216. catch (SocketException ex)
  217. {
  218. // The listener is stopped through a Dispose() call, which in turn causes
  219. // Socket.EndReceive(...) to throw a SocketException or
  220. // ObjectDisposedException
  221. //
  222. // Since we consider such an exception normal when the listener is being
  223. // stopped, we only write a message to stderr if the listener is considered
  224. // to be up and running
  225. if (_started)
  226. {
  227. Console.Error.WriteLine("[{0}] Failure receiving new data: {1}",
  228. typeof(AsyncSocketListener).FullName,
  229. ex);
  230. }
  231. return;
  232. }
  233. catch (ObjectDisposedException ex)
  234. {
  235. // The listener is stopped through a Dispose() call, which in turn causes
  236. // Socket.EndReceive(...) to throw a SocketException or
  237. // ObjectDisposedException
  238. //
  239. // Since we consider such an exception normal when the listener is being
  240. // stopped, we only write a message to stderr if the listener is considered
  241. // to be up and running
  242. if (_started)
  243. {
  244. Console.Error.WriteLine("[{0}] Failure receiving new data: {1}",
  245. typeof(AsyncSocketListener).FullName,
  246. ex);
  247. }
  248. return;
  249. }
  250. void ConnectionDisconnected(bool doShutdownIfApplicable)
  251. {
  252. SignalDisconnected(handler);
  253. if (ShutdownRemoteCommunicationSocket)
  254. {
  255. lock (_syncLock)
  256. {
  257. if (!_started)
  258. {
  259. return;
  260. }
  261. try
  262. {
  263. if (doShutdownIfApplicable)
  264. {
  265. handler.Shutdown(SocketShutdown.Send);
  266. handler.Close();
  267. }
  268. }
  269. catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset)
  270. {
  271. // On .NET 7 we got Socker Exception with ConnectionReset from Shutdown method
  272. // when the socket is disposed
  273. }
  274. catch (SocketException ex)
  275. {
  276. throw new Exception("Exception in ReadCallback: " + ex.SocketErrorCode + " " + _stackTrace, ex);
  277. }
  278. catch (Exception ex)
  279. {
  280. throw new Exception("Exception in ReadCallback: " + _stackTrace, ex);
  281. }
  282. _ = _connectedClients.Remove(handler);
  283. }
  284. }
  285. }
  286. if (bytesRead > 0)
  287. {
  288. var bytesReceived = new byte[bytesRead];
  289. Array.Copy(state.Buffer, bytesReceived, bytesRead);
  290. SignalBytesReceived(bytesReceived, handler);
  291. try
  292. {
  293. _ = handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state);
  294. }
  295. catch (ObjectDisposedException)
  296. {
  297. // TODO On .NET 7, sometimes we get ObjectDisposedException when _started but only on appveyor, locally it works
  298. ConnectionDisconnected(doShutdownIfApplicable: false);
  299. }
  300. catch (SocketException ex)
  301. {
  302. if (!_started)
  303. {
  304. throw new Exception("BeginReceive while stopping!", ex);
  305. }
  306. throw new Exception("BeginReceive while started!: " + ex.SocketErrorCode + " " + _stackTrace, ex);
  307. }
  308. }
  309. else
  310. {
  311. ConnectionDisconnected(doShutdownIfApplicable: true);
  312. }
  313. }
  314. private void SignalBytesReceived(byte[] bytesReceived, Socket client)
  315. {
  316. BytesReceived?.Invoke(bytesReceived, client);
  317. }
  318. private void SignalConnected(Socket client)
  319. {
  320. Connected?.Invoke(client);
  321. }
  322. private void SignalDisconnected(Socket client)
  323. {
  324. Disconnected?.Invoke(client);
  325. }
  326. private static void DrainSocket(Socket socket)
  327. {
  328. var buffer = new byte[128];
  329. try
  330. {
  331. while (true && socket.Connected)
  332. {
  333. var bytesRead = socket.Receive(buffer);
  334. if (bytesRead == 0)
  335. {
  336. break;
  337. }
  338. }
  339. }
  340. catch (SocketException ex)
  341. {
  342. Console.Error.WriteLine("[{0}] Failure draining socket ({1}): {2}",
  343. typeof(AsyncSocketListener).FullName,
  344. ex.SocketErrorCode.ToString("G"),
  345. ex);
  346. }
  347. }
  348. private class SocketStateObject
  349. {
  350. public Socket Socket { get; private set; }
  351. public readonly byte[] Buffer = new byte[2048];
  352. public SocketStateObject(Socket handler)
  353. {
  354. Socket = handler;
  355. }
  356. }
  357. }
  358. }