Skip to content

Commit 4f008d3

Browse files
committed
Don't dispose channel when completing SshCommand
1 parent 16d84d0 commit 4f008d3

File tree

2 files changed

+53
-20
lines changed

2 files changed

+53
-20
lines changed

src/Renci.SshNet/SshCommand.cs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class SshCommand : IDisposable
2121
private readonly ISession _session;
2222
private readonly Encoding _encoding;
2323

24-
private IChannelSession? _channel;
24+
private IChannelSession _channel;
2525
private TaskCompletionSource<object>? _tcs;
2626
private CancellationTokenSource? _cts;
2727
private CancellationTokenRegistration _tokenRegistration;
@@ -142,14 +142,14 @@ public int? ExitStatus
142142
/// </example>
143143
public Stream CreateInputStream()
144144
{
145-
if (_channel == null)
145+
if (!_channel.IsOpen)
146146
{
147-
throw new InvalidOperationException($"The input stream can be used only after calling BeginExecute and before calling EndExecute.");
147+
throw new InvalidOperationException("The input stream can be used only during execution.");
148148
}
149149

150150
if (_inputStream != null)
151151
{
152-
throw new InvalidOperationException($"The input stream already exists.");
152+
throw new InvalidOperationException("The input stream already exists.");
153153
}
154154

155155
_inputStream = new ChannelInputStream(_channel);
@@ -226,6 +226,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding)
226226
ExtendedOutputStream = new PipeStream();
227227
_session.Disconnected += Session_Disconnected;
228228
_session.ErrorOccured += Session_ErrorOccured;
229+
_channel = _session.CreateChannelSession();
229230
}
230231

231232
/// <summary>
@@ -257,6 +258,8 @@ public Task ExecuteAsync(CancellationToken cancellationToken = default)
257258
throw new InvalidOperationException("Asynchronous operation is already in progress.");
258259
}
259260

261+
UnsubscribeFromChannelEvents(dispose: true);
262+
260263
OutputStream.Dispose();
261264
ExtendedOutputStream.Dispose();
262265

@@ -265,6 +268,7 @@ public Task ExecuteAsync(CancellationToken cancellationToken = default)
265268
// so we just need to reinitialise them for subsequent executions.
266269
OutputStream = new PipeStream();
267270
ExtendedOutputStream = new PipeStream();
271+
_channel = _session.CreateChannelSession();
268272
}
269273

270274
_exitStatus = default;
@@ -282,7 +286,6 @@ public Task ExecuteAsync(CancellationToken cancellationToken = default)
282286
_tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
283287
_userToken = cancellationToken;
284288

285-
_channel = _session.CreateChannelSession();
286289
_channel.DataReceived += Channel_DataReceived;
287290
_channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
288291
_channel.RequestReceived += Channel_RequestReceived;
@@ -542,7 +545,10 @@ private void SetAsyncComplete(bool setResult = true)
542545
}
543546
}
544547

545-
UnsubscribeFromEventsAndDisposeChannel();
548+
// We don't dispose the channel here to avoid a race condition
549+
// where SSH_MSG_CHANNEL_CLOSE arrives before _channel starts
550+
// waiting for a response in _channel.SendExecRequest().
551+
UnsubscribeFromChannelEvents(dispose: false);
546552

547553
OutputStream.Dispose();
548554
ExtendedOutputStream.Dispose();
@@ -568,7 +574,7 @@ private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
568574

569575
Debug.Assert(!exitSignalInfo.WantReply, "exit-signal is want_reply := false by definition.");
570576
}
571-
else if (e.Info.WantReply && _channel?.RemoteChannelNumber is uint remoteChannelNumber)
577+
else if (e.Info.WantReply && sender is IChannel { RemoteChannelNumber: uint remoteChannelNumber })
572578
{
573579
var replyMessage = new ChannelFailureMessage(remoteChannelNumber);
574580
_session.SendMessage(replyMessage);
@@ -591,29 +597,24 @@ private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
591597
}
592598

593599
/// <summary>
594-
/// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
595-
/// the <see cref="_channel"/>.
600+
/// Unsubscribes the current <see cref="SshCommand"/> from channel events, and optionally,
601+
/// disposes <see cref="_channel"/>.
596602
/// </summary>
597-
private void UnsubscribeFromEventsAndDisposeChannel()
603+
private void UnsubscribeFromChannelEvents(bool dispose)
598604
{
599605
var channel = _channel;
600606

601-
if (channel is null)
602-
{
603-
return;
604-
}
605-
606-
_channel = null;
607-
608607
// unsubscribe from events as we do not want to be signaled should these get fired
609608
// during the dispose of the channel
610609
channel.DataReceived -= Channel_DataReceived;
611610
channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
612611
channel.RequestReceived -= Channel_RequestReceived;
613612
channel.Closed -= Channel_Closed;
614613

615-
// actually dispose the channel
616-
channel.Dispose();
614+
if (dispose)
615+
{
616+
channel.Dispose();
617+
}
617618
}
618619

619620
/// <summary>
@@ -645,7 +646,7 @@ protected virtual void Dispose(bool disposing)
645646

646647
// unsubscribe from channel events to ensure other objects that we're going to dispose
647648
// are not accessed while disposing
648-
UnsubscribeFromEventsAndDisposeChannel();
649+
UnsubscribeFromChannelEvents(dispose: true);
649650

650651
_inputStream?.Dispose();
651652
_inputStream = null;

test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,38 @@ public void Test_Run_SingleCommand()
3333
}
3434
}
3535

36+
[TestMethod]
37+
public async Task Z()
38+
{
39+
using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
40+
{
41+
client.Connect();
42+
43+
for (int i = 0; i < 1000; i++)
44+
{
45+
using var command = client.CreateCommand("echo Hello World!");
46+
await command.ExecuteAsync();
47+
Assert.AreEqual("Hello World!\n", command.Result);
48+
}
49+
}
50+
}
51+
52+
[TestMethod]
53+
public async Task Z2()
54+
{
55+
using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password))
56+
{
57+
client.Connect();
58+
59+
using var command = client.CreateCommand("echo Hello World!");
60+
for (int i = 0; i < 1000; i++)
61+
{
62+
await command.ExecuteAsync();
63+
Assert.AreEqual("Hello World!\n", command.Result);
64+
}
65+
}
66+
}
67+
3668
[TestMethod]
3769
public void Test_Execute_SingleCommand()
3870
{

0 commit comments

Comments
 (0)