How to retrieve a variable value from a script block (job) - powershell

I'm trying to get the value of a variable modified inside a scriptblock:
function Test-Function {
$var = "apple"
Start-Job -ScriptBlock {
$var = "banana"
}
Write-Host "Variable is $var"
}
Test-Function
Variable is: apple
I am trying to get the output 'banana'. Is this possible?

Since you are using PS Jobs in your code, you need to use wait for the job to get completed using wait-job and finally you have to receive the job using receive-job. Replace your code with the below:
function Test-Function {
$var = "apple"
Start-Job -ScriptBlock {
$var = "banana"
Write-Host "Variable is $var"
} | Wait-Job -Any |Receive-Job
#Write-Host "Variable is $var"
}
Test-Function
Hope it helps.

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'
``

How to add Parameters into a Job using argument list

I'm trying to create a responsive form, using jobs, and the Add-Jobtracker matrix.
The problem that I have is when I try to call variables inside the job. I used the -Argumentlist parameter but it is always Null, and I'm sure that the way that I'm using it is wrong, could you please guide me with this?
This is just an example, as my original script is a little bit more complex, below script will only write text from 2 variables to a textbox. I've created the same outside and inside the jobscript, used argument list and param inside it and it didn't work.
$Var1 = "sdaasdsadsa"
$Var2 = "asdasdsadsadsa"
$JobScript = {
Param($Var1, $Var2)
Write-Host $Var1, $Var2
}
$UpdateScript = {
Param($Job)
$texbox.Text = 'Working...'
}
$CompletedScript = {
Param($Job)
$results = Receive-Job -Job $Job
$textbox.Text = $results
}
Add-JobTracker -Name "test" -JobScript $JobScript -UpdateScript $UpdateScript -CompletedScript $CompletedScript -ArgumentList $var1, $Var2
At this point it is not doing anything, I have an alternative code but it is longer than this one, and I did not want to bother you guys with a lot of lines.
The job is running under a different scope than the rest of your script, and in that scope the variables aren't defined and therefore $null.
To call variables inside a job, use the $Using:Varname syntax. Eg:
$Test = "TEST!"
$null = Start-Job {
$Test
}
$Job = Get-Job
Start-Sleep -Seconds 2
Receive-Job -Job $Job
Yields no output at all, but:
$Test = "TEST!"
$null = Start-Job {
$Using:Test
}
$Job = Get-Job
Start-Sleep -Seconds 2
Receive-Job -Job $Job
Yields:
TEST!
Read more about Remote Variables here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_variables?view=powershell-6

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

Powershell: passing parameters to functions stored in variables

I'm trying to get a simple working example of using functions inside of jobs. I've managed to pass my function into the scriptblock used for my job, but I can't seem to get parameters to the function.
# concurrency
$Logx =
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
$scriptBlock =
{
Invoke-Expression -Command $args[1]
Start-Sleep 3
}
Write-Host "Processing: " $_
Start-Job -scriptblock $scriptBlock -args $_, $Logx
}
Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *
Here's the output:
Processing: red
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
175 Job175 Running True localhost ...
Processing: blue
177 Job177 Running True localhost ...
Processing: green
179 Job179 Running True localhost ...
179 Job179 Running True localhost ...
177 Job177 Running True localhost ...
175 Job175 Running True localhost ...
Running...
Running...
OUT:
OUT:
OUT:
So as evidenced by the OUT: x3 in the output my function is getting called, but I haven't found any syntax that allows me to get the parameter to the function. Thoughts?
EDIT:
Note in Shawn's observation below and my response I tried using functions as variables because using a traditional function does not seem to work. If there is a way to get that working I'd be more than happy to not have to pass my functions around as variables.
The answer is to use the initializationscript parameter of Start-Job. If you define all your functions in a block and pass the block they become available.
Solution was found in this post:
How do I Start a job of a function i just defined?
Here is my example from before, now working:
# concurrency
$func = {
function Logx
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
$scriptBlock =
{
Logx $args[0]
Start-Sleep 9
}
Write-Host "Processing: " $_
Start-Job -InitializationScript $func -scriptblock $scriptBlock -args $_
}
Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-Job *
If you do not prefix your function name with keyword function, PowerShell does not know to treat it as such. As you have written your script it is basically a variable with some special text in it. Which as your output shows it is only executing the commands it recognizes within that variable's content: Write-Host "OUT:".
Using the correct syntax will tell PowerShell it is a function and that you have variables to pass into it that you need executed:
function Logx
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
Then when you call it within your script you will just use Logx
Got this far. Have to run out, will try back later.
PS: What is getting passed at args[1], I am getting a lot of red,
CategoryInfo : InvalidData: (:) [Invoke-Expression], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.InvokeExpressionCommand
here is what I've managed so far.
# concurrency
$Logx =
{
param(
[parameter(ValueFromPipeline=$true)]
$msg
)
Write-Host ("OUT:"+$msg)
}
# Execution starts here
cls
$colors = #("red","blue","green")
$colors | %{
& $scriptBlock =
{ Invoke-Expression -Command $args[1]
Start-Sleep 3
}
Write-Host "Processing: " $_
Start-Job -scriptblock $scriptBlock -ArgumentList #($_, $Logx)
}
# Get-Job
while(Get-Job -State "Running")
{
write-host "Running..."
Start-Sleep 2
}
# Output
Get-Job | Receive-Job
# Cleanup jobs
Remove-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