I want to asynchronously run a progress bar (which shows elapsed time/estimated time) while waiting for silent installs. For instance,
RunWithProgressBar "cmd /c """" /Wait ""Setup.exe""" $(New-Timespan -Minutes 5)
## Functions
function global:BottleOfBeer {
$beer = $args[0]
if ($beer -eq 1) {
echo "$beer beer on the wall.";
} elseif ($beer -eq 0) {
echo "No beer left on the wall!";
} else {
echo "$beer beers left on the wall.";
}
sleep 1
}
function global:BeersOnTheWall {
$NumBeers = $args[0]
for ($i=$NumBeers; $i -ge 0; $i--) {
BottleOfBeer $i
}
}
function global:Install {
cmd /c @"
"AutoHotkey112306_Install.exe" /S /D="%cd%"
"@
}
function global:Uninstall {
cmd /c start "" /wait "Installer.ahk" /Uninstall
}
####START Progress Bar Stuff
function global:DisplayProgress {
$display = $args[0]
Write-Progress -Activity "Running..." -Status "$display"
}
function global:FormatDisplay {
$StartTime = $args[0]
$RunningTime = ($args[1]).Elapsed
$EstimatedTime = $args[2]
$RunningTimeDisplay = $([string]::Format("{0:d2}:{1:d2}:{2:d2}",
$RunningTime.hours,
$RunningTime.minutes,
$RunningTime.seconds))
$EstimatedEnd = $StartTime + $EstimatedTime
return $([string]::Format("(Start: {0}) (Elapsed/Estimated: {1}/{2}) (EstimatedEnd: {3})",
$StartTime.ToShortTimeString(),
$RunningTimeDisplay,
$EstimatedTime,
$EstimatedEnd.ToShortTimeString()))
}
function global:TearDownProgressBar {
$job = $args[0]
$event = $args[1]
$job,$event | Stop-Job -PassThru | Remove-Job #stop the job and event listener
Write-Progress -Activity "Working..." -Completed -Status "All done."
}
function RunWithProgressBar {
$Payload = $args[0]
$EstimatedTime = $args[1]
$global:StartTime = Get-Date
$global:RunningTime = [System.Diagnostics.Stopwatch]::StartNew()
$global:EstimatedTime = $EstimatedTime
$progressTask = {
while($true) {
Register-EngineEvent -SourceIdentifier MyNewMessage -Forward
$null = New-Event -SourceIdentifier MyNewMessage -MessageData "Pingback from job."
Start-Sleep -Seconds 1
}
}
$job = Start-Job -ScriptBlock $progressTask
$event = Register-EngineEvent -SourceIdentifier MyNewMessage -Action {
DisplayProgress $(FormatDisplay $global:StartTime $global:RunningTime $global:EstimatedTime)
}
try {
sleep 1
Invoke-Expression $Payload
} finally {
TearDownProgressBar $job $event
}
}
####END Progress Bar Stuff
## MAIN
RunWithProgressBar "BeersOnTheWall 2" $(New-Timespan -Seconds 3)
RunWithProgressBar "Install" $(New-Timespan -Seconds 30)
RunWithProgressBar "Uninstall" $(New-Timespan -Seconds 5)
RunWithProgressBar "BeersOnTheWall 2" $(New-Timespan -Seconds 3)
Although the above implementation runs as intended, whenever the payload argument of RunWithProgressBar
is an install the event which updates the progress bar stops getting triggered.
How to modify my current implementation to update the progress bar every second, even while performing an install?
While the solution provided earned the bounty, it's not what I ended up doing. Here's the final version of RunWithProgressBar():
param (
[alias("IM")]
[bool]$IgnoreMain = $false
)
function RunAsBAT([string]$commands) {
# Write commands to bat file
$tempFile = $global:scriptDir + '\TemporaryBatFile.bat'
$commands = "@echo off `n" + $commands
Out-File -InputObject $commands -FilePath $tempFile -Encoding ascii
# Wait for bat file to run
& $tempFile
# Delete bat file
Remove-Item -Path $tempFile
}
function DisplayProgress([string]$display) {
Write-Progress -Activity "Running..." -Status "$display"
}
function FormatDisplay([System.DateTime]$StartTime, [System.TimeSpan]$RunningTime, [System.TimeSpan]$EstimatedTime) {
$RunningTimeDisplay = $([string]::Format("{0:d2}:{1:d2}:{2:d2}",
$RunningTime.hours,
$RunningTime.minutes,
$RunningTime.seconds))
$EstimatedEnd = $StartTime + $EstimatedTime
return $([string]::Format("(Start: {0}) (Elapsed/Estimated: {1}/{2}) (EstimatedEnd: {3})",
$StartTime.ToShortTimeString(),
$RunningTimeDisplay,
$EstimatedTime,
$EstimatedEnd.ToShortTimeString()))
}
function RunWithProgressBar([scriptblock]$payload, [System.TimeSpan]$EstimatedTime) {
$global:StartTime = Get-Date
$global:RunningTime = [System.Diagnostics.Stopwatch]::StartNew()
$global:EstimatedTime = $EstimatedTime
try {
$logFile = $global:scriptDir + '\TemporaryLogFile.txt'
$StartInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
FileName = 'Powershell'
# load this script but don't run MAIN (to expose functions/variables);
# run the payload (and also log to file);
# if error, pause (so the window stays open to display the error)
Arguments = ". $global:scriptPath -IM 1; & $payload | Tee-Object -file $logFile;" + ' if ( $LastExitCode -ne 0 ) { cmd /c pause }'
UseShellExecute = $true
}
$Process = New-Object System.Diagnostics.Process
$Process.StartInfo = $StartInfo
[void]$Process.Start()
do
{
DisplayProgress $(FormatDisplay $global:StartTime ($global:RunningTime).Elapsed $global:EstimatedTime)
Start-Sleep -Seconds 1
}
while (!$Process.HasExited)
}
finally {
if (Test-Path $logFile) {
Get-Content -Path $logFile
Remove-Item -Path $logFile
} else {
Write-Host "No output was logged..."
}
Write-Progress -Activity "Working..." -Completed -Status "All done."
}
}
function TestBlockingCall {
RunAsBAT(@"
timeout 5
"@)
}
## MAIN
if (-Not $IgnoreMain) {
RunWithProgressBar { TestBlockingCall } $(New-Timespan -Seconds 7)
}