-
-
Notifications
You must be signed in to change notification settings - Fork 967
Sockets cleanup #1351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Sockets cleanup #1351
Conversation
The existing AwaitableSocketAsyncEventArgs is useful in principal for being reusable in order to save on allocations. However, we don't reuse it and the implementation is flawed. Instead, use implementations based on TaskCompletionSource, and add a SendAsync method. Because sockets are only natively cancellable on modern .NET, I was torn between 3 options for cancellation on the targets which use SocketExtensions: 1. Do not respect the CancellationToken once the socket operation has started. I believe this is what earlier versions of .NET Core did when CancellationToken overloads were first added via SocketTaskExtensions. 2. Do not close the socket upon cancellation, meaning the socket operation continues to run after the Task has completed. This is what the previous implementation effectively does. 3. Close the socket when the CancellationToken is cancelled, in order to stop the socket operation. The behaviour of a socket after (proper) cancellation is undefined(?), so in any case it should not make sense to use the socket after triggering cancellation. I felt that option 2 was the worst of them. This iteration goes for option 3.
Some methods in SocketAbstraction have code to retry a socket operation if it
returns certain error codes. However AFAIK, these errors are only pertinent to
nonblocking sockets, which we do not use.
For blocking sockets, Socket.Send{Async} only returns when all of the bytes are
sent. There is no need for a loop.
These changes combined mean there is no need for Send methods in SocketAbstraction.
* Use "using" and ManualResetEventSlim in Connect * Delete unused and unnecessary methods
In order to ensure the buffer is read completely, as in SocketAbstraction.Read
|
Do you want to finish something here? |
|
I think it's ready for review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors socket operations to simplify implementations and improve cancellation handling by replacing custom awaitable types with standard TaskCompletionSource patterns, removing unnecessary retry logic for blocking sockets, and ensuring consistent async/sync behaviors.
Key changes:
- Replaced
AwaitableSocketAsyncEventArgswith TaskCompletionSource-based implementations in SocketExtensions - Removed retry logic for resumable socket errors (only relevant for non-blocking sockets)
- Added loop to
ReadAsyncto ensure complete buffer reads, matching synchronousReadbehavior
Reviewed Changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
test/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyVersion.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyCode.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingDestinationAddress.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_TimeoutReadingIdentificationString.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingStatusLine.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingHttpContent.cs |
Updated test assertions to expect SocketException as inner exception of timeout errors |
test/Renci.SshNet.Tests/Classes/Channels/ChannelForwardedTcpipTest_Dispose_SessionIsConnectedAndChannelIsOpen.cs |
Added SocketFactory parameter to ChannelForwardedTcpip constructor |
test/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs |
Replaced SocketAbstraction methods with direct Socket calls and fixed comment typos |
src/Renci.SshNet/Session.cs |
Replaced SocketAbstraction.Send with direct Socket.Send call |
src/Renci.SshNet/ForwardedPortDynamic.cs |
Replaced SocketAbstraction methods with direct Socket.Send calls |
src/Renci.SshNet/Connection/Socks5Connector.cs |
Replaced SocketAbstraction.Send with direct Socket.Send and updated Read call signature |
src/Renci.SshNet/Connection/Socks4Connector.cs |
Replaced SocketAbstraction.Send with direct Socket.Send |
src/Renci.SshNet/Connection/ProtocolVersionExchange.cs |
Updated to use direct Socket methods and simplified async version exchange |
src/Renci.SshNet/Connection/HttpConnector.cs |
Replaced SshData.Ascii with Encoding.ASCII and SocketAbstraction.Send with Socket.Send |
src/Renci.SshNet/Connection/ConnectorBase.cs |
Replaced SocketAbstraction.ConnectAsync with direct Socket.ConnectAsync |
src/Renci.SshNet/Common/Extensions.cs |
Moved socket check logic from SocketAbstraction to extension methods |
src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs |
Added SocketFactory dependency and replaced SocketAbstraction methods with direct Socket calls |
src/Renci.SshNet/Channels/ChannelDirectTcpip.cs |
Replaced SocketAbstraction.Send with direct Socket.Send |
src/Renci.SshNet/Abstractions/SocketExtensions.cs |
Completely rewrote with TaskCompletionSource-based async methods |
src/Renci.SshNet/Abstractions/SocketAbstraction.cs |
Removed unused methods and retry logic; simplified Connect and Read methods |
src/Renci.SshNet/Abstractions/SocketAbstraction.Async.cs |
Deleted file as functionality moved to unified implementation |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| readTimeout.TotalMilliseconds), | ||
| ex); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SshOperationTimeoutException constructor now receives the SocketException as an inner exception. However, the synchronous ReadByte method at line 99 does not follow this pattern and creates the timeout exception without an inner exception. This inconsistency in error handling between Read and ReadByte could make debugging more difficult.
| throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, | ||
| "Socket read operation has timed out after {0:F0} milliseconds.", | ||
| socket.ReceiveTimeout), | ||
| ex); |
Copilot
AI
Oct 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using socket.ReceiveTimeout in the error message is incorrect for the async ReadAsync method, which doesn't use Socket.ReceiveTimeout. The timeout in async methods is controlled by the CancellationToken, not the socket's timeout property. This will display an incorrect or misleading timeout duration in the error message.
| throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, | |
| "Socket read operation has timed out after {0:F0} milliseconds.", | |
| socket.ReceiveTimeout), | |
| ex); | |
| throw new SshOperationTimeoutException("Socket read operation has timed out.", ex); |
1. Replace AwaitableSocketAsyncEventArgs in SocketExtensions
The existing AwaitableSocketAsyncEventArgs is useful in principal for being
reusable in order to save on allocations. However, we don't reuse it and the
implementation is flawed (#913). Instead, use implementations based on
TaskCompletionSource, and add a SendAsync method.
Because sockets are only natively cancellable on modern .NET, I was torn between
3 options for cancellation on the targets which use SocketExtensions:
I believe this is what earlier versions of .NET Core did when CancellationToken
overloads were first added via SocketTaskExtensions.
continues to run after the Task has completed. This is what the previous
implementation effectively does.
the socket operation. The behaviour of a socket after proper cancellation
is undefined(?), so in any case it should not make sense to use the
socket after triggering cancellation.
I felt that option 2 was the worst of them. This iteration goes for option 3.
2. Remove "IsErrorResumable" and SocketAbstraction.Send{Async}
Some methods in SocketAbstraction have code to retry a socket operation if it
returns certain error codes. However AFAIK, these errors are only pertinent to
nonblocking sockets, which we do not use.
For blocking sockets, Socket.Send{Async} only returns when all of the bytes are
sent. There is no need for a loop.
These changes combined mean there is no need for Send methods in SocketAbstraction.
3. Cleanup SocketAbstraction
4. Add a loop to SocketAbstraction.ReadAsync
In order to ensure the buffer is read completely, as in SocketAbstraction.Read