Reputation: 1331
How do I Start a job of a function i just defined?
function FOO { write-host "HEY" }
Start-Job -ScriptBlock { FOO } |
Receive-Job -Wait -AutoRemoveJob
Result:
Receive-Job: The term 'FOO' is not recognized as the name of cmdlet,
function ,script file or operable program.
What do I do? Thanks.
Upvotes: 30
Views: 49971
Reputation: 437953
There's good information in the existing answers, but let me attempt a systematic summary:
PowerShell's background jobs[1] run in an out-of-process runspace (a hidden child process) and therefore share no state with the caller.
Therefore, function
definition created by the caller in-session are not visible to background jobs and must be recreated in the context of the job.[2]
The simplest way to recreate a function definition is to combine namespace variable notation (e.g, $function:FOO
- see this answer) with the $using:scope
, as shown below.
$using:
references do not work in script blocks passed to Start-Job
's (as well as Start-ThreadJob
's) -InitializationScript
parameter, as of this writing (Windows PowerShell, PowerShell (Core) 7.3.6) - see GitHub issue #4530A self-contained example:
function FOO { "HEY" }
Start-Job -ScriptBlock {
# Redefine function FOO in the context of this job.
$function:FOO = "$using:function:FOO"
# Now FOO can be invoked.
FOO
} | Receive-Job -Wait -AutoRemoveJob
The above outputs string HEY
, as intended.
Note:
Assigning to $function:FOO
implicitly creates function FOO
(on demand) and makes the assigned value the function's body; the assigned value may either be a [scriptblock]
instance or a [string]
, i.e. source-code text.
Referencing $function:FOO
retrieves a preexisting FOO
function's body as a [scriptblock]
instance. Prepending the $using:
scope ($using:function:FOO
) retrieves the the body of the FOO
function from the caller's scope.
Note:
Due to $using:
, $using:function:FOO
is not a [scriptblock]
instance, but a [string]
in the case of Start-Job
, due to the - surprising - manner in which [scriptblock]
instance are deserialized when undergoing cross-process serialization; the behavior was declared to be by design - see GitHub issue #11698 for a discussion.
As such, the "..."
around $using:function:FOO
are redundant for Start-Job
, but not for Start-ThreadJob
, where serialization is not involved, and recreating the body from a string is necessary to avoid state corruption (see GitHub issue #16461 for details).
The fact that Start-ThreadJob
allows $using:function:FOO
references at all is probably an oversight, given that they have been explicitly disallowed in script blocks used with ForEach-Object -Parallel
(PowerShell v7+) - see GitHub issue #12378.
Therefore, with ForEach-Object -Parallel
a helper variable that stringifies the function body on the caller's side first is required - see this answer.
[1] This answer doesn't just apply to the child-process-based jobs created by Start-Job
, but analogously also to the generally preferable thread-based jobs created with Start-ThreadJob
and the thread-based parallelism available in PowerShell (Core) 7+ with ForEach-Object
-Parallel
, as well as to PowerShell remoting via Invoke-Command
- in short: it applies to any scenario in which PowerShell executes out-of-runspace (in a different runspace).
[2] The alternative is to provide such definitions via script files (*.ps1
) or modules that the job would have to dot-source (.
) or import.
Upvotes: 5
Reputation: 21
As long as the function passed to the InitializationScript param on Start-Job isn't large Rynant's answer will work, but if the function is large you may run into the below error.
[localhost] There is an error launching the background process. Error reported: The filename or extension is too long"
Capturing the function's definition and then using Invoke-Expression on it in the ScriptBlock is a better alternative.
function Get-Foo {
param
(
[string]$output
)
Write-Output $output
}
$getFooFunc = $(Get-Command Get-Foo).Definition
Start-Job -ScriptBlock {
Invoke-Expression "function Get-Foo {$using:getFooFunc}"
Get-Foo -output "bar"
}
Get-Job | Receive-Job
PS C:\Users\rohopkin> Get-Job | Receive-Job
bar
Upvotes: 2
Reputation: 1101
@Ben Power's comment under the accepted answer was my concern also, so I googled how to get function definitions, and I found Get-Command
- though this gets only the function body. But it can be used also if the function is coming from elsewhere, like a dot-sourced file. So I came up with the following (hold my naming convention :)), the idea is to re-build the function definitions delimited by newlines:
Filter Greeting {param ([string]$Greeting) return $Greeting}
Filter FullName {param ([string]$FirstName, [string]$LastName) return $FirstName + " " + $LastName}
$ScriptText = ""
$ScriptText += "Filter Greeting {" + (Get-Command Greeting).Definition + "}`n"
$ScriptText += "Filter FullName {" + (Get-Command FullName).Definition + "}`n"
$Job = Start-Job `
-InitializationScript $([ScriptBlock]::Create($ScriptText)) `
-ScriptBlock {(Greeting -Greeting "Hello") + " " + (FullName -FirstName "PowerShell" -LastName "Programmer")}
$Result = $Job | Wait-Job | Receive-Job
$Result
$Job | Remove-Job
Upvotes: 0
Reputation: 27443
A slightly different take. A function is just a scriptblock assigned to a variable. Oh, it has to be a threadjob. It can't be foreach-object -parallel.
$func = { 'hi' } # or
function hi { 'hi' }; $func = $function:hi
start-threadjob { & $using:func } | receive-job -auto -wait
hi
Upvotes: 0
Reputation: 459
An improvement to @Rynant's answer:
You can define the function as normal in the main body of your script:
Function FOO
{
Write-Host "HEY"
}
and then recycle this definition within a scriptblock:
$export_functions = [scriptblock]::Create(@"
Function Foo { $function:FOO }
"@)
(makes more sense if you have a substantial function body) and then pass them to Start-Job
as above:
Start-Job -ScriptBlock {FOO} -InitializationScript $export_functions| Wait-Job | Receive-Job
I like this way, as it is easier to debug jobs by running them locally under the debugger.
Upvotes: 5
Reputation: 301147
@Rynant's suggestion of InitializationScript
is great
I thought the purpose of (script) blocks is so that you can pass them around. So depending on how you are doing it, I would say go for:
$FOO = {write-host "HEY"}
Start-Job -ScriptBlock $FOO | wait-job |Receive-Job
Of course you can parameterize script blocks as well:
$foo = {param($bar) write-host $bar}
Start-Job -ScriptBlock $foo -ArgumentList "HEY" | wait-job | receive-job
Upvotes: 15
Reputation: 24283
As @Shay points out, FOO
needs to be defined for the job. Another way to do this is to use the -InitializationScript
parameter to prepare the session.
For your example:
$functions = {
function FOO { write-host "HEY" }
}
Start-Job -InitializationScript $functions -ScriptBlock {FOO}|
Wait-Job| Receive-Job
This can be useful if you want to use the same functions for different jobs.
Upvotes: 37
Reputation: 126752
The function needs to be inside the scriptblock:
Start-Job -ScriptBlock { function FOO { write-host "HEY" } ; FOO } | Wait-Job | Receive-Job
Upvotes: 3