diff --git a/OpenTap.Plugins.Ssh/Background/Pid.cs b/OpenTap.Plugins.Ssh/Background/Pid.cs new file mode 100644 index 0000000..2cdd42b --- /dev/null +++ b/OpenTap.Plugins.Ssh/Background/Pid.cs @@ -0,0 +1,47 @@ +//Copyright 2019-2020 Keysight Technologies +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel; +using Renci.SshNet; + +namespace OpenTap.Plugins.Ssh +{ + + public class Pid + { + #region Settings + public uint pid { get; private set; } + + #endregion + + public Pid(uint pid) + { + this.pid = pid; + } + + public Pid(string pid) + { + this.pid = uint.Parse(pid); + } + + public override string ToString() + { + return pid.ToString(); + } + + } +} diff --git a/OpenTap.Plugins.Ssh/Background/SshBackgroundCommandStep.cs b/OpenTap.Plugins.Ssh/Background/SshBackgroundCommandStep.cs new file mode 100644 index 0000000..265cbe7 --- /dev/null +++ b/OpenTap.Plugins.Ssh/Background/SshBackgroundCommandStep.cs @@ -0,0 +1,47 @@ +//Copyright 2019-2020 Keysight Technologies +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel; +using Renci.SshNet; + +namespace OpenTap.Plugins.Ssh.Background +{ + + [Display("Background SSH Command", "Run a command in the background using a session setup by an SSH Session step, SSH Instrument or SSH Dut.", Groups: new[] { "SSH", "Background (Linux only)" })] + public class BackgroundSshCommandStep : SshStepBase + { + #region Settings + public string Command { get; set; } + + [Output] + [Display("Process PID", Description:"The PID of the background process", Group: "Response")] + public Pid pid { get; private set; } + + #endregion + + public BackgroundSshCommandStep() + { + Name = "Background SSH Command: {Command}"; + } + + public override void Run() + { + pid = SshResource.StartBackgroundProcess(Command); + Log.Info($"Running command `{Command}` in the background"); + } + } +} diff --git a/OpenTap.Plugins.Ssh/Background/SshCheckBackgroundCommandStep.cs b/OpenTap.Plugins.Ssh/Background/SshCheckBackgroundCommandStep.cs new file mode 100644 index 0000000..26fb7a9 --- /dev/null +++ b/OpenTap.Plugins.Ssh/Background/SshCheckBackgroundCommandStep.cs @@ -0,0 +1,60 @@ +//Copyright 2019-2020 Keysight Technologies +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel; +using Renci.SshNet; + +namespace OpenTap.Plugins.Ssh.Background +{ + + [Display("Check Background SSH Command", "Checks if a background command is still running.", Groups: new[] { "SSH", "Background (Linux only)" })] + public class CheckBackgroundSshCommandStep : SshStepBase + { + #region Settings + [Display("Process PID", "Select the Background SSH Command to kill")] + + public Input InputPid { get; set; } + + #endregion + + public CheckBackgroundSshCommandStep() + { + Name = "Check Background SSH Command: PID={Process PID}"; + InputPid = new Input(); + } + + public override void Run() + { + if (InputPid == null || InputPid.Value == null) + { + throw new ArgumentNullException("No PID was set"); + } + Pid pid = InputPid.Value; + + bool isRunning = SshResource.IsBackgroundProcessRunning(pid); + if (isRunning) + { + Log.Info($"Process with PID={pid} is alive"); + UpgradeVerdict(Verdict.Pass); + return; + } + + Log.Info($"Process with PID={pid} is not alive"); + UpgradeVerdict(Verdict.Fail); + } + } +} diff --git a/OpenTap.Plugins.Ssh/Background/SshKillBackgroundCommandStep.cs b/OpenTap.Plugins.Ssh/Background/SshKillBackgroundCommandStep.cs new file mode 100644 index 0000000..6106e01 --- /dev/null +++ b/OpenTap.Plugins.Ssh/Background/SshKillBackgroundCommandStep.cs @@ -0,0 +1,57 @@ +//Copyright 2019-2020 Keysight Technologies +// +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.ComponentModel; +using Renci.SshNet; + +namespace OpenTap.Plugins.Ssh.Background +{ + + [Display("Kill Background SSH Command", "Kills a command running in the background.", Groups: new[] { "SSH", "Background (Linux only)" })] + public class KillBackgroundSshCommandStep : SshStepBase + { + #region Settings + [Display("Process PID", "Select the Background SSH Command to kill")] + + public Input InputPid { get; set; } + + #endregion + + public KillBackgroundSshCommandStep() + { + Name = "Kill Background SSH Command: PID={Process PID}"; + InputPid = new Input(); + } + + public override void Run() + { + if (InputPid == null) + { + throw new ArgumentNullException("No PID was set"); + } + Pid pid = InputPid.Value; + + if (SshResource.KillBackgroundProcess(pid)) + { + UpgradeVerdict(Verdict.Pass); + return; + } + + UpgradeVerdict(Verdict.Fail); + } + } +} diff --git a/OpenTap.Plugins.Ssh/SshResource.cs b/OpenTap.Plugins.Ssh/SshResource.cs index 3ebaa19..c8ff8de 100644 --- a/OpenTap.Plugins.Ssh/SshResource.cs +++ b/OpenTap.Plugins.Ssh/SshResource.cs @@ -13,7 +13,10 @@ //limitations under the License. using Renci.SshNet; +using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Security.Cryptography; namespace OpenTap.Plugins.Ssh { @@ -32,12 +35,15 @@ public abstract class SshResource : Resource public bool LazyConnectSsh { get; set; } = false; [Display("Lazy SCP connection", "Connect SCP client lazily (when it is needed by a Test Step) instead of at the beginning of the Test Plan run.", "Advanced", Order: 7)] public bool LazyConnectScp { get; set; } = true; + + public List BackgroundProcesses { get; set; } #endregion public SshResource() { Name = "Ssh"; Connection = new SshConnectionInfo() { Owner = this }; + BackgroundProcesses = new List(); } protected SshResource(bool session, ITestStep step) { @@ -45,6 +51,7 @@ protected SshResource(bool session, ITestStep step) Connection = new SshConnectionInfo() { Owner = this }; _step = step; IsSession = session; + BackgroundProcesses = new List(); } /// @@ -106,6 +113,12 @@ public override void Open() /// public override void Close() { + // Close all bakground processes before disconnecting ssh client + if (!_KillAllBackgroundProcesses()) + { + Log.Warning("Some background processes could not be killed correctly"); + } + IsOpened = false; if (sshClient != null) sshClient.Disconnect(); @@ -129,5 +142,113 @@ public override string ToString() if (IsSession) return _step.GetFormattedName(); return base.ToString() + $"({Connection.Username}@{Connection.Host}:{Connection.Port})"; } + + // + // Starts a process with the given command in the background + // The process PID is stored in BackgroundProcesses to keep track of all processes started by this SshResource + // NOTE: this only works in Linux systems + // + public Pid StartBackgroundProcess(string command) + { + // nohup allows the command to run even when the SSH session ends + // output is suppressed + // the PID of the last background process '$!' is printed to output + SshCommand sshCmd = SshClient.CreateCommand($"nohup {command} > /dev/null 2>&1 & echo $!"); + string cmdOut = sshCmd.Execute(); + + Log.Debug("Running command: " + sshCmd.CommandText); + + Pid pid = new Pid(cmdOut); + BackgroundProcesses.Add(pid); + return pid; + } + + // + // Checks if background process with the given pid is running + // This only works if the process was started within the same session of this SshResource + // NOTE: this only works in Linux systems + // + public bool IsBackgroundProcessRunning(Pid pid) + { + if (!BackgroundProcesses.Contains(pid)) + { + Log.Debug($"PID={pid} was not started in this session or it is not alive anymore"); + return false; + } + + SshCommand command = SshClient.CreateCommand($"ps -p {pid}"); + command.Execute(); + + if (command.ExitStatus == 0) + { + Log.Debug($"Process with PID={pid} is alive"); + return true; + } + + Log.Debug($"Process with PID={pid} is not alive. Removing it from the list of background processes"); + BackgroundProcesses.Remove(pid); + return false; + } + + // + // Kills a background process + // This only works if the process was started within the same session of this SshResource + // NOTE: this only works in Linux systems + // + public bool KillBackgroundProcess(Pid pid) + { + if (!IsBackgroundProcessRunning(pid)) + { + Log.Debug($"No process with PID={pid} to be killed"); + return false; + } + + SshCommand command = SshClient.CreateCommand($"kill {pid}"); + command.Execute(); + + if (command.ExitStatus == 0) + { + Log.Debug($"Gracefully killed PID={pid}"); + BackgroundProcesses.Remove(pid); + return true; + } + + command = SshClient.CreateCommand($"kill -9 {pid}"); + command.Execute(); + + if (command.ExitStatus == 0) + { + Log.Debug($"SIGKILLed PID={pid}"); + BackgroundProcesses.Remove(pid); + return true; + } + + Log.Error($"Failed killing PID={pid}"); + return false; + } + + // + // Iterates over all background processes, killing them if they are still running + // If all processes were killed correctly, it returns true, otherwise it returns false + // + private bool _KillAllBackgroundProcesses() + { + Log.Debug("Killing all background processes"); + + bool killedAll = true; + while (BackgroundProcesses.Count > 0) + { + Pid pid = BackgroundProcesses[0]; + bool killed = KillBackgroundProcess(pid); + + if (!killed) + { + killedAll = false; + Log.Debug($"Process with PID={pid} could not be killed correctly"); + } + } + + return killedAll; + } } }