Invoke-Command Using Local Function With Array As Single Parameter - powershell

I have a function that takes a single string array as a parameter in my PowerShell .pm1 that I want to be able to call on a remote server using a second function in my .pm1 (I do not want to rely on the server having a copy of the function). I found this Using Invoke-Command -ScriptBlock on a function with arguments but it only seems to work for 'non-arrays' or for multiple parameters (where array variable is not last)
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people
#output => "hello bob" only
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people, ""
#output => "hello bob hello joe"
I can modify my argument list like -ArgumentList $people, "" (above) to make it work by forcing the function to see the $persons variable as a single parameter and not an array of parameters, but that seems like bad practice and I sure that I am just missing something simple.
EDIT:
I was directed here ArgumentList parameter in Invoke-Command don't send all array and while it works for this exact example, it requires that I KNOW which parameters require an array. Is there a generic way to pass an any arguments that would prevent this issue? I.E. I build my argument list as an array of parameters and there could be 0 or more of them and any number of them could be arrays - or am I stuck with putting this in front of calls?
foreach($parg in $myCustomGeneratedArguments) {
if($parg -is [array]) {$paramArgs += ,$parg}
else {$paramArgs += $parg}
}

Looking at your edit I'm afraid the linked answer doesn't lead you to the easier path, which is to not use -ArgumentList at all, instead, refer to your Hello_Worlds function and to your $people array with the $using: scope modifier:
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
# store the function definition locally
$func = ${function:Hello_Worlds}.ToString()
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock {
# define the function in the remote scope
${function:Hello_Worlds} = $using:func
# now you can use it normally
Hello_Worlds -persons $using:people
}

Related

Passing scriptblock as an argument to Invoke-Command scriptblock in powershell

I have a powershell script in which I have been trying to pass scriptblock(i.e $sb) as an argument to another scriptblock. However, I keep getting error that:
Cannot convert the "{get-member}" value of type "System.String" to
type "System.Management.Automation.ScriptBlock
a.psm1:
$sb = {get-member}
# $type = $sb.GetType().FullName => Returns scriptblock as data type
$result = Invoke-Command -Session "DC" -Scriptblock
{
Param(
[Parameter(Mandatory=$true)]
[scriptblock]
$sb
)
//Do some task
} -ArgumentList $sb
I am not able to figure out why $sb is treated as a string instead of Scriptblock?
The only way it works is changing the argument inside the Invoke-Command scriptblock to be of type string instead of Scriptblock.
I am not sure why scriptblocks gets implicitly converted to string while passing argument to Invoke-Command scriptblock.
When a script block (type [scriptblock], { ... } as a literal) is passed to code that executes out-of-process, such as during remoting (your case) or in background jobs, XML-based serialization and deserialization must be performed.
On deserialization in the target process, [scriptblock] instances indeed unexpectedly become strings.
Unfortunately and bewilderingly, this behavior has been declared by design(!) - see GitHub issue #11698.
Your only option is to pass the script block('s source code) as a string, and convert it back to a script block via [scriptblock]::Create(); a simple example, using a background job:
Start-Job {
param([string] $scriptBlockSourceCode) # Note the type: [string]
# Use [scriptblock]::Create() to turn the string into a script block,
# and execute it with &
& ([scriptblock]::Create($scriptBlockSourceCode))
} -ArgumentList { 'hi' } |
Receive-Job -Wait -AutoRemove

Invoke-Command - use predefined function and object

i want to use a predefined function on a remote server with ps invoke and i need to send an object as parameter. This code always writes objects first element.
$object = #('a','b','c')
function testA {
[CmdletBinding()]
param (
[Parameter()]
[System.Object]
$test
)
write-host $test
}
Invoke-Command -Session $session -ScriptBlock ${function:testA} -ArgumentList $object
Is there a way to pass function and object to the remote server with Invoke-Command?
Thx.
If you want to pass the array stored in $object as a single argument and it is the only argument to pass, you need to wrap it in a transient single-element wrapper array when you pass it to -ArgumentList - otherwise, the array elements are passed as individual arguments:
# Alias -Args for -ArgumentList is used for brevity.
Invoke-Command -Session $session -ScriptBlock ${function:testA} -Args (, $object)
, $object uses the unary form of ,, the array constructor operator, to construct a single-element array whose only element is the $object array. -ArgumentList (-Args) then enumerates the elements of this transient array to form the individual arguments, and therefore passes the one and only element - which is the $object array - as-is.
Note that the problem only arises because you want to pass only a single argument; with two or more arguments, use of the binary form of , is needed anyway, which unambiguously makes $object as a whole its own argument; e.g., ... -ArgumentList $object, 'foo'

Get All Remote Variables in Script Block

Is there a way to get a list of all remote variables in a script block?
Consider the following:
$x = 1
$block = { Write-Host $using:x }
Invoke-Command -Session (New-PSSession) -ScriptBlock $block
Inside of $block, is there any way of getting a list of available $using: scoped variables?
$x = 1
$block = { Get-Variable }
Invoke-Command -Session (New-PSSession) -ScriptBlock $block
Does not yield $x as an available variable
The short answer is: you can't.
The remote side doesn't know anything about the variables. They get serialized and then the deserialization code and the literal serialized XML is embedded.
If you're the one writing the script bock, then I recommend you just assign each $Using: variable to a local variable inside the scriptblock:
$block = {
$x = $Using:x
$y = $Using:y
}
I wrote a more detailed explanation of how $Using: is implemented on my blog regarding using it in DSC Script resources.

Synax Highlighting w/ Dynamic Scriptblock

I'm creating a dynamic ScriptBlock the way below so I can use local functions and variables and easily pass them to remote computers via Invoke-Command. The issue is that since all the text inside Create is enclosed with double quotes, I loose all my syntax highlighting since all editors see the code as one big string.
While this is only a cosmetic issue, I'd like to find a work around that allow my code to be passed without having double quotes. I've tried passing a variable inside Create instead of the actually text, but it does not get interpreted.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = [ScriptBlock]::Create(#"
#Define Function
function local_admin {$Function:local_admin}
local_admin domain username
"#)
Invoke-Command -ComputerName server2 -ScriptBlock $SB
You can pass the function into the remote session using the following example. This allows you to define the ScriptBlock using curly braces instead of as a string.
# Define the function
function foo {
"bar";
}
$sb = {
# Import the function definition into the remote session
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
foo;
};
#(gi function:foo) | select *
Invoke-Command -ComputerName . -ScriptBlock $sb -ArgumentList (Get-Item -Path function:foo);
Here is a modified version of your function. Please take note that the domain and username can be dynamically passed into the remote ScriptBlock using the -ArgumentList parameter. I am using the $args automatic variable to pass objects into the ScriptBlock.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = {
#Define Function
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
local_admin $args[1] $args[2];
}
Invoke-Command -ComputerName server2 -ScriptBlock $SB -ArgumentList (Get-Item -Path function:local_admin), 'domain', 'username';

Powershell: Passing parameters to function

So I'm doing a bit of 'House Keepying' on my script and I've found one area than can be reduced/tidied.
Take this GUI I've created:
Both menu bar add_click events to Restart HostnameA and HostnameB call seperate functions even though the code in both of the functions is pratically the same, the only difference is this variable for the hostname (see below).
Code for button events.
$Restart_current_machine.Add_Click(
{
restart_current_machines
})
$Restart_target_machine.Add_Click(
{
restart_target_machines
})
# Function Blocks
function restart_target_machines
{
restart-computer -computer $combobox1.text -force
}
function restart_current_machines
{
restart-computer -computer $combobox2.text -force
}
My question is this:
is there a way I can use Param() (or something like that) to get rid of function restart_current_machinesthereby only having one function to restart either of the machines?
Something like?
$Restart_current_machine.Add_Click(
{
param($input = combobox1.text)
$input | restart_current_machines
})
$Restart_target_machine.Add_Click(
{
param($input = combobox2.text)
$input | restart_current_machines
})
# Only needing one function
function restart_target_machines
{
restart-computer -computer $input -force
}
I know that is in all probability wrong, but just to give you a better idea of what I'm trying to do.
Create a generic function that defines a ComputerName parameter and pass that parameter to the underlying cmdlet:
function restart-machine ([string[]]$ComputerName)
{
Restart-Computer -ComputerName $ComputerName -Force
}
The Rastart-Compter cmdlet ComputerName parameter accepts a collection of names so the parameter is defained as a string array.
Now, from anywhere in your code just call restart-machine and pass the computer names to restart to the ComputerName parameter. To restart multiple machines, delimit each name with a comma (i.e restart-machine -computerName $combobox1.text,$combobox2.text)
$Restart_target_machine.Add_Click(
{
restart-machine -computerName $combobox1.text
})