From f12729dd5eed1c0f57212c1efdac02352ec65713 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Wed, 28 Feb 2024 11:54:52 +1100 Subject: [PATCH 01/10] Add ExecuteAsync, make Result and Error Obsolete --- src/Renci.SshNet/SshCommand.cs | 76 +++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index b348c4ec9..1dadc676a 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -4,6 +4,7 @@ using System.Runtime.ExceptionServices; using System.Text; using System.Threading; +using System.Threading.Tasks; using Renci.SshNet.Abstractions; using Renci.SshNet.Channels; @@ -105,6 +106,9 @@ public Stream CreateInputStream() /// /// Gets the command execution result. /// +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("Please read the result from the OutputStream. I.e. new StreamReader(shell.OutputStream).ReadToEnd().")] +#pragma warning disable S1133 // Deprecated code should be removed public string Result { get @@ -130,6 +134,9 @@ public string Result /// /// Gets the command execution error. /// +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("Please read the error result from the ExtendedOutputStream. I.e. new StreamReader(shell.ExtendedOutputStream).ReadToEnd().")] +#pragma warning disable S1133 // Deprecated code should be removed public string Error { get @@ -348,8 +355,74 @@ public string EndExecute(IAsyncResult asyncResult) _channel = null; commandAsyncResult.EndCalled = true; - +#pragma warning disable CS0618 return Result; +#pragma warning disable CS0618 + } + } + + /// + /// Waits for the pending asynchronous command execution to complete. + /// + /// The reference to the pending asynchronous request to finish. + /// Command execution exit status. + /// + /// + /// + /// Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult. + /// is null. + public int EndExecuteWithStatus(IAsyncResult asyncResult) + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + + var commandAsyncResult = asyncResult switch + { + CommandAsyncResult result when result == _asyncResult => result, + _ => throw new ArgumentException( + $"The {nameof(IAsyncResult)} object was not returned from the corresponding asynchronous method on this class.") + }; + + lock (_endExecuteLock) + { + if (commandAsyncResult.EndCalled) + { + throw new ArgumentException("EndExecute can only be called once for each asynchronous operation."); + } + + // wait for operation to complete (or time out) + WaitOnHandle(_asyncResult.AsyncWaitHandle); + UnsubscribeFromEventsAndDisposeChannel(_channel); + _channel = null; + + commandAsyncResult.EndCalled = true; + + return ExitStatus; + } + } + + /// + /// Executes the the command asynchronously. + /// + /// The to observe. + /// Exit status of the operation. + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + var asyncResult = BeginExecute(); + using var ctr = cancellationToken.Register(() => CancelAsync()); + + try + { + int status = await Task.Factory.FromAsync(asyncResult, EndExecuteWithStatus).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + + return status; + } + catch (Exception) when (cancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException("Command execution has been cancelled.", cancellationToken); } } @@ -358,6 +431,7 @@ public string EndExecute(IAsyncResult asyncResult) /// public void CancelAsync() { + _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); if (_channel is not null && _channel.IsOpen && _asyncResult is not null) { // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? From 9a27d9fe708da4a9c986cec3a38184d2830de5be Mon Sep 17 00:00:00 2001 From: zeotuan Date: Wed, 28 Feb 2024 14:30:04 +1100 Subject: [PATCH 02/10] Deprecate Error and Result property --- src/Renci.SshNet/SshCommand.cs | 74 ++++++++++------- .../SshClientBenchmark.cs | 6 +- .../ConnectivityTests.cs | 4 +- .../OldIntegrationTests/SshCommandTest.cs | 83 +++++++++++++++---- .../RemoteSshd.cs | 4 +- .../SftpTests.cs | 36 ++++---- .../SshClientTests.cs | 14 ++-- .../SshConnectionDisruptor.cs | 8 +- .../SshConnectionRestorer.cs | 4 +- .../Renci.SshNet.IntegrationTests/SshTests.cs | 18 ++-- .../SshCommandTest_EndExecute_ChannelOpen.cs | 4 +- 11 files changed, 162 insertions(+), 93 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 1dadc676a..fb547f435 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -12,6 +13,9 @@ using Renci.SshNet.Messages.Connection; using Renci.SshNet.Messages.Transport; +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests.OldIntegrationTests")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationBenchmarks")] namespace Renci.SshNet { /// @@ -111,24 +115,26 @@ public Stream CreateInputStream() #pragma warning disable S1133 // Deprecated code should be removed public string Result { - get - { - _result ??= new StringBuilder(); + get => GetResult(); + } + + internal string GetResult() + { + _result ??= new StringBuilder(); - if (OutputStream != null && OutputStream.Length > 0) + if (OutputStream != null && OutputStream.Length > 0) + { + using (var sr = new StreamReader(OutputStream, + _encoding, + detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, + leaveOpen: true)) { - using (var sr = new StreamReader(OutputStream, - _encoding, - detectEncodingFromByteOrderMarks: true, - bufferSize: 1024, - leaveOpen: true)) - { - _ = _result.Append(sr.ReadToEnd()); - } + _ = _result.Append(sr.ReadToEnd()); } - - return _result.ToString(); } + + return _result.ToString(); } /// @@ -139,29 +145,31 @@ public string Result #pragma warning disable S1133 // Deprecated code should be removed public string Error { - get + get => GetError(); + } + + internal string GetError() + { + if (_hasError) { - if (_hasError) - { - _error ??= new StringBuilder(); + _error ??= new StringBuilder(); - if (ExtendedOutputStream != null && ExtendedOutputStream.Length > 0) + if (ExtendedOutputStream != null && ExtendedOutputStream.Length > 0) + { + using (var sr = new StreamReader(ExtendedOutputStream, + _encoding, + detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, + leaveOpen: true)) { - using (var sr = new StreamReader(ExtendedOutputStream, - _encoding, - detectEncodingFromByteOrderMarks: true, - bufferSize: 1024, - leaveOpen: true)) - { - _ = _error.Append(sr.ReadToEnd()); - } + _ = _error.Append(sr.ReadToEnd()); } - - return _error.ToString(); } - return string.Empty; + return _error.ToString(); } + + return string.Empty; } /// @@ -403,6 +411,12 @@ public int EndExecuteWithStatus(IAsyncResult asyncResult) } } + /// + /// Executes the the command asynchronously. + /// + /// Exit status of the operation. + public Task ExecuteAsync() => ExecuteAsync(default); + /// /// Executes the the command asynchronously. /// diff --git a/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs b/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs index 68c8cf034..ddd4a8139 100644 --- a/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs +++ b/test/Renci.SshNet.IntegrationBenchmarks/SshClientBenchmark.cs @@ -55,7 +55,7 @@ public string ConnectAndRunCommand() { using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); sshClient.Connect(); - return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result; + return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").GetResult(); } [Benchmark] @@ -63,13 +63,13 @@ public async Task ConnectAsyncAndRunCommand() { using var sshClient = new SshClient(_infrastructureFixture.SshServerHostName, _infrastructureFixture.SshServerPort, _infrastructureFixture.User.UserName, _infrastructureFixture.User.Password); await sshClient.ConnectAsync(CancellationToken.None).ConfigureAwait(false); - return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result; + return sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").GetResult(); } [Benchmark] public string RunCommand() { - return _sshClient!.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").Result; + return _sshClient!.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'").GetResult(); } [Benchmark] diff --git a/test/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/test/Renci.SshNet.IntegrationTests/ConnectivityTests.cs index 61ba9e424..0dd1040d6 100644 --- a/test/Renci.SshNet.IntegrationTests/ConnectivityTests.cs +++ b/test/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -76,7 +76,7 @@ public void Common_DisposeAfterLossOfNetworkConnectivity() hostNetworkConnectionDisabled = true; WaitForConnectionInterruption(client); } - + Assert.IsNotNull(errorOccurred); Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); @@ -309,7 +309,7 @@ public void Common_DetectSessionKilledOnServer() var command = $"sudo ps --no-headers -u {client.ConnectionInfo.Username} -f | grep \"{client.ConnectionInfo.Username}@notty\" | awk '{{print $2}}' | xargs sudo kill -9"; var sshCommand = adminClient.CreateCommand(command); var result = sshCommand.Execute(); - Assert.AreEqual(0, sshCommand.ExitStatus, sshCommand.Error); + Assert.AreEqual(0, sshCommand.ExitStatus, sshCommand.GetError()); } Assert.IsTrue(errorOccurredSignaled.WaitOne(200)); diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index aefe1d6d0..0c1ef77f0 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -20,7 +20,7 @@ public void Test_Run_SingleCommand() var testValue = Guid.NewGuid().ToString(); var command = client.RunCommand(string.Format("echo {0}", testValue)); - var result = command.Result; + var result = command.GetResult(); result = result.Substring(0, result.Length - 1); // Remove \n character returned by command client.Disconnect(); @@ -51,6 +51,61 @@ public void Test_Execute_SingleCommand() } } + [TestMethod] + public async Task Test_Execute_SingleCommandAsync() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand ExecuteAsync + client.Connect(); + + var testValue = Guid.NewGuid().ToString(); + var cmd = client.CreateCommand($"echo {testValue}"); + await cmd.ExecuteAsync(); + using var reader = new StreamReader(cmd.OutputStream); + var result = await reader.ReadToEndAsync(); + result = result.Substring(0, result.Length - 1); + client.Disconnect(); + #endregion + + Assert.IsTrue(result.Equals(testValue)); + } + } + + [TestMethod] + public async Task Test_Execute_SingleCommandAsync_WithCancelledToken() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand ExecuteAsync With Cancelled Token + using var cts = new CancellationTokenSource(); + await client.ConnectAsync(cts.Token); + var expectedCancelledResult = "canceled"; + + var command = $"echo {Guid.NewGuid().ToString()};/bin/sleep 5"; + var cmd = client.CreateCommand(command); + string result; + try + { + var cmdExecution = cmd.ExecuteAsync(cts.Token); + cts.CancelAfter(100); + await cmdExecution; + using var reader = new StreamReader(cmd.OutputStream); + result = await reader.ReadToEndAsync(); + result = result.Substring(0, result.Length - 1); + } + catch (OperationCanceledException) + { + result = expectedCancelledResult; + } + + client.Disconnect(); + #endregion + + Assert.IsTrue(result.Equals(expectedCancelledResult)); + } + } + [TestMethod] public void Test_Execute_OutputStream() { @@ -146,7 +201,7 @@ public void Test_Execute_InvalidCommand() var cmd = client.CreateCommand(";"); cmd.Execute(); - if (string.IsNullOrEmpty(cmd.Error)) + if (string.IsNullOrEmpty(cmd.GetError())) { Assert.Fail("Operation should fail"); } @@ -164,7 +219,7 @@ public void Test_Execute_InvalidCommand_Then_Execute_ValidCommand() client.Connect(); var cmd = client.CreateCommand(";"); cmd.Execute(); - if (string.IsNullOrEmpty(cmd.Error)) + if (string.IsNullOrEmpty(cmd.GetError())) { Assert.Fail("Operation should fail"); } @@ -191,7 +246,7 @@ public void Test_Execute_Command_with_ExtendedOutput() var extendedData = new StreamReader(cmd.ExtendedOutputStream, Encoding.ASCII).ReadToEnd(); client.Disconnect(); - Assert.AreEqual("12345\n", cmd.Result); + Assert.AreEqual("12345\n", cmd.GetResult()); Assert.AreEqual("654321\n", extendedData); } } @@ -222,7 +277,7 @@ public void Test_Execute_Command_ExitStatus() client.Connect(); var cmd = client.RunCommand("exit 128"); - + Console.WriteLine(cmd.ExitStatus); client.Disconnect(); @@ -248,7 +303,7 @@ public void Test_Execute_Command_Asynchronously() cmd.EndExecute(asyncResult); - Assert.IsTrue(cmd.Result == "test\n"); + Assert.IsTrue(cmd.GetError() == "test\n"); client.Disconnect(); } @@ -270,7 +325,7 @@ public void Test_Execute_Command_Asynchronously_With_Error() cmd.EndExecute(asyncResult); - Assert.IsFalse(string.IsNullOrEmpty(cmd.Error)); + Assert.IsFalse(string.IsNullOrEmpty(cmd.GetError())); client.Disconnect(); } @@ -338,9 +393,9 @@ public void Test_Execute_Command_Same_Object_Different_Commands() client.Connect(); var cmd = client.CreateCommand("echo 12345"); cmd.Execute(); - Assert.AreEqual("12345\n", cmd.Result); + Assert.AreEqual("12345\n", cmd.GetResult()); cmd.Execute("echo 23456"); - Assert.AreEqual("23456\n", cmd.Result); + Assert.AreEqual("23456\n", cmd.GetResult()); client.Disconnect(); } } @@ -353,7 +408,7 @@ public void Test_Get_Result_Without_Execution() client.Connect(); var cmd = client.CreateCommand("ls -l"); - Assert.IsTrue(string.IsNullOrEmpty(cmd.Result)); + Assert.IsTrue(string.IsNullOrEmpty(cmd.GetResult())); client.Disconnect(); } } @@ -366,7 +421,7 @@ public void Test_Get_Error_Without_Execution() client.Connect(); var cmd = client.CreateCommand("ls -l"); - Assert.IsTrue(string.IsNullOrEmpty(cmd.Error)); + Assert.IsTrue(string.IsNullOrEmpty(cmd.GetError())); client.Disconnect(); } } @@ -429,9 +484,9 @@ public void Test_Execute_Invalid_Command() var cmd = client.CreateCommand(";"); cmd.Execute(); - if (!string.IsNullOrEmpty(cmd.Error)) + if (!string.IsNullOrEmpty(cmd.GetError())) { - Console.WriteLine(cmd.Error); + Console.WriteLine(cmd.GetError()); } client.Disconnect(); @@ -443,7 +498,7 @@ public void Test_Execute_Invalid_Command() } [TestMethod] - + public void Test_MultipleThread_100_MultipleConnections() { try diff --git a/test/Renci.SshNet.IntegrationTests/RemoteSshd.cs b/test/Renci.SshNet.IntegrationTests/RemoteSshd.cs index 476671f1a..8f9fb1c61 100644 --- a/test/Renci.SshNet.IntegrationTests/RemoteSshd.cs +++ b/test/Renci.SshNet.IntegrationTests/RemoteSshd.cs @@ -26,14 +26,14 @@ public RemoteSshd Restart() var stopOutput = stopCommand.Execute(); if (stopCommand.ExitStatus != 0) { - throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}\r\n{stopCommand.Error}"); + throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}\r\n{stopCommand.GetError()}"); } var resetFailedCommand = client.CreateCommand("sudo /usr/sbin/sshd.pam"); var resetFailedOutput = resetFailedCommand.Execute(); if (resetFailedCommand.ExitStatus != 0) { - throw new ApplicationException($"Reset failures for ssh service failed with exit code {resetFailedCommand.ExitStatus}.\r\n{resetFailedOutput}\r\n{resetFailedCommand.Error}"); + throw new ApplicationException($"Reset failures for ssh service failed with exit code {resetFailedCommand.ExitStatus}.\r\n{resetFailedOutput}\r\n{resetFailedCommand.GetError()}"); } } diff --git a/test/Renci.SshNet.IntegrationTests/SftpTests.cs b/test/Renci.SshNet.IntegrationTests/SftpTests.cs index 87b59c7a8..f48c84d8a 100644 --- a/test/Renci.SshNet.IntegrationTests/SftpTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SftpTests.cs @@ -1389,7 +1389,7 @@ public void Sftp_CreateText_Encoding_FileDoesNotExist() finally { if (client.Exists(remoteFile)) - { + { client.DeleteFile(remoteFile); } } @@ -1464,7 +1464,7 @@ public void Sftp_ReadAllBytes_ExistingFile() finally { if (client.Exists(remoteFile)) - { + { client.DeleteFile(remoteFile); } } @@ -3646,32 +3646,32 @@ public void Sftp_Exists() using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}") ) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } #endregion Clean-up @@ -3681,25 +3681,25 @@ public void Sftp_Exists() using (var command = client.CreateCommand($"touch {remoteHome + "/file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"mkdir {remoteHome + "/directory.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"ln -s {remoteHome + "/file.exists"} {remoteHome + "/symlink.to.file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"ln -s {remoteHome + "/directory.exists"} {remoteHome + "/symlink.to.directory.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } #endregion Setup @@ -3731,31 +3731,31 @@ public void Sftp_Exists() using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}")) { command.Execute(); - Assert.AreEqual(0, command.ExitStatus, command.Error); + Assert.AreEqual(0, command.ExitStatus, command.GetError()); } } @@ -6186,7 +6186,7 @@ public void Sftp_SetLastAccessTimeUtc() } finally { - client.DeleteFile(testFilePath); + client.DeleteFile(testFilePath); } } @@ -6230,7 +6230,7 @@ public void Sftp_SetLastWriteTimeUtc() client.Connect(); using var fileStream = new MemoryStream(Encoding.UTF8.GetBytes(testContent)); - + client.UploadFile(fileStream, testFilePath); try { diff --git a/test/Renci.SshNet.IntegrationTests/SshClientTests.cs b/test/Renci.SshNet.IntegrationTests/SshClientTests.cs index 2cfeb53c3..5052a7837 100644 --- a/test/Renci.SshNet.IntegrationTests/SshClientTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SshClientTests.cs @@ -19,7 +19,7 @@ public void Echo_Command_with_all_characters() { var response = _sshClient.RunCommand("echo $'test !@#$%^&*()_+{}:,./<>[];\\|'"); - Assert.AreEqual("test !@#$%^&*()_+{}:,./<>[];\\|\n", response.Result); + Assert.AreEqual("test !@#$%^&*()_+{}:,./<>[];\\|\n", response.GetResult()); } [TestMethod] @@ -39,8 +39,8 @@ public void Send_InputStream_to_Command() command.EndExecute(asyncResult); - Assert.AreEqual("Hello world!", command.Result); - Assert.AreEqual(string.Empty, command.Error); + Assert.AreEqual("Hello world!", command.GetResult()); + Assert.AreEqual(string.Empty, command.GetError()); } } @@ -64,8 +64,8 @@ public void Send_InputStream_to_Command_OneByteAtATime() command.EndExecute(asyncResult); - Assert.AreEqual("Hello world!", command.Result); - Assert.AreEqual(string.Empty, command.Error); + Assert.AreEqual("Hello world!", command.GetResult()); + Assert.AreEqual(string.Empty, command.GetError()); } } @@ -87,8 +87,8 @@ public void Send_InputStream_to_Command_InputStreamNotInUsingBlock_StillWorks() command.EndExecute(asyncResult); - Assert.AreEqual("Hello world!", command.Result); - Assert.AreEqual(string.Empty, command.Error); + Assert.AreEqual("Hello world!", command.GetResult()); + Assert.AreEqual(string.Empty, command.GetError()); } } diff --git a/test/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs b/test/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs index 741134114..acbc4bf35 100644 --- a/test/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs +++ b/test/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs @@ -12,11 +12,11 @@ public SshConnectionDisruptor(IConnectionInfoFactory connectionInfoFactory) public SshConnectionRestorer BreakConnections() { var client = new SshClient(_connectionInfoFactory.Create()); - + client.Connect(); PauseSshd(client); - + return new SshConnectionRestorer(client); } @@ -27,14 +27,14 @@ private static void PauseSshd(SshClient client) if (command.ExitStatus != 0) { throw new ApplicationException( - $"Blocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Blocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.GetError()}"); } command = client.CreateCommand("sudo pkill -9 -U sshnet -f sshd.pam"); output = command.Execute(); if (command.ExitStatus != 0) { throw new ApplicationException( - $"Killing sshd.pam service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Killing sshd.pam service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.GetError()}"); } } } diff --git a/test/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs b/test/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs index d7c6437db..8f6af187e 100644 --- a/test/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs +++ b/test/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs @@ -16,14 +16,14 @@ public void RestoreConnections() if (command.ExitStatus != 0) { throw new ApplicationException( - $"Unblocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Unblocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.GetError()}"); } command = _sshClient.CreateCommand("sudo /usr/sbin/sshd.pam"); output = command.Execute(); if (command.ExitStatus != 0) { throw new ApplicationException( - $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.GetError()}"); } } diff --git a/test/Renci.SshNet.IntegrationTests/SshTests.cs b/test/Renci.SshNet.IntegrationTests/SshTests.cs index fe05e3482..4ddec5da6 100644 --- a/test/Renci.SshNet.IntegrationTests/SshTests.cs +++ b/test/Renci.SshNet.IntegrationTests/SshTests.cs @@ -208,7 +208,7 @@ public void Ssh_Command_IntermittendOutput_EndExecute() { cmd.Execute(); - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } using (var command = sshClient.CreateCommand(remoteFile)) @@ -217,7 +217,7 @@ public void Ssh_Command_IntermittendOutput_EndExecute() var actualResult = command.EndExecute(asyncResult); Assert.AreEqual(expectedResult, actualResult); - Assert.AreEqual(expectedResult, command.Result); + Assert.AreEqual(expectedResult, command.GetResult()); Assert.AreEqual(13, command.ExitStatus); } } @@ -232,7 +232,7 @@ public void Ssh_Command_IntermittendOutput_EndExecute() /// Ignored for now, because: /// * OutputStream.Read(...) does not block when no data is available /// * SshCommand.(Begin)Execute consumes *OutputStream*, advancing its position. - /// + /// /// https://github.com/sshnet/SSH.NET/issues/650 /// [TestMethod] @@ -273,7 +273,7 @@ public void Ssh_Command_IntermittendOutput_OutputStream() { cmd.Execute(); - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } using (var command = sshClient.CreateCommand(remoteFile)) @@ -297,7 +297,7 @@ public void Ssh_Command_IntermittendOutput_OutputStream() var actualResult = command.EndExecute(asyncResult); Assert.AreEqual(expectedResult, actualResult); - Assert.AreEqual(expectedResult, command.Result); + Assert.AreEqual(expectedResult, command.GetResult()); } } finally @@ -707,7 +707,7 @@ public void Ssh_ExecuteShellScript() { var runChmod = client.RunCommand("chmod u+x " + remoteFile); runChmod.Execute(); - Assert.AreEqual(0, runChmod.ExitStatus, runChmod.Error); + Assert.AreEqual(0, runChmod.ExitStatus, runChmod.GetError()); var runLs = client.RunCommand("ls " + remoteFile); var asyncResultLs = runLs.BeginExecute(); @@ -788,7 +788,7 @@ private static bool AddOrUpdateHostsEntry(IConnectionInfoFactory linuxAdminConne continue; } - // If hostname is currently mapped to another IP address, then remove the + // If hostname is currently mapped to another IP address, then remove the // current mapping hostConfig.Entries.RemoveAt(i); } @@ -940,7 +940,7 @@ private static void CreateShellScript(IConnectionInfoFactory connectionInfoFacto using (var sftpClient = new SftpClient(connectionInfoFactory.Create())) { sftpClient.Connect(); - + using (var sw = sftpClient.CreateText(remoteFile, new UTF8Encoding(false))) { sw.Write(script); @@ -955,7 +955,7 @@ private static void RemoveFileOrDirectory(SshClient client, string remoteFile) using (var cmd = client.CreateCommand("rm -Rf " + remoteFile)) { cmd.Execute(); - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetResult()); } } } diff --git a/test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs b/test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs index 0001a9184..29a904b70 100644 --- a/test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs +++ b/test/Renci.SshNet.Tests/Classes/SshCommandTest_EndExecute_ChannelOpen.cs @@ -109,7 +109,7 @@ public void EndExecuteShouldThrowArgumentExceptionWhenInvokedAgainWithSameAsyncR [TestMethod] public void ErrorShouldReturnZeroLengthString() { - Assert.AreEqual(string.Empty, _sshCommand.Error); + Assert.AreEqual(string.Empty, _sshCommand.GetError()); } [TestMethod] @@ -144,7 +144,7 @@ public void OutputStreamShouldBeEmpty() [TestMethod] public void ResultShouldReturnAllDataReceived() { - Assert.AreEqual(string.Concat(_dataA, _dataB), _sshCommand.Result); + Assert.AreEqual(string.Concat(_dataA, _dataB), _sshCommand.GetResult()); } } } From d297a49482a78a104307a4be961688bca6f9f69b Mon Sep 17 00:00:00 2001 From: zeotuan Date: Wed, 28 Feb 2024 21:04:20 +1100 Subject: [PATCH 03/10] Early Command Cancellation --- src/Renci.SshNet/SshCommand.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index fb547f435..81ad161b5 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -31,6 +31,7 @@ public class SshCommand : IDisposable private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; + private EventWaitHandle _commandCancelledWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; @@ -201,6 +202,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false); + _commandCancelledWaitHandle = new AutoResetEvent(initialState: false); _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; @@ -448,6 +450,8 @@ public void CancelAsync() _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); if (_channel is not null && _channel.IsOpen && _asyncResult is not null) { + _ = _commandCancelledWaitHandle.Set(); + // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? _channel.Dispose(); } @@ -593,6 +597,7 @@ private void WaitOnHandle(WaitHandle waitHandle) { var waitHandles = new[] { + _commandCancelledWaitHandle, _sessionErrorOccuredWaitHandle, waitHandle }; @@ -708,6 +713,9 @@ protected virtual void Dispose(bool disposing) _sessionErrorOccuredWaitHandle = null; } + _commandCancelledWaitHandle?.Dispose(); + _commandCancelledWaitHandle = null; + _isDisposed = true; } } From 2bd06ef4c16645c8f8b3296d65baef6fad288b54 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Wed, 28 Feb 2024 21:04:20 +1100 Subject: [PATCH 04/10] Early Command Cancellation --- src/Renci.SshNet/SshCommand.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index fb547f435..81ad161b5 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -31,6 +31,7 @@ public class SshCommand : IDisposable private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; + private EventWaitHandle _commandCancelledWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; @@ -201,6 +202,7 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false); + _commandCancelledWaitHandle = new AutoResetEvent(initialState: false); _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; @@ -448,6 +450,8 @@ public void CancelAsync() _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); if (_channel is not null && _channel.IsOpen && _asyncResult is not null) { + _ = _commandCancelledWaitHandle.Set(); + // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? _channel.Dispose(); } @@ -593,6 +597,7 @@ private void WaitOnHandle(WaitHandle waitHandle) { var waitHandles = new[] { + _commandCancelledWaitHandle, _sessionErrorOccuredWaitHandle, waitHandle }; @@ -708,6 +713,9 @@ protected virtual void Dispose(bool disposing) _sessionErrorOccuredWaitHandle = null; } + _commandCancelledWaitHandle?.Dispose(); + _commandCancelledWaitHandle = null; + _isDisposed = true; } } From a0804abc131bf7a99ccc9a182cd95f9350e5e8a3 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Thu, 29 Feb 2024 20:52:32 +1100 Subject: [PATCH 05/10] Add Attribute to assemblyInfo --- src/Renci.SshNet/Properties/AssemblyInfo.cs | 2 ++ src/Renci.SshNet/SshCommand.cs | 3 --- test/Renci.SshNet.IntegrationTests/AuthenticationTests.cs | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Renci.SshNet/Properties/AssemblyInfo.cs b/src/Renci.SshNet/Properties/AssemblyInfo.cs index 8c1693ede..5983ab7d8 100644 --- a/src/Renci.SshNet/Properties/AssemblyInfo.cs +++ b/src/Renci.SshNet/Properties/AssemblyInfo.cs @@ -8,3 +8,5 @@ [assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] [assembly: InternalsVisibleTo("Renci.SshNet.Benchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationBenchmarks")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests.OldIntegrationTests")] diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 81ad161b5..416dfd4ab 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -13,9 +13,6 @@ using Renci.SshNet.Messages.Connection; using Renci.SshNet.Messages.Transport; -[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests.OldIntegrationTests")] -[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests")] -[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationBenchmarks")] namespace Renci.SshNet { /// diff --git a/test/Renci.SshNet.IntegrationTests/AuthenticationTests.cs b/test/Renci.SshNet.IntegrationTests/AuthenticationTests.cs index dd0a4f3d9..eb0cd4941 100644 --- a/test/Renci.SshNet.IntegrationTests/AuthenticationTests.cs +++ b/test/Renci.SshNet.IntegrationTests/AuthenticationTests.cs @@ -32,13 +32,13 @@ public void TearDown() // Reset the password back to the "regular" password. using (var cmd = client.RunCommand($"echo \"{Users.Regular.Password}\n{Users.Regular.Password}\" | sudo passwd " + Users.Regular.UserName)) { - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } // Remove password expiration using (var cmd = client.RunCommand($"sudo chage --expiredate -1 " + Users.Regular.UserName)) { - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } } } @@ -324,13 +324,13 @@ public void KeyboardInteractive_PasswordExpired() // the "regular" password. using (var cmd = client.RunCommand($"echo \"{temporaryPassword}\n{temporaryPassword}\" | sudo passwd " + Users.Regular.UserName)) { - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } // Force the password to expire immediately using (var cmd = client.RunCommand($"sudo chage -d 0 " + Users.Regular.UserName)) { - Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + Assert.AreEqual(0, cmd.ExitStatus, cmd.GetError()); } } From a53fdc5b7d6d19b5aacf1f3dccfdd1d8617c25ca Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sat, 2 Mar 2024 11:33:15 +1100 Subject: [PATCH 06/10] Add publicKey, Using wait using for new .net ver --- src/Renci.SshNet/Properties/AssemblyInfo.cs | 3 +-- src/Renci.SshNet/SshCommand.cs | 26 +++++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Renci.SshNet/Properties/AssemblyInfo.cs b/src/Renci.SshNet/Properties/AssemblyInfo.cs index 5983ab7d8..c06501cc9 100644 --- a/src/Renci.SshNet/Properties/AssemblyInfo.cs +++ b/src/Renci.SshNet/Properties/AssemblyInfo.cs @@ -8,5 +8,4 @@ [assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] [assembly: InternalsVisibleTo("Renci.SshNet.Benchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] -[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationBenchmarks")] -[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests.OldIntegrationTests")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 416dfd4ab..5db7e024e 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -113,7 +112,10 @@ public Stream CreateInputStream() #pragma warning disable S1133 // Deprecated code should be removed public string Result { - get => GetResult(); + get + { + return GetResult(); + } } internal string GetResult() @@ -143,7 +145,10 @@ internal string GetResult() #pragma warning disable S1133 // Deprecated code should be removed public string Error { - get => GetError(); + get + { + return GetError(); + } } internal string GetError() @@ -414,7 +419,10 @@ public int EndExecuteWithStatus(IAsyncResult asyncResult) /// Executes the the command asynchronously. /// /// Exit status of the operation. - public Task ExecuteAsync() => ExecuteAsync(default); + public Task ExecuteAsync() + { + return ExecuteAsync(default); + } /// /// Executes the the command asynchronously. @@ -424,11 +432,15 @@ public int EndExecuteWithStatus(IAsyncResult asyncResult) public async Task ExecuteAsync(CancellationToken cancellationToken) { var asyncResult = BeginExecute(); - using var ctr = cancellationToken.Register(() => CancelAsync()); +#if NET || NETSTANDARD2_1_OR_GREATER + await using var ctr = cancellationToken.Register(CancelAsync, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false); +#else + using var ctr = cancellationToken.Register(CancelAsync, useSynchronizationContext: false); +#endif // NET || NETSTANDARD2_1_OR_GREATER try { - int status = await Task.Factory.FromAsync(asyncResult, EndExecuteWithStatus).ConfigureAwait(false); + var status = await Task.Factory.FromAsync(asyncResult, EndExecuteWithStatus).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return status; @@ -444,7 +456,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) /// public void CancelAsync() { - _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); + _ = _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); if (_channel is not null && _channel.IsOpen && _asyncResult is not null) { _ = _commandCancelledWaitHandle.Set(); From c65125c1a36f9dc7776d9ed22825191175498025 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sat, 2 Mar 2024 12:25:34 +1100 Subject: [PATCH 07/10] Add cancellationToken --- .../OldIntegrationTests/SshCommandTest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index 0c1ef77f0..9f8c01b52 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -91,7 +91,12 @@ public async Task Test_Execute_SingleCommandAsync_WithCancelledToken() cts.CancelAfter(100); await cmdExecution; using var reader = new StreamReader(cmd.OutputStream); - result = await reader.ReadToEndAsync(); + using var readCts = new CancellationTokenSource(); +#if NET7_0_OR_GREATER + result = await reader.ReadToEndAsync(readCts.Token).ConfigureAwait(false); +#else + result = await reader.ReadToEndAsync().ConfigureAwait(false); +#endif // NET7_0_OR_GREATER result = result.Substring(0, result.Length - 1); } catch (OperationCanceledException) From 1e7fa18fc9a6bcf83148920b511f392ab2e1eaac Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sat, 2 Mar 2024 14:15:27 +1100 Subject: [PATCH 08/10] Throw Exception When cancelled --- src/Renci.SshNet/SshCommand.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index 5db7e024e..d4ccf76a1 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -615,9 +615,11 @@ private void WaitOnHandle(WaitHandle waitHandle) switch (signaledElement) { case 0: + throw new OperationCanceledException($"Command {CommandText} has been cancelled."); + case 1: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 1: + case 2: // Specified waithandle was signaled break; case WaitHandle.WaitTimeout: From 73b2a798981bed7cec04e6f6132944f5c3ea93af Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sat, 2 Mar 2024 20:56:10 +1100 Subject: [PATCH 09/10] Simplify Cancel Test --- src/Renci.SshNet/SshCommand.cs | 15 ++------------- .../OldIntegrationTests/SshCommandTest.cs | 15 +++------------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index d4ccf76a1..f7adfadab 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -27,7 +27,6 @@ public class SshCommand : IDisposable private CommandAsyncResult _asyncResult; private AsyncCallback _callback; private EventWaitHandle _sessionErrorOccuredWaitHandle; - private EventWaitHandle _commandCancelledWaitHandle; private Exception _exception; private StringBuilder _result; private StringBuilder _error; @@ -204,7 +203,6 @@ internal SshCommand(ISession session, string commandText, Encoding encoding) _encoding = encoding; CommandTimeout = Session.InfiniteTimeSpan; _sessionErrorOccuredWaitHandle = new AutoResetEvent(initialState: false); - _commandCancelledWaitHandle = new AutoResetEvent(initialState: false); _session.Disconnected += Session_Disconnected; _session.ErrorOccured += Session_ErrorOccured; @@ -431,7 +429,6 @@ public Task ExecuteAsync() /// Exit status of the operation. public async Task ExecuteAsync(CancellationToken cancellationToken) { - var asyncResult = BeginExecute(); #if NET || NETSTANDARD2_1_OR_GREATER await using var ctr = cancellationToken.Register(CancelAsync, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false); #else @@ -440,7 +437,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) try { - var status = await Task.Factory.FromAsync(asyncResult, EndExecuteWithStatus).ConfigureAwait(false); + var status = await Task.Factory.FromAsync(BeginExecute(), EndExecuteWithStatus).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); return status; @@ -459,8 +456,6 @@ public void CancelAsync() _ = _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); if (_channel is not null && _channel.IsOpen && _asyncResult is not null) { - _ = _commandCancelledWaitHandle.Set(); - // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? _channel.Dispose(); } @@ -606,7 +601,6 @@ private void WaitOnHandle(WaitHandle waitHandle) { var waitHandles = new[] { - _commandCancelledWaitHandle, _sessionErrorOccuredWaitHandle, waitHandle }; @@ -615,11 +609,9 @@ private void WaitOnHandle(WaitHandle waitHandle) switch (signaledElement) { case 0: - throw new OperationCanceledException($"Command {CommandText} has been cancelled."); - case 1: ExceptionDispatchInfo.Capture(_exception).Throw(); break; - case 2: + case 1: // Specified waithandle was signaled break; case WaitHandle.WaitTimeout: @@ -724,9 +716,6 @@ protected virtual void Dispose(bool disposing) _sessionErrorOccuredWaitHandle = null; } - _commandCancelledWaitHandle?.Dispose(); - _commandCancelledWaitHandle = null; - _isDisposed = true; } } diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index 9f8c01b52..e4e93c0e8 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -84,20 +84,11 @@ public async Task Test_Execute_SingleCommandAsync_WithCancelledToken() var command = $"echo {Guid.NewGuid().ToString()};/bin/sleep 5"; var cmd = client.CreateCommand(command); - string result; + var result = string.Empty; try { - var cmdExecution = cmd.ExecuteAsync(cts.Token); - cts.CancelAfter(100); - await cmdExecution; - using var reader = new StreamReader(cmd.OutputStream); - using var readCts = new CancellationTokenSource(); -#if NET7_0_OR_GREATER - result = await reader.ReadToEndAsync(readCts.Token).ConfigureAwait(false); -#else - result = await reader.ReadToEndAsync().ConfigureAwait(false); -#endif // NET7_0_OR_GREATER - result = result.Substring(0, result.Length - 1); + cts.CancelAfter(500); + await cmd.ExecuteAsync(cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { From fe15fe660aa30aecb67b5610afee3ca73f282a23 Mon Sep 17 00:00:00 2001 From: zeotuan Date: Sun, 3 Mar 2024 22:39:47 +1100 Subject: [PATCH 10/10] Fix Deadlock with cancel Async --- src/Renci.SshNet/SshCommand.cs | 5 ----- .../OldIntegrationTests/SshCommandTest.cs | 18 +++--------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/Renci.SshNet/SshCommand.cs b/src/Renci.SshNet/SshCommand.cs index f7adfadab..f30e9f60f 100644 --- a/src/Renci.SshNet/SshCommand.cs +++ b/src/Renci.SshNet/SshCommand.cs @@ -454,11 +454,6 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) public void CancelAsync() { _ = _channel?.SendExitSignalRequest("TERM", coreDumped: false, "Command execution has been cancelled.", "en"); - if (_channel is not null && _channel.IsOpen && _asyncResult is not null) - { - // TODO: check with Oleg if we shouldn't dispose the channel and uninitialize it ? - _channel.Dispose(); - } } /// diff --git a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs index e4e93c0e8..b30570264 100644 --- a/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs +++ b/test/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -73,6 +73,7 @@ public async Task Test_Execute_SingleCommandAsync() } [TestMethod] + [ExpectedException(typeof(OperationCanceledException))] public async Task Test_Execute_SingleCommandAsync_WithCancelledToken() { using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) @@ -80,25 +81,12 @@ public async Task Test_Execute_SingleCommandAsync_WithCancelledToken() #region Example SshCommand CreateCommand ExecuteAsync With Cancelled Token using var cts = new CancellationTokenSource(); await client.ConnectAsync(cts.Token); - var expectedCancelledResult = "canceled"; - var command = $"echo {Guid.NewGuid().ToString()};/bin/sleep 5"; var cmd = client.CreateCommand(command); - var result = string.Empty; - try - { - cts.CancelAfter(500); - await cmd.ExecuteAsync(cts.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - result = expectedCancelledResult; - } - + cts.CancelAfter(500); + await cmd.ExecuteAsync(cts.Token).ConfigureAwait(false); client.Disconnect(); #endregion - - Assert.IsTrue(result.Equals(expectedCancelledResult)); } }