How do I kill main\parent task through background job? - powershell

I have a requirement where I need to kill\stop main task if it has been running for a long time. I am thinking of creating a background job that monitors the time and kills the main job after timeout with proper message but I'm not sure how to do that.
Something like this..
function Kill-MainTask{
$sb = {
Start-Sleep -Seconds 5
throw "main task should end"
}
$job1 = Start-Job -ScriptBlock $sb
Write-Host "main task running"
Start-Sleep -Seconds 10
#This statement should not run
Write-Host "main task not finished"
}
Kill-MainTask
When I call Kill-MainTask function it should print "mian task running" but after 5 seconds should throw.

There are several existing samples that show how to use this main and child job monitoring use case. See the below sample and resource links.
Monitoring the Progress of a PowerShell Job
Quick Tip To Find Out What Your Background Jobs Are Doing
Example:
This is a simple script that create 5 background jobs (as
defined by $Num). After submitting we’ll begin monitoring the
progress of the job. We define $TotProg as 0 to start, then query the
StatusDescription–
and since this returns an array we only want the
last element, hence the -1 element reference–and add it to our
$TotProg. After that we check if $TotProg is greater than 0
(otherwise you’ll get a divide by zero error), display our progress
bar, wait a few seconds and loop again. Go ahead and run the code
(don’t step through it in an ISE, to really see what I mean you have
to just run it).
$Num = 5
$Jobs = #()
ForEach ($Job in (1..$Num))
{ $Jobs += Start-Job -ScriptBlock {
$Count = 1
Do {
Write-Progress -Id 2 -Activity "Background Job" -Status $Count -PercentComplete 1
$Count ++
Start-Sleep -Seconds 4
} Until ($Count -gt 5)
}
}
$Total = $Num * 5
Write-Progress -Id 1 -Activity "Watching Background Jobs" -Status "Waiting for background jobs to started" -PercentComplete 0
Do {
$TotProg = 0
ForEach ($Job in $Jobs)
{ Try {
$Info =
$TotProg += ($Job | Get-Job).ChildJobs[0].Progress.StatusDescription[-1]
}
Catch {
Start-Sleep -Seconds 3
Continue
}
}
If ($TotProg)
{ Write-Progress -Id 1 -Activity "Watching Background Jobs" -Status "Waiting for background jobs to complete: $TotProg of $Total" -PercentComplete (($TotProg / $Total) * 100)
}
Start-Sleep -Seconds 3
} Until (($Jobs | Where State -eq "Running").Count -eq 0)
$Jobs | Remove-Job -Force
Write-Progress -Id 1 -Activity "Watching Background Jobs" -Status "Completed! $Total of $Total" -PercentComplete 100
Start-Sleep -Seconds 1
Write-Progress -Id 1 -Activity "Watching Background Jobs" -Status "Completed! $Total of $Total" -Completed
Or this approach
Creating A Timeout Feature In Your PowerShell Scripts
# define a timeout
$Timeout = 10 ## seconds
# Code for the scriptblock
$jobs = Get-Job
$Condition = {param($jobs) 'Running' -notin $jobs.State }
$ConditionArgs = $jobs
# define how long in between checks
$RetryInterval = 5
# Start the timer
$timer = [Diagnostics.Stopwatch]::StartNew()
# Invoke code actions
while (($timer.Elapsed.TotalSeconds -lt $Timeout) -and (& $Condition $ConditionArgs)) {
## Wait a specific interval
Start-Sleep -Seconds $RetryInterval
## Check the time
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds,0)
Write-Verbose -Message "Still waiting for action to complete after [$totalSecs] seconds..."
}
# The action either completed or timed out. Stop the timer.
$timer.Stop()
# Return status of what happened
if ($timer.Elapsed.TotalSeconds -gt $Timeout)
{ throw 'Action did not complete before timeout period.' }
else
{ Write-Verbose -Message 'Action completed before the timeout period.' }

Related

Progress bar is not showing correctly in PowerShell 7 in ForEach -Parallel

this show 0%:
this show 4%:
the realted script:
$iWrapper = [hashtable]::Synchronized(#{ i = 0 })
$srcfile = "C:\Users\wnune\OneDrive\Escritorio\imagenes\cardlist.txt"
$urls = Get-Content $srcfile
$lines = 0
switch -File $srcfile { default { ++$lines } }
Write-Host "Total Urls to process: $lines "
Write-Progress -Activity "Downloading files" -Status "In progress" -PercentComplete $i;
$urls | ForEach-Object -Parallel {
try {
$url = $_
$filename = Split-Path $url -Leaf
$destination = "C:\Users\wnune\OneDrive\Escritorio\imagenes\$filename"
$ProgressPreference = 'SilentlyContinue'
$response = Invoke-WebRequest -Uri $url -ErrorAction SilentlyContinue
if ($response.StatusCode -ne 200) {
Write-Warning "============================================="
Write-Warning "Url $url return Error. "
continue
}
if (Test-Path $destination) {
Write-Warning "============================================="
Write-Warning "File Exist in Destination: $filename "
continue
}
$job = Start-BitsTransfer -Source $url -Destination $destination -Asynchronous
while (($job | Get-BitsTransfer).JobState -eq "Transferring" -or ($job | Get-BitsTransfer).JobState -eq "Connecting")
{
Start-Sleep -m 250
}
Switch(($job | Get-BitsTransfer).JobState)
{
"Transferred" {
Complete-BitsTransfer -BitsJob $job
}
"Error" {
$job | Format-List
}
}
}
catch
{
Write-Warning "============================================="
Write-Warning "There was an error Downloading"
Write-Warning "url: $url"
Write-Warning "file: $filename"
Write-Warning "Exception Message:"
Write-Warning "$($_.Exception.Message)"
}
$j = ++($using:iWrapper).i
$k = $using:lines
$percent = [int](100 * $j / $k)
Write-Host "PercentCalculated: $percent"
Write-Host "Progress bar not Show the %"
Write-Progress -Activity "Downloading files " -Status " In progress $percent" -PercentComplete $percent
}
Write-Progress -Activity "Downloading files" -Status "Completed" -Completed
If I am passing in -PercentComplete $percent which is an integer why does the progress bar not receive it correctly?
I have verified that the script and the environment are correctly configured but I cannot validate because the progress bar is not seen correctly.
Note:
A potential future enhancement has been proposed in GitHub issue #13433, suggesting adding parameter(s) such as -ShowProgressBar to ForEach-Object -Parallel so that it would automatically show a progress bar based on how many parallel threads have completed so far.
Leaving the discussion about whether Start-BitsTransfer alone is sufficient aside:
At least as of PowerShell v7.3.1, it seemingly is possible to call Write-Progress from inside threads created by ForEach-Object -Parallel, based on a running counter of how many threads have exited so far.
However, there are two challenges:
You cannot directly update a counter variable in the caller's runspace (thread), you can only refer to an object that is an instance of a .NET reference type in the caller's runspace...
...and modifying such an object, e.g. a hashtable must be done in a thread-safe manner, such as via System.Threading.Monitor.
Note that I don't know whether calling Write-Progress from different threads is officially supported, but it seems to work in practice, at least when the call is made in a thread-safe manner, as below.
Bug alert, as of PowerShell 7.3.1: Irrespective of whether you use ForEach-Object -Parallel or not, If you call Write-Progress too quickly in succession, only the first in such a sequence of calls takes effect:
See GitHub issue #18848
As a workaround, the code below inserts a Start-Sleep -Milliseconds 200 call after each Write-Progress call.
Not only should this not be necessary, it slows down overall execution, because threads then take longer to exit, which affects not only a given thread, but overall execution time, because it delays when threads "give up their slot" in the context of thread throttling (5 threads are allowed to run concurrently by default; use -ThrottleLimit to change that.
A simple proof of concept:
# Sample pipeline input
$urls = 1..100 | ForEach-Object { "foo$_" }
# Helper hashtable to keep a running count of completed threads.
$completedCount = #{ Value = 0 }
$urls |
ForEach-Object -parallel { # Process the input objects in parallel threads.
# Simulate thread activity of varying duration.
Start-Sleep -Milliseconds (Get-Random -Min 0 -max 3000)
# Produce output.
$_
# Update the count of completed threads in a thread-safe manner
# and update the progress display.
[System.Threading.Monitor]::Enter($using:completedCount) # lock access
($using:completedCount).Value++
# Calculate the percentage completed.
[int] $percentComplete = (($using:completedCount).Value / ($using:urls).Count) * 100
# Update the progress display, *before* releasing the lock.
Write-Progress -Activity Test -Status "$percentComplete% complete" -PercentComplete $percentComplete
# !! Workaround for the bug above - should *not* be needed.
Start-Sleep -Milliseconds 200
[System.Threading.Monitor]::Exit($using:completedCount) # release lock
}
An alternative approach in which the calling thread centrally tracks the progress of all parallel threads:
Doing so requires adding the -AsJob switch to ForEach-Object -Parallel, which, instead of the synchronous execution that happens by default, starts a (thread-based) background job, and returns a [System.Management.Automation.PSTasks.PSTaskJob] instance that represents all parallel threads as PowerShell (thread) jobs in the .ChildJobs property.
A simple proof of concept:
# Sample pipeline input
$urls = 1..100 | ForEach-Object { "foo$_" }
Write-Progress -Activity "Downloading files" -Status "Initializing..."
# Launch the parallel threads *as a background (thread) job*.
$job =
$urls |
ForEach-Object -AsJob -Parallel {
# Simulate thread activity of varying duration.
Start-Sleep -Milliseconds (Get-Random -Min 0 -max 3000)
$_ # Sample output: pass the URL through
}
# Monitor and report the progress of the thread job's
# child jobs, each of which tracks a parallel thread.
do {
# Sleep a bit to allow the threads to run - adjust as desired.
Start-Sleep -Seconds 1
# Determine how many jobs have completed so far.
$completedJobsCount =
$job.ChildJobs.Where({ $_.State -notin 'NotStarted', 'Running' }).Count
# Relay any pending output from the child jobs.
$job | Receive-Job
# Update the progress display.
[int] $percent = ($completedJobsCount / $job.ChildJobs.Count) * 100
Write-Progress -Activity "Downloading files" -Status "$percent% complete" -PercentComplete $percent
} while ($completedJobsCount -lt $job.ChildJobs.Count)
# Clean up the job.
$job | Remove-Job
While this is more work and less efficient due to the polling loop, it has two advantages:
The script blocks running in the parallel threads need not be burdened with progress-reporting code.
The polling loop affords the opportunity to perform other foreground activity while the parallel threads are running in the background.
The bug discussed above needn't be worked around, assuming your Start-Sleep interval in the polling loop is at least 200 msecs.

Add Write-Progress to Get-Job/Wait-Job

I'm using the below code to display the results of PowerShell Jobs with a timeout of 120 seconds. I would like to enhance this code by incorporating Write-Progress (based on number of jobs completed). I tried using this example as a reference, however, when I try to incorporate that code, the progress bar displays briefly after all the jobs are all done already.
$Jobs = #()
$ForceStoppedIds = #{}
$Jobs += Get-Job
$Jobs | Wait-Job -Timeout 120 | Out-Null
$Jobs | ?{$_.State -eq 'Running'} | Stop-Job -PassThru | %{$ForceStoppedIds[$_.Id] = $true}
foreach ($Job in $Jobs) {
$Name = $Job.Name
$Output = (Get-Job -Name $Name | Receive-Job)
if ($ForceStoppedIds.Contains($Job.Id)) {
Write-Output "$($Name) - Device unable to process request within 2 minutes"
} else {
Write-Output $Output
}
}
Wait-Job -Timeout 120 will block the thread until the specified timeout or all jobs have completed, hence, is not possible to display progress and wait for them at the same time.
There are 2 alternatives that I can think of, the first one would be to create a proxy command / proxy function around this cmdlet to extend it's functionality.
These blogs demonstrate how to do it:
https://devblogs.microsoft.com/scripting/proxy-functions-spice-up-your-powershell-core-cmdlets/
https://devblogs.microsoft.com/powershell/extending-andor-modifing-commands-with-proxies/
proxy function
You can also follow the indications from this helpful answer.
The other alternative is to define your own function that does a similar work as Wait-Job but, instead of blocking the thread, you can add a loop that will run based on 2 conditions:
That the elapsed time is lower than or equal to the Timeout we passed as argument to the function (we can use Diagnostics.Stopwatch for this).
And, that the jobs are still Running (the $jobs List<T> is still populated).
Note, the function below should work in most cases however is purely for demonstration purposes only and should not be relied upon.
First we define a new function that can be used to display progress as well as wait for our jobs based on a timeout:
using namespace System.Collections.Generic
using namespace System.Diagnostics
using namespace System.Threading
using namespace System.Management.Automation
function Wait-JobWithProgress {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[object[]] $InputObject,
[parameter()]
[double] $TimeOut
)
begin {
$jobs = [List[object]]::new()
}
process {
foreach($job in $InputObject) {
$jobs.Add($job)
}
}
end {
$timer = [Stopwatch]::StartNew()
$total = $jobs.Count
$completed = 0.1
$expression = { $true }
if($PSBoundParameters.ContainsKey('TimeOut')) {
$expression = { $timer.Elapsed.TotalSeconds -le $TimeOut }
}
while((& $expression) -and $jobs) {
$remaining = $total - $completed
$average = $timer.Elapsed.TotalSeconds / $completed
$estimate = [math]::Round($remaining * $average)
$status = 'Completed Jobs: {0:0} of {1} - ETC: {2}s' -f $completed, $total, $estimate
$progress = #{
Activity = 'Waiting for Jobs'
PercentComplete = $completed / $total * 100
Status = $status
}
Write-Progress #progress
$id = [WaitHandle]::WaitAny($jobs.Finished, 200)
if($id -eq [WaitHandle]::WaitTimeout) {
continue
}
# output this job
$jobs[$id]
# remove this job
$jobs.RemoveAt($id)
$completed++
}
# Stop the jobs not yet Completed and remove them
$jobs | Stop-Job -PassThru | ForEach-Object {
Remove-Job -Job $_
"Job [#{0} - {1}] did not complete on time and was removed." -f $_.Id, $_.Name
} | Write-Warning
Write-Progress #progress -Completed
}
}
Then for testing it, we can create a few jobs with a random timer:
0..10 | ForEach-Object {
Start-Job {
Start-Sleep (Get-Random -Minimum 5 -Maximum 15)
[pscustomobject]#{
Job = $using:_
Result = 'Hello from [Job #{0:D2}]' -f $using:_
}
}
} | Wait-JobWithProgress -TimeOut 10 |
Receive-Job -AutoRemoveJob -Wait | Format-Table -AutoSize

How to limit while loop by time in powershell

I have a script which starts a process only after specific service is running.
It's a loop that's trying to Get-Service its status.
I can't find how to limit loop by time.
The part where I'm stuck:
#add Start button
$button_start = New-Object System.Windows.Forms.Button
$button_start.Location = New-Object System.Drawing.Size(25,70)
$button_start.Size = New-Object System.Drawing.Size(240,32)
$button_start.TextAlign = "MiddleCenter"
$button_start.font = New-Object System.Drawing.Font("Segoe UI",14,[System.Drawing.FontStyle]::Regular)
$button_start.BackColor = "seashell"
$button_start.Text = "Start"
$button_start.Add_Click({
#add statement
while ((Get-Service -ComputerName $textBox_IP.text -ServiceName wscsvc).Status -ne "Running") {
# Pause before next check
Start-Sleep -Seconds 1
}
#only then..
Start-Process -FilePath "C:\Users\username\Desktop\software.exe" -verb RunAs -ArgumentList $textBox_IP.text
})
$Form_remoteControl.Controls.Add($button_start)
I've tried internet searching information on network without any success.
Define a time limit and check if the current time exceeds that limit.
$limit = (Get-Date).AddMinutes(5)
while (... -or (Get-Date) -le $limit) {
Start-Sleep -Seconds 1
}
If you want to skip starting the external program when the service still isn't running after that add another check after the loop upon which you return:
if ((Get-Service ...).Status -ne "Running") {
return
}
This is an example how to stop a service and wait until it is stopped or timeout applies.
You can modify to start a service.
Function StopService ($serv)
{
Write-Host "Config service " $serv " ..."
$service = Get-Service $serv -ErrorAction SilentlyContinue
if ($service)
{
if($service.status -eq "running")
{
write-host "Stop service" $serv
Stop-Service $serv -Force
# Wait until service is stopped (max. 1 minute)
$acttime = 0
$waittime = 100
$maxtime = 60000
$TestService = Get-Service $serv
While($TestService | Where-Object {$_.Status -eq 'Running'})
{
Start-Sleep -m $waittime
$acttime += $waittime
if ($acttime -gt $maxtime)
{
write-host "ERROR: Service" $serv " could not be stopped!" -ForegroundColor Red
return $False
}
}
}
else
{
write-host "Service already stopped!" -ForegroundColor Green
return $True
}
}
else
{
write-host "Service not installed" -ForegroundColor Green
return $True
}
}
I recommend you not using any polling While loops (with Start-Sleep cmdlets) in a Windows forms interface. It will stall your interface for important form events as button clicks etc.
Instead, I would anticipate on the Windows.Forms Timer class by creating a timer event and take appropriate checks and actions after a certain time period (e.g. a new Start-Process depending on a service state).

Get child job's child job output

If you start a job (A) and this job, in turn, starts another job (B), is it possible to fetch the output of (B) by using something as such?
(Get-Job -Name A).ChildJobs[0].ChildJobs[0]...
I was hoping I could recursively drill down into the object but curiously (Get-Job -Name A).ChildJobs[0] always has an empty ChildJobs collection. This may be just a misunderstanding on my part of how jobs are created.
One workaround to this is to wait until job B finishes, fetch its output, store it in a variable, and perform a Write-Output on the variable so the parent script can handle it. It works but that means I have to wait until job B finishes which might take 10-40 minutes.
I could perhaps (in job B) write output immediately as it occurs to a flat file or perhaps a SQLite database but I was hoping I could fetch it from the top-most scope of the script.
Here is an example
Get-Job | Remove-Job
$ErrorActionPreference = "stop"
$Level1Job = {
$Level2Job = {
$Level3Job = {
Write-Output "Level 3, start"
Start-Sleep -s 5
Write-Output "Level 3, end"
}
Write-Output "Level 2, start"
# start the third job...
Start-Job -ScriptBlock $Level3Job -Name "level 3"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep -s 5
Write-Output "Level 2, end"
}
Write-Output "Level 1, start"
# start the second job on the remote computer...
Start-Job -ScriptBlock $Level2Job -Name "level 2"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep -s 5
Write-Output "Level 1, end"
}
# start the first job...
Start-Job -ScriptBlock $Level1Job -Name "level 1"
# wait for the job to complete
while(get-job | where-object { ($_.State -ne "completed") -and ($_.State -ne "failed") }){ Start-Sleep -s 2 }
Start-Sleep –s 5
(get-job)[0].ChildJobs[0] | fl *
ChildJobs are used for remote-jobs. If you use for example Invoke-Command -ScriptBlock { ... } -AsJob -ComputerName YourRemoteComputer you get a remote-job (the ChildJob) and a local job, that handles the remote-job.
You have the Job-Object from start-Job. if your first job returns this object, you can receive it and read the output

Call Batch file and track progress

So here is what I'm trying to do.
I have a Powershell script that calls bunch of batch files which installs software. Is there anyway to have a progress bar (GUI would be my choice) to track the status of those batch files that is being called?
Thanks in advance.
I found this on TechNet; the article was written by Ed Wilson.
When using the Write-Progress cmdlet, two parameters are required. The first is the activity parameter. The string supplied for this parameter appears on the first line in the progress dialog. The second required parameter is the status parameter. It appears under the Activity line.
I can provide an example from my PowerShell scripts. Used This for the timer.
The code below has a FOR loop to loop over items in a $variable.Count
$time = 7
$percentage = $i / $time
$remaining = New-TimeSpan -Seconds ($time - $i)
$message = "{0:p0} complete" -f $percentage, $remaining
Write-Progress -Activity "Working" -status $message -PercentComplete ($percentage * 100)
This example is slightly different and uses a ForEach to iterate over items in a collection and update a progress bar. Below will run and update a progress bar over 60 seconds.
$time = 60 # seconds
foreach($i in (1..$time)) {
$percentage = $i / $time
$remaining = New-TimeSpan -Seconds ($time - $i)
$message = "{0:p0} complete, remaining time {1}" -f $percentage, $remaining
Write-Progress -Activity "Wait for SCCM scan" -status $message -PercentComplete ($percentage * 100)
Start-Sleep 1
--Edit:
Here's code I came up with that successfully launches 5 batch files that each contain ping 1.1.1.1 -n 2 >NUL and the -n count increases to simulate time elapsed. *Note the PercentComplete option is misbehaving, and my inexperience really shines as I'm unsure it would work in this example. -edit, forgot to credit this post for getting the Write-Progress to work.
$commands = gc C:\test4\l.txt
$i = 0
foreach ($bat in $commands){
Start-Process cmd -ArgumentList "/c $bat" -Wait -WindowStyle Minimized
$i++
Write-Progress -Activity "Batch File Test" -Status "Completed: $i of $($commands.Count)" -PercentComplete (($i / $commands.Count) * 100)
}#END FOREACH
Write-Host "Batch files finished running!"