Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions OpenTap.Plugins.Ssh/Background/Pid.cs
Original file line number Diff line number Diff line change
@@ -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();
}

}
}
47 changes: 47 additions & 0 deletions OpenTap.Plugins.Ssh/Background/SshBackgroundCommandStep.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
60 changes: 60 additions & 0 deletions OpenTap.Plugins.Ssh/Background/SshCheckBackgroundCommandStep.cs
Original file line number Diff line number Diff line change
@@ -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<Pid> InputPid { get; set; }

#endregion

public CheckBackgroundSshCommandStep()
{
Name = "Check Background SSH Command: PID={Process PID}";
InputPid = new Input<Pid>();
}

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);
}
}
}
57 changes: 57 additions & 0 deletions OpenTap.Plugins.Ssh/Background/SshKillBackgroundCommandStep.cs
Original file line number Diff line number Diff line change
@@ -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<Pid> InputPid { get; set; }

#endregion

public KillBackgroundSshCommandStep()
{
Name = "Kill Background SSH Command: PID={Process PID}";
InputPid = new Input<Pid>();
}

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);
}
}
}
121 changes: 121 additions & 0 deletions OpenTap.Plugins.Ssh/SshResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -32,19 +35,23 @@ 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<Pid> BackgroundProcesses { get; set; }
#endregion

public SshResource()
{
Name = "Ssh";
Connection = new SshConnectionInfo() { Owner = this };
BackgroundProcesses = new List<Pid>();
}
protected SshResource(bool session, ITestStep step)
{
Name = "Ssh";
Connection = new SshConnectionInfo() { Owner = this };
_step = step;
IsSession = session;
BackgroundProcesses = new List<Pid>();
}

/// <summary>
Expand Down Expand Up @@ -106,6 +113,12 @@ public override void Open()
/// </summary>
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();
Expand All @@ -129,5 +142,113 @@ public override string ToString()
if (IsSession) return _step.GetFormattedName();
return base.ToString() + $"({Connection.Username}@{Connection.Host}:{Connection.Port})";
}

// <summary>
// 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
// </summary>
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;
}

// <summary>
// 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
// </summary>
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;
}

// <summary>
// 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
// </summary>
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;
}
Comment on lines +209 to +214

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exit status doesn't check if the background process exited. It checks if the kill command successfully sent the SIGTERM signal to the process. If the process ignores the signal, or doesn't handle it (because it's hanging for example) then kill will still return 0 without exiting the process


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;
}

// <summary>
// 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
// <summary>
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;
}
}
}