How to solve the error while invoking function through Start-Job? - powershell

I am having a file named "build.ps1" where there is a function called "Execute-build" available.
I am calling that function from another file named "Dailybuild.ps1" like below.
. ./Build.ps1
# starting different jobs (parallel processing)
$job1 = Start-Job { Execute-Build "List.txt" }
$job2 = Start-Job { Execute-Build "List2.txt" }
# synchronizing all jobs, waiting for all to be done
Wait-Job $job1, $job2
# receiving all results
Receive-Job $job1, $job2
# cleanup
Remove-Job $job1, $job2
But i am receiving error like follows
Receive-Job : The term 'Execute-Build' is not recognized as the name
of a cmdle t, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path
is correct and try again.
Why this error occurs and how to resolve this?

The dot sourced code will not be available in the background job.
One way to solve this is to dot source Build.ps1 in the background job like this:
$job1 = Start-Job {
. "C:\Path\To\Build.ps1"
Execute-Build "List.txt"
}
You can also pass the path as a parameter like this:
$path = (Resolve-Path ./Build.ps1).Path
$job1 = Start-Job {
param ($ScriptPath)
. "$ScriptPath"
Execute-Build "List.txt"
} -ArgumentList $path

Start-Job open a new instance of PowerShell.exe which doesn't have your Execute-Build function. You need to include it in the script block and then call it or use -InitializationScript parameter:
$a = { function myfunction {return "whatever!"} }
$job = Start-Job {myfunction} -InitializationScript $a
Get-Job

Related

Send string parameters to a Start-Job script block

I need to initialize a job using the shell. The job will be a delay plus a call to a vbScript. The following code works fine. For my example, the vbScript is just a single line with a MsgBox "Hello world!"
$functions = {
Function execute_vbs {
param ([string]$path_VBScript, [int]$secs)
Start-Sleep -Seconds $secs
cscript /nologo $path_VBScript
}
}
$seconds = 2
Start-Job -InitializationScript $functions -ScriptBlock {execute_vbs -path_VBScript 'C:\Users\[USERNAME]\Desktop\hello_world.vbs' -secs $seconds} -Name MyJob
The problem comes the moment I want to parameterize the vbScript path. (the idea is to do several different calls to some different vbScripts).
When I do this, the command seems to ignore the parameter input. I did other tests with int parameter and they work fine, the problem looks to be only with the string parameters. The following code does not work:
$functions = {
Function execute_vbs {
param ([string]$path_VBScript, [int]$secs)
Start-Sleep -Seconds $secs
cscript /nologo $path_VBScript
}
}
$input = 'C:\Users\[USERNAME]\Desktop\hello_world.vbs'
$seconds = 2
Start-Job -InitializationScript $functions -ScriptBlock {execute_vbs -path_VBScript $input -secs $seconds} -Name MyJob
I've also tried using the [-ArgumentList] command, but it has the same problem.
Any idea?
The problem is that the $input and $seconds variables inside your script block are in a different scope and are effectively different variables to the ones in the main script.
I've modified your script slightly to remove the call to VBScript to make it easier to reproduce here - my example code is:
$functions = {
Function execute_vbs {
param ([string]$path_VBScript, [int]$secs)
Start-Sleep -Seconds $secs
write-output "filename = '$path_VBScript'"
write-output "secs = '$secs'"
}
}
Here's two ways to fix it:
The Using: scope modifier
See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-7#the-using-scope-modifier for the full details, but basically:
For any script or command that executes out of session, you need the Using scope modifier to embed variable values from the calling session scope, so that out of session code can access them.
$filename = 'C:\Users\[USERNAME]\Desktop\hello_world.vbs'
$seconds = 2
$job = Start-Job -InitializationScript $functions -ScriptBlock {
execute_vbs -path_VBScript $using:filename -secs $using:seconds
} -Name MyJob
wait-job $job
receive-job $job
# output:
# filename = 'C:\Users\[USERNAME]\Desktop\hello_world.vbs'
# secs = '2'
Note the $using before the variable names inside the script block - this allows you to "inject" the variables from your main script into the scriptblock.
ScriptBlock Parameters
You can define parameters on the script block similar to how you do it with a function, and then provide the values in the -ArgumentList parameter when you invoke Start-Job.
$filename = 'C:\Users\[USERNAME]\Desktop\hello_world.vbs'
$seconds = 2
$job = Start-Job -InitializationScript $functions -ScriptBlock {
param( [string] $f, [int] $s )
execute_vbs -path_VBScript $f -secs $s
} -ArgumentList #($filename, $seconds) -Name MyJob
wait-job $job
receive-job $job
# output:
# filename = 'C:\Users\[USERNAME]\Desktop\hello_world.vbs'
# secs = '2'
``

Function not accessible in a ScriptBlock

I have a script that has some functions and then multiple jobs in the very same script that use those functions. When I start a new job they don't seem to be accessible in the [ScriptBlock] that I have for my jobs.
Here's a minimal example demonstrating this:
# A simple test function
function Test([string] $string)
{
Write-Output "I'm a $string"
}
# My test job
[ScriptBlock] $test =
{
Test "test function"
}
# Start the test job
Start-Job -ScriptBlock $test -Name "Test" | Out-Null
# Wait for jobs to complete and print their output
#(Get-Job).ForEach({
Wait-Job -Job $_ |Out-Null
Receive-Job -Job $_ | Write-Host
})
# Remove the completed jobs
Remove-Job -State Completed
The error that I get in PowerShell ISE is:
The term 'Test' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
+ CategoryInfo : ObjectNotFound: (Test:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
+ PSComputerName : localhost
Start-Job run jobs in separate PowerShell processes. So that, jobs do not have access to session state of calling PowerShell session. You need to define functions, which get used by jobs, in every job. An easy way to do that without duplicating the code would be using of -InitializationScript parameter, where all common functions can be defined.
$IS = {
function CommonFunction1 {
'Do something'
}
function CommonFunction2 {
'Do something else'
}
}
$SB1 = {
CommonFunction1
CommonFunction2
}
$SB2 = {
CommonFunction2
CommonFunction1
}
$Job1 = Start-Job -InitializationScript $IS -ScriptBlock $SB1
$Job2 = Start-Job -InitializationScript $IS -ScriptBlock $SB2
Receive-Job $Job1,$Job2 -Wait -AutoRemoveJob
Just extending PetSerAl's answer, you can use Runspaces for this, if you want faster code and a little bit more organised. Check out this question:
39180266
So when you run something in different runspace, you need to import functions in both of them. So finished structure would look like:
Module: functions.ps1 - you store here functions to share with both scopes.
Main script: script.ps1 - it's basically your script, with runspaces, but without functions from functions.ps1.
And in beginning of your script.ps1, just simply call Import-module .\functions.ps1, to get access to your functions. Remember that runscape has different scope, and in their scriptblock, you have to call import-module once again. Full example:
#file functions.ps1
function add($inp) {
return $inp + 2
}
#file script.ps1
Import-module .\functions.ps1 #or you can use "dot call": . .\function.ps1
Import-module .\invoke-parallel.ps1 #it's extern module
$argument = 10 #it may be any object, even your custom class
$results = $argument | Invoke-Parallel -ScriptBlock {
import-module .\functions.ps1 #you may have to use here absolute path, because in a new runspace PSScriptRoot may be different/undefined
return (add $_) # $_ is simply passed object from "parent" scope, in fact, the relationship between scopes is not child-parent
}
echo $result # it's 12
echo (add 5) # it's 7

Calling Another Powershell Script With A Timeout

I have this function:
function getHDriveSize($usersHomeDirectory)
{
$timeOutSeconds = 600
$code =
{
$hDriveSize = powershell.exe $script:getHDriveSizePath - path $usersDirectory
return $hDriveSize
}
$job = Start-Job -ScriptBlock $code
if (Wait-Job $job -Timeout $timeOutSeconds)
{
$receivedJob = Receive-Job $job
return $receivedJob
}
else
{
return "Timed Out"
}
}
When I call it, I get a CommandNotFoundException:
-path : The term '-path' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
However, the line:
$hDriveSize = powershell.exe $script:getHDriveSizePath - path $usersDirectory
by itself works fine.
How can I call the powershell script within the $code variable successfully?
Variables and functions defined outside the scriptblock are not available inside the scriptblock. Because of this, both $script:getHDriveSizePath and $usersDirectory inside the scriptblock are $null, so that you're actually trying to run the statement powershell.exe -Path, which produces the error you observed. You need to pass variables as parameters into the scriptblock:
function getHDriveSize($usersHomeDirectory) {
$timeOutSeconds = 600
$code = {
& powershell.exe $args[0] -Path $args[1]
}
$job = Start-Job -ScriptBlock $code -ArgumentList $script:getHDriveSizePath, $usersHomeDirectory
if (Wait-Job $job -Timeout $timeOutSeconds) {
Receive-Job $job
} else {
'Timed Out'
}
}

Powershell start-job -scriptblock cannot recognize the function defined in the same file?

I have the following code.
function createZip
{
Param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
try {
Start-Job -ScriptBlock { createZip "abd" "acd" }
}
catch {
$_ | fl * -force
}
Get-Job | Wait-Job
Get-Job | receive-job
Get-Job | Remove-Job
However, the script returns the following error.
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
309 Job309 Running True localhost createZip "a...
309 Job309 Failed False localhost createZip "a...
Receive-Job : The term 'createZip' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:17 char:22
+ Get-Job | receive-job <<<<
+ CategoryInfo : ObjectNotFound: (function:createZip:String) [Receive-Job], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
It seems the function name cannot be recognized inside the script block of start-job. I tried function:createZip too.
Start-Job actually spins up another instance of PowerShell.exe which doesn't have your createZip function. You need to include it all in a script block:
$createZip = {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
An example returning an error message from the background job:
$createZip = {
param ([String] $source, [String] $zipfile)
$output = & zip.exe $source $zipfile 2>&1
if ($LASTEXITCODE -ne 0) {
throw $output
}
}
$job = Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
$job | Wait-Job | Receive-Job
Also note that by using a throw the job object State will be "Failed" so you can get only the jobs which failed: Get-Job -State Failed.
If you are still new to using start-job and receive-job, and want to debug your function more easily, try this form:
$createZip = {
function createzipFunc {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
#other funcs and constants here if wanted...
}
# some secret sauce, this defines the function(s) for us as locals
invoke-expression $createzip
#now test it out without any job plumbing to confuse you
createzipFunc "abd" "acd"
# once debugged, unfortunately this makes calling the function from the job
# slightly harder, but here goes...
Start-Job -initializationScript $createZip -scriptblock {param($a,$b) `
createzipFunc $a $b } -ArgumentList "abc","def"
All not made simpler by the fact I did not define my function as a simple filter as you have, but which I did because I wanted to pass a number of functions into my Job in the end.
Sorry for digging this thread out, but it solved my problem too and so elegantly at that. And so I just had to add this little bit of sauce which I had written while debugging my powershell job.

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

How do I Start a job of a function i just defined?
function FOO { write-host "HEY" } Start-Job -ScriptBlock { FOO } |
Receive-Job
Receive-Job: The term 'FOO' is not recognized as the name of cmdlet,
function ,script file or operable program.
What do I do?
Thanks.
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.
#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
It worked for me as:
Start-Job -ScriptBlock ${Function:FOO}
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.
The function needs to be inside the scriptblock:
Start-Job -ScriptBlock { function FOO { write-host "HEY" } ; FOO } | Wait-Job | Receive-Job
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
#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
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