PowerShell implementing -AsJob for a cmdlet - powershell

Is there a nice way to implement the switch parameter -AsJob in custom cmdlets, like Invoke-Command has?
The only way I thought about this is:
function Use-AsJob {
[CmdletBinding()]
[OutputType()]
param (
[Parameter(Mandatory = $true)]
[string]
$Message,
[switch]
$AsJob
)
# Wrap Script block in a variable
$myScriptBlock = {
# stuff
}
if ($AsJob) {
Invoke-Command -ScriptBlock $myScriptBlock -AsJob
}
else {
Invoke-Command -ScriptBlock $myScriptBlock
}
}
Is there a better approach? I couldn't find Microsoft docs on this, any lead helps.

If we make the following assumptions:
Command is a script function
Function does not rely on module state
Then you can use the following boilerplate for any command:
function Test-AsJob {
param(
[string]$Parameter = '123',
[switch]$AsJob
)
if ($AsJob) {
# Remove the `-AsJob` parameter, leave everything else as is
[void]$PSBoundParameters.Remove('AsJob')
# Start new job that executes a copy of this function against the remaining parameter args
return Start-Job -ScriptBlock {
param(
[string]$myFunction,
[System.Collections.IDictionary]$argTable
)
$cmd = [scriptblock]::Create($myFunction)
& $cmd #argTable
} -ArgumentList $MyInvocation.MyCommand.Definition,$PSBoundParameters
}
# here is where we execute the actual function
return "Parameter was '$Parameter'"
}
Now you can do either:
PS C:\> Test-AsJob
Parameter was '123'
PS C:\> Test-AsJob -AsJob |Receive-Job -Wait
Parameter was '123'

Related

How to call a function, to be executed remotely, using a different function

How would I be able to make this script work?
function funcOne
{
$output = <# some script #>
}
function funcTwo
{
$output = <# some script #>
}
function invokingFunc
{
param (
$Function
)
Invoke-Command $session ${Function:< funcOne or funcTwo here! >}
}
Trying to keep it as simple as possible, as is the PS way, or am I already making this too complicated for Powershell?
You need to pass it as part of the ScriptBlock. More info here:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks
You may do the following:
function funcOne
{
"funcOne output"
}
function funcTwo
{
"funcTwo output"
}
function Invoke-Func
{
param (
[scriptblock]$Function,
[System.Management.Automation.Runspaces.PSSession]$Session
)
Invoke-Command -Session $Session -ScriptBlock $Function
}
Invoke-Func -Session $session -Function ${Function:funcone}
When a function is defined, it is accessible through the Function: drive. You can access the scriptblock of the function definition in numerous ways. See below for some examples:
${Function:FuncOne} # Supports functions with special characters in the name
$Function:FuncOne # Only for functions with simple names, ie no -
(Get-ChildItem Function:\FuncOne).ScriptBlock

How do I pass a hashtable to a scriptblock?

I tried to pass a hashtable to a scriptblock like this where $arg3 is my hashtable. However, it failed. How do I do it in the correct way?
It doesn't seem like it is passing anything to the script block.
$commandParameters.ComputerName = $ComputerName
$commandParameters.ScriptBlock = {
param(
[Parameter()]
[switch]$arg1 = $false,
[Parameter()]
[array]$arg2,
[Parameter()]
[hashtable]$arg3
)
enter code here
Doing something here
}
Invoke-Command #commandParameters -ArgumentList $arg1, #($arg2), $arg3
=================================================================
I found the answer myself and it works for me. This is how I build the associative array and then pass it to the script block.
I am not sure why, but I was using the dot notation ($hash.a.b) to reference the hash table in a function and it works, but it doesn't work for a script block. It looks like I need to use [ ] (e.g. $hash[a][b])in the script block.
$compADGroups = #{}
foreach ( $adGroup in $adGroups ) {
if ( $compADGroups.$computerNameGroup -eq $null ) {
$compADGroups[$computerName] = #{}
$compADGroups[$computerName]["Group"] = #{}
$compADGroups[$computerName]["Group"] = $hashString
}
}
$session = New-PSSession -ComputerName 'Computer1'
Invoke-Command -Session $session -ArgumentList $compADGroups -ScriptBlock { param($compADGroups) $compADGroups[$env:computername]["Group"]}
Get-PSSession | Remove-PSSession
Make sure you're using Invoke-Command correctly.
$ScriptBlock = {
param(
[Parameter(Mandatory=$True, Position=1)]
[hashtable]$myHashTable
)
# Code here
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ([hashtable]$hashtable)
If you specify certain arguments for a scriptblock, make sure you also describe the position value, and more often than not whether it's mandatory. If you are trying to pass your hashtable in as the second argument in an implicitly defined array of arguments, write your scriptblock so that it takes the hashtable at that specific position.
For example,
$ScriptBlock= {
param(
[Parameter(Position=2)] # Take note of the position you set here
[hashtable]$myHashTable,
[Parameter(Position=1)]
[string]$myString,
[Parameter(Position=3)]
[int]$myInteger
)
# Do stuff
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ($myString, $myHashTable, $myInteger);
# ^ variable is in second position

PowerShell Splatting the Argumentlist on Invoke-Command

How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
Structure = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList #CopyParams
Whatever I try, it's always complaining about the 'Source':
Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
value "System.Collections.Hashtable" did not return true. Determine why the validation script failed
This blog talks about a similar problem, but I can't get it to work.
The same is true for a simple Copy-Item within Invoke-Command, example:
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList #CopyParams
Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList #CopyParams
Thank you for your help.
One-liner, to convert a remote script to accept named parameters from a hash.
Given a scriptblock which you wish to call like this:
$Options = #{
Parameter1 = "foo"
Parameter2 = "bar"
}
Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
}
You can convert it like so
Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
} #Options}
What's going on? Essentially we've wrapped the original script block like so:
{param($Options)& <# Original script block (including {} braces)#> #options }
This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing #options to splat the hash.
Here's one way to approach passing named parameters:
function Copy-FilesHC
{
param ($Source,$Destination,$Structure)
"Source is $Source"
"Desintation is $Destination"
"Structure is $Structure"
}
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
Structure = 'yyyy-MM-dd'
}
$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}#CopyParams)")
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB
Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.
I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.
Function Add-FunctionHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[String]$Name
)
Process {
Try {
$Module = (Get-Command $Name -EA Stop).ModuleName
}
Catch {
Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
$Global:Error.RemoveAt('1')
Break
}
if (-not (Get-Module -Name $Module)) {
Import-Module -Name $Module
}
}
}
# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}
$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more
Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
Param (
$RemoteFunctions
)
. ([ScriptBlock]::Create($RemoteFunctions))
$CopyParams = $using:CopyParams
Copy-FilesHC #CopyParams
}
The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.
I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)
it looks something like this:
$Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure = 'yyyy-MM-dd'
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
$CopyParms= #{
'Source'=$Using:Source
'Destination'=$Using:Destination
'Structure'=$Using:Structure
}
Function:Copy-FilesHC #CopyParms
}
This is what works for me:
$hash = #{
PARAM1="meaning of life"
PARAM2=42
PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.
Here's an example:
$param=#{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet #a } -ArgumentList $param

How do I run a ps1 script file passed by parameter from inside a powershell function?

I need to write a function in PowerShell that receives a bunch of parameters, one of them being a ps1 file. I need to execute this file as part of my function code, but I don't know how to do that.
This is probably a very silly detail, but I failed horribly in trying to search for this.
This is my function at the moment. I tried using Invoke-Command there but it's not working:
Function Start-Dsc {
Param(
[Parameter(Mandatory = $true)]
[string] $configurationFile,
[Parameter(Mandatory = $true)]
[string] $configurationName,
[Parameter()]
[string] $configurationData,
[Parameter(Mandatory = $true)]
[string] $computerName
)
Begin {}
Process
{
Invoke-Command -Command "$configurationFile -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
End{}
}
UPDATE:
With Bacon Bits' help, I managed to make it work. The final script is a bit different than what I initially posted though. This is the final process block:
Process
{
Invoke-Command -FilePath $configurationFile -ComputerName 'localhost';
Invoke-Expression -Command "$configurationName -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
Arguments are a separate option in Invoke-Command. Try:
Invoke-Command -Command "$configurationFile" -ArgumentList "-ConfigurationData $configurationData";
You may also need to change -Command to -FilePath.

How do I pass a scriptblock as one of the parameters in start-job

I'm trying to create a background job, which executes a scriptblock. I need to pass this scriptblock in as a parameter, but I can't seem to get the syntax to work. The scriptblock is being converted to a string somewhere along the way.
It works fine when I pass the script block to a local function, but not through start-job
The following syntax works:
function LocalFunction
{
param (
[parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
&$ScriptBlock | % { echo "got $_" }
}
LocalFunction -ScriptBlock { echo "hello" }
This outputs "got hello" as expected.
But the following fails:
$job = start-job -argumentlist { echo "hello" } -scriptblock {
param (
[parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
&$ScriptBlock | % { echo "got $_" }
}
start-sleep -s 1
receive-job $job
The error it returns is
Receive-Job : Cannot process argument transformation on parameter 'ScriptBlock'. Cannot convert the " echo "hello" " value of type "System.String" to type "System.Management.Automation.ScriptBlock".
So if I'm reading the error right, it appears that -argumentlist is somehow forcing its arguments into strings.
Here's one way to solve this, pass the scriptblock code as a string, then create a scriptblock from the string inside the job and execute it
Start-Job -ArgumentList "write-host hello" -scriptblock {
param (
[parameter(Mandatory=$true)][string]$ScriptBlock
)
& ([scriptblock]::Create($ScriptBlock))
} | Wait-Job | Receive-Job
Looks like this works today.
function LocalFunction
{
param
(
[scriptblock] $block
)
$block.Invoke() | % { "got $_" }
}
LocalFunction { "Hello"}
Based on my experiments, PowerShell is parsing -ArgumentList, which is an Object[], as a string, even when you pass in a script block. The following code:
$job = start-job -scriptblock { $args[0].GetType().FullName } -argumentlist { echo "hello" }
start-sleep -s 1
receive-job $job
results in the following output:
System.String
As far as I know, the only solution here is Shay's, though you don't need to pass in the -ArgumentList as a string as PowerShell will parse your script block as a string in this case.
You have to read it in as a string and then convert it to a scriptblock.
In powershell v1 you can do this:
$ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($string)
And in powershell v2 you can do this:
$ScriptBlock = [scriptblock]::Create($string)
So your code would look like this:
function LocalFunction
{
param (
[parameter(Mandatory=$true)]
$ScriptBlock
)
$sb = [scriptblock]::Create($ScriptBlock)
$sb | % { echo "got $_" }
}
LocalFunction -ScriptBlock "echo 'hello'"
The '[scriptblock]::Create($ScriptBlock)' will place the curly braces around the string for you creating the script block.
Found the info here http://get-powershell.com/post/2008/12/15/ConvertTo-ScriptBlock.aspx
So if your desire is to insert an inline scriptblock, then Shay's solution (as noted) is probably the best. On the other hand if you simply want to pass a scriptblock as a parameter consider using a variable of type scriptblock and then passing that as the value of the -ScriptBlock parameter.
function LocalFunction
{
param (
[parameter(Mandatory=$true)]
[ScriptBlock]$ScriptBlock
)
&$ScriptBlock | % { echo "got $_" }
}
[scriptblock]$sb = { echo "hello" }
LocalFunction -ScriptBlock $sb