Calling Another Powershell Script With A Timeout - powershell

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

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

Background-Job run Scripts without hardcoding them

I am running a PowerShell GUI that calls different scripts. I'm currently using hardcoded paths such as:
function start-jobhere([scriptblock]$block) {
Start-Job -ArgumentList (Get-Location),$block {
Set-Location $args[0];
Invoke-Expression $args[1]
}
}
$handler_button1_Click = {
$job1 = start-jobhere {& K:\Uploader\Import\DataUploader.ps1} -Name "Uploader"
}
I tried to avoid it using:
$LocalPath = ($MyInvocation.MyCommand.Path).ToLower().Replace("datauploader.ps1", "")
$handler_button1_Click = {
$job1 = start-jobhere {& $LocalPath\DataUploader.ps1} -Name "Uploader"
}
But it doesn't seem to work. I have some scripts in the same folder and some are on a different harddrive. Is there a way to avoid the hardcoded paths?
You're passing a scriptblock to the function. The code inside the scriptblock doesn't know anything about variables in the rest of the script unless you make them known via the using scope modifier:
$job1 = start-jobhere {& "$using:LocalPath\DataUploader.ps1"} -Name "Uploader"
With that said, if you want to run scripts anyway, why aren't you passing their path as a string to your function?
function start-jobhere([string]$Name, [string]$Script) {
Start-Job -Name $Name -ScriptBlock {
Set-Location $args[0]
& $args[1]
} -ArgumentList (Get-Location), $Script
}
$handler_button1_Click = {
$job1 = start-jobhere "$LocalPath\DataUploader.ps1" -Name "Uploader"
}

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

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