From 93647d9dceaece61603243df7154430ec39aa524 Mon Sep 17 00:00:00 2001 From: Jacob Slusser Date: Sun, 10 Dec 2023 22:10:32 -0800 Subject: [PATCH 1/4] Changes _socketDisposeLock to a SemaphoreSlim so it can play nice with async/await --- src/Renci.SshNet/Session.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index a57d5a1e7..4f1f72b85 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -119,7 +119,7 @@ public class Session : ISession /// This is also used to ensure that will not be disposed /// while performing a given operation or set of operations on . /// - private readonly object _socketDisposeLock = new object(); + private readonly SemaphoreSlim _socketDisposeLock = new SemaphoreSlim(1, 1); /// /// Holds an object that is used to ensure only a single thread can connect @@ -1127,12 +1127,14 @@ internal void SendMessage(Message message) /// /// /// This method is only to be used when the connection is established, as the locking - /// overhead is not required while establising the connection. + /// overhead is not required while establishing the connection. /// /// private void SendPacket(byte[] packet, int offset, int length) { - lock (_socketDisposeLock) + _socketDisposeLock.Wait(); + + try { if (!_socket.IsConnected()) { @@ -1141,6 +1143,10 @@ private void SendPacket(byte[] packet, int offset, int length) SocketAbstraction.Send(_socket, packet, offset, length); } + finally + { + _ = _socketDisposeLock.Release(); + } } /// @@ -1791,7 +1797,9 @@ internal static string ToHex(byte[] bytes) /// private bool IsSocketConnected() { - lock (_socketDisposeLock) + _socketDisposeLock.Wait(); + + try { if (!_socket.IsConnected()) { @@ -1804,6 +1812,10 @@ private bool IsSocketConnected() return !(connectionClosedOrDataAvailable && _socket.Available == 0); } } + finally + { + _ = _socketDisposeLock.Release(); + } } /// @@ -1830,7 +1842,9 @@ private void SocketDisconnectAndDispose() { if (_socket != null) { - lock (_socketDisposeLock) + _socketDisposeLock.Wait(); + + try { if (_socket != null) { @@ -1861,6 +1875,10 @@ private void SocketDisconnectAndDispose() _socket = null; } } + finally + { + _ = _socketDisposeLock.Release(); + } } } From 2ef5d3f11c768fb82076c4f30da919d79a7283ee Mon Sep 17 00:00:00 2001 From: Jacob Slusser Date: Sat, 23 Dec 2023 20:10:09 -0800 Subject: [PATCH 2/4] Adds a SendAsync method for .NET6+ --- .../Abstractions/SocketAbstraction.Async.cs | 49 +++++++++++++++++++ .../Abstractions/SocketAbstraction.cs | 9 +--- .../Connection/ProtocolVersionExchange.cs | 4 ++ 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs new file mode 100644 index 000000000..4203510b3 --- /dev/null +++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs @@ -0,0 +1,49 @@ +#if NET6_0_OR_GREATER + +using System; +using System.Diagnostics; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Renci.SshNet.Abstractions +{ + internal static partial class SocketAbstraction + { + public static ValueTask ReadAsync(Socket socket, byte[] buffer, CancellationToken cancellationToken) + { + return socket.ReceiveAsync(buffer, SocketFlags.None, cancellationToken); + } + + public static ValueTask SendAsync(Socket socket, ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + Debug.Assert(socket != null); + Debug.Assert(data.Length > 0); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return SendAsyncCore(socket, data, cancellationToken); + static async ValueTask SendAsyncCore(Socket socket, ReadOnlyMemory data, CancellationToken cancellationToken) + { + do + { + try + { + var bytesSent = await socket.SendAsync(data, SocketFlags.None, cancellationToken).ConfigureAwait(false); + data = data.Slice(bytesSent); + } + catch (SocketException ex) when (IsErrorResumable(ex.SocketErrorCode)) + { + // Buffer may be full; attempt a short delay and retry + await Task.Delay(30, cancellationToken).ConfigureAwait(false); + } + } + while (data.Length > 0); + } + } + } +} +#endif // NET6_0_OR_GREATER diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs index f5f840336..25c72de29 100644 --- a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs +++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs @@ -10,7 +10,7 @@ namespace Renci.SshNet.Abstractions { - internal static class SocketAbstraction + internal static partial class SocketAbstraction { public static bool CanRead(Socket socket) { @@ -325,12 +325,7 @@ public static int Read(Socket socket, byte[] buffer, int offset, int size, TimeS return totalBytesRead; } -#if NET6_0_OR_GREATER - public static async Task ReadAsync(Socket socket, byte[] buffer, CancellationToken cancellationToken) - { - return await socket.ReceiveAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); - } -#else +#if NET6_0_OR_GREATER == false public static Task ReadAsync(Socket socket, byte[] buffer, CancellationToken cancellationToken) { return socket.ReceiveAsync(buffer, 0, buffer.Length, cancellationToken); diff --git a/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs index bde732c06..b14da93c0 100644 --- a/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs +++ b/src/Renci.SshNet/Connection/ProtocolVersionExchange.cs @@ -81,7 +81,11 @@ public async Task StartAsync(string clientVersion, Socket soc { // Immediately send the identification string since the spec states both sides MUST send an identification string // when the connection has been established +#if NET6_0_OR_GREATER + await SocketAbstraction.SendAsync(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A"), cancellationToken).ConfigureAwait(false); +#else SocketAbstraction.Send(socket, Encoding.UTF8.GetBytes(clientVersion + "\x0D\x0A")); +#endif // NET6_0_OR_GREATER var bytesReceived = new List(); From f53889966733cfa85e3df0110435b98b9f59f011 Mon Sep 17 00:00:00 2001 From: Rob Hague Date: Sat, 6 Jan 2024 16:48:43 +0100 Subject: [PATCH 3/4] Fix false positive analyzer error --- src/Renci.SshNet/Session.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index b3b2652f6..122e28d3d 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -1862,7 +1862,9 @@ private void SocketDisconnectAndDispose() try { +#pragma warning disable CA1508 // Avoid dead conditional code; Value could have been changed by another thread. if (_socket != null) +#pragma warning restore CA1508 // Avoid dead conditional code { if (_socket.Connected) { From d89a56cd7dc1c165229307b8a3a303c0b2b58ea7 Mon Sep 17 00:00:00 2001 From: Jacob Slusser Date: Tue, 16 Jan 2024 08:52:50 -0800 Subject: [PATCH 4/4] Formatting --- src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs index 4203510b3..6cc6918ea 100644 --- a/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs +++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs @@ -26,6 +26,7 @@ public static ValueTask SendAsync(Socket socket, ReadOnlyMemory data, Canc } return SendAsyncCore(socket, data, cancellationToken); + static async ValueTask SendAsyncCore(Socket socket, ReadOnlyMemory data, CancellationToken cancellationToken) { do