Run a PowerShell script from C# - c#

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

Related

Powershell command not recognized when calling from C#

This is in continuation to this Question here, I have a PowerShell command which I have created and am able to call the command in a PowerShell window, but when trying to call from C# method, I am getting error as the cmdlet is not recognized, I tried with other existing commands and get same error, so I suspect issue in Importing the Module, though I don't get that error in streams. Error. The only error I get is "Get-RowAndPartitionKey is not a recognized cmndlt, please check the spelling.....".
Would like to know if there is any other way, I should try it or if I can debug more here to see if my Module fetches all command or not. right now I am clueless how to fix this.
public string RunScript( string contentScript, Dictionary<string, EntityProperty> parameters )
{
List<string> parameterList = new List<string>();
foreach( var item in parameters )
{
parameterList.Add( item.Value.ToString() );
}
using( PowerShell ps = PowerShell.Create() )
{
IAsyncResult async =
ps.AddCommand( "Import-Module" ).AddArgument( #"C:\Users\...\.D.PowerShell.dll" )
.AddStatement()
.AddCommand( "Get-RowAndPartitionKey" ).AddParameter( "Properties", "test" )
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach( PSObject result in ps.EndInvoke( async ) )
{
stringBuilder.AppendLine( result.ToString() );
}
return stringBuilder.ToString();
}
}
}
Below method do not return any error in Streams.Error or Verbose but no output also:
public async Task<IEnumerable<object>> RunScript( string scriptContents, List<string> scriptParameters )
{
// create a new hosted PowerShell instance using the default runspace.
// wrap in a using statement to ensure resources are cleaned up.
using( PowerShell ps = PowerShell.Create() )
{
// specify the script code to run.
ps.AddScript( scriptContents );
// specify the parameters to pass into the script.
ps.AddParameter( "Properties" ,"test") ;
// execute the script and await the result.
var pipelineObjects = await ps.InvokeAsync().ConfigureAwait( false );
return pipelineObjects;
}
}
scriptContent
"\"$path = 'C:\\Users...\\.TabularData.PowerShell.dll'\\r\\nImport-Module $path\\r\\nGet-RowAndPartitionKeys\""
The following is self-contained PowerShell sample code that uses on-demand compilation of C# code:
It shows that the approach works in principle, as described in this answer to your original question.
Prerequisites: The PowerShell SDK package and .NET runtime used in the C# project that calls your custom Get-RowAndPartitionKey" cmdlet must be compatible with the PowerShell SDK and .NET runtime that you used to compile the assembly DLL that houses that cmdlet, to be imported via Import-Module.
The sample code below ensures that implicitly, by running directly from PowerShell, using the Add-Type cmdlet to compile C# code on demand - it works in Windows PowerShell as well as in PowerShell (Core) 7+.
In practice I've found that a .NET Framework-compiled DLL (from Windows PowerShell) also works in PowerShell (Core) (.NET (Core) 5.0), but not vice versa.
It shows troubleshooting techniques, namely:
Adding the -Verbose switch to the Import-Module call to produce verbose output that lists the commands being imported from the given module (DLL).
Printing these verbose messages (look for // --- TROUBLESHOOTING CODE)
Printing any non-terminating PowerShell errors that occurred (as opposed to exceptions that you'd have to handle in C# code).
# Create a (temporary) assembly containing cmdlet "Get-RowAndPartitionKey".
# This assembly can directly be imported as a module from PowerShell.
# The cmdlet simply outputs "Hi from Get-RowAndPartitionKey" and
# echoes the elements of the list passed to -Properties, one by one.
$tempModuleDll = Join-Path ([IO.Path]::GetTempPath()) "TempModule_$PID.dll"
Remove-Item -ErrorAction Ignore $tempModuleDll
Add-Type #'
using System.Management.Automation;
using System.Collections.Generic;
[Cmdlet("Get", "RowAndPartitionKey")]
public class GetRowAndPartitionKeyCmdlet : PSCmdlet {
[Parameter] public List<string> Properties { get; set; }
protected override void ProcessRecord() {
WriteObject("Hi from Get-RowAndPartitionKey: ");
WriteObject(Properties, true);
}
}
'# -ErrorAction Stop -OutputAssembly $tempModuleDll
# Compile a C# class ad hoc to simulate your project, and call its static
# method, which imports the module and effectively calls
# Get-RowAndPartitionKey -Properties "foo", "bar"
(Add-Type #"
using System;
using System.Management.Automation;
using System.Collections.Generic;
using System.Text;
public static class Foo {
public static string RunScript(List<string> parameterList)
{
using (System.Management.Automation.PowerShell ps = PowerShell.Create())
{
IAsyncResult async =
// Add -Verbose to the Import-Module call, so that the list of
// commands being imported is written to the verbose output stream.
ps.AddCommand("Import-Module").AddArgument(#"$tempModuleDll").AddParameter("Verbose", true)
.AddStatement()
.AddCommand("Get-RowAndPartitionKey").AddParameter("Properties", parameterList)
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject result in ps.EndInvoke(async))
{
stringBuilder.AppendLine(result.ToString());
}
// --- TROUBLESHOOTING CODE
// Print verbose output from the Import-Module call
foreach (var v in ps.Streams.Verbose) { Console.WriteLine("VERBOSE: " + v.ToString()); }
// Print any errors.
foreach (var e in ps.Streams.Error) { Console.WriteLine("ERROR: " + e.ToString()); }
// ---
return stringBuilder.ToString();
}
}
}
"# -ErrorAction Stop -PassThru)::RunScript(("foo", "bar"))
# Clean-up instructions:
if ($env:OS -eq 'Windows_NT') {
Write-Verbose -vb "NOTE: Re-running this code requires you to start a NEW SESSION."
Write-Verbose -vb "After exiting this session, you can delete the temporary module DLL(s) with:`n`n Remove-Item $($tempModuleDll -replace '_.+', '_*.dll')`n "
} else {
Write-Verbose -vb "NOTE: Re-running this code after modifying the embedded C# code requires you to start a NEW SESSION."
Remove-Item $tempModuleDll
}
On my Windows 10 machine, both from PowerShell (Core) 7.0.5 and Windows PowerShell 5.1, the above yields (clean-up instructions omitted) the following, showing that everything worked as intended:
VERBOSE: Loading module from path 'C:\Users\jdoe\AppData\Local\Temp\TempModule_11876.dll'.
VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'.
Hi from Get-RowAndPartitionKey:
foo
bar
Specifically, line VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'. indicates that the custom cmdlet was successfully imported into the session.

How to discover PowerShell Script parameters in c#

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

How to start Azure Storage Emulator from within a program

I have some unit tests that use Azure Storage. When running these locally, I want them to use the Azure Storage emulator which is part of the Azure SDK v1.5. If the emulator isn't running, I want it to be started.
To start the emulator from the command line, I can use this:
"C:\Program Files\Windows Azure SDK\v1.5\bin\csrun" /devstore
This works fine.
When I try to start it using this C# code, it crashes:
using System.IO;
using System.Diagnostics;
...
ProcessStartInfo processToStart = new ProcessStartInfo()
{
FileName = Path.Combine(SDKDirectory, "csrun"),
Arguments = "/devstore"
};
Process.Start(processToStart);
I've tried fiddling with a number of ProcessStartInfo settings, but nothing seems to work. Is anybody else having this problem?
I've checked the Application Event Log and found the following two entries:
Event ID: 1023
.NET Runtime version 2.0.50727.5446 - Fatal Execution Engine Error (000007FEF46B40D2) (80131506)
Event ID: 1000
Faulting application name: DSService.exe, version: 6.0.6002.18312, time stamp: 0x4e5d8cf3
Faulting module name: mscorwks.dll, version: 2.0.50727.5446, time stamp: 0x4d8cdb54
Exception code: 0xc0000005
Fault offset: 0x00000000001de8d4
Faulting process id: 0x%9
Faulting application start time: 0x%10
Faulting application path: %11
Faulting module path: %12
Report Id: %13
Updated 7/12/2022:
If you are running Visual Studio 2022, azurite.exe is the replacement for the now-deprecated AzureStorageEmulator.exe which can be found here:
C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\Extensions\Microsoft\Azure Storage Emulator\azurite.exe
NB: if you are running Professional (or another) Edition, you'll need to replace Community with Professional (or the appropriate edition name) in the path.
Updated 1/19/2015:
After doing more testing (i.e., running several builds), I've discovered that WAStorageEmulator.exe's status API is actually broken in a couple of significant ways (which may or may not have impact on how you use it).
The status reports False even when an existing process is running if the user differs between the existing running process and the user used to launch the status process. This incorrect status report will lead to a failure to launch the process that looks like this:
C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator>WAStorageEmulator.exe status
Windows Azure Storage Emulator 3.4.0.0 command line tool
IsRunning: False
BlobEndpoint: http://127.0.0.1:10000/
QueueEndpoint: http://127.0.0.1:10001/
TableEndpoint: http://127.0.0.1:10002/
C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator>WAStorageEmulator.exe start
Windows Azure Storage Emulator 3.4.0.0 command line tool
Error: Port conflict with existing application.
Additionally, the status command appears only to report the endpoints specified in WAStorageEmulator.exe.config, not those of the existing running process. I.e., if you start the emulator, then make a change to the config file, and then call status, it will report the endpoints listed in the config.
Given all of these caveats, it may, in fact, simply be better to use the original implementation as it appears to be more reliable.
I will leave both so others can choose whichever solution works for them.
Updated 1/18/2015:
I have fully rewritten this code to properly leverage WAStorageEmulator.exe's status API per #RobertKoritnik's request.
public static class AzureStorageEmulatorManager
{
public static bool IsProcessRunning()
{
bool status;
using (Process process = Process.Start(StorageEmulatorProcessFactory.Create(ProcessCommand.Status)))
{
if (process == null)
{
throw new InvalidOperationException("Unable to start process.");
}
status = GetStatus(process);
process.WaitForExit();
}
return status;
}
public static void StartStorageEmulator()
{
if (!IsProcessRunning())
{
ExecuteProcess(ProcessCommand.Start);
}
}
public static void StopStorageEmulator()
{
if (IsProcessRunning())
{
ExecuteProcess(ProcessCommand.Stop);
}
}
private static void ExecuteProcess(ProcessCommand command)
{
string error;
using (Process process = Process.Start(StorageEmulatorProcessFactory.Create(command)))
{
if (process == null)
{
throw new InvalidOperationException("Unable to start process.");
}
error = GetError(process);
process.WaitForExit();
}
if (!String.IsNullOrEmpty(error))
{
throw new InvalidOperationException(error);
}
}
private static class StorageEmulatorProcessFactory
{
public static ProcessStartInfo Create(ProcessCommand command)
{
return new ProcessStartInfo
{
FileName = #"C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\WAStorageEmulator.exe",
Arguments = command.ToString().ToLower(),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
}
}
private enum ProcessCommand
{
Start,
Stop,
Status
}
private static bool GetStatus(Process process)
{
string output = process.StandardOutput.ReadToEnd();
string isRunningLine = output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).SingleOrDefault(line => line.StartsWith("IsRunning"));
if (isRunningLine == null)
{
return false;
}
return Boolean.Parse(isRunningLine.Split(':').Select(part => part.Trim()).Last());
}
private static string GetError(Process process)
{
string output = process.StandardError.ReadToEnd();
return output.Split(':').Select(part => part.Trim()).Last();
}
}
And the corresponding tests:
[TestFixture]
public class When_starting_process
{
[Test]
public void Should_return_started_status()
{
if (AzureStorageEmulatorManager.IsProcessRunning())
{
AzureStorageEmulatorManager.StopStorageEmulator();
Assert.That(AzureStorageEmulatorManager.IsProcessRunning(), Is.False);
}
AzureStorageEmulatorManager.StartStorageEmulator();
Assert.That(AzureStorageEmulatorManager.IsProcessRunning(), Is.True);
}
}
[TestFixture]
public class When_stopping_process
{
[Test]
public void Should_return_stopped_status()
{
if (!AzureStorageEmulatorManager.IsProcessRunning())
{
AzureStorageEmulatorManager.StartStorageEmulator();
Assert.That(AzureStorageEmulatorManager.IsProcessRunning(), Is.True);
}
AzureStorageEmulatorManager.StopStorageEmulator();
Assert.That(AzureStorageEmulatorManager.IsProcessRunning(), Is.False);
}
}
Original post:
I took Doug Clutter's and Smarx's code one step further and created a utility class:
The code below has been updated to work on both Windows 7 and 8 and now points at the new storage emulator path as of SDK 2.4.**
public static class AzureStorageEmulatorManager
{
private const string _windowsAzureStorageEmulatorPath = #"C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\WAStorageEmulator.exe";
private const string _win7ProcessName = "WAStorageEmulator";
private const string _win8ProcessName = "WASTOR~1";
private static readonly ProcessStartInfo startStorageEmulator = new ProcessStartInfo
{
FileName = _windowsAzureStorageEmulatorPath,
Arguments = "start",
};
private static readonly ProcessStartInfo stopStorageEmulator = new ProcessStartInfo
{
FileName = _windowsAzureStorageEmulatorPath,
Arguments = "stop",
};
private static Process GetProcess()
{
return Process.GetProcessesByName(_win7ProcessName).FirstOrDefault() ?? Process.GetProcessesByName(_win8ProcessName).FirstOrDefault();
}
public static bool IsProcessStarted()
{
return GetProcess() != null;
}
public static void StartStorageEmulator()
{
if (!IsProcessStarted())
{
using (Process process = Process.Start(startStorageEmulator))
{
process.WaitForExit();
}
}
}
public static void StopStorageEmulator()
{
using (Process process = Process.Start(stopStorageEmulator))
{
process.WaitForExit();
}
}
}
This program worked fine for me. Give it a try, and if it works for you too, work backwards from there. (What about your app is different from this?)
using System.Diagnostics;
public class Program
{
public static void Main() {
Process.Start(#"c:\program files\windows azure sdk\v1.5\bin\csrun", "/devstore").WaitForExit();
}
}
The file name in v4.6 is "AzureStorageEmulator.exe". The full path is: "C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe"
For Windows Azure Storage Emulator v5.2, the following helper class can be used to start the emulator:
using System.Diagnostics;
public static class StorageEmulatorHelper {
/* Usage:
* ======
AzureStorageEmulator.exe init : Initialize the emulator database and configuration.
AzureStorageEmulator.exe start : Start the emulator.
AzureStorageEmulator.exe stop : Stop the emulator.
AzureStorageEmulator.exe status : Get current emulator status.
AzureStorageEmulator.exe clear : Delete all data in the emulator.
AzureStorageEmulator.exe help [command] : Show general or command-specific help.
*/
public enum StorageEmulatorCommand {
Init,
Start,
Stop,
Status,
Clear
}
public static int StartStorageEmulator() {
return ExecuteStorageEmulatorCommand(StorageEmulatorCommand.Start);
}
public static int StopStorageEmulator() {
return ExecuteStorageEmulatorCommand(StorageEmulatorCommand.Stop);
}
public static int ExecuteStorageEmulatorCommand(StorageEmulatorCommand command) {
var start = new ProcessStartInfo {
Arguments = command.ToString(),
FileName = #"C:\Program Files (x86)\Microsoft SDKs\Azure\Storage Emulator\AzureStorageEmulator.exe"
};
var exitCode = executeProcess(start);
return exitCode;
}
private static int executeProcess(ProcessStartInfo startInfo) {
int exitCode = -1;
try {
using (var proc = new Process {StartInfo = startInfo}) {
proc.Start();
proc.WaitForExit();
exitCode = proc.ExitCode;
}
}
catch {
//
}
return exitCode;
}
}
[Thanks to huha for the boilerplate code to execute a shell command.]
FYI - The 1.6 default location is C:\Program Files\Windows Azure Emulator\emulator as stated on the MSDN docs.
We are running into the same issue. We have the concept of a "smoke test" which runs between groups of tests, and which ensure the environment is in a good state before the next group starts. We have a .cmd file that kicks off the smoke tests, and it works just fine starting the devfabric emulator, but the devstore emulator only runs as long as the .cmd process runs.
Apparently the implementation of the DSServiceSQL.exe is different than DFService.exe. DFService seems to run like a windows service - kick it off, and it keeps running. DSServiceSQL dies as soon as the process that started it dies.
I uninstalled all of the Windows Azure bits:
WA SDK v1.5.20830.1814
WA Tools for Visual Studio: v1.5.40909.1602
WA AppFabric: v1.5.37
WA AppFabric: v2.0.224
Then, I downloaded and installed everything using the unified installer. Everything came back except the AppFabric v2. All the version numbers are the same. Reran my tests and still having a problem.
And then...(this is weird)...it would work every now and then. Rebooted the machine and now it works. Have shutdown and rebooted a number of times now...and it just works. (sigh)
Thanks to everyone who provided feedback and/or ideas!
The final code is:
static void StartAzureStorageEmulator()
{
ProcessStartInfo processStartInfo = new ProcessStartInfo()
{
FileName = Path.Combine(SDKDirectory, "csrun.exe"),
Arguments = "/devstore",
};
using (Process process = Process.Start(processStartInfo))
{
process.WaitForExit();
}
}
maybe caused by file not found?
try this
FileName = Path.Combine(SDKDirectory, "csrun.exe")
Here we go: Pass the string "start" to the method ExecuteWAStorageEmulator().
The NUnit.Framework is used for the Assert only.
using System.Diagnostics;
using NUnit.Framework;
private static void ExecuteWAStorageEmulator(string argument)
{
var start = new ProcessStartInfo
{
Arguments = argument,
FileName = #"c:\Program Files (x86)\Microsoft SDKs\Windows Azure\Storage Emulator\WAStorageEmulator.exe"
};
var exitCode = ExecuteProcess(start);
Assert.AreEqual(exitCode, 0, "Error {0} executing {1} {2}", exitCode, start.FileName, start.Arguments);
}
private static int ExecuteProcess(ProcessStartInfo start)
{
int exitCode;
using (var proc = new Process { StartInfo = start })
{
proc.Start();
proc.WaitForExit();
exitCode = proc.ExitCode;
}
return exitCode;
}
See also my new self-answered question
There's now a neat little NuGet package to assist with starting/stopping the Azure Storage Emulator programmatically: RimDev.Automation.StorageEmulator.
The source code is available in this GitHub repository, but you can essentially do things like this:
if(!AzureStorageEmulatorAutomation.IsEmulatorRunning())
{
AzureStorageEmulatorAutomation emulator = new AzureStorageEmulatorAutomation();
emulator.Start();
// Even clear some things
emulator.ClearBlobs();
emulator.ClearTables();
emulator.ClearQueues();
emulator.Stop();
}
It feels like the cleanest option to me.

Formatting Output of Hosted PowerShell

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.

How to capture a Powershell CmdLet's verbose output when the CmdLet is programmatically Invoked from C#

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.

Categories

Resources