Send string parameters to a Start-Job script block - powershell

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

Related

Pass command line arguments on to to "self" but with Start-Job

At [Asynchronous start][1] I had a question about starting a power-shell script asynchronously which creates a form. As answered in that question this can be solved using start-job
eg
Start-Job -ScriptBlock { test NW -NoWarning -Paranoia:2 }
So I have tried to write the Test.PS1 script routine so it re-calls itself with "Start-Job Test -NoSpawn" The switch nospawn then means it runs without a second call. I have tested this with the example code the above line now has to be and it works
Start-Job -ScriptBlock { test NW -NoSpawn -NoWarning -Paranoia:2 }
However I'm struggling to get the parameters from the original command line to passthrough to the job
I have tried creating a string in the correct format , an array , list the arguments manually , I either get repeated arguments being passed or all of the string ending up in the first Parameter $ComputerList -
A summary of the parameters and the attempts are
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
...... <Snip>
Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia }
Doesn't work due to scope - also switches are wrong way to do this
Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList
Insufficent arguments but works - But I think One Argument is possible ?
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia")
Everything ends up in $ComputerList
Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia"
Everything ends up in $ComputerList
Full code follows
Param ([string]$ComputerList = 'status\edi.csv',[switch]$NoSpawn,[switch]$NoWarning,[switch]$Debug,[INT]$Paranoia=6)
$Log_Paranoia=$Paranoia
If ($Debug) { $debugPreference="Continue"} #enable debug messages if -debug is specified
If ($NoWarning) { $WarningPreference="SilentlyContinue"} #turn off warning messages
function Write-Paranoid($Level, $message) {
$CS=Get-PSCallStack
$Caller = $CS[1]
$Module = "$($Caller.FunctionName)[$($Caller.ScriptLineNumber)]"
$Diff=$level - $Log_Paranoia
$MSG= "$Module($($Level),$($Log_Paranoia)):$message"
if ($level - $Log_Paranoia -le 0 ) {
Write-host $MSG
}
if($Error.Count -gt 0 ) {
$MSG= "$Module($Level)ERROR:$Error[0]"
Write-Error $MSG
}
$error.clear()
}
Function AddStatusBar($form , $Txt) {
Write-Paranoid 10 "Enter"
$statusBar = New-Object System.Windows.Forms.StatusBar
$statusBar.DataBindings.DefaultDataSourceUpdateMode = 0
$statusBar.TabIndex = 4
$statusBar.Size = SDS 428 22
$statusBar.Location = SDP 0 337
$statusBar.Text = $Txt
$form.Controls.Add($statusBar)
$statusBar
Write-Paranoid 10 "Exit"
}
Function Create-Form ($Title)
{
Write-Paranoid 10 "Enter"
$form1 = New-Object System.Windows.Forms.Form
$form1.Text = $Title
$form1.DataBindings.DefaultDataSourceUpdateMode = 0
$form1.ClientSize = SDS 890 359
$form1.StartPosition = 0
$form1.BackColor = [System.Drawing.Color]::FromArgb(255,185,209,234)
$form1
Write-Paranoid 10 "Exit"
}
Function GenerateTestForm
{
Write-Paranoid 10 "Enter"
[reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null
[reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null
$Form1 = Create-Form "Test Form"
$Alist = Get-CommandLine
$StatusBar = AddStatusBar $form1 $AList
$form1.ShowDialog() | Out-Null # Suspends calller
Write-Paranoid 10 "Exit"
}
if ($NoSpawn )
{
Write-Paranoid 3 " NoSpawn "
Write-Paranoid 5 "Call GenerateForm"
if ($Test) {
GenerateTestForm
} else {
GenerateTestForm
}
} else {
Write-Paranoid 3 "NOT NoSpawn restarting as job"
# note that test.ps1 is in the path so it will restart this script
# Start-Job -ScriptBlock { test $ComputerList -NoSpawn -NoWarning:$NoWarning -Paranoia:$Paranoia } #Wrong scope
# Start-Job -ScriptBlock { test -NoSpawn $Args } -argumentlist $ComputerList # Insufficent aruments but works - ONLY One Argument possible -
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist #("-NoWarning:$NoWarning","-ComputerList:$ComputerList","-Paranoia:$Paranoia") # Everything ends up in $ComputerList
# Start-Job -ScriptBlock { test $Args -NoSpawn } -argumentlist "-NoWarning:$NoWarning -ComputerList:$ComputerList -Paranoia:$Paranoia" # Everything ends up in $ComputerList
}
Your problem can be reduced to this:
How can I re-invoke the script at hand as a background job, passing all original arguments (parameter values, including default parameter values) through?
A simplified example:
param (
[string] $ComputerList = 'status\edi.csv',
[switch] $NoSpawn,
[switch] $NoWarning,
[switch] $Debug,
[int] $Paranoia=6
)
if ($NoSpawn) { # already running as a background job.
"I'm now running in the background with the following arguments:"
$PSBoundParameters
} else { # must re-invoke via a background job
# Add *default* parameter values, if necessary, given that
# they're *not* reflected in $PSBoundParameters.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultValue = Get-Variable -Scope Local -ValueOnly $paramName
if (-not ($null -eq $defaultValue -or ($defaultValue -is [switch] -and -not $defaultValue))) {
$PSBoundParameters[$paramName] = $defaultValue
}
}
}
# Start a background job that reinvokes this script with the original
# arguments / default values.
Start-Job {
$argsHash = $using:PSBoundParameters
& $using:PSCommandPath -NoSpawn #argsHash
} |
Receive-Job -Wait -AutoRemoveJob
}
Note:
For demonstration purposes, the initial call waits for the re-invocation via a background job to finish, using Receive-Job -Wait -AutoRemoveJob
In your real code, you can simply discard Start-Job's output (a job-information object) with $null = Start-Job { ... }, and then rely on the job getting cleaned up when the caller's session as a whole exits.
The extra code needed to propagate parameter default values is somewhat cumbersome, but necessary, given that the automatic $PSBoundParameters variable does not reflect default values.
GitHub issue #3285 discusses this limitation, and suggests a potential future solution.

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

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)

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: Call functions outside scriptblock

I was reading this post about getting functions passed into a scriptblock for use with jobs:
Powershell start-job -scriptblock cannot recognize the function defined in the same file?
I get how that works by passing the function in as variable and it works for the simple example. What about a real world solution though, is there a more elegant way of handling this?
I have script I'm using to deploy changes to vendor software. It reads an xml that tells it how to navigate the environment and performs the various tasks, ie: map drives, stop services, call a perl installation script. I would like to provide a parameter to the script to allow it to run concurrently, this way if the perl script takes 5 minutes (not uncommon) and you're rolling out to 11 servers you're not waiting for the script to run for an hour.
I'm just going to post some snippets since the full script is a little lengthy. A log function:
function Log
{
Param(
[parameter(ValueFromPipeline=$true)]
$InputObject,
[parameter()]
[alias("v")]
$verbosity = $debug
)
$messageIndex = [array]::IndexOf($verbosityArray, $verbosity)
$verbosityIndex = [array]::IndexOf($verbosityArray, $loggingVerbosity)
if($messageIndex -ge $VerbosityIndex)
{
switch($verbosity)
{
$debug {Write-Host $verbosity ": " $InputObject}
$info {Write-Host $verbosity ": " $InputObject}
$warn {Write-Host $verbosity ": " $InputObject -ForegroundColor yellow}
$error {Write-Host $verbosity ": " $InputObject -ForegroundColor red}
}
}
}
Here's another function that calls the log function:
function ExecuteRollout
{
param(
[parameter(Mandatory=$true)]
[alias("ses")]
$session,
[parameter(Mandatory=$true)]
$command
)
#invoke command
Invoke-Command -session $session -ScriptBlock {$res = cmd /v /k `"$args[0]`"} -args $command
#get the return code from the remote session
$res = Invoke-Command -session $session {$res}
Log ("Command Output: "+$res)
$res = [string] $res
$exitCode = $res.substring($res.IndexOf("ExitCode:"), 10)
$exitCode = $exitCode.substring(9,1)
Log ("Exit code: "+$exitCode)
return $exitCode
}
And lastly a snippet from my main so you can get an idea of what's going on. $target.Destinations.Destination will contain all the servers and relevant information about them that the deployment will go to. I removed some variable setup and logging to make this more compact so yes you'll see variables referenced that are never defined:
#Execute concurrently
$target.Destinations.Destination | %{
$ScriptBlock = {
$destination = $args[0]
Log -v $info ("Starting remote session on: "+$destination.Server)
$session = New-PSSession -computerName $destination.Server
$InitializeRemote -session $session -destination $destination
#Gets a little tricky here, we need to keep the cmd session so it doesn't lose the sys vars set by env.bat
#String everything together with &'s
$cmdString = $destDrive + ": & call "+$lesDestDir+"data\env.bat & cd "+$rolloutDir+" & perl ..\JDH-rollout-2010.pl "+$rollout+" NC,r:\les & echo ExitCode:!errorlevel!"
Log ("cmdString: "+$cmdString)
Log -v $info ("Please wait, executing the rollout now...")
$exitCode = $ExecuteRollout -session $session -command $cmdString
Log ("ExitCode: "+$exitCode)
#respond to return code from rollout script
$HandleExitCode -session $session -destination $destination -exitCode $exitCode
$CleanUpRemote -session $session -destination $destination
}
Start-Job $ScriptBlock -Args $_
}
So if i go with the approach in the link I'd be converting all my functions to variables and passing them in to the script block. Currently, my log function will by default log in DEBUG unless the verbosity parameter is explicitly passed as a different verbosity. If I convert my functins to variables however powershell doesn't seem to like this syntax:
$Log ("Print this to the log")
So I think I'd need to use the parameter all the time now:
$Log ("Print this to the log" -v $debug
So bottom line it looks like I just need to pass all my functions as variables to the script block and change some formatting when I call them. It's not a huge effort, but I'd like to know if there's a better way before I start hacking my script up. Thanks for the input and for looking, I know this is quite a long post.
I started another post about passing parameters to functions stored as variables, the answer to that also resolves this issue. That post can be found here:
Powershell: passing parameters to functions stored in variables
The short answer is you can use the initializationscript parameter of Start-Job to feed all your functions in if you wrap them in a block and store that in a variable.
Example:
# 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 *