Skip to content

Conversation

@Rob-Hague
Copy link
Collaborator

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:

  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.

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

  • Use "using" and ManualResetEventSlim in Connect
  • Delete unused and unnecessary methods

4. Add a loop to SocketAbstraction.ReadAsync

In order to ensure the buffer is read completely, as in SocketAbstraction.Read

@Rob-Hague Rob-Hague marked this pull request as draft March 9, 2024 10:04
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
@WojciechNagorski
Copy link
Collaborator

Do you want to finish something here?

@Rob-Hague Rob-Hague marked this pull request as ready for review March 14, 2024 12:55
@Rob-Hague
Copy link
Collaborator Author

I think it's ready for review

@Rob-Hague Rob-Hague marked this pull request as draft July 10, 2024 11:24
Copy link

Copilot AI left a 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 AwaitableSocketAsyncEventArgs with TaskCompletionSource-based implementations in SocketExtensions
  • Removed retry logic for resumable socket errors (only relevant for non-blocking sockets)
  • Added loop to ReadAsync to ensure complete buffer reads, matching synchronous Read behavior

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.

Comment on lines +143 to +144
readTimeout.TotalMilliseconds),
ex);
Copy link

Copilot AI Oct 23, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +174
throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture,
"Socket read operation has timed out after {0:F0} milliseconds.",
socket.ReceiveTimeout),
ex);
Copy link

Copilot AI Oct 23, 2025

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants