How do I Start a job of a function i just defined?
Asked Answered
F

9

30

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.

Fasciculus answered 23/8, 2011 at 13:53 Comment(0)
T
37

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.

Thumbtack answered 23/8, 2011 at 14:41 Comment(2)
This is good, but how can I use the same functions in the main script?Bosomed
@BenPower See my answer below.Venezuela
D
14

@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
Dewain answered 23/8, 2011 at 15:19 Comment(2)
I agree that if all @Fasciculus wants to do is run a single function a parameterized ScriptBlock is the way to go. But if he is defining multiple functions or an advanced function, InitializationScript can be very helpful. For example: you could define functions FOO and BAR in the initialization script, then run a job with scriptblock {FOO; BAR} and another job with {BAR; FOO}Thumbtack
@Thumbtack - I agree. That is why I said "depending on how you are doing it". PS: Got the name wrong in my answer. Meant Rynant and not Matt.Dewain
E
10

It worked for me as:

Start-Job -ScriptBlock ${Function:FOO}
Eleusis answered 13/11, 2019 at 13:48 Comment(3)
This worked for me with also adding -ArgumentList. Thanks!Venezuela
Actually this answer is way better then the marked oneUnconformable
This is great, but it should be noted that this only works if the function in question doesn't call another function in the global scope. As far as I can tell this amounts to using the named ${Function:FOO}'s contents as the script block, no more no less. This is really just the same as the accepted answer, with the one difference that it reuses an existing function definition instead of assigning and then using a (script) block.Faruq
S
5

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.

Swiercz answered 4/10, 2016 at 16:19 Comment(8)
I'm unable to get this to work, the job just outputs "$function:FOO" instead of "HEY"Veneaux
I've updated it with a version that definitely works. Funny, sure it was working before but can confirm the old version wasn'tSwiercz
How do you pass parameters using this syntax?Hamon
Add a Param block to the FOO definition and then -ArgumentList $args to the Start-Job invocationSwiercz
Does this technique work with exporting 3 functions, and some code outside a function (which you would call from Start-Job, and that code would then call the functions)? That's my scenario and trying to figure out how variables get passed to the code outside the function (but inside the -ScriptBlock )Hamon
Please post a separate question for this - it's not clear from such a short description. Might be easier to dot-source the fileSwiercz
I really wish that someone had answered @Hamon 's comment here. It was very clear what he was asking for; it's exactly what I wanted to know. I have no idea where to find his question on stackoverflow.Jacintajacinth
I see you have posted your question and it has been answeredSwiercz
T
3

The function needs to be inside the scriptblock:

Start-Job -ScriptBlock { function FOO { write-host "HEY" } ; FOO } | Wait-Job | Receive-Job
Triecious answered 23/8, 2011 at 14:19 Comment(2)
Mine is completed, not sure why yours is still running. Can you test this again in a fresh instance?Triecious
I think there's an issue with xp sp3 and posh 2.0. Now I'm on a windows 7 and backgroudjobs work great. I've googled but found nothing. Maybe just my personal issue on xp sp3. Some similar was start-job with import-module with w2k3 or xp sp3... On w2k8 and w2k8r2 no problems.Danford
R
3

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.

    • Sadly, due to a long-standing bug, $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 #4530

A 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.

Romilda answered 6/8, 2023 at 17:31 Comment(3)
Is it possible to define the function name inside the Scriptblock with a variable?Patty
@secondplace: Yes: function Foo { 'foo!' }; Start-Job { $funcName = 'Foo'; $null = New-Item Function:$funcName -Value "$using:function:Foo"; Foo } | Receive-Job -Wait -AutoRemoveJob. Note that you can always invoke a script block (stored in a variable) directly: $sb = { 'foo!' }; & $sbRomilda
The $using:function:Foo itself cannot be concatenated with $funcName? Currently I hardcode the available functions using another example you posted ${function:Foo} = ${using:function:Foo} and then call the one I need via & $funcName. Using your ${...} example I found to be more versatile since it allows for - and numbers in the function name.Patty
B
2

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
Bellbottoms answered 9/4, 2022 at 0:10 Comment(2)
How would this look if the function had named arguments?Dispensary
@DHummel, I updated my example to answer your question.Bellbottoms
F
0

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
Fabre answered 29/1, 2020 at 16:17 Comment(0)
R
0

@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
Recline answered 22/4, 2020 at 17:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.