We want to store and manage PowerShell scripts in a database an execute them via C#.
How can we discover the parameters of such a script before executing it? so we can set them to known values or prompt a user for values.
Some clarification:
We create a management system MS.
An admin stores a PowerShell script in the MS database.
Later a different admin selects this script from a list offered by the MS.
MS discovers the parameters of the script.
MS prompts the admin for values.
MS executes the script with the parameters supplied.
string testScript = #"
{
param(
[ValidateNotNullOrEmpty()]
[string]$Name
)
get-process $name
";
Dictionary<string,object> DiscoverParameters()
{
using (PowerShell psi = PowerShell.Create())
{
psi.AddScript(testScript);
var pars = new Dictionary<string,object>();
//How do we get at the parameters
return pars;
}
}
void ExecuteScript(Dictionary<string,object> pars)
{
using (PowerShell psi = PowerShell.Create())
{
psi.AddScript(testScript);
pars.ToList().ForEach(p => psi.AddParameter(p.Key, p.Value));
Collection<PSObject> PSOutput = psi.Invoke();
//...
}
}
mjolinor is correct that using the PowerShell parser is probably the best way to get the parameters. That example is in PowerShell, below is an example in C#. I'm not quite sure what you are looking for with the parameters being Dictionary<string, object>. Here we just stick the names into the list although there is other info you could pull out like the static type.
using System.Management.Automation;
using System.Management.Automation.Language;
static void Main(string[] args)
{
const string testScript = #"
param(
[ValidateNotNullOrEmpty()]
[string]$Name
)
get-process $name
";
foreach(var parameter in GetScriptParameters(testScript))
{
Console.WriteLine(parameter);
}
}
private static List<string> GetScriptParameters(string script)
{
Token[] tokens;
ParseError[] errors;
var ast = Parser.ParseInput(script, out tokens, out errors);
if (errors.Length != 0)
{
Console.WriteLine("Errors: {0}", errors.Length);
foreach (var error in errors)
{
Console.WriteLine(error);
}
return null;
}
return ast.ParamBlock.Parameters.Select(p => p.Name.ToString()).ToList();
}
You can use the PS parser, and access the parameter information via AST:
$scriptfile = '<full path to script file>'
$AST = [System.Management.Automation.Language.Parser]::ParseFile( $scriptfile,[ref]$null,[ref]$null)
$AST.ParamBlock.Parameters | ft
Related
I'm trying to build a graphic platform using Visual Studio. And I'm not a developer, I want to run PowerShell or batch files when I click a button. Thing is when I'm trying C# syntax it does not work even if I installed PowerShell extension.
I tried some code that I found on the internet, using process.start or trying to create a command in all cases the name of the command is not defined and it does not work.
private void Button1_Click(object sender, EventArgs e)
{
Process.Start("path\to\Powershell.exe",#"""ScriptwithArguments.ps1"" ""arg1"" ""arg2""");
}
I want to launch my .ps1 script but I get an error
name process is not defined
Calling C# code in Powershell and vice versa
C# in Powershell
$MyCode = #"
public class Calc
{
public int Add(int a,int b)
{
return a+b;
}
public int Mul(int a,int b)
{
return a*b;
}
public static float Divide(int a,int b)
{
return a/b;
}
}
"#
Add-Type -TypeDefinition $CalcInstance
$CalcInstance = New-Object -TypeName Calc
$CalcInstance.Add(20,30)
Powershell in C#
All the Powershell related functions are sitting in
System.Management.Automation namespace, ... reference that in your project
static void Main(string[] args)
{
var script = "Get-Process | select -Property #{N='Name';E={$_.Name}},#{N='CPU';E={$_.CPU}}";
var powerShell = PowerShell.Create().AddScript(script);
foreach (dynamic item in powerShell.Invoke().ToList())
{
//check if the CPU usage is greater than 10
if (item.CPU > 10)
{
Console.WriteLine("The process greater than 10 CPU counts is : " + item.Name);
}
}
Console.Read();
}
So, your query is also really a duplicate of many similar posts on stackoverflow.
Powershell Command in C#
Here's what it worked for me, including cases when the arguments contains spaces:
using (PowerShell PowerShellInst = PowerShell.Create())
{
PowerShell ps = PowerShell.Create();
string param1= "my param";
string param2= "another param";
string scriptPath = <path to script>;
ps.AddScript(File.ReadAllText(scriptPath));
ps.AddArgument(param1);
ps.AddArgument(param2);
ps.Invoke();
}
The .ps1 file would be something as this (make sure you declare the parameters in the .ps1 script):
Param($param1, $param2)
$message = $param1 + " & " + $param2"
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[System.Windows.Forms.MessageBox]::Show($message)
I find this approach very easy to understand and very clear.
string path = #"C:\1.ps1";
Process.Start(new ProcessStartInfo("Powershell.exe",path) { UseShellExecute = true })
I have a simple powershell script
param
(
[Parameter(Mandatory=$true)]
[int]$loop = 2
)
for ($i=0; $i -le $loop; $i++)
{
$v += get-process
}
$v
I want to execute it through C#. I am able to execute simple scripts but now when I want to pass value to the $loop parameter it says
{"Cannot process command because of one or more missing mandatory
parameters: loop."}
I am using the below code:
using (PowerShell powerShellInstance = PowerShell.Create(RunspaceMode.NewRunspace))
{
powerShellInstance.Runspace = runspace;
powerShellInstance.AddScript(script);
if (parameters != null && parameters.Any())
{
foreach (var parameter in parameters)
{
if (parameter.Type == ParameterType.Int32)
{
int value = Convert.ToInt32(parameter.Value.Trim());
powerShellInstance.AddParameter(parameter.Name.Trim(), value);
}
else
{
powerShellInstance.AddParameter(parameter.Name.Trim(), parameter.Value.Trim());
}
}
}
Here, I see in the debug mode of visual studio that the parameter name is $loop and its value is being clearly set through the Addparameter Api
But I get the above exception when I call
Collection<PSObject> output = powerShellInstance.Invoke();
NOt sure, where am I going wrong. Please help
It would help to see the rest of your code where you define parameters and script. One thing to note is that the name of parameter is actually loop, not $loop.
Here is some very simplified code that shows this working.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
namespace PSTest
{
class Program
{
static void Main(string[] args)
{
using (PowerShell powerShellInstance = PowerShell.Create(RunspaceMode.NewRunspace))
{
var script = "param($param1) $output = 'testing params in C#:' + $param1; $output";
powerShellInstance.AddScript(script);
powerShellInstance.AddParameter("param1", "ParamsinC#");
Collection<PSObject> PSOutput = powerShellInstance.Invoke();
foreach (PSObject outputItem in PSOutput)
{
if (outputItem != null)
{
Console.WriteLine(outputItem);
}
}
Console.ReadKey();
}
}
}
}
I am trying to work out an efficient way of calling a Powershell cmdlet for 20-30 files asynchronously.
Although the below code is working, the Import-Module step is run for every file which is processed. Unfortunately this Module takes between 3 or 4 seconds to import.
Searching on the web I can find references to RunspacePools & InitialSessionState, but have had issues trying to create a PSHost object which is required in the CreateRunspacePool overload.
Any help would be appreciated.
Thanks
Gavin
.
.
Code sample from my application:
I am using a Parallel ForEach to distribute the files between threads.
Parallel.ForEach(files, (currentFile) =>
{
ProcessFile(currentfile);
});
private void ProcessFile(string filepath)
{
//
// Some non powershell related code removed for simplicity
//
// Start PS Session, Import-Module and Process file
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript("param($path) Import-Module MyModule; Process-File -Path $path");
PowerShellInstance.AddParameter("path", filepath);
PowerShellInstance.Invoke();
}
}
As it has already been explained in the comments, this won't work with PSJobs because objects are serialized and the jobs themselves run in a separate process.
What you can do is create a RunspacePool with an InitialSessionState that has the module imported:
private RunspacePool rsPool;
public void ProcessFiles(string[] files)
{
// Set up InitialSessionState
InitialSessionState initState = InitialSessionState.Create();
initState.ImportPSModule(new string[] { "MyModule" });
initState.LanguageMode = PSLanguageMode.FullLanguage;
// Set up the RunspacePool
rsPool = RunspaceFactory.CreateRunspacePool(initialSessionState: initState);
rsPool.SetMinRunspaces(1);
rsPool.SetMaxRunspaces(8);
rsPool.Open();
// Run ForEach()
Parallel.ForEach(files, ProcessFile);
}
private void ProcessFile(string filepath)
{
// Start PS Session and Process file
using (PowerShell PowerShellInstance = PowerShell.Create())
{
// Assign the instance to the RunspacePool
PowerShellInstance.RunspacePool = rsPool;
// Run your script, MyModule has already been imported
PowerShellInstance.AddScript("param($path) Process-File #PSBoundParameters").AddParameter("path", filepath);
PowerShellInstance.Invoke();
}
}
I found a C# example here of invoking a PowerShell script asynchronously from a host application (in folder Chapter 6 - Reading With Events) and am trying to use it in a Windows Forms application.
I have a button (button1) to start the PowerShell script, textBox1 is to enter the script and textBox2 displays the script output. Here is my current code:
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Windows.Forms;
namespace PSTestApp
{
delegate void SetTextDelegate(string text);
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
textBox2.Text = "";
Runspace runspace =
RunspaceFactory.CreateRunspace();
runspace.Open();
Pipeline pipeline =
runspace.CreatePipeline(textBox1.Text);
pipeline.Output.DataReady +=
new EventHandler(HandleDataReady);
pipeline.Error.DataReady +=
new EventHandler(HandleDataReady);
pipeline.InvokeAsync();
pipeline.Input.Close();
}
private void HandleDataReady(object sender, EventArgs e)
{
PipelineReader<PSObject> output =
sender as PipelineReader<PSObject>;
if (output != null)
{
while (output.Count > 0)
{
SetText(output.Read().ToString());
}
return;
}
PipelineReader<object> error =
sender as PipelineReader<object>;
if (error != null)
{
while (error.Count > 0)
{
SetText(error.Read().ToString());
}
return;
}
}
private void SetText(string text)
{
if (textBox2.InvokeRequired)
{
SetTextDelegate d = new SetTextDelegate(SetText);
this.Invoke(d, new Object[] { text });
}
else
{
textBox2.Text += (text + Environment.NewLine);
}
}
}
}
The code works, but I have a problem handling the output. Pipeline.Output.Read() returns an instance of PSObject so ToString() returns different things for different objects. For example, if I use this PowerShell command:
Get-ChildItem
the output is:
PSTestApp.exe
PSTestApp.pdb
PSTestApp.vshost.exe
PSTestApp.vshost.exe.manifest
and if I use:
Get-Process
the output is:
...
System.Diagnostics.Process (csrss)
System.Diagnostics.Process (ctfmon)
System.Diagnostics.Process (devenv)
System.Diagnostics.Process (devenv)
...
I could use the returned PSObject instances to construct the output, but it would be nice If I could use the existing PowerShell formatting and get the same output as in the console. When I run the application and check Runspace.RunspaceConfiguration.Formats, the count is 9, and DotNetTypes.format.ps1xml is present, but I don't know how to apply the format.
I have noticed that if I add Out-String at the end of the script:
...
Pipeline pipeline =
runspace.CreatePipeline(textBox1.Text);
pipeline.Commands.Add("Out-String");
...
the output is formatted just as in the standard PowerShell console. This works, but if I run a script with a long output that takes some time to execute:
gci d:\ -recurse
Pipeline.Output.DataReady event is raised only once (after the ending Out-String is executed) and only then is the output added to the text box.
Is there a way to use standard PowerShell output formatting in a hosted PowerShell instance?
If you use the -stream parameter on out-string, I think you'll find that it doesn't block.
Also, if you actually build a host (implement the host interface, including the UI and possibly the rawui), you'll implement methods to handle the "standard" host output.
You might also try using out-default instead of out-string. I know in self-hosted environments I usually use that.
BACKGROUND
I am using Powershell 2.0 on Windows 7.
I am writing a cmdlet in a Powershell module ("module" is new to Powershell 2.0).
To test the cmdlet I am writing Unit tests in Visual Studio 2008 that programmatically invoke the cmdlet.
REFERENCE
This Article on MSDN called "How to Invoke a Cmdlet from Within a Cmdlet" shows how to call a cmdlet from C#.
THE SOURCE CODE
This is a distilled version of my actual code — I've made it as small as possible so that you can see the problem I am having clearly:
using System;
using System.Management.Automation;
namespace DemoCmdLet1
{
class Program
{
static void Main(string[] args)
{
var cmd = new GetColorsCommand();
foreach ( var i in cmd.Invoke<string>())
{
Console.WriteLine("- " + i );
}
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Hello");
this.WriteVerbose("World");
}
}
}
COMMENTS
I understand how to enable and capture verbose output from the Powershell command line; that's not the problem.
In this case I am programmatically invoking the cmdlet from C#.
Nothing I've found addresses my specific scenario. Some articles suggest I should implement my own PSHost, but seems expensive and also it seems like a have to call the cmdlet as text, which I would like to avoid because that is not as strongly typed.
UPDATE ON 2009-07-20
Here is is the source code based on the answer below.
Some things are still not clear to me:
* How to call the "Get-Colors" cmdlet (ideally without having to pass it as a string to the ps objet)
* How to get the verbose output as it is generated instead of getting an collection of them at the end.
using System;
using System.Management.Automation;
namespace DemoCmdLet1
{
class Program
{
static void Main(string[] args)
{
var ps = System.Management.Automation.PowerShell.Create();
ps.Commands.AddScript("$verbosepreference='continue'; write-verbose 42");
foreach ( var i in ps.Invoke<string>())
{
Console.WriteLine("normal output: {0}" , i );
}
foreach (var i in ps.Streams.Verbose)
{
Console.WriteLine("verbose output: {0}" , i);
}
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Red");
this.WriteVerbose("r");
this.WriteObject("Green");
this.WriteVerbose("g");
this.WriteObject("Blue");
this.WriteVerbose("b");
}
}
}
The code above generates this output:
d:\DemoCmdLet1\DemoCmdLet1>bin\Debug\DemoCmdLet1.exe
verbose output: 42
UPDATE ON 2010-01-16
by using the Powershell class (found in System.Management.Automation but only in the version of the assembly that comes with the powershell 2.0 SDK, not what comes out-of-the-box on Windows 7) I can programmatically call the cmdlet and get the verbose output. The remaining part is to actually add a custom cmdlet to that powershell instance - because that was my original goal - to unit test my cmdlets not those that come with powershell.
class Program
{
static void Main(string[] args)
{
var ps = System.Management.Automation.PowerShell.Create();
ps.AddCommand("Get-Process");
ps.AddParameter("Verbose");
ps.Streams.Verbose.DataAdded += Verbose_DataAdded;
foreach (PSObject result in ps.Invoke())
{
Console.WriteLine(
"output: {0,-24}{1}",
result.Members["ProcessName"].Value,
result.Members["Id"].Value);
}
Console.ReadKey();
}
static void Verbose_DataAdded(object sender, DataAddedEventArgs e)
{
Console.WriteLine( "verbose output: {0}", e.Index);
}
}
[Cmdlet("Get", "Colors")]
public class GetColorsCommand : Cmdlet
{
protected override void ProcessRecord()
{
this.WriteObject("Hello");
this.WriteVerbose("World");
}
}
Verbose output is not actually output unless $VerbosePreference is set at least to "Continue."
Use the PowerShell type to run your cmdlet, and read VerboseRecord instances from the Streams.Verbose propery
Example in powershell script:
ps> $ps = [powershell]::create()
ps> $ps.Commands.AddScript("`$verbosepreference='continue'; write-verbose 42")
ps> $ps.invoke()
ps> $ps.streams.verbose
Message InvocationInfo PipelineIterationInfo
------- -------------- ---------------------
42 System.Management.Automation.Invocat... {0, 0}
This should be easy to translate into C#.
1. string scriptFile = "Test.ps1";
2. using (PowerShell ps = PowerShell.Create())
3. {
4. const string getverbose = "$verbosepreference='continue'";
5. ps.AddScript(string.Format(getverbose));
6. ps.Invoke();
7. ps.Commands.Clear();
8. ps.AddScript(#".\" + scriptFile);
9. ps.Invoke();
10. foreach (var v in ps.Streams.Verbose)
11. {
12. Console.WriteLine(v.Message);
13. }
14. }
Important lines are line 5 and 6. This basically set the $verbosepreference for the session and for upcoming new commands and scripts.
First off, if you are unit testing cmdlets, likely Pester is a better (and easier) option.
As per your many updates, all you are likely missing at this point is a strongly typed approach to reference the C# cmdlet
ps.AddCommand(new CmdletInfo("Get-MyCS", typeof(GetMyCS)));
DISCLAIMER: I know this works for PowerShell 5.0, but don't have experience with the older PowerShell 2.0.