From f7f06b693fb7c77e01a568f08f55cf352a154b9f Mon Sep 17 00:00:00 2001 From: Felix Salcher Date: Tue, 21 Apr 2026 10:26:09 +0200 Subject: [PATCH 1/3] add continous output --- OpenTap.Plugins.Ssh/SshCommandStep.cs | 52 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/OpenTap.Plugins.Ssh/SshCommandStep.cs b/OpenTap.Plugins.Ssh/SshCommandStep.cs index ac90d6c..cee9252 100644 --- a/OpenTap.Plugins.Ssh/SshCommandStep.cs +++ b/OpenTap.Plugins.Ssh/SshCommandStep.cs @@ -16,6 +16,9 @@ using System.Collections.Generic; using System.Linq; using System.ComponentModel; +using System.IO; +using System.Text; +using System.Threading.Tasks; using Renci.SshNet; namespace OpenTap.Plugins.Ssh @@ -36,7 +39,7 @@ public IEnumerable sshSessions .Concat(DutSettings.Current.OfType())); } } - + private SshResource BackingResource; [Display("Connection", "Use SSH session defined by this Instrument, DUT or Parent step.")] @@ -67,20 +70,26 @@ public class SshCommandStep : SshStepBase #region Settings public string Command { get; set; } = "pwd"; - [Display("Add To Log", Group: "Response", Collapsed: true, Order:0)] + [Display("Add To Log", Group: "Response", Collapsed: true, Order: 0)] public bool AddToLog { get; set; } = true; - [Display("Output Response", "Sets if the output of the ssh command should be saved as an output.", Group: "Response", Collapsed: true, Order:0)] + + [EnabledIf(nameof(AddToLog), HideIfDisabled = true)] + [Display("Continous Output", Description: "Whether the output should be logged all at once after the command has executed, or continuously as it is produced.", Group: "Response", Collapsed: true, Order: 0)] + public bool ContinousOutput { get; set; } = false; + + [Display("Output Response", "Sets if the output of the ssh command should be saved as an output.", Group: "Response", Collapsed: true, Order: 0)] public bool OutputResponse { get; set; } [Output] [Browsable(true)] [EnabledIf(nameof(OutputResponse), HideIfDisabled = true)] - [Display("Response", Description:"The standard output (stdout) of the executed program.", Group: "Response", Collapsed: true, Order:1)] + [Display("Response", Description: "The standard output (stdout) of the executed program.", Group: "Response", Collapsed: true, Order: 1)] public string Response { get; private set; } + [Output] [Browsable(true)] - [Display("Exit Code", Description:"The exit code of the command.", Group: "Response", Collapsed: true, Order:1)] + [Display("Exit Code", Description: "The exit code of the command.", Group: "Response", Collapsed: true, Order: 1)] public int ExitCode { get; private set; } [Display("Enabled", Group: "Timeout")] @@ -90,7 +99,7 @@ public class SshCommandStep : SshStepBase [EnabledIf(nameof(TimeoutEnabled), HideIfDisabled = true)] public double Timeout { get; set; } = 5.0; - [Display("Check Exit Code", Description: "Sets the test step verdict based on the exit code. Exit code 0 will cause the step to pass, all other exit codes will cause it to fail.", Group: "Response", Collapsed: true, Order:0)] + [Display("Check Exit Code", Description: "Sets the test step verdict based on the exit code. Exit code 0 will cause the step to pass, all other exit codes will cause it to fail.", Group: "Response", Collapsed: true, Order: 0)] public bool CheckExitCode { get; set; } #endregion @@ -104,14 +113,18 @@ public override void Run() SshCommand command = SshResource.SshClient.CreateCommand(Command); if (TimeoutEnabled) command.CommandTimeout = TimeSpan.FromSeconds(Timeout); - command.Execute(); + + if (ContinousOutput) + ExecuteAsync(command).GetAwaiter().GetResult(); + else + command.Execute(); ExitCode = command.ExitStatus; if (OutputResponse) { Response = command.Result.TrimEnd('\n'); } - if(command.ExitStatus == 0) + if (command.ExitStatus == 0) { if (AddToLog) { @@ -138,5 +151,28 @@ public override void Run() } } } + + public async Task ExecuteAsync(SshCommand command) + { + var result = command.BeginExecute(); + var reader = new StreamReader(command.OutputStream, Encoding.UTF8); + + char[] buffer = new char[1024]; + while (!result.IsCompleted || !reader.EndOfStream) + { + var read = await reader.ReadAsync(buffer, 0, buffer.Length); + if (read > 0) + { + var text = new string(buffer, 0, read); + Log.Info(text); + } + else + { + await Task.Delay(50); + } + } + + command.EndExecute(result); + } } } From 54d833efc9284e489052f42d302a9376746f5fca Mon Sep 17 00:00:00 2001 From: Felix Salcher Date: Thu, 23 Apr 2026 17:44:52 +0200 Subject: [PATCH 2/3] removed "Continous Output" setting, as there shouldn't be any downside of using it at all --- OpenTap.Plugins.Ssh/SshCommandStep.cs | 46 ++++++++++----------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/OpenTap.Plugins.Ssh/SshCommandStep.cs b/OpenTap.Plugins.Ssh/SshCommandStep.cs index cee9252..4eedfdc 100644 --- a/OpenTap.Plugins.Ssh/SshCommandStep.cs +++ b/OpenTap.Plugins.Ssh/SshCommandStep.cs @@ -73,10 +73,6 @@ public class SshCommandStep : SshStepBase [Display("Add To Log", Group: "Response", Collapsed: true, Order: 0)] public bool AddToLog { get; set; } = true; - [EnabledIf(nameof(AddToLog), HideIfDisabled = true)] - [Display("Continous Output", Description: "Whether the output should be logged all at once after the command has executed, or continuously as it is produced.", Group: "Response", Collapsed: true, Order: 0)] - public bool ContinousOutput { get; set; } = false; - [Display("Output Response", "Sets if the output of the ssh command should be saved as an output.", Group: "Response", Collapsed: true, Order: 0)] public bool OutputResponse { get; set; } @@ -114,31 +110,12 @@ public override void Run() if (TimeoutEnabled) command.CommandTimeout = TimeSpan.FromSeconds(Timeout); - if (ContinousOutput) + if (AddToLog) ExecuteAsync(command).GetAwaiter().GetResult(); else command.Execute(); ExitCode = command.ExitStatus; - if (OutputResponse) - { - Response = command.Result.TrimEnd('\n'); - } - if (command.ExitStatus == 0) - { - if (AddToLog) - { - foreach (var line in command.Result.Trim().Split('\n')) - { - Log.Info(line); - } - } - } - else - { - if (AddToLog) - Log.Warning(command.Error); - } if (CheckExitCode) { if (ExitCode == 0) @@ -152,27 +129,36 @@ public override void Run() } } - public async Task ExecuteAsync(SshCommand command) + private async Task ExecuteAsync(SshCommand command) { var result = command.BeginExecute(); - var reader = new StreamReader(command.OutputStream, Encoding.UTF8); + var outputReader = new StreamReader(command.OutputStream, Encoding.UTF8); + var errorReader = new StreamReader(command.ExtendedOutputStream, Encoding.UTF8); + + var outputTask = ReadStreamAsync(outputReader, line => Log.Info(line), result); + var errorTask = ReadStreamAsync(errorReader, line => Log.Warning(line), result); + + await Task.WhenAll(outputTask, errorTask); - char[] buffer = new char[1024]; + command.EndExecute(result); + } + + private async Task ReadStreamAsync(StreamReader reader, Action logAction, IAsyncResult result) + { + var buffer = new char[1024]; while (!result.IsCompleted || !reader.EndOfStream) { var read = await reader.ReadAsync(buffer, 0, buffer.Length); if (read > 0) { var text = new string(buffer, 0, read); - Log.Info(text); + logAction(text.TrimEnd('\n')); } else { await Task.Delay(50); } } - - command.EndExecute(result); } } } From 686cf6ff3f8a35c3d4b0b7a770818e1e4bd17d47 Mon Sep 17 00:00:00 2001 From: Felix Salcher Date: Fri, 24 Apr 2026 11:51:51 +0200 Subject: [PATCH 3/3] use ReadLineAsync to make sure chars are properly displayed --- OpenTap.Plugins.Ssh/SshCommandStep.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/OpenTap.Plugins.Ssh/SshCommandStep.cs b/OpenTap.Plugins.Ssh/SshCommandStep.cs index 4eedfdc..21b3ea4 100644 --- a/OpenTap.Plugins.Ssh/SshCommandStep.cs +++ b/OpenTap.Plugins.Ssh/SshCommandStep.cs @@ -146,18 +146,11 @@ private async Task ExecuteAsync(SshCommand command) private async Task ReadStreamAsync(StreamReader reader, Action logAction, IAsyncResult result) { var buffer = new char[1024]; - while (!result.IsCompleted || !reader.EndOfStream) + var line = await reader.ReadLineAsync(); + while (!result.IsCompleted || line != null) { - var read = await reader.ReadAsync(buffer, 0, buffer.Length); - if (read > 0) - { - var text = new string(buffer, 0, read); - logAction(text.TrimEnd('\n')); - } - else - { - await Task.Delay(50); - } + logAction(line); + line = await reader.ReadLineAsync(); } } }