Show a clock during execution of script - powershell

I am looping through a list of items and doing a write-progress on each item.
Is it possible to show a timer during the script execution?
For example show $elapsedTime with
$elapsedTime = [system.diagnostics.stopwatch]::StartNew()
as basis each second. But would this be possible in the same powershell window?

like this:
$elapsedTime = [system.diagnostics.stopwatch]::StartNew()
1..1000 | %{write-progress -activity "Working..." -status "$([string]::Format("Time Elapsed: {0:d2}:{1:d2}:{2:d2}", $elapsedTime.Elapsed.hours, $elapsedTime.Elapsed.minutes, $elapsedTime.Elapsed.seconds))" -percentcomplete ($_/10);}
$elapsedTime.stop()

Wouldn't something like this work:
1..100 | %{write-progress -activity "Search in Progress" -status "$_ Time: $($elapsedTime.Elapsed)" -percentcomplete $_;}

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.

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

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

Powershell carriage return not working

$startTime = $(get-date)
write-host "`rElapsed:00:00:00"
$NoEvent = $true
While ($NoEvent)
{
Start-Sleep 1
$elapsedTime = new-timespan $startTime $(get-date)
write-host "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))"
#Handle event
if(event){$NoEvent = $false}
}
I've tried running this in the ISE as well as through the regular console. The returns are never output.
I eventually got it working using -NoNewLine switch
write-host -NoNewLine "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))"
`r only issues a CR, not a CR+LF (which is probably what you want). Use `n (‘newline’) instead.
I don't think there is any PS cmdlet that can help with overwriting text from the same line unless you are clearing the entire window with clear-host or cls, but PowerShell has a built in write-progress cmdlet if that is something else you would want to consider.
You can try:
$startTime = $(get-date)
$NoEvent = $true
While ($NoEvent)
{
for ($a=1; $a -lt 100; $a++) {
Start-Sleep 1
$elapsedTime = new-timespan $startTime $(get-date)
Write-Progress -Activity "`rElapsed:$($elapsedTime.ToString('hh\:mm\:ss'))" -PercentComplete $a -CurrentOperation "$a% Processed" -Status "Please wait."
#Handle event
if(event){$NoEvent = $false}
}
}
See: https://learn.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Utility/Write-Progress?view=powershell-5.1 for info on write-progress
and here is a related question: PowerShell - Overwriting line written with Write-Host

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!"

Write-Progress displaying outside of console buffer

I have a script which calls out to an external program (SoX) for each file in a directory. I'm calling Write-Progress before each call to SoX, but the progress bar is pushed off the top of the console buffer by the output of SoX (regardless of the size of the console). Is there anything I can do to avoid this?
Here's the script:
$audioFiles = ls -Exclude *.ps1 | ? { !$_.PSIsContainer }
foreach ($audioFile in $audioFiles)
{
$i++
Write-Progress -Activity "Transforming Audio" -Status $audioFile.Name -PercentComplete (($i / #($audioFiles).length) * 100)
& 'C:\Program Files (x86)\sox-14-4-0\sox.exe' "$audioFile" ('Fast/' + $audioFile.Name) -S -G tempo -s 1.3
}
Write-Progress -Activity "Transforming Audio" -PercentComplete 100 -Completed
[void] [Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”)
[windows.forms.messagebox]::show(“All done!”)
Maybe you can set the same console cursor position before the sox output ?
please try
$audioFiles = ls -Exclude *.ps1 | ? { !$_.PSIsContainer }
foreach ($audioFile in $audioFiles){
$i++
Write-Progress -Activity "Transforming Audio" -Status $audioFile.Name -PercentComplete (($i / #($audioFiles).length) * 100)
# get windows height
$y=[int]($host.ui.rawui.WindowSize.Height -5)
# will set cursor position to bottom of the screen
$Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 2,$y
#clear current line
$sbOut = new-object System.Text.Stringbuilder
(0.. $Host.UI.RawUI.WindowSize.Width)|%{$sbOut.append(' ')} |out-Null
write-Host $sbOut.toString() -NoNewline
& 'C:\Program Files (x86)\sox-14-4-0\sox.exe' "$audioFile" ('Fast/' + $audioFile.Name) -S -G tempo -s 1.3
}
Write-Progress -Activity "Transforming Audio" -PercentComplete 100 -Completed
[void] [Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”)
[windows.forms.messagebox]::show(“All done!”)
you could also redirect sox output to a file or if you dont care about sox output just redirect it to out-null :
& 'C:\Program Files (x86)\sox-14-4-0\sox.exe' "$audioFile" ('Fast/' + $audioFile.Name) -S -G tempo -s 1.3 | out-null