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#.
Related
I'm trying to get some data from Azure Active Directory using C# code with reference to System.Management.Automation. I've got no errors with code execution, just null results and no output to textfile. Does anyone have this problem before or maybe I missed something? Thank you!
public void RunScriptTest()
{
string username = "Username";
string password = "Password";
List<String> listResults = new List<String>();
PowerShell powershell = PowerShell.Create();
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
powershell.Runspace = runspace;
powershell.AddScript("Install-Module -Name AzureAD -Force; \n");
powershell.AddScript("Import-Module -Name AzureAD -Verbose \n");
powershell.AddScript("$username = \"" + username + "\"; \n" +
"$password = convertTo-securestring '" + password + "' -AsPlainText -Force; \n" +
"$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $password; \n" +
"Connect-AzureAD - Credential $cred; \n");
powershell.AddScript("Get-AzureADUser | Out-File -FilePath " + #"C:\TestResults\1.txt");
Collection<PSObject> results = powershell.Invoke();
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach(PSObject obj in results)
{
listresults.Add(obj.ToString());
}
}
Sequencing .AddScript() calls without intervening .AddStatement() calls makes only the last .AddScript() call effective - all previous calls are ignored.
In order to examine errors that may have occurred during execution via .Invoke(), you must access the powershell.Streams.Error stream.
Therefore, the immediate fix is to replace your powershell.AddScript(...) calls with $powershell.AddStatement().AddScript(...)
Note that your PowerShellCode appears designed not to produce any output, so there's no point in trying to populate listresults.
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();
}
I want to extract a zip file in Downloads folder to Desktop using PowerShell and C#.
I need it to work with Windows 7, 8, & 10.
I'm trying to take these PowerShell commands
extract https://stackoverflow.com/a/36472063/6806643
overwrite https://stackoverflow.com/a/5711383/6806643
chain https://superuser.com/a/612413/740888
$shell = New-Object -ComObject shell.application
$zip = $shell.NameSpace("zip file path")
foreach ($item in $zip.items()) {
$shell.Namespace("unzip destination path").CopyHere($item)
}
And run it through a C# Process.Start()
Process.Start("powershell.exe",
"timeout 3; "
+ "$shell = New-Object -ComObject shell.application; "
+ "$zip = $shell.NameSpace(\"C:\\Users\\Matt\\Downloads\\MyFile.zip\"); "
+ "foreach ($item in $zip.items()) {$shell.Namespace(\"C:\\Users\\Matt\\Desktop\\\").CopyHere($item, 0x14)}"
);
Problem
PowerShell launches but fails to extract, and closes out before I can read the error.
However, if I copy paste those chained commands into PowerShell without C#, it works.
$shell = New-Object -ComObject shell.application; $zip = $shell.NameSpace('C:\Users\Matt\Downloads\MyFile.zip'); foreach ($item in $zip.items()) {$shell.Namespace('C:\Users\Matt\Desktop\').CopyHere($item, 0x14)}
This works but it's for PowerShell 5 only.
Process.Start("powershell.exe",
"timeout 3; Expand-Archive 'C:\\Users\\Matt\\Downloads\\MyFile.zip' -Force -DestinationPath 'C:\\Users\\Matt\\Desktop\\'"
);
I may have solved it while writing out the question and refactoring other chained commands.
I found this article and combined it with the other code.
https://www.howtogeek.com/tips/how-to-extract-zip-files-using-powershell/
Process.Start("powershell.exe",
"-nologo -noprofile -command "
+ "timeout 3; "
+ "$shell = new-object -com shell.application; "
+ "$zip = $shell.NameSpace('C:\\Users\\Matt\\Downloads\\MyFile.zip'); "
+ "foreach ($item in $zip.items()) {$shell.Namespace('C:\\Users\\Matt\\Desktop\\').CopyHere($item, 0x14)}"
);
The differences are:
adding -nologo -noprofile -command
-com instead of -ComObject
single quotes ' instead of double " around paths.
And chaining messages with Write-Host \"hello\" -NoNewLine instead of echo.
I have a .NET 4.5 web app (ASP.NET MVC 4 Web Application) and my problem seems to be that running powershell code with some simple azure storage functionality will not work when it is executed from C#. But it will work when executed through the normal powershell console or the ISE.
Powershell (Just creates a table and inserts a column for testing):
function Test-SetTableValues($storageAccount, $storageKey, $tableName, $valuesHash)
{
$storageContext = New-AzureStorageContext $storageAccount -StorageAccountKey $storageKey
$table = New-AzureStorageTable –Name $tableName –Context $storageContext
$entity = New-Object Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity "PartitionKey", "RowKey"
foreach ($key in $valuesHash.Keys)
{
$entity.Properties.Add($key, $valuesHash[$key])
}
$result = $table.CloudTable.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::InsertOrMerge($entity), $null, $null)
}
Test-SetTableValues("STORAGE_ACCOUNT", "STORAGE_KEY", "TABLE_NAME", #{MyColumn="MyValue"})
It doesn't work when executed through C#:
using System.Management.Automation;
using System.Management.Automation.Runspaces;
public static void Test()
{
var runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
var powershell = PowerShell.Create();
powershell.Runspace = runspace;
powershell.AddScript(/*powershell code here*/);
powershell.AddCommand("out-default");
powershell.Invoke();
}
And the reason is because the library Microsoft.WindowsAzure.Storage is included.
If we remove it, the code will work. But I need it for other things.
The CLR Version is the same for all environments(c#/ise/powershell_console) as I tried outputing the variable $PSVersionTable (CLRVersion 4.0.30319.17400).
The error we get is this:
Cannot convert argument "operation", with value:
"Microsoft.WindowsAzure.Storag e.Table.TableOperation", for "Execute"
to type "Microsoft.WindowsAzure.Storage. Table.TableOperation":
"Cannot convert the "Microsoft.WindowsAzure.Storage.Tabl
e.TableOperation" value of type
"Microsoft.WindowsAzure.Storage.Table.TableOper ation" to type
"Microsoft.WindowsAzure.Storage.Table.TableOperation"." At
OurPowershellFile.ps1:90 char:5
+ $result = $table.CloudTable.Execute([Microsoft.WindowsAzure.Storage.Table .Ta
...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument
Thanks in advance for any answers. I just can't find a solution for this!
Thanks to microsoft support is seems like modifying the powershell code slightly "fixes" the issue. Not sure why exactly but the below code works.
function Test-SetTableValues($storageAccount, $storageKey, $tableName, $valuesHash)
{
$accountCredentials = New-Object "Microsoft.WindowsAzure.Storage.Auth.StorageCredentials" $storageAccount, $storageKey
$storageAccount = New-Object "Microsoft.WindowsAzure.Storage.CloudStorageAccount" $accountCredentials, $true
$tableClient = $storageAccount.CreateCloudTableClient()
$table = $tableClient.GetTableReference($tableName)
$table.CreateIfNotExists()
$entity = New-Object Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity "PartitionKey", "RowKey"
foreach ($key in $valuesHash.Keys)
{
$entity.Properties.Add($key, $valuesHash[$key])
}
$result = $table.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::InsertOrMerge($entity))
}
Test-SetTableValues("STORAGE_ACCOUNT", "STORAGE_KEY", "TABLE_NAME", #{MyColumn="MyValue"})
Really struggling with this. I have tried various different way, but nothing seems to work.
-using addScript: I get an error telling me that I can't call parameters this way an should use a UI like ISE ?!
-using FilePath parameter, I can't find the right way to pass the arguments (trouble binding)
This is the latest version I tried, and is lifting no errors, but the script is not executed, nothing happens...
Help would be much appreciated.
runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
pipeline = runspace.CreatePipeline();
string script =
#"{param($merchantName, $appType, $gruntDirectory, $merchantInstanceDirectory, $editorConnectionString) "+
_config.MerchantInstance.Directory + #"\Generate_And_Compile_LESS.ps1"
+ " –merchantName $merchantName"
+ " –appType $appType"
+ " –gruntDirectory $gruntDirectory"
+ " -merchantInstanceDirectory $merchantInstanceDirectory"
+ " -editorConnectionString $editorConnectionString }";
Command compileCommand = new Command("Invoke-Command");
compileCommand.Parameters.Add("Scriptblock", ScriptBlock.Create(script));
var args = new List<string>();
args.Add(merchantName);
args.Add(appType.GetHashCode().ToString());
args.Add("'" + _config.Grunt.Directory + "'");
args.Add("'" + _config.MerchantInstance.Directory + "'");
args.Add("'" + _connectionStrings.AppConnectionString + "'");
compileCommand.Parameters.Add("ArgumentList", String.Join(",", args));
pipeline.Commands.Add(compileCommand);
Collection<PSObject> results = pipeline.Invoke();
You can use this code, which I personally just tested.
static void Main(string[] args)
{
PowerShell ps = PowerShell.Create();
ps.AddScript(#"c:\test\test.ps1").AddParameter("param1", "paramvalue1");
ps.Invoke();
}
Here is my test script, located in c:\test\test.ps1.
[CmdletBinding()]
param (
[string] $param1
)
Set-Content -Path $PSScriptRoot\test.txt -Value $param1;
FYI, make sure that you launch 32-bit (x86) PowerShell, and set the execution policy to Unrestricted. Visual Studio is a 32-bit process, and invokes the 32-bit PowerShell engine by default.