How to pass a powershell script to Windows PowerShell Host in C#? - c#

I would like to use the methods of Windows PowerShell Host on C# project (.NETFramework)
I had installed System.Management.Automation.dll on my project to run the commands of PowerShell on C#.
My goal is pass my ps1 file that contains:
$ProcessName = "Notepad"
$Path = "D:\FolderName\data.txt"
$CpuCores = (Get-WMIObject Win32_ComputerSystem).NumberOfLogicalProcessors
$Samples = (Get-Counter "\Process($Processname*)\% Processor Time").CounterSamples
$Samples | Select #{Name="CPU %";Expression={[Decimal]::Round(($_.CookedValue / $CpuCores), 2)}} | Out-File -FilePath $Path -Append
to a native implementation in C#. This return the CPU usage of a process.
I want to use the PowerShell Object to avoid to have the ps1 file, because I want to write the previous commands on C# using the System.Management.Automation.PowerShell class, example:
PowerShell powerShellCommand = PowerShell.Create();
powerShellCommand.AddCommand("Get-WMIObject");
powerShellCommand.AddArgument("Win32_ComputerSystem");
powerShellCommand.AddArgument("NumberOfLogicalProcessors ");
Do you have any idea how to transfer it to powershell Object and methods on C#?

You can add parameters block (param()) to your script and invoke it:
const string script = #"
param(
[string] $ProcessName,
[string] $Path
)
$CpuCores = (Get-WMIObject Win32_ComputerSystem).NumberOfLogicalProcessors
$Samples = (Get-Counter ""\Process($ProcessName*)\% Processor Time"").CounterSamples
$Samples |
Select #{Name=""CPU %"";Expression={[Decimal]::Round(($_.CookedValue / $CpuCores), 2)}} |
Out-File -FilePath $Path -Append";
PowerShell powerShellCommand = PowerShell.Create();
powerShellCommand
.AddScript(script)
.AddParameters(new PSPrimitiveDictionary
{
{ "ProcessName", "Notepad" },
{ "Path", #"D:\FolderName\data.txt" }
})
.Invoke();

Related

Run PowerShell script in PowerShell 6 from C#

I have a PowerShell script which communicates with a REST server. This script only works in PowerShell 6.
I want to call it from C#, because the C# program needs the info from the REST server, and I don't want to rewrite the REST code in C#.
So basically, I want to run a PowerShell script from C#. However, in C#, PowerShell.Create(); creates a PowerShell instance that uses PowerShell 5.
I already replaced pwsh.exe in the default folder, deleted PowerShell 5 everywhere etc. and when I shift+right click anywhere to use "Run PowerShell here" I get a PowerShell 6 window. But for some reason, C# sticks to using PowerShell 5, when using the PowerShell class.
This is the PowerShell code I want to reuse:
function Get-JSONWebToken {
param (
[Parameter(Mandatory=$True)][string] $BaseUri,
[Parameter(Mandatory=$True)][string] $ApiToken
)
if ($PSVersionTable.PSVersion.Major -lt 6) {
$version = $PSVersionTable.PSVersion
Throw "Your PowerShell version is: $version. Please upgrade to PowerShell 6 or above"
}
$uri = "$BaseUri/auth/token"
$bodyJson = ConvertTo-Json #{token = $ApiToken} -Compress
Write-Host "Authenticating ..."
try {
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-ContentType "application/json" `
-Body $bodyJson
$jwtToken = $response.token
$secureToken = ConvertTo-SecureString $jwtToken -AsPlainText -Force
return $secureToken
}
catch {
#handle error
}
}
So now I am trying to call PowerShell 6 manually, importing a module first and then using it. Here are my three attempts, which are all supposed to do the same thing: call Get-JSONWebToken (in rest-api.psm1) and retrieve the output correctly.
C# version 1, using PowerShell class:
ps = PowerShell.Create();
//module import...
PSCommand cmd = ps.Commands.AddCommand("Get-JSONWebToken");
cmd.AddParameter("baseUri", baseUri);
cmd.AddParameter("apiToken", apiToken);
ps.Invoke();
This always runs on PowerShell 5 for some reason so it can't be used.
C# version 2, using a Process instead
Process ps6 = new Process();
ps6.StartInfo = new ProcessStartInfo {
FileName = "C:/Program Files/PowerShell/6/pwsh.exe",
Arguments = "-Command {\n" +
"Import-Module " + modulePath + ";\n" +
"Get-JSONWebToken " + apiToken + ";\n" +
"}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = false
};
ps6.Start()
This runs on PowerShell 6, but only outputs the arguments I passed, and not the output of Get-JSONWebToken.
C# version 3: Calling PS6 from PS5 from C#
PSCommand cmd = ps.Commands.AddCommand("C:/Program Files/PowerShell/6/pwsh.exe");
ScriptBlock sb = ScriptBlock.Create("Import-Module " + modulePath + "; Get-JSONWebToken " + apiToken + ";");
cmd.AddParameter("Command", sb);
ps.Invoke();
This doesn't work at all:
Result: Usage: pwsh[.exe] [[-File] <filePath> [args]]
Result: [-Command { - | <script-block> [-args <arg-array>]
Result: | <string> [<CommandParameters>] } ]
Result: [-ConfigurationName <string>] [-CustomPipeName <string>]
...
...
PowerShell version:
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $Ps6Path
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.CreateNoWindow = $false
$pinfo.Arguments = "-Command {Import-Module <myPath>\rest-api.psm1; Get-JSONWebToken 123inputStringExample;}"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
Write-Host "stdout: $stdout"
Write-Host "stderr: $stderr"
Write-Host "exit code: " + $p.ExitCode
This also only outputs the arguments I passed when called either from C# or from PS6 or PS5
This doesn't technically solve the problem, but I did as #MindSwipe suggested and rewrote the code in C# entirely. It wasn't trivially easy but it's a nice and elegant solution in the end.
If you have an idea on how to solve this question properly, please post it here as I'm still interested in how to call stuff on PowerShell 6 from C#.

ExitCode always has null does not show the actual return value from exe

I am calling my exe from powershell script like shown below.
$file = $PSScriptRoot + "\executor.exe"
$code = (Start-Process -WindowStyle Hidden $file -Verb runAs -ArgumentList $Logfile).StandardOutput.ToString;
$nid = (Get-Process "executor.exe").id
Wait-Process -Id $nid
if ($code -eq 1) {
LogWrite "Execution succeeded"
} else
{
LogWrite "Execution Failed"
}
I have a int main function in my exe program which would return 1 on success, and 0 on failure.
When i try to get the ExitCode(using $LASTEXITCODE) from powershell script it always shows null(neither 1 nor 0), But my exe is returning 1 as expected.
How do i capture the return value of exe in powershell script?
You can use this:
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = # path to your exe file
# additional options:
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $false
$psi.WindowStyle = "Maximized"
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $psi
$p.Start() | Out-Null # returns $true if the process started, $false otherwise
$p.WaitForExit()
# here's the exitcode
$exitCode = $p.ExitCode
Create the process start info, to specify the executable path, and additional options. It's important to use .WaitForExit() to wait until the process finishes.
What you have tried doesn't get the app exitcode, but what the application write to standard console, which, in your case, I assume is nothing. If you could modify the exe to write to the console, what you did would work.

Get PowerShell.Invoke() output in C#

I have an application, that allows the user to configure basic WMI settings on a Win 10 IoT machine.
I am currently struggling with reading all WEKF_PredefinedKey settings, that are enabled.
I am simply running a skript, that I added as string to the project settings named ReadEnabledKeys:
$CommonParams = #{"namespace"="root\standardcimv2\embedded"}
$CommonParams += $PSBoundParameters
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned;
$keys = Get-WMIObject -class WEKF_PredefinedKey #CommonParams
foreach($k in $keys)
{
if($k.Enabled -eq $false)
{
"$k";
}
}
My call in C# code looks like this (Note: using System.Management.Automation):
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript(Properties.Settings.Default.ReadEnabledKeys);
var result = PowerShellInstance.Invoke();
}
My variable result will always stay empty.
If I run the skript in Powershell directly, the output is just fine (all shortcuts, that are currently not disabled).
I have something similar programmed with the unified write filter, where I enable and disable it:
$COMPUTER = "localhost"
$NAMESPACE = "root\standardcimv2\embedded"
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned;
$objUWFInstance = Get-WMIObject -namespace $NAMESPACE -class UWF_Filter;
$retval = $objUWFInstance.Enable();
if ($retval.ReturnValue -eq 0) {"Unified Write Filter will be enabled after the next system restart."}
else {"Unknown Error: " + "{0:x0}" -f $retval.ReturnValue}
And the C# call:
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript(Properties.Settings.Default.EnableUWF);
// [0] = result or error
var result = PowerShellInstance.Invoke();
if (result[0].ToString().ToLower().Contains("enabled"))
MessageBox.Show(result[0].ToString(), "", MessageBoxButton.OK, MessageBoxImage.Information);
else
MessageBox.Show("Error when enabling the filter! " + Environment.NewLine + result[0].ToString(), "",
MessageBoxButton.OK, MessageBoxImage.Error);
}
Here my result variable will be filled with the expected strings.
I have tried Write-Host $k, as I suspected something wrong with the stream, but this was without any success.
The output in Powershell looks like this:
PS C:\Users\Administrator> C:\Users\Administrator\Desktop\Newfolder\Untitled1.ps1
\\DESKTOP-RMGOBMG\root\standardcimv2\embedded:WEKF_PredefinedKey.Id="Alt"\\DESKTOP-RMGOBMG\root\standardcimv2\embedded:WEKF_PredefinedKey.Id="Application"
\\DESKTOP-RMGOBMG\root\standardcimv2\embedded:WEKF_PredefinedKey.Id="Ctrl+Esc"
\\DESKTOP-RMGOBMG\root\standardcimv2\embedded:WEKF_PredefinedKey.Id="Ctrl+F4"
\\DESKTOP-RMGOBMG\root\standardcimv2\embedded:WEKF_PredefinedKey.Id="Ctrl+Tab"
.
.
.
Can anyone tell me, what the problem is?
The problem appears to be with your script. Setting the ExecutionPolicy midstream doesn't do anything and you aren't writing a function so adding $PSBoundParameters also doesn't do anything. Here's an example that should work (I'd specify PS version in the future. I know you're on v5.1/win10 due to keyboard filtering)
$collection = [System.Collections.Generic.List[string]]::new()
foreach ($key in (Get-CimInstance -Namespace 'root\standardcimv2\embedded' -ClassName WEKF_PredefinedKey)) {
if (-not $key.Enabled) {
$collection.Add($key.ToString())
}
}
return $collection
(simplified)
#(Get-CimInstance -Namespace root\standardcimv2\embedded -ClassName WEKF_PredefinedKey).
Where{-not $_.Enabled}.
ForEach('ToString')
Example:
using (PowerShell ps = PowerShell.Create())
{
string script = #"Import-Module -Name C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Management\Microsoft.PowerShell.Management.psd1 -ErrorAction Stop; #(Get-WmiObject -Namespace root\standardcimv2\embedded -Class WEKF_PredefinedKey -ErrorAction Stop).Where{-not $_.Enabled}.ForEach('ToString')";
ps.AddScript(script);
var result = ps.Invoke();
}

Inserting a PowerShell command into c#

I want to use this PowerShell command in a C# project:
Get-VM -Name Win8-Henry | Get-VMNetworkAdapter | Select MacAddress
This is what I normally do in c#:
public static void MacAdd(string machineName,Runspace run) {
// Get-VM -Name Win8-Henry | Get-VMNetworkAdapter | Select MacAddress
Command command = new Command("Get-VM");
command.Parameters.Add("Name", machineName);
using (Pipeline hostPipeline = run.CreatePipeline())
{
hostPipeline.Commands.Add(command);
Collection<PSObject> echos = hostPipeline.Invoke();
hostPipeline.Stop();
}
}
What I need help with is adding the second command, and then using the pipeline.
Use the AddScript() method on the PowerShell class.
var Command = String.Format("Get-VM -Name {0} | Get-VMNetworkAdapter | Select MacAddress", computername);
var PowerShell = PowerShell.Create();
PowerShell.AddScript(Command);
PowerShell.Invoke();
Just add the other commands to the pipeline too :)
hostPipeline.Commands.Add(new Command("Get-VMNetworkAdapter"));
Just to share what I do when I have a script that I need to execute in an application. I can then replace the parameters (i.e., "$!$machineName$!$" easily and get the result cleanly.
Dim lxScript As XElement = <Script>
.{
Get-VM -Name $!$machineName$!$ | Get-VMNetworkAdapter | Select MacAddress
}
</Script>

What is the command to access Exchange Management Tools from C# code in Exchange 2010

In Exchange 2007 this line of code is used to load the Exchange Poweshell commands snapin:
PSSnapInInfo info = rsConfig.AddPSSnapIn(
"Microsoft.Exchange.Management.PowerShell.Admin",
out snapInException);
However, this does not exist in Exchange 2010 and I am pulling my hair out trying to find out how to access the Exchange Powershell commands from C# code. Microsoft.Exchange.Management.PowerShell.Admin does not exist anywhere on the Exchange Server and I can find nothing on Google that talks about an equivalent line of code.
How do I access Exchange Management Tools from C# code in Exchange 2010?
Below is my complete code for reference, it all works until I add the line of code:
//Creating and Opening a Runspace
RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
PSSnapInException snapInException = null;
PSSnapInInfo info = rsConfig.AddPSSnapIn(
"Microsoft.Exchange.Management.PowerShell.Admin",
out snapInException);
Runspace myRunSpace = RunspaceFactory.CreateRunspace(rsConfig);
myRunSpace.Open();
//How Do I Run a Cmdlet?
//create a new instance of the Pipeline class
Pipeline pipeLine = myRunSpace.CreatePipeline();
//create an instance of the Command class
// by using the name of the cmdlet that you want to run
Command myCommand = new Command(txtCommand.Text);
//add the command to the Commands collection of the pipeline
pipeLine.Commands.Add(myCommand);
Collection<PSObject> commandResults = pipeLine.Invoke();
// iterate through the commandResults collection
// and get the name of each cmdlet
txtResult.Text = "start ....";
foreach (PSObject cmdlet in commandResults)
{
string cmdletName = cmdlet.Properties["Name"].Value.ToString();
System.Diagnostics.Debug.Print(cmdletName);
txtResult.Text += "cmdletName: " + cmdletName;
}
txtResult.Text += ".... end";
I don't know for sure, but Exchange 2010 powershell might be implemented as a powershell 2.0 module, which is loaded in a different manner. To find out, go to a system with the exchange management shell on it, and fire it up. Next, run:
ps> get-module
This will list the loaded v2 modules. I would expect the exchange one to appear if you have started the dedicated exchange management shell. If you loaded the regular powershell console, try:
ps> get-module -list
This will list all modules available to load. If you spot the right one, then you'll need to build your code against the v2 system.management.automation dll. For reasons beyond the scope of this reply, v2 powershell's assembly has the same strong name as v1's, so you cannot easily have both versions of powershell on the same machine. Build this from a machine with v2 powershell installed:
InitialSessionState initial = InitialSessionState.CreateDefault();
initialSession.ImportPSModule(new[] { *modulePathOrModuleName* });
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
runspace.Open();
RunspaceInvoke invoker = new RunspaceInvoke(runspace);
Collection<PSObject> results = invoker.Invoke(*myScript*);
Hope this helps,
-Oisin
After a lot of trial and error, I finally figured this out. The problem with the code above is it works great when run against Exchange 2007 but things have changed in Exchange 2010. Instead of the snapin called "Microsoft.Exchange.Management.PowerShell.Admin", use this snapin, "Microsoft.Exchange.Management.PowerShell.E2010".
The complete code to run a Powershell command from C# looks like this. Hope this helps someone else trying to do this.
You will need references to System.Management.Automation.Runspaces, System.Collections.ObjectModel and System.Management.Automation also.
I found that the reference to System.Management.Automation had to be manually added to the csproj file itself in the ItemGroup section using notepad like this:
<Reference Include="System.Management.Automation" />
code below:
private class z_test
{
//set up
private RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
private PSSnapInException snapInException = null;
private Runspace runSpace;
private void RunPowerShell()
{
//create the runspace
runSpace = RunspaceFactory.CreateRunspace(rsConfig);
runSpace.Open();
rsConfig.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.E2010", out snapInException);
//set up the pipeline to run the powershell command
Pipeline pipeLine = runSpace.CreatePipeline();
//create the script to run
String sScript = "get-mailbox -identity 'rj'";
//invoke the command
pipeLine.Commands.AddScript(sScript);
Collection<PSObject> commandResults = pipeLine.Invoke();
//loop through the results of the command and load the SamAccountName into the list
foreach (PSObject results in commandResults)
{
Console.WriteLine(results.Properties["SamAccountName"].Value.ToString());
}
pipeLine.Dispose();
runSpace.Close();
}
}
This is what I am doing:
$sessionOptionsTimeout=180000
$sessionOptionsTimeout=180000
$so = New-PSSessionOption -OperationTimeout $sessionOptionsTimeout -IdleTimeout $sessionOptionsTimeout -OpenTimeout $sessionOptionsTimeout
$connectionUri="http://$fqdn/powershell?serializationLevel=Full;ExchClientVer=14.3.91.1"
$s = New-PSSession -ConnectionURI "$connectionUri" -ConfigurationName Microsoft.Exchange -SessionOption $so
$s | Enter-PSSession
PS>get-mailboxserver
EncryptionRequired AutoDatabaseMountDial DatabaseCopyAutoActivationPo
licy
------------------ --------------------- ----------------------------
e GoodAvailability Unrestricted
e GoodAvailability Unrestricted
Now, converting above to .net (c#) should be easy...
Essentially an exerpt from: "C:\Program Files\Microsoft\Exchange Server\V14\Bin\ConnectFunctions.ps1"
Please refer to the following function:
function _NewExchangeRunspace([String]$fqdn,
[System.Management.Automation.PSCredential]
$credential=$null,
[bool]$UseWIA=$true,
[bool]$SuppressError=$false,
$ClientApplication=$null,
$AllowRedirection=$false)
{
$hostFQDN = _GetHostFqdn
if (($fqdn -ne $null) -and ($hostFQDN -ne $null) -and ($hostFQDN.ToLower() -eq $fqdn.ToLower()))
{
$ServicesRunning = _CheckServicesStarted
if ($ServicesRunning -eq $false)
{
return
}
}
Write-Verbose ($ConnectFunctions_LocalizedStrings.res_0005 -f $fqdn)
$so = New-PSSessionOption -OperationTimeout $sessionOptionsTimeout -IdleTimeout $sessionOptionsTimeout -OpenTimeout $sessionOptionsTimeout;
$setupRegistryEntry = get-itemproperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup -erroraction:silentlycontinue
if ( $setupRegistryEntry -ne $null)
{
$clientVersion = "{0}.{1}.{2}.{3}" -f $setupRegistryEntry.MsiProductMajor, $setupRegistryEntry.MsiProductMinor, $setupRegistryEntry.MsiBuildMajor, $setupRegistryEntry.MsiBuildMinor
$connectionUri = "http://$fqdn/powershell?serializationLevel=Full;ExchClientVer=$clientVersion"
}
else
{
$connectionUri = "http://$fqdn/powershell?serializationLevel=Full"
}
if ($ClientApplication -ne $null)
{
$connectionUri = $connectionUri + ";clientApplication=$ClientApplication"
}
write-host -fore Yellow ("connectionUri: " + $connectionUri)
$contents = 'New-PSSession -ConnectionURI "$connectionUri" -ConfigurationName Microsoft.Exchange -SessionOption $so'
if (-not $UseWIA)
{
$contents = $contents + ' -Authentication Kerberos -Credential $credential'
}
if ($SuppressError)
{
$contents = $contents + ' -erroraction silentlycontinue'
}
if ($AllowRedirection)
{
$contents = $contents + ' -AllowRedirection'
}
write-host -fore Yellow ("contents: " + $contents)
write-host -fore Yellow ("join n contents: " + [string]::join("`n", $contents))
[ScriptBlock] $command = $executioncontext.InvokeCommand.NewScriptBlock([string]::join("`n", $contents))
$session=invoke-command -Scriptblock $command
if (!$?)
{
# ERROR_ACCESS_DENIED = 5
# ERROR_LOGON_FAILURE = 1326
if (!(5 -eq $error[0].exception.errorcode) -and
!(1326 -eq $error[0].exception.errorcode))
{
#Write-Verbose ($ConnectFunctions_LocalizedStrings.res_0006 -f $fqdn)
return
}
else
{
# no retries if we get 5 (access denied) or 1326 (logon failure)
#$REVIEW$ connectedFqdn is not set. Is it okay?
break connectScope
}
}
$session
}

Categories

Resources