diff --git a/Renci.SshClient/Build/nuget/SSH.NET.nuspec b/Renci.SshClient/Build/nuget/SSH.NET.nuspec
index dd1c4cdf..137bd380 100644
--- a/Renci.SshClient/Build/nuget/SSH.NET.nuspec
+++ b/Renci.SshClient/Build/nuget/SSH.NET.nuspec
@@ -16,11 +16,11 @@
* Added callbacks to UploadFile, DownloadFile and ListDirectory in SftpClient (issue #1324)
* Allow a given private key file to be used concurrently
* Performance improvements:
- - optimization of payload size for both read and write operations (SftpClient only)
- - increase window size from 1MB to 2MB
- - increase buffer size from 16KB to 64KB for SftpClient
- - take into account the maximum remote packet size of the channel for write operations
- - increase maximum size of packets that we can receive from 32 KB to 64 KB
+ - optimization of payload size for both read and write operations (SftpClient only)
+ - increase window size from 1MB to 2MB
+ - increase buffer size from 16KB to 64KB for SftpClient
+ - take into account the maximum remote packet size of the channel for write operations
+ - increase maximum size of packets that we can receive from 32 KB to 64 KB
* Improve exception message for authentication failures
Breaking changes:
@@ -30,6 +30,7 @@
Fixes:
+ * Timeout sending data to server with low window size (issue #1706)
* No connection possible with the same auth method requested multiple times (issue #1930)
* Unobserved exception rethrown by finalizer thread (issue #1298 and #1587)
* Client cipher is used to decrypt server messages (issue #1917)
diff --git a/Renci.SshClient/Renci.SshNet/Channels/Channel.cs b/Renci.SshClient/Renci.SshNet/Channels/Channel.cs
index 62227e00..d2833ea2 100644
--- a/Renci.SshClient/Renci.SshNet/Channels/Channel.cs
+++ b/Renci.SshClient/Renci.SshNet/Channels/Channel.cs
@@ -1,4 +1,4 @@
-using System;
+ using System;
using System.Threading;
using Renci.SshNet.Common;
using Renci.SshNet.Messages;
@@ -360,7 +360,7 @@ namespace Renci.SshNet.Channels
/// The message.
protected void SendMessage(Message message)
{
- // Send channel messages only while channel is open
+ // send channel messages only while channel is open
if (!this.IsOpen)
return;
@@ -373,99 +373,109 @@ namespace Renci.SshNet.Channels
/// The message to send.
private void SendMessage(ChannelCloseMessage message)
{
- // Send channel messages only while channel is open
+ // send channel messages only while channel is open
if (!this.IsOpen)
return;
this._session.SendMessage(message);
- // When channel close message is sent channel considered to be closed
+ // when channel close message is sent channel considered to be closed
this.IsOpen = false;
}
///
/// Sends channel data message to the servers.
///
- /// This method takes care of managing the window size.
/// Channel data message.
- /// The data of exceeds the maximum packet size of the channel.
+ ///
+ ///
+ /// When the data of the message exceeds the maximum packet size or the remote window
+ /// size does not allow the full message to be sent, then this method will send the
+ /// data in multiple chunks and will only wait for the remote window size to be adjusted
+ /// when its zero.
+ ///
+ ///
+ /// This is done to support SSH servers will a small window size that do not agressively
+ /// increase their window size. We need to take into account that there may be SSH
+ /// servers that only increase their window size when it has reached zero.
+ ///
+ ///
protected void SendMessage(ChannelDataMessage message)
{
- // Send channel messages only while channel is open
+ // send channel messages only while channel is open
if (!this.IsOpen)
return;
- var messageLength = message.Data.Length;
+ var totalDataLength = (uint) message.Data.Length;
+ var totalDataSent = 0u;
- // RFC4254:
- // The maximum amount of data allowed is determined by the maximum packet size
- // for the channel
- //
- // there's some ambiguity in the RFC, but most ssh implementations take only the
- // data into account for determining the size of a packet; the 4 bytes for the
- // packet length and the 9 bytes of the header are not considered part of the
- // data
- if (messageLength > RemotePacketSize)
- throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
- "The payload of the data message is {0} bytes while the maximum packet size of the channel is {1} bytes.",
- messageLength, RemotePacketSize));
-
- do
+ var totalBytesToSend = totalDataLength;
+ while (totalBytesToSend > 0)
{
- lock (this._serverWindowSizeLock)
+ var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend);
+ if (dataThatCanBeSentInMessage == totalDataLength)
{
- var serverWindowSize = this.RemoteWindowSize;
- if (serverWindowSize < messageLength)
- {
- // Wait for window to be big enough for this message
- this._channelServerWindowAdjustWaitHandle.Reset();
- }
- else
- {
- this.RemoteWindowSize -= (uint)messageLength;
- break;
- }
+ // we can send the message in one chunk
+ this._session.SendMessage(message);
}
- // Wait for window to change
- this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
- } while (true);
-
- this._session.SendMessage(message);
+ else
+ {
+ // we need to send the message in multiple chunks
+ var dataToSend = new byte[dataThatCanBeSentInMessage];
+ Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage);
+ this._session.SendMessage(new ChannelDataMessage(message.LocalChannelNumber, dataToSend));
+ }
+ totalDataSent += dataThatCanBeSentInMessage;
+ totalBytesToSend -= dataThatCanBeSentInMessage;
+ }
}
///
/// Sends channel extended data message to the servers.
///
- /// This method takes care of managing the window size.
/// Channel data message.
+ ///
+ ///
+ /// When the data of the message exceeds the maximum packet size or the remote window
+ /// size does not allow the full message to be sent, then this method will send the
+ /// data in multiple chunks and will only wait for the remote window size to be adjusted
+ /// when its zero.
+ ///
+ ///
+ /// This is done to support SSH servers will a small window size that do not agressively
+ /// increase their window size. We need to take into account that there may be SSH
+ /// servers that only increase their window size when it has reached zero.
+ ///
+ ///
protected void SendMessage(ChannelExtendedDataMessage message)
{
- // Send channel messages only while channel is open
+ // end channel messages only while channel is open
if (!this.IsOpen)
return;
- var messageLength = message.Data.Length;
- do
- {
- lock (this._serverWindowSizeLock)
- {
- var serverWindowSize = this.RemoteWindowSize;
- if (serverWindowSize < messageLength)
- {
- // Wait for window to be big enough for this message
- this._channelServerWindowAdjustWaitHandle.Reset();
- }
- else
- {
- this.RemoteWindowSize -= (uint)messageLength;
- break;
- }
- }
- // Wait for window to change
- this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
- } while (true);
+ var totalDataLength = (uint) message.Data.Length;
+ var totalDataSent = 0u;
- this._session.SendMessage(message);
+ var totalBytesToSend = totalDataLength;
+ while (totalBytesToSend > 0)
+ {
+ var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend);
+ if (dataThatCanBeSentInMessage == totalDataLength)
+ {
+ // we can send the message in one chunk
+ this._session.SendMessage(message);
+ }
+ else
+ {
+ // we need to send the message in multiple chunks
+ var dataToSend = new byte[dataThatCanBeSentInMessage];
+ Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage);
+ this._session.SendMessage(new ChannelExtendedDataMessage(message.LocalChannelNumber,
+ message.DataTypeCode, dataToSend));
+ }
+ totalDataSent += dataThatCanBeSentInMessage;
+ totalBytesToSend -= dataThatCanBeSentInMessage;
+ }
}
///
@@ -479,8 +489,8 @@ namespace Renci.SshNet.Channels
protected virtual void Close(bool wait)
{
- // Send message to close the channel on the server
- // Ignore sending close message when client not connected
+ // send message to close the channel on the server
+ // ignore sending close message when client not connected
if (!_closeMessageSent && this.IsConnected)
{
lock (this)
@@ -498,7 +508,7 @@ namespace Renci.SshNet.Channels
IsOpen = false;
}
- // Wait for channel to be closed
+ // wait for channel to be closed
if (wait)
{
WaitOnHandle(this._channelClosedWaitHandle);
@@ -637,6 +647,38 @@ namespace Renci.SshNet.Channels
}
}
+ ///
+ /// Determines the length of data that currently can be sent in a single message.
+ ///
+ /// The length of the message that must be sent.
+ ///
+ /// The actual data length that currently can be sent.
+ ///
+ private uint GetDataLengthThatCanBeSentInMessage(uint messageLength)
+ {
+ do
+ {
+ lock (this._serverWindowSizeLock)
+ {
+ var serverWindowSize = RemoteWindowSize;
+ if (serverWindowSize == 0)
+ {
+ // allow us to be signal when remote window size is adjusted
+ this._channelServerWindowAdjustWaitHandle.Reset();
+ }
+ else
+ {
+ var bytesThatCanBeSent = Math.Min(Math.Min(RemotePacketSize, messageLength),
+ serverWindowSize);
+ this.RemoteWindowSize -= bytesThatCanBeSent;
+ return bytesThatCanBeSent;
+ }
+ }
+ // wait for remote window size to change
+ this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
+ } while (true);
+ }
+
private InvalidOperationException CreateRemoteChannelInfoNotAvailableException()
{
throw new InvalidOperationException("The channel has not been opened, or the open has not yet been confirmed.");
diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs
index a38164cc..60c7e55a 100644
--- a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs
+++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs
@@ -1101,6 +1101,9 @@ namespace Renci.SshNet.Sftp
///
/// The optimal size of the buffer to write data on the channel.
///
+ ///
+ /// Currently, we do not take the remote window size into account.
+ ///
internal uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle)
{
// 1-4: package length of SSH_FXP_WRITE message