Asynchronously running multiple PowerShell scripts from C# - c#

Short description of what I`m trying to do:
I am working on a .Net WinForm application from where I am trying to run multiple PowerShell scripts on a remote server and display results on the form.
At this moment I`m executing the scripts synchronously and this is causing me problems with long running scripts.
Any idea on how I could make this function to be executed Asynchronously?
public string NewPsSession(string ServerName, string command)
{
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
PowerShell psSession = PowerShell.Create();
psSession.Commands.AddScript("$sessions = New-PSSession -ComputerName " + ServerName + Environment.NewLine
+ "Invoke-Command -session $sessions -ScriptBlock {" + command + "}" + Environment.NewLine
+ "Remove-PSSession -Session $sessions" + Environment.NewLine);
psSession.Commands.AddCommand("Out-String");
Collection<PSObject> results = new Collection<PSObject>();
try
{
results = psSession.Invoke();
}
catch (Exception ex)
{
results.Add(new PSObject((object)ex.Message));
}
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
return stringBuilder.ToString();
}
Any suggestion would be much appreciated. Thanks!

Change your NewPsSesson method to async and return a Task<string>. Then move your code into a Task<string>.Run() block and await it. Then you can either await your NewPsSession() method or monitor it as a task as I have done in Main()
class Program
{
public static void Main(string[] args)
{
Task<string> task = NewPsSession("", "");
while (!task.IsCompleted)
{
Task.Delay(500).Wait();
Console.WriteLine("Waiting...");
}
Console.WriteLine(task.Result);
Console.WriteLine("Done");
}
public static async Task<string> NewPsSession(string ServerName, string command)
{
var result = await Task<string>.Run(() =>
{
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
PowerShell psSession = PowerShell.Create();
psSession.Commands.AddScript("$sessions = New-PSSession -ComputerName " + ServerName + Environment.NewLine
+ "Invoke-Command -session $sessions -ScriptBlock {" + command + "}" + Environment.NewLine
+ "Remove-PSSession -Session $sessions" + Environment.NewLine);
psSession.Commands.AddCommand("Out-String");
Collection<PSObject> results = new Collection<PSObject>();
try
{
results = psSession.Invoke();
}
catch (Exception ex)
{
results.Add(new PSObject((object)ex.Message));
}
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
return stringBuilder.ToString();
});
return result;
}
}

Related

Calling powershell from .Net Console application

I have written a console application wherein I have called a powershell script from the console. In the powershell script I have written hello world as a return variable and it is running as expected but next time when I change the string from hello world to How are you it is not displaying the changed string. I cannot figure out myself what needs to be done to clear the pipeline or cache.
I have used the below namespace apart from default namespaces
using System.Management;
using System.Management.Automation;
using System.Collections.ObjectModel;
using System.Management.Automation.Runspaces;
static void Main(string[] args)
{
string _str = string.Empty;
_str= RunScript(#"C:\Powershell_Scripts\Test.ps1");
Console.WriteLine("Input String is =" + str);
Console.Read();
}
private static string RunScript(string scriptText)
{
// create Powershell runspace
Runspace runspace = RunspaceFactory.CreateRunspace();
// open it
runspace.Open();
// create a pipeline and feed it the script text
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted");
pipeline.Commands.AddScript(scriptText);
// add an extra command to transform the script
// output objects into nicely formatted strings
// remove this line to get the actual objects
// that the script returns. For example, the script
// "Get-Process" returns a collection
// of System.Diagnostics.Process instances.
pipeline.Commands.Add("Out-String");
// execute the script
Collection <PSObject> results = pipeline.Invoke();
pipeline.Streams.ClearStreams();
// close the runspace
runspace.Close();
// convert the script result into a single string
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
return stringBuilder.ToString();
}
Powershell Script i.e. Test1.ps1
sleep 3
$a=""
$a = "Hello word"
return $a
Here is a sample how to use the PowerShell in a Runspace
using System;
using System.Collections.Generic;
using System.Text;
using System.Management;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Create a runspace.
using (Runspace myRunSpace = RunspaceFactory.CreateRunspace())
{
myRunSpace.Open();
using (PowerShell powershell = PowerShell.Create())
{
// Create a pipeline with the Get-Command command.
powershell.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted");
powershell.AddScript(#"C:\Users\you\Desktop\a.ps1");
// add an extra command to transform the script output objects into nicely formatted strings
// remove this line to get the actual objects
powershell.AddCommand("Out-String");
// execute the script
var results = powershell.Invoke();
powershell.Streams.ClearStreams();
powershell.Commands.Clear();
// convert the script result into a single string
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
Console.WriteLine(stringBuilder.ToString());
}
}
}
}
}
Further reference: Creating a constrained runspace
Here is a code which runs as expected
enter code here
try
{
if (System.IO.File.Exists(#"c:\disk\op.txt") && (new FileInfo(#"c:\disk\op.txt").Length != 0))
{
System.IO.File.Move(#"c:\disk\op.txt", #"c:\disk\Previous_op_" + DateTime.Now.ToString("dd_MM_yyyy hh mm") + ".txt");
log.Info("Previous Output File Successfully Renamed");
}
// TODO: Add delete logic here
log.Info("Input ActionResult - Server " + Server);
log.Info("Input ActionResult - Volume " + Volume);
log.Info("Input ActionResult - Size " + size);
string userID = "dir\\" + Session["Uname"].ToString();
string userpassword = Session["Upwd"].ToString();
log.Info("username " + userID);
StringBuilder stringBuilder = new StringBuilder();
var con = new WSManConnectionInfo();
log.Info("Pushing username in PSCredential- " + userID.ToString().Trim());
con.Credential = new PSCredential(userID.ToString().Trim(), userpassword.ToString().Trim().ToSecureString());
Runspace runspace = RunspaceFactory.CreateRunspace(con);
runspace.Open();
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted");
string _str = #"-Server " + Server + " -Volumeletter " + Volume + ": -deltasize " + size + " -Logfile c:\\disk\\op.txt -username " + userID + " -password " + userpassword;
log.Info("Parameter string format- " + _str.Substring(0, _str.IndexOf("-password") + 9));
pipeline.Commands.AddScript(#"C:\disk\diskerr.ps1 " + _str.ToString());
pipeline.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
pipeline.Commands.Add("Out-String");
var results = pipeline.Invoke();
runspace.Close();
runspace.Dispose();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
log.Info("Output from powershell: " + obj.ToString());
}
if (System.IO.File.Exists(#"c:\disk\op.txt") && (new FileInfo(#"c:\disk\op.txt").Length != 0))
{
fileStream = new FileStream(#"c:\test\op.txt", FileMode.Open, FileAccess.Read);
using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
{
_consoleOutput = streamReader.ReadToEnd();
}
_output = Regex.Replace(_consoleOutput, #"\r\n?|\n", "<br />");
}
return Content(_output);
}
catch (Exception ex)
{
log.Info("Error stackTrace inside Input ActionResult " + ex.StackTrace.ToString());
log.Info("Error Message inside Input ActionResult " + ex.Message.ToString());
return View();
}
You need to change the directory path and the powershell file name.

Runspace PowerShell output

I have powershell script file, which does something and outputs the result. If I'm manually running it on my remote host using cmd, I'll get the clear output.
But when I'm trying to execute script via PowerShell Runspace using c# I'm not able to see script's output. Only command itself.
using (PowerShell powershell = PowerShell.Create())
{
try
{
PowerShell ps = PowerShell.Create();
ps.AddScript("c:\temp\myps.ps1");
ICollection<PSObject> results = ps.Invoke();
using (StringWriter sw = new StringWriter())
{
foreach (PSObject invoke in results)
sw.WriteLine(invoke.ToString());
outp = sw.ToString();
}
//Script errors
if (ps.Streams.Error.Count > 0)
{
outp += Environment.NewLine + string.Format("{0} errors: ", ps.Streams.Error.Count);
foreach (ErrorRecord err in ps.Streams.Error)
outp += Environment.NewLine + err.ToString();
}
}
catch (Exception e)
{
// Terminating errors
outp = "Critical error: " + e.Message;
}
}
remoteRunspace.Close();
//In the end outp for me = "c:\temp\myps.ps1"
How can I access the output from C#?
See this documentation about the PowerShell class: https://msdn.microsoft.com/en-us/library/system.management.automation.powershell(v=vs.85).aspx
Looks like you need to access the members of the PSObject to get the output. Otherwise, it will just send laim ToString() data out to the StringBuilder object. Do you have any example of what you're getting back you can show? I may be able to provide a better answer with a sample. Also, where's the remote runspace located?
Thanks!
Next code working for me:
try
{
PowerShell ps = PowerShell.Create().AddScript("c:\temp\myps.ps1", true);
ps.Runspace = remoteRunspace;
ICollection<PSObject> results = ps.Invoke();
using (StringWriter sw = new StringWriter())
{
foreach (PSObject invoke in results)
sw.WriteLine(invoke.ToString());
outp = sw.ToString();
}
//Script errors
if (ps.Streams.Error.Count > 0)
{
outp += Environment.NewLine + string.Format("{0} errors: ", ps.Streams.Error.Count);
foreach (ErrorRecord err in ps.Streams.Error)
outp += Environment.NewLine + err.ToString();
}
}
catch (Exception e)
{
// Terminating errors
outp = "Critical error: " + e.Message;
}
}
remoteRunspace.Close();

Get-MailboxPermission exception calling "GetSteppablePipeline"

I am getting an Exception while running the following PoweShell command:
Get-mailboxPermission -Identity MAILBOX_EMAIL |
fl User,AccessRights,IsInherited,Deny | Out-String -Width 300
Exception:
Exception calling "GetSteppablePipeline" with "1" argument(s): "Cannot find the type for custom attribute 'Parameter'.
Make sure that the assembly that contains this type is loaded."
Followed by this
Get-mailboxPermission -Identity "mailboxidentity" |
fl User,AccessRights,IsInherited,Deny | Out-String -Width 300
Which is getting me a ParameterBindingException
A parameter cannot be found that matches parameter name 'CommandName'.
This is the part of my code:
I will send the above command to executeRemoteCommand function for all the users one by one.
private void initializeRunspace() {
runSpace = RunspaceFactory.CreateRunspace();
runSpace.Open();
createRemotePowershell();
executeRemoteCommand("Set-ExecutionPolicy -ExecutionPolicy RemoteSigned",true);
executeRemoteCommand("Import-PSSession -Session $session -CommandName 'Get-MailboxPermission' ,'Get-MailboxDatabase','Add-ADPermission', 'Get-ADPermission', 'Set-EventLogLevel', 'Get-EventLogLevel'",true);
log("Remote powershell initialization ends");
}
public void createRemotePowershell() {
powershell = PowerShell.Create();
powershell.Runspace = runSpace;
PSCommand rmcommand = new PSCommand();
rmcommand.AddCommand("New-PSSession");
rmcommand.AddParameter("ConfigurationName", "Microsoft.Exchange");
rmcommand.AddParameter("ConnectionUri", uri);
if (creds != null) {
rmcommand.AddParameter("Credential", creds);
}
rmcommand.AddParameter("Authentication", "kerberos");
PSSessionOption sessionOption = new PSSessionOption();
sessionOption.SkipCACheck = true;
sessionOption.SkipCNCheck = true;
sessionOption.SkipRevocationCheck = true;
rmcommand.AddParameter("SessionOption", sessionOption);
powershell.Commands = rmcommand;
Collection<PSSession> result = powershell.Invoke<PSSession>();
log("Remote Powershell Count: "+result.Count);
if (result.Count != 1) {
throw new Exception("Unexpected number of Remote Runspace connections returned.");
}
rmcommand = new PSCommand();
rmcommand.AddCommand("Set-Variable");
rmcommand.AddParameter("Name", "session");
rmcommand.AddParameter("Value", result[0]);
powershell.Commands = rmcommand;
powershell.Invoke();
}
private String executeRemoteCommand(String shellcommand,bool retryOnSessionExpiry) {
try {
if (runSpace == null) {
initializeRunspace();
}
rmcommand = new PSCommand();
rmcommand.AddScript(shellcommand);
rmcommand.Commands.Add("Out-String");
powershell.Commands = rmcommand;
Collection<PSObject> results = powershell.Invoke();
StringBuilder sb = new StringBuilder();
foreach (PSObject obj in results) {
sb.Append(obj.ToString());
}
return sb.ToString();
} catch (CmdletInvocationException ex) {
log("RemotePowershell: CmdletInvocationException - " + shellcommand + " - " +ex.Message);
return "Invalid";
} catch (ParseException ex) {
log("RemotePowershell: ParseException - " + shellcommand + " - " +ex.Message);
return "Invalid";
} catch (ParameterBindingException ex) {
log("RemotePowershell: ParameterBindingException - " + shellcommand + " - " +ex.Message);
return "Invalid";
} catch (CommandNotFoundException ex) {
log("RemotePowershell: CommandNotFoundException - " + shellcommand + " - " +ex.Message);
return "Invalid";
} catch (PSArgumentException ex) {
log("RemotePowershell: PSArgumentException - " + shellcommand + " - " +ex.Message);
return "EMS";
} catch (Exception e) {
if(e.Message.Equals("Unexpected number of Remote Runspace connections returned.")) {
log("remoteShell cannot be established");
return "remoteShell Failed";
} else if(e.Message.Contains("PromptForCredential") && retryOnSessionExpiry) {
log("Session Expired reinitializing runspace");
try {
closeRunSpace();
initializeRunspace();
log("Session Expired runspace reinitialized");
executeRemoteCommand(shellcommand,false);
} catch(Exception ex) {
if(ex.Message.Equals("Unexpected number of Remote Runspace connections returned.")) {
log("Session Expired: remoteShell cannot be established - " + shellcommand + " - " +ex.Message);
closeRunSpace();
return "remoteShell Failed";
}
}
}
log("RemotePowershell: Exception - " + shellcommand + " - " +e.Message);
return "EMSError" + e;
}
}

C# System.Management.Automation.PowerShell Performance

Why is performance of executing a script from PowerShell directly much quicker than when executing the script from the System.Management.Automation.PowerShell class? When I execute my script directly in PS, it takes less than a second, though when I execute it through the code below, the Invoke takes several minutes.
public bool Execute(string filename, out string result)
{
StringBuilder sb = new StringBuilder();
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace.SessionStateProxy.SetVariable("filename", filename);
ps.AddScript(this.script);
Collection<PSObject> psOutput = ps.Invoke();
foreach (PSObject item in psOutput)
{
Console.WriteLine(item);
}
PSDataCollection<ErrorRecord> errors = ps.Streams.Error;
foreach (ErrorRecord err in errors)
{
Console.WriteLine(err);
}
result = sb.ToString();
return errors.Count == 0;
}
}
Script text:
[regex]$r = "\$\%\$##(.+)\$\%\$##";
(Get-Content $filename) |
Foreach-Object {
$line = $_;
$find = $r.matches($line);
if ($find[0].Success) {
foreach ($match in $find) {
$found = $match.value
$replace = $found -replace "[^A-Za-z0-9\s]", "";
$line.Replace($found, $replace);
}
}
else
{
$line;
}
} |
Set-Content $filename
I tested the execution of the script against this method in c#, and this method takes less than 2 seconds to execute the script.
public bool Execute(string filename, out string result)
{
StringBuilder standardOutput = new StringBuilder();
string currentPath = Path.GetDirectoryName(filename);
string psFile = Path.Combine(currentPath, Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".ps1");
this.script = this.script.Replace("$filename", "\"" + filename + "\"");
File.WriteAllText(psFile, this.script);
using (Process process = new Process())
{
process.StartInfo = new ProcessStartInfo("powershell.exe", String.Format("-executionpolicy unrestricted \"{0}\"", psFile))
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
RedirectStandardOutput = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
while (!process.HasExited)
{
standardOutput.Append(process.StandardOutput.ReadToEnd());
}
process.WaitForExit();
standardOutput.Append(process.StandardOutput.ReadToEnd());
result = standardOutput.ToString();
return process.ExitCode == 0;
}
}

How to call powershell script with configfile,parameters in c#

I am new powershell script in c#. I have a powershell script file ps.ps1 and powershell settingfile ConsoleSettings.psc1
C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe -psconsolefile "D:\e\ConsoleSettings.psc1" -noexit -command ". 'D:\e\ps.ps1'"
run it and get "
Get-RST -SearchRoot 'erd/user' -PasswordNeverExpires:$false
-PasswordNotChangedFor 60 -enabled
my function result correctly.
Now, i want to get this result in c# . My code is;
private void button1_Click(object sender, EventArgs e)
{
RunScript(LoadScript(#"d:\e\ps.ps1"));
}
private string RunScript(string scriptText)
{
PSConsoleLoadException x = null; ;
RunspaceConfiguration rsconfig = RunspaceConfiguration.Create(#"d:\e\ConsoleSettings.psc1", out x);
Runspace runspace = RunspaceFactory.CreateRunspace(rsconfig);
runspace.Open();
RunspaceInvoke runSpaceInvoker = new RunspaceInvoke(runspace);
runSpaceInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
Pipeline pipeline = runspace.CreatePipeline();
pipeline.Commands.AddScript(scriptText);
pipeline.Commands.Add("Get-RST -SearchRoot 'erd/user' -PasswordNeverExpires:$false -PasswordNotChangedFor 60 -enabled");
Collection<PSObject> results = pipeline.Invoke();
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
return stringBuilder.ToString();
}
private string LoadScript(string filename)
{
try
{
using (StreamReader sr = new StreamReader(filename))
{
StringBuilder fileContents = new StringBuilder();
string curLine;
while ((curLine = sr.ReadLine()) != null)
{
fileContents.Append(curLine + "\n");
}
return fileContents.ToString();
}
}
catch (Exception e)
{
string errorText = "The file could not be read:";
errorText += e.Message + "\n";
return errorText;
}
}
And then i have a error : the term "Get-RST -SearchRoot 'erd/user' -PasswordNeverExpires:$false -PasswordNotChangedFor 60 -enabled" is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
How to solve this problem, or how to call powershell script with configfile, parameter like (Get-RST -SearchRoot 'erd/user' -PasswordNeverExpires:$false -PasswordNotChangedFor 60 -enabled) in c#
please help me...
You are adding your command line as a Command rather than a script. Commands are intended for things like cmdlets or functions without parameters. You will use the additional methods to add the parameters. A simple solution would be to just use AddScript again.
pipeline.AddScript("Get-RST -SearchRoot 'erd/user' -PasswordNeverExpires:$false -PasswordNotChangedFor 60 -enabled");

Categories