Function not accessible in a ScriptBlock - powershell

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

Related

Getting an error when executing a nested ScriptBlock from Invoke-Command

I'm looking for method to create a wrapper around Invoke-Command that restores the current directory that I'm using on the remote machine before invoking my command. Here's what I tried to do:
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $target
}
$sb = $using:ScriptBlock
$sb.Invoke() | out-host
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
Command Line:
PS> nice_invoke -Computer pv3039 -ScriptBlock {get-location |out-host; get-ChildItem | out-host }
Error Message:
Method invocation failed because [System.String]
does not contain a method named 'Invoke'.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName : pv3039
You can't pass a ScriptBlock like this with the $using: scope, it will get rendered to a string-literal first. Use the [ScriptBlock]::Create(string) method instead within your $wrapper block to create a ScriptBlock from a String:
$sb = [ScriptBlock]::Create($using:ScriptBlock)
$sb.Invoke() | Out-Host
Alternatively, you could also use Invoke-Command -ArgumentList $ScriptBlock, but you still have the same issue with the ScriptBlock getting rendered as a string. Nonetheless, here is an example for this case as well:
# Call `Invoke-Command -ArgumentList $ScriptBlock`
# $args[0] is the first argument passed into the `Invoke-Command` block
$sb = [ScriptBlock]::Create($args[0])
$sb.Invoke() | Out-Host
Note: While I kept the format here in the way you were attempting to run the ScriptBlock in your original code, the idiomatic way to run ScriptBlocks locally (from the perspective the nested ScriptBlock it is a local execution on the remote machine) is to use the Call Operator like & $sb rather than using $sb.Invoke().
With either approach, the nested ScriptBlock will execute for you from the nested block now. This limitation is similar to how some other types are incompatible with shipping across remote connections or will not survive serialization with Export/Import-CliXml; it is simply a limitation of the ScriptBlock type.
Worthy to note, this limitation persists whether using Invoke-Command or another cmdlet that initiates execution via a child PowerShell session such as Start-Job. So the solution will be the same either way.
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $using:cwd
}
$sb = [scriptblock]::Create($using:ScriptBlock)
$sb.Invoke()
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
nice_invoke -Computer pv3039 -ScriptBlock {
hostname
get-location
#dir
}

Passing arguments to job initialization script

I have multiple jobs and for every job I want to have the same initialization script that sets some things up. I'd like to pass some arguments to the initialization script, but unfortunately the arguments passed using -ArgumentList seem to be only accessible in the actual job script.
Here's an example that demonstrates the argument only being accessible in the actual script:
function StartJob([ScriptBlock] $script, [string] $name, [ScriptBlock] $initialization_script = $null, $argument = $null)
{
Start-Job -ScriptBlock $script -Name $name -InitializationScript $initialization_script -ArgumentList $argument | Out-Null
}
[ScriptBlock] $initialization_script =
{
# The argument given to StartJob should be accessible here
param($test)
echo "Test: $test"
}
[ScriptBlock] $actual_script =
{
param($test)
echo "Test: $test"
}
StartJob $actual_script "Test job" $initialization_script "Have this string in the `$initialization_script"
#(Get-Job).ForEach({
# Wait for the job to finish, remove it and output its results
Write-Host "$($_.Name) results:"
Receive-Job -Job $_ -Wait -AutoRemoveJob | Write-Host
})
How would I be able to be access the arguments passed in the $initialization_script?
AFAIK it's not possible to pass parameters to initialization scripts. Init scripts are designed to be reusable scripblocks to load known resources. If something can't be defined once, then it's unique to that job's scriptblock and doesn't belong in a init. script. You have a few alternatives:
If you have a module (.psm1 and maybe a .psd1), then place it in one of the module-folders (see $env:PSModulePath for paths) so you could simply write Import-Module MyImportantModule in your initialization script.
If you can't use the solution above, I would add a paramter to the actual script and pass in the path as a regular argument.
[ScriptBlock] $actual_script =
{
# The argument given to StartJob should be accessible here
param($test, $ModulePath)
#Import-Module $ModulePath
echo "Test: $test"
}
Start-Job -ScriptBlock $actual_script -Name "Test job" -ArgumentList "First argument", "c:\mymodule.ps1"
Or you could generate the initialization scriptblock in your script so it's dynamic:
$ModulePath = "c:\mymodule.ps1"
$init = #"
#Import-Module "$ModulePath"
#Something-Else
"#
$initsb = [scriptblock]::Create($init)

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

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

How do I call Start-Job which depends on a function in the same powershell module as the function calling Start-Job?

I'm writing some powershell to talk to the AWS API, in a single module. I have written one function, Get-CloudFormation, which returns the status of a CloudFormation. I've written another function, Delete-CloudFormation, which after firing off a delete-CF API request, tries to start a job which polls the status of the CloudFormation using my Get-CloudFormation.
I call Export-ModuleMember on Get-CloudFormation (but not Delete-CloudFormation; that's a private function). Get-CloudFormation is defined earlier in the module-file than Delete-CloudFormation.
My Start-Job call (inside Delete-CloudFormation) looks like:
$job = Start-Job -Name "CloudFormationWaitForDeleteSuccess" -ScriptBlock {
$status = ""
$time = 0
while($status -ne "DELETE_COMPLETE") {
Write-Verbose ("Checking CloudFormation status")
$stack = Get-CloudFormation -accessKey $accessKey -secretKey $secretKey -stackName $stackName
$status = $stack.Status
Start-Sleep -seconds 10
$time += 10
}
Write-Host "CloudFormation delete-complete after $time seconds $stackName"
}
When Delete-CloudFormation runs, I get an exception:
The term 'Get-CloudFormation' 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: (Get-CloudFormation:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
Why? And how do I fix it?
I found 7152090 which I think is similar, but calling Start-Job with -InitializationScript { Get-CloudFormation } gives roughly the same error.
If I call Start-Job with -InitializationScript { Import-Module ".\awsutils.psm1" } then . is my profile's documents directory. Even if I bind a variable to Get-Location outside the Start-Job and call it like -InitializationScript { Import-Module "$location\awsutils.psm1" }.
move you module awsutils.psm1 in the canonical path for powershell modules:
$env:userprofile\documents\WindowsPowerShell\Modules\awsutils"
then initialize start-job like this
-InitializationScript { Import-Module awsutils }
Tested with my custom modules and start-job works.
try also, if you don't want move your psm1 this:
-InizializationScript { import-module -name c:\yourpath\yourmodulefolder\ }
where yourmoduleforder contain only one psm1 file.
Background jobs are autonomous things. They aren't a separate thread sharing resources, they are actually run in a whole new PowerShell.exe process. So I think you will need to use Import-Module inside your script block to have you module members available there.
$root = $PSScriptRoot
$initScript = [scriptblock]::Create("Import-Module -Name '$root\Modules\Publish-Assigned_CB_Reports.psm1'")
$job1 = Start-Job -InitializationScript $initScript -ScriptBlock {} -ArgumentList
What I ended up doing was setting $env:WhereAmI = Get-Location before the call to Start-Job, and then changing to -InitializationScript { Import-Module "$env:WhereAmI\awsutils.psm1 }. After the Start-Job call, I called Remove-Item env:\WhereAmI to clean-up.
(I wanted a solution that didn't require me to be developing the module within the $PSModulePath, because then source-control is a little more painful to set up.)
Thanks for the responses.

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.