前言

這兩個月的工作是不斷地對系統進行壓力/效能測試與調校,漸漸開始有自動化壓力測試的需求。由於我們採取方式 Remote Testing,每一次進行測試前需要啟動每一台 JMeter Sever,才能開始執行測試腳本。為了能執行遠端伺服器 JMeter Sever ,我們在服務內撰寫執行 remote powershell 的方法,藉此達到目的。本篇文章簡單介紹:

1. C# 執行 Powershell Scripts
2. 執行遠端 Powersehll Command
3. 背景執行 Powershell Scripts
4. 如何背景執行 Powershell Scripts 時取得 output 與 status

若有任何錯誤或建議,請各位先進不吝指教。

本篇執行環境:
Visual Studio 2017
.Net Core with .Net Framework

範例下載:
https://github.com/matsurigoto/powershell_with_csharp_example



介紹

 C# 執行 powershell Scripts

當您想要使用 C# 撰寫 Powersehll 指令,必須將 System.Management.Automation.dll 加入參考,而這個 dll 位置是在:
C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0



加入參考後,我們即可開始撰寫powershell。下面範例使用 powershell 列印目前時間:
PowerShell PowerShellInstance1 = PowerShell.Create();
var cmd01 = "$t = get-date; $t;";
PowerShellInstance1.AddScript(cmd01);
Collection<PSObject> psObjects = PowerShellInstance1.Invoke();

foreach (PSObject output in psObjects)
{
    if (output != null)
    {
        Console.WriteLine(output.BaseObject.ToString());
    }
}
呈現結果如下圖:


我們也可以加入參數方式 (AddParameter) 執行powershell,範例如下:
PowerShell PowerShellInstance2 = PowerShell.Create();
var cmd02 = "param($parameter) ; $parameter";
PowerShellInstance2.AddScript(cmd02);
PowerShellInstance2.AddParameter("parameter", "Just for testing");
Collection<PSObject> psObjects2 = PowerShellInstance2.Invoke();

foreach (PSObject output in psObjects2)
{
    if (output != null)
    {
        Console.WriteLine(output.BaseObject.ToString());
    }
}

執行結果如下:




執行遠端 Powersehll Command

我們可以透過 powershell 執行遠端命令,但我們需要該伺服器使用者帳號密碼才能在遠端電腦執行命令。透過下列兩段指令,我們能建立Credential物件。
$password=ConvertTo-SecureString -String "password" -AsPlainText -Force;
$Cred = New-Object System.Management.Automation.PsCredential("user_name",$password);
注:user_name 與 password 請替換成您的帳號密碼

接續上面指令,你可以透過下列指令撰寫欲在遠端執行 powershell 指令 (這個範例為開啟遠端電腦的 jmeter-server.bat
Invoke-Command -ComputerName computer_name -Credential $Cred {Set-Location -Path "C:\\apache-jmeter-3.2\\bin";cmd.exe /c jmeter-server.bat;}
注:computer_name 請替換成您要遠端操控的電腦名稱/IP,若為網域外的電腦,請參考問題排除

撰寫範例如下:
PowerShell PowerShellInstance3 = PowerShell.Create();
var cmd03 = "$password=ConvertTo-SecureString -String \"password\" -AsPlainText -Force;$Cred = New-Object System.Management.Automation.PsCredential(\"user_name\",$password);Invoke-Command -ComputerName computer_name -Credential $Cred {Set-Location -Path \"C:\\apache-jmeter-3.2\\bin\";cmd.exe /c jmeter-server.bat;}";
PowerShellInstance3.AddScript(cmd03);
Collection<PSObject> psObjects3 = PowerShellInstance3.Invoke();

foreach (PSObject output in psObjects3)
{
    if (output != null)
    {
        Console.WriteLine(output.BaseObject.ToString());
    }
}



雖然成功執行,但你會發現 C# 程式卡住了,無法繼續下去。我們必須在背景執行(run in background),我們才能執行後續其他程式。




背景執行 Powershell Scripts

我們將 Invoke() 方法改成  BeginInvoke 即可以背景執行powershell指令,而無需等待程式完成後才能執行其他動作,範例如下:

PowerShell PowerShellInstance4 = PowerShell.Create();

var cmd04 = "$password=ConvertTo-SecureString -String \"password\" -AsPlainText -Force; $Cred = New-Object System.Management.Automation.PsCredential(\"user_name\",$password); Invoke-Command -ComputerName computer_name -Credential $Cred {Set-Location -Path \"C:\\apache-jmeter-3.2\\bin\";cmd.exe /c jmeter-server.bat;}";

PowerShellInstance4.AddScript(cmd04);
PSDataCollection<PSObject> outputCollection4 = new PSDataCollection<PSObject>();
IAsyncResult = PowerShellInstance4.BeginInvoke<PSObject, PSObject>(null, outputCollection4);
cool, 大功告成,我們完成背景執行的動作。




如何背景執行 Powershell Scripts 時取得 output 與 status 

在撰寫這類型的程式,常常不知道為什麼出錯,而遠端遙控的程式就強制結束了。但是我們一加上了印出 output, 程式執行過程中會造成持續等待 (因為 powershell 未完成),延續上一個範例,我們增加下列程式碼。
while (resutlt.IsCompleted == false)
{
    Thread.Sleep(1000);
}

foreach (var item in outputCollection4)
{
    Console.WriteLine(resutlt);
}
當你想要印出訊息又不想讓程式持續等待,你可以使用EventHandler。首先,我們先新增兩個靜態方法
private static void Output_DataAdded(object sender, DataAddedEventArgs e)
{
    PSDataCollection<PSObject> myp = (PSDataCollection<PSObject>)sender;
    Collection<PSObject> results = myp.ReadAll();
    foreach (PSObject result in results)
    {
        Console.WriteLine(result.ToString());
    }
}

private static void Powershell_InvocationStateChanged(object sender, PSInvocationStateChangedEventArgs e)
{
    Console.WriteLine("PowerShell object state changed: state: {0}\n", e.InvocationStateInfo.State);
}

接下來使用 EventHandler 改寫程式如下 (增加兩行):
PowerShell PowerShellInstance5 = PowerShell.Create();
var cmd05 = "$password=ConvertTo-SecureString -String \"password\" -AsPlainText -Force; $Cred = New-Object System.Management.Automation.PsCredential(\"user_name\",$password); Invoke-Command -ComputerName computer_name -Credential $Cred {Set-Location -Path \"C:\\apache-jmeter-3.2\\bin\";cmd.exe /c jmeter-server.bat;}";

PowerShellInstance5.AddScript(cmd05);
PSDataCollection<PSObject> outputCollection5 = new PSDataCollection<PSObject>();

//add this:
outputCollection5.DataAdded += new EventHandler<DataAddedEventArgs>(Output_DataAdded);
PowerShellInstance5.InvocationStateChanged += new EventHandler<PSInvocationStateChangedEventArgs>(Powershell_InvocationStateChanged);

PowerShellInstance5.BeginInvoke<PSObject, PSObject>(null, outputCollection5);
即可以在不影響背景程式運作情況下,也能看到powershell 執行內容與狀態。





問題排除
Q1. Connecting to remote server failed with the following error message : The WinRM client
cannot process the request. Default authentication may be used with an IP address under the following conditions: the transport is HTTPS or the destination is in the TrustedHosts list, and explicit credentials are provided. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. For more information on how to set TrustedHosts run the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic.

A:使用以下指令增加信任清單
winrm set winrm/config/client '@{TrustedHosts="machineA,machineB"}'


Q2. Connecting to remote server xx.xx.xx.xx failed with the following error message :
WinRM cannot complete the operation. Verify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer. By default, the WinRM firewall exception for public profiles limits access to remote computers within the same local subnet. For more information, see the about_Remote_Troubleshooting Help topic.

A.輸入指令 Enable-PSRemoting
參考下面這篇部落格進行設定:
https://blogs.technet.microsoft.com/heyscriptingguy/2012/12/30/understanding-powershell-remote-management/


參考資料

1. Executing PowerShell scripts from C# - Microsoft Developer
2. 如何透過 C# 應用程式執行或呼叫 PowerShell 命令 - The Will Will Web
3. Runspace09 Sample