In my C# app, I have to run some PowerShell scripts. I copied paste from this site code about how to run the scripts.
My question: suppose I want to use the code from the link, how can I extract the PowerShell output to some string or to some .txt file?
EDIT:
If you want to test this code for answering this post, you need:
add reference to System.Management.Automation dll
add requireAdministrator inside app.manifest
Based on this link, you can extract the output with Collection<PSObject> results = pipeline.Invoke();, here a code example, note this would extract the PowerShell output to StringBuiler and would hide the PowerShell window:
string RunScript(string pathToYourScript){
RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
using (var runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration))
{
runspace.Open();
runspace.SessionStateProxy.SetVariable("prog", this);
using (Pipeline pipeline = runspace.CreatePipeline())
{
if (!string.IsNullOrEmpty(path))
pipeline.Commands.AddScript(string.Format("$env:path = \"{0};\" + $env:path", pathToYourScript));
pipeline.Commands.AddScript(path);
pipeline.Commands.Add("Out-String");
Collection<PSObject> results = pipeline.Invoke();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
var outDefault = new Command("out-default");
outDefault.MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
pipeline.Commands.Add(outDefault);
return stringBuilder;
}
}
}
Related
I am running a PowerShell script using C#. The build is not able to determine the different file path written in the script but if I run script from command line it is working fine.
Here is my code for the running script:
private const string ScriptPath = "F:\\";
private const string SubPath = "build\\Build.ps1";
public Collection<PSObject> ExecuteBuildScript(BuildParams buildParams)
{
string executablePath = String.Empty;
string[] subdirectories = Directory.GetDirectories(ScriptPath);
Collection<PSObject> psOutput = null;
//Get path of appropriate branch
foreach (var subdirectory in subdirectories)
{
if (subdirectory.Contains(buildParams.Branch))
{
executablePath = subdirectory;
break;
}
}
if (!String.IsNullOrEmpty(executablePath))
{
using (PowerShell ps = PowerShell.Create())
{
//Enable the powershell execution on the system
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
RunspaceInvoke runSpaceInvoker = new RunspaceInvoke(runspace);
runSpaceInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
ps.AddScript(Path.Combine(executablePath, SubPath));
ps.AddParameter("kit", !string.IsNullOrEmpty(buildParams.Kit) ? buildParams.Kit : "3CLogic");
ps.AddParameter("config", !string.IsNullOrEmpty(buildParams.Config) ? buildParams.Config : "Release");
ps.AddParameter("version", !string.IsNullOrEmpty(buildParams.ClientVersion) ? buildParams.ClientVersion : "latest");
ps.AddParameter("revision", !string.IsNullOrEmpty(buildParams.ClientRevision) ? buildParams.ClientRevision : "latest");
ps.AddParameter("serviceversion", !string.IsNullOrEmpty(buildParams.ServiceVersion) ? buildParams.ServiceVersion : "latest");
psOutput = ps.Invoke();
// check the other output streams (for example, the error stream)
if (ps.Streams.Error.Count > 0)
{
Console.WriteLine(ps.Streams.Error[0]);
// error records were written to the error stream.
// do something with the items found.
}
}
}
return psOutput;
}
Let say I want to import another script from called script it just failed to get path. Example importing include.ps1 from build.ps1 just not working and also Get-Location pick location of IIS server location.
. build\include.ps1
Use the $MyInvocation variable to determine the current script directory and combine the path using the Join-Path cmdlet:
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
. (Join-Path $scriptPath 'build\include.ps1')
I have seen quite a number of solutions, but none addresses my problem.
I am trying to import a custom PowerShell module called "DSInternals" to my C# DLL.
https://github.com/MichaelGrafnetter/DSInternals
Everything in my code seems just fine, but when I try to get the available module it's not loaded.
The stream responds with
The term 'Get-ADReplAccount' 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.
Where Am I going wrong with this code?
InitialSessionState init = InitialSessionState.CreateDefault();
init.ImportPSModule(new string[] { #"D:\\DSInternals\\dsinternals.psd1" }); //location of the module files
Runspace runspace = RunspaceFactory.CreateRunspace(init);
runspace.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.Commands.AddCommand("Get-ADReplAccount"); //this command is un-recognized
foreach (PSObject result in ps.Invoke())
{
Console.WriteLine(result); //this always returns null
}
The issue was the .NET framework version in which the module was built in.
Adding a module which was built with a higher version of the .NET framework to the C# class will not work.
The module was built in 4.5.1 and I was working with version 2, adding
init.ThrowOnRunspaceOpenError=true;
Helped in catching the cause of the error.
Here is my final code that works
InitialSessionState init = InitialSessionState.CreateDefault();
init.ImportPSModule(new string[] { #"D:\\DSInternals\\dsinternals.psd1" }); //location of the module files
init.ThrowOnRunspaceOpenError = true;
Runspace runspace = RunspaceFactory.CreateRunspace(init);
runspace.Open();
var script =
"Get-ADReplAccount -SamAccountName peter -Domain BLABLA -Server dc.BLABLA.co.za -Credential $cred -Protocol TCP"; //my powershell script
_powershell = PowerShell.Create().AddScript(script);
_powershell.Runspace = runspace;
var results = _powershell.Invoke();
foreach (var errorRecord in _powershell.Streams.Progress)
Console.WriteLine(errorRecord);
foreach (var errorRecord in _powershell.Streams.Debug)
Console.WriteLine(errorRecord);
foreach (var errorRecord in _powershell.Streams.Error)
Console.WriteLine(errorRecord);
foreach (var errorRecord in _powershell.Streams.Warning)
Console.WriteLine(errorRecord);
var stringBuilder = new StringBuilder();
foreach (var obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
return stringBuilder.ToString();
I've been attempting to create a site collection from a custom web service I have built in C#. I already had some methods in this that ran some powershell commands so I figured I would just create the site using powershell commands like the code below. This code runs fine without error but does not create a site collection, seems as if it is doing nothing. Is there a better way or can someone see what may be wrong below?
public string CreateSiteCollection(string urlroot, string urlname, string database, string primaryadmin, string secondadmin, string language, string description, string title, string template)
{
//Find language and template code
string lang_code = get_lang_code(language);
string temp_code = get_temp_code(template);
Call the PowerShell.Create() method to create an empty pipeline
PowerShell ps = PowerShell.Create();
// Place our script in the string myscript
string myscript = string.Format(#"
Add-PSSnapin Microsoft.SharePoint.Powershell -erroraction 'silentlycontinue'
$template = Get-SPWebTemplate ""{0}""
New-SPSite {1}{2} -OwnerAlias '{3}' -SecondaryOwnerAlias '{4}' -Language {5} -Description '{6}' -ContentDatabase {7} -Template $template -Name '{8}'
", temp_code, urlroot, urlname, primaryadmin, secondadmin, lang_code, description, database, title);
// Create PowerShell runspace
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
Pipeline pipeline = runspace.CreatePipeline(); // create pipepline then feed it myscript
pipeline.Commands.AddScript(myscript);
pipeline.Commands.Add("Out-String");
Collection<PSObject> results;
try
{
// Executing the script
results = pipeline.Invoke();
}
catch (Exception e)
{
// An error occurred, return the exception
return string.Format("Exception caught: {0}", e);
}
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.AppendLine(obj.ToString());
}
string output = stringBuilder.ToString().Trim();
if (output == "")
{
return "PowerShell Commands ran sucessfully with no output";
}
else
{
return String.Format("PowerShell Commands ran sucessfully and returned the following: {0}", output);
}
} // End of CreateSiteColleciton
I may use the Admin.asmx web service instead, but it would be easier if I could get this working because it allows me more customization.
Is there a reason why you can't call the SharePoint server side object model directly from your web service like this, http://msdn.microsoft.com/en-us/library/office/ms411953(v=office.14).aspx.
Or is there a requirement to go through a set of CmdLets?
I decided to go the with the object model just because it makes more sense. Below is the code for creating a new site collection:
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite("http://servername:port/"))
{
using (SPWeb web = site.OpenWeb())
{
HttpContext.Current = null;
site.AllowUnsafeUpdates = true;
web.AllowUnsafeUpdates = true;
var newSite = site.WebApplication.Sites.Add(....);
}
}
});
I have the following sample Powershell script that is embedded in my C# application.
Powershell Code
$MeasureProps = "AssociatedItemCount", "ItemCount", "TotalItemSize"
$Databases = Get-MailboxDatabase -Status
foreach($Database in $Databases) {
$AllMBStats = Get-MailboxStatistics -Database $Database.Name
$MBItemAssocCount = $AllMBStats | %{$_.AssociatedItemCount.value} | Measure-Object -Average -Sum
$MBItemCount = $AllMBStats | %{$_.ItemCount.value} | Measure-Object -Average -Sum
New-Object PSObject -Property #{
Server = $Database.Server.Name
DatabaseName = $Database.Name
ItemCount = $MBItemCount.Sum
}
}
Visual Studio offers me the following embedding options:
Every PowerShell sample I've seen (MSDN on Exchange, and MSFT Dev Center) required me to chop up the Powershell command into "bits" and send it through a parser.
I don't want to leave lots of PS1 files with my application, I need to have a single binary with no other "supporting" PS1 file.
How can I make it so myapp.exe is the only thing that my customer sees?
Many customers are averse to moving away from a restricted execution policy because they don't really understand it. It's not a security boundary - it's just an extra hoop to jump through so you don't shoot yourself in the foot. If you want to run ps1 scripts in your own application, simply use your own runspace and use the base authorization manager which pays no heed to system execution policy:
InitialSessionState initial = InitialSessionState.CreateDefault();
// Replace PSAuthorizationManager with a null manager which ignores execution policy
initial.AuthorizationManager = new
System.Management.Automation.AuthorizationManager("MyShellId");
// Extract psm1 from resource, save locally
// ...
// load my extracted module with my commands
initial.ImportPSModule(new[] { <path_to_psm1> });
// open runspace
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
runspace.Open();
RunspaceInvoke invoker = new RunspaceInvoke(runspace);
// execute a command from my module
Collection<PSObject> results = invoker.Invoke("my-command");
// or run a ps1 script
Collection<PSObject> results = invoker.Invoke("c:\temp\extracted\my.ps1");
By using a null authorization manager, execution policy is completed ignored. Remember - this is not some "hack" because execution policy is something for protecting users against themselves. It's not for protecting against malicious third parties.
http://www.nivot.org/nivot2/post/2012/02/10/Bypassing-Restricted-Execution-Policy-in-Code-or-in-Script.aspx
First of all you should try removing your customer's aversion To scripts. Read up about script signing, execution policy etc.
Having said that, you can have the script as a multiline string in C# code itself and execute it.Since you have only one simple script, this is the easiest approach.
You can use the AddScript ,ethos which takes the script as a string ( not script path)
http://msdn.microsoft.com/en-us/library/dd182436(v=vs.85).aspx
You can embed it as a resource and retrieve it via reflection at runtime. Here's a link from MSDN. The article is retrieving embedded images, but the principle is the same.
You sort of hovered the answer out yourself. By adding it as content, you can get access to it at runtime (see Application.GetResourceStream). Then you can either store that as a temp file and execute, or figure out a way to invoke powershell without the use of files.
Store your POSH scripts as embedded resources then run them as needed using something like the code from this MSDN thread:
public static Collection<PSObject> RunScript(string strScript)
{
HttpContext.Current.Session["ScriptError"] = "";
System.Uri serverUri = new Uri(String.Format("http://exchangsserver.contoso.com/powershell?serializationLevel=Full"));
RunspaceConfiguration rc = RunspaceConfiguration.Create();
WSManConnectionInfo wsManInfo = new WSManConnectionInfo(serverUri, SHELL_URI, (PSCredential)null);
using (Runspace runSpace = RunspaceFactory.CreateRunspace(wsManInfo))
{
runSpace.Open();
RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace);
scriptInvoker.Invoke("Set-ExecutionPolicy Unrestricted");
PowerShell posh = PowerShell.Create();
posh.Runspace = runSpace;
posh.AddScript(strScript);
Collection<PSObject> results = posh.Invoke();
if (posh.Streams.Error.Count > 0)
{
bool blTesting = false;
string strType = HttpContext.Current.Session["Type"].ToString();
ErrorRecord err = posh.Streams.Error[0];
if (err.CategoryInfo.Reason == "ManagementObjectNotFoundException")
{
HttpContext.Current.Session["ScriptError"] = "Management Object Not Found Exception Error " + err + " running command " + strScript;
runSpace.Close();
return null;
}
else if (err.Exception.Message.ToString().ToLower().Contains("is of type usermailbox.") && (strType.ToLower() == "mailbox"))
{
HttpContext.Current.Session["ScriptError"] = "Mailbox already exists.";
runSpace.Close();
return null;
}
else
{
HttpContext.Current.Session["ScriptError"] = "Error " + err + "<br />Running command " + strScript;
fnWriteLog(HttpContext.Current.Session["ScriptError"].ToString(), "error", strType, blTesting);
runSpace.Close();
return null;
}
}
runSpace.Close();
runSpace.Dispose();
posh.Dispose();
posh = null;
rc = null;
if (results.Count != 0)
{
return results;
}
else
{
return null;
}
}
}
The customer just can't see the PowerShell script in what you deploy, right? You can do whatever you want at runtime. So write it to a temporary directory--even try a named pipe, if you want to get fancy and avoid files--and simply start the PowerShell process on that.
You could even try piping it directly to stdin. That's probably what I'd try first, actually. Then you don't have any record of it being anywhere on the computer. The Process class is versatile enough to do stuff like that without touching the Windows API directly.
I'm trying to call a function in a powershell file as follows:
string script = System.IO.File.ReadAllText(#"C:\Users\Bob\Desktop\CallPS.ps1");
using (Runspace runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
using (Pipeline pipeline = runspace.CreatePipeline(script))
{
Command c = new Command("BatAvg",false);
c.Parameters.Add("Name", "John");
c.Parameters.Add("Runs", "6996");
c.Parameters.Add("Outs", "70");
pipeline.Commands.Add(c);
Collection<PSObject> results = pipeline.Invoke();
foreach (PSObject obj in results)
{
// do somethingConsole.WriteLine(obj.ToString());
}
}
}
The powershell function is in CallPS.ps1:
Function BatAvg
{
param ($Name, $Runs, $Outs)
$Avg = [int]($Runs / $Outs*100)/100
Write-Output "$Name's Average = $Avg, $Runs, $Outs "
}
I'm getting the following exception:
The term 'BatAvg' is not recognized as the name of a cmdlet, function, script file, or operable program.
What am I doing wrong, I admit, I know very little about PowerShell.
This seems to work for me:
using (Runspace runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.AddScript(script);
ps.Invoke();
ps.AddCommand("BatAvg").AddParameters(new Dictionary<string, string>
{
{"Name" , "John"},
{"Runs", "6996"},
{"Outs","70"}
});
foreach (PSObject result in ps.Invoke())
{
Console.WriteLine(result);
}
}
As it seems the Runspace need to be connected to a Powershell to make that work - see the sample code at MSDN.
The solution can be simplified further as in this case a non-default Runspace is not needed.
var ps = PowerShell.Create();
ps.AddScript(script);
ps.Invoke();
ps.AddCommand("BatAvg").AddParameters(new Dictionary<string, string>
{
{"Name" , "John"}, {"Runs", "6996"}, {"Outs","70"}
});
foreach (var result in ps.Invoke())
{
Console.WriteLine(result);
}
One other pitfall would be to use AddScript(script, true) in order to use a local scope. The same exception would be encountered (i.e. "The term 'BatAvg' is not recognized as the name of a cmdlet, function, script file, or operable program.").