Trying to use a powershell script with a function as follows:
function MoveCompressFiles{
Param
(
[Parameter(Mandatory=$true )]
[string] $Des,
[Parameter(Mandatory=$true)]
[string] $Src
)
Add-Type -AssemblyName System.Drawing
$files = Get-ChildItem $Src
foreach ($f in $files) {
if (($f.Length / 1KB) -lt [int32]200) {
Copy-Item -Path $f.FullName -Destination $Des
}
else {
Copy-Item -Path $f.FullName -Destination $Des
while (((Get-Item (($Des).ToString() + "\$f")).Length / 1KB ) -gt 500) {
$img = [System.Drawing.Image]::FromFile((($Des).ToString() + "$f"))
[int32]$new_width = $img.Width * (20 / 100);
[int32]$new_height = $img.Height * (20 / 100);
$img2 = New-Object System.Drawing.Bitmap($new_width, $new_height)
$graph = [System.Drawing.Graphics]::FromImage($img2)
$graph.DrawImage($img, 0, 0, $new_width, $new_height)
$newImgName = "M".ToString() + $f.ToString()
$img2.Save(($Des).ToString()+"\$newImgName")
$img.Dispose()
$img2.Dispose()
Remove-Item ($Des.ToString()+$f)
Rename-Item -Path ($Des.ToString()+$newImgName) -NewName "$f"
Write-Host ((Get-Item ($Des.ToString()+$f)).Length / 1KB )
}
$filesize = $f.Length * 0.8
$filesize=($filesize / 1KB)
#$filesize = [math]::round(($filesize / 1KB), 0)
$abc = "KB"
$filesizeSTR = $filesize.ToString() + $abc
Push-Location $Src
mogrify -path $Des -define jpeg:extent=$filesizeSTR $f
Pop-Location
Write-Host "Moved file $f"
}
}
}
Works in Powershell, however when i try to do it it in my solution,
private static void Powershell()
{
string SCRIPT_PATH = System.IO.File.ReadAllText(#"C:\Untitled2.ps1");
using (Runspace runspace = RunspaceFactory.CreateRunspace())
{
runspace.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.AddScript(SCRIPT_PATH);
ps.Invoke();
ps.AddCommand("MoveCompressFiles").AddParameters(new Dictionary<string, string>
{
{"Des" , #"C:\Des"},
{"Src", #"C:\Src"}
});
}
}
It doesn't work, I've tried some other methods of calling a function from a ps script but it still fails to even move the files to another location
Since you need to dot-source your script file (. <script>) in order to make the MoveCompressFiles function available, which requires an .AddScript() call,
I suggest constructing a single piece of PowerShell code in a string variable that both dot-sources the script and invokes your function via a single .AddScript() call.
However, in order to guarantee that .AddScript() works, you must first ensure that the PowerShell execution policy allows script invocation, using a call to Set-ExecutionPolicy; the code below uses -Scope Process, so as to limit the change to the current process.
Update: There's a simpler way to configure the execution policy, via the initial session state - see this answer.
var SCRIPT_PATH = #"C:\Untitled2.ps1";
var src = #"C:\Src";
var des = #"C:\Des";
var script = $#". ""{SCRIPT_PATH}""; MoveCompressFiles -Des ""{des}"" -Src ""{src}""";
using (PowerShell ps = PowerShell.Create())
{
// Make sure that script execution is allowed.
ps.AddCommand("Set-ExecutionPolicy")
.AddParameter("Scope", "Process")
.AddParameter("ExecutionPolicy", "Bypass")
.AddParameter("Force", true);
ps.Invoke();
// Add the PowerShell code constructed above and invoke it.
ps.AddScript(script);
// Use foreach (var o in ps.Invoke()) { Console.WriteLine(o); } to print the output.
ps.Invoke();
}
Note the simplified, implicit runspace creation, by using PowerShell.Create() only.
The embedded PowerShell code dot-sources your script file (. <script>) in order to define the MoveCompressFiles function, and then invokes the function.
Note that the above, as your own code, doesn't capture or print the output from the PowerShell code (.Invoke()'s output).
To see if errors occurred, you can check ps.HadErrors and examine ps.Streams.Error or any of the other streams, such as .ps.Streams.Information for the Write-Host output (the success stream's output is what .Invoke() returns directly).
For instance, use something like the following to print all errors (messages only) that occurred to the console's standard error stream:
foreach (var o in ps.Streams.Error) {
Console.Error.WriteLine(o);
}
As for what you tried:
ps.AddScript(SCRIPT_PATH); ps.Invoke();
While this executes your script, it does so in a child scope, so the embedded function MoveCompressFiles definition is not added to your session's top-level scope, so the subsequent .AddCommand() call fails, because the MoveCompressFiles function isn't available.
Instead, you must dot-source your script (. <script>), which makes it run in the caller's scope and therefore makes its function definition available there.
As an aside: Despite the .AddScript() method's name, its primary purpose is to execute a piece of PowerShell code, not a script file.
To execute the latter (without dot-sourcing), use .AddCommand().
Related
I need to execute Powershell script containing my custom Commandlets ( exist in different assembly) from C#. I tried following approach but it just invokes the commandlet only once and that's it, while in the script that commandlet is written more than once.
Get-MyParameter myTasks
Start-Process notepad.exe
Get-MyParameter myTasks
Get-MyParameter myTasks
Get-MyParameter myTasks
While, MyParameter is written in different assembly.
Tried Code is :
var powerShellInstance = PowerShell.Create();
powerShellInstance.Runspace = runSpace;
var command = new Command("Import-Module");
command.Parameters.Add("Assembly", Assembly.LoadFrom(#"..\CommandLets\bin\Debug\Commandlets.dll"));
powerShellInstance.Commands.AddCommand(command);
powerShellInstance.Invoke();
powerShellInstance.Commands.Clear();
powerShellInstance.Commands.AddCommand(new Command("Get-MyParameter"));
powerShellInstance.AddScript(psScript);
var result = powerShellInstance.Invoke();
What am I doing wrong here?
You are adding the script to the same pipeline you added the Get-MyParameter command to. In effect, you are doing
get-myparameter | { … your script … }
Try using separate pipelines instead.
var result1 = powerShellInstance.AddCommand("Get-MyParameter").Invoke()
var result2 = powerShellInstance.AddScript(psScript).Invoke();
Also, you can simplify your module loading code to
powerShellInstance.AddCommand("Import-Module").
AddParameter("Name", #"..\CommandLets\bin\Debug\Commandlets.dll")).
Invoke();
I have a custom function that accepts a [scriptblock] parameter, The scriptblock is serailzed using [System.Management.Automation.PSSerializer]::Serialize() before it is sent to a remote process to be deserialized and invoked. The remote process does not have access to the local variables. I would like to allow the variables to be placed into the scriptblock on function call, however i would settle with a second parameter $ArgumentList to pass the arguments\parameters to the scriptblock. I browsed through System.Management.Automation.dll at Microsoft.PowerShell.Commands.InvokeCommandCommand to see if i could determine how Invoke-command adds this functionality but i'm a beginner in C# and could't figure it out.
How can I expand the variables within the scriptblock before they are sent along their way?
An example of the local serialize function:
Function Send-Command
{
param(
[scriptblock]$Scriptblock
)
[String]$Serialized = [System.Management.Automation.PSSerializer]::Serialize($Scriptblock)
[byte[]]$MessageBytes = [System.Text.Encoding]::UTF8.GetBytes($Serialized)
return $MessageBytes
}
And on the other end:
[byte[]]$Serialized = [System.Text.Encoding]::UTF8.GetString($MessageBytes)
[String]$Deserialized = [System.Management.Automation.PSSerializer]::Deserialize($Serialized.ToString())
return $Deserialized
I call the function with local variables:
$String = "This is a Test"
Send-Command -Scriptblock {Write-Output $String }
And on the other end:
Command: Write-Output $String
Should Output:
Command: Write-Output "This is a Test"
Using PowerShell I would suggest te following:
Why don't you use Invoke-Command with the scriptblock to run the scriptblock on the remote computer? You can combine that both with the $using:LocalVariableName statement to access local variables from the remote host or with local variable expansion as described above, or even combine both.
I guess something like this should do the job:
$ScriptBlock = {
Write-Output $using:Text
}
$Text = 'This is a test'
function Send-Command {
param (
[scriptblock] $ScriptBlock,
[string] $ComputerName,
[System.Management.Automation.PSCredential] $Credential
)
Invoke-Command -ComputerName $Computername -ScriptBlock $ScriptBlock -Credential $Credential
}
$ComputerName = 'Server0123'
$Credential = Get-Credential -Message "Enter credential for computer $($ComputerName)"
Send-Command $ScriptBlock $ComputerName $Credential
PowerShell: Invoke-Command
PowerShell: about_remote_variables
There's a way to deserialize a ScriptBlock.
You have to first create a scriptblock from the string.
Then you have to serialize all the variables that are bounded to the scope.
After it we use a regular expression to replace the variables with a payload to deserialize their values.
When the code is deserialized, it will have in the ScriptBlock an injected code to deserialize the value of the variables, so they retain the contents.
Then you need to call GetNewClosure() to unbound variables from the deserializer scope (any parameters for example), then they will be free to be bounded in the next call.
This solution is based on what the powershell DSC infraestructure does.
Function Serialize-Command
{
param(
[scriptblock]$Scriptblock
)
$rxp = '\$using:(?<var>\w+)'
$ssb = $Scriptblock.ToString()
$cb = {
$v = (Get-Variable -Name $args[0].Groups['var'] -ValueOnly)
$ser = [System.Management.Automation.PSSerializer]::Serialize($v)
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$sb = [RegEx]::Replace($ssb, $rxp, $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$Serialized = [System.Management.Automation.PSSerializer]::Serialize($sb)
[System.Text.Encoding]::UTF8.GetBytes($Serialized)
}
Function Deserialize-Command
{
param(
[byte[]]$ScriptblockString
)
$Serialized = [System.Text.Encoding]::UTF8.GetString($ScriptblockString)
$sb = [System.Management.Automation.PSSerializer]::Deserialize($Serialized.ToString())
[Scriptblock]::Create($sb).GetNewClosure()
}
$a = "is this what you wanted?"
$b = Serialize-Command { "test $using:a" }
$a = "okay"
$c = Deserialize-Command $b
$a = "right"
PS> & $c
is this what you wanted?
sources:
https://www.briantist.com/how-to/use-duplicate-dsc-script-resources-in-loop/
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.getnewclosure?view=powershellsdk-1.1.0#System_Management_Automation_ScriptBlock_GetNewClosure
https://devblogs.microsoft.com/powershell/get-closure-with-getnewclosure/
The three common ways to expands variables are the below:
$VariableName
"$VariableName"
$($VariableName)
... but there are other considerations depending on what you are doing.
I am using .NET with powershell trying to retrieve result of Get-Acl command of specific AD object. Unfortunately when I run the code from C# code I get 0 result. Also the ThrowIfError is not throwing any error.
Command test01 = new Command("import-module");
test01.Parameters.Add("name", "activedirectory");
session.Commands.AddCommand(test01);
Command test0 = new Command("Set-Location");
test0.Parameters.Add("Path", "AD:");
session.Commands.AddCommand(test0);
Command test1 = new Command("Get-Acl");
test1.Parameters.Add("Path", identity);
session.Commands.AddCommand(test1);
session.AddCommand("select-object");
session.AddParameter("Property", "Access");
var tempResults1 = session.Invoke();
ThrowIfError();
private void ThrowIfError()
{
var errors = session.Streams.Error;
if (errors.Count > 0)
{
var ex = errors[0].Exception;
session.Streams.ClearStreams();
// Never close session to dispose already running scripts.
throw ex;
}
}
This code running on server in powershell is working correctly:
PS AD:\> Import-Module -Name activedirectory
PS AD:\> set-location ad:
PS AD:\> get-acl -path <distinguishedNameOfADObject>
Question
How to get the same result like I get from Powershell? I should get atleast something not a zero result.
Little background:
I am trying to get Send-As rights not using Get-ADPermission cmdlet because its taking too long time when I need to search for rights within thousands of mailboxes. Using this article link I am trying another approach to get the rights. I have already the slower version working using C# code:
Command command = new Command("Get-ADPermission");
command.Parameters.Add("Identity", identity);
session.Commands.AddCommand(command);
session.AddCommand("where-object");
ScriptBlock filter = ScriptBlock.Create("$_.ExtendedRights -eq 'send-as'");
session.AddParameter("FilterScript", filter);
session.AddCommand("select-object");
session.AddParameter("Property", "User");
tempResults = session.Invoke();
The better way is to define a powershell-script instead of multiple commands to get the values you need. Example with your powershell-code:
using System.Collections.ObjectModel;
using System.DirectoryServices;
using System.Management.Automation;
namespace GetAclPowershellTest
{
class Program
{
static void Main(string[] args)
{
/****Create Powershell-Environment****/
PowerShell PSI = PowerShell.Create();
/****Insert PowershellScript****/
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; Get-ACL -Path $object"; //Add Scrip
PSI.AddScript(Content);
PSI.AddParameter("object", "<distinguishedNameOfADObject>");
/****Run your Script with PSI.Invoke()***/
Collection<PSObject> PSIResults = PSI.Invoke();
/****All Errors****/
Collection<ErrorRecord> Errors = PSI.Streams.Error.ReadAll();
/****needed, because garbagecollector ignores PSI otherwise****/
PSI.Dispose();
/**** Your ACL-Object ****/
ActiveDirectorySecurity MyACL = (ActiveDirectorySecurity)PSIResults[0].BaseObject;
/*insert your code here*/
}
}
}
This example works for me.
You have to set a reference to the Powershell-Assembly (Usually you can find it at "C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll")
Benefit of this solution is, you could read a .ps1-File you got from someone, fill the parameters with the objects you have and the script runs like in a standard powershell-session. The only requirement to set parameters is the param-part in the Script.
More Infos about param: https://technet.microsoft.com/en-us/library/jj554301.aspx
Hope, this helps...
Greetings, Ronny
Update:
string Content = "param($object); Import-Module ActiveDirectory; Set-Location AD:; (Get-ACL -Path $object).Access | Where-Object{($_.ActiveDirectoryRights -eq 'ExtendedRight') -and ($_.objectType -eq 'ab721a54-1e2f-11d0-9819-00aa0040529b')}";
And the loop at the end looks like this now:
foreach (PSObject o in PSIResults)
{
ActiveDirectoryAccessRule AccessRule = (ActiveDirectoryAccessRule)o.BaseObject;
/**do something with the AccessRule here**/
}
I've recently started to explore powershell scripting as well as powershell hosting.
From a powershell command prompt the following line executes as I would expect.
"fee" | &{ process { $_; $args } } "fi" "fo" "fum"
This script outputs the "fee" that's in the pipeline ($_) plus the "fi", "fo", and "fum" that are passed in as arguments to the scriptBlock ($args).
I am trying to achieve this same result by using the System.Management.Automation.Powershell class.
When I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddScript(#"&{ process { $_; $args} }")
.AddArgument("fi")
.AddArgument("fo")
.AddArgument("fum")
.Invoke(Enumerable.Repeat("fee", 1));
}
my result array contains a single null element (not sure what's going on there).
If I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddCommand("Invoke-Command")
.AddParameter("ScriptBlock", ScriptBlock.Create(#"&{ process { $_; $args } }"))
.AddParameter("ArgumentList", new string[] { "fi", "fo", "fum"})
.Invoke(Enumerable.Repeat("fee", 1));
}
only "fee" is output. The $args array is empty.
Finally, when I try this:
using (var ps = PowerShell.Create())
{
var result = ps
.AddCommand("Invoke-Command")
.AddParameter("ScriptBlock", ScriptBlock.Create(#"&{ process { $_; $args } } ""fi"" ""fo"" ""fum"""))
.Invoke(Enumerable.Repeat("fee", 1));
}
I get closest. The pipelined "fee", and the "fi", "fo", and "fum" arguments are all output. The problem with this, however, is that I ideally would prefer to dynamically pass my arguments to the scriptBlock rather than concatenating them into the script block expression.
My question is how can I create a script block in a hosted powershell environment that can both access data in the pipeline from which it is invoked as well as accept parameters? I've only been able to achieve one or the other. I am using version 4.5 of the .Net framework and version 3 of the System.Management.Automation assembly.
It's because a wrong script is used for AddScript, &{} should not be used because it is actually done by Invoke() in the C# code. Here is the PowerShell analogue of C# code that works:
$ps = [powershell]::Create()
$ps.AddScript('process { $_; $args}').
AddArgument('fi').
AddArgument('fo').
AddArgument('fum').
Invoke(#('fee'))
Output:
fee
fi
fo
fum
as expected.
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.