Reputation: 897
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?
Upvotes: 2
Views: 3360
Reputation: 897
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)
}
Upvotes: 1
Reputation: 58931
I feel like you are trying to reinvent the wheel in your RunWithProgressBar
function:
$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)
}
This is basically a timer and can be refactored:
$timer = new-object timers.timer
$action = {DisplayProgress $(FormatDisplay $global:StartTime $global:RunningTime $global:EstimatedTime)}
$timer.Interval = 1000
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action $action | out-null
$timer.Start()
However, to solve your problem, you could execute the task in a job and wait until they finish.
Instead of making the functions public, consider define them in a scriptblock
:
$initializationScriptBlock = {
$functions = @{
Install = {
cmd /c '"AutoHotkey112306_Install.exe" /S /D="%cd%"'
}
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
}
BeersOnTheWall = {
$NumBeers = $args[0]
for ($i=$NumBeers; $i -ge 0; $i--)
{
BottleOfBeer $i
}
}
Uninstall = {
cmd /c start "" /wait "Installer.ahk" /Uninstall
}
}
}
Now, intead of Invoke-Expression $Payload
you start a new job by passing the initializationScriptBlock
and the actual function $functionToExecute
as the scriptblock
to execute:
$executingJob = start-Job -InitializationScript $initializationScriptBlock -ScriptBlock $functionToExecute
while ((Get-job $executingJob.Id).State -eq 'Running')
{
sleep 1;
}
Your RunWithProgressBar
function now using PowerShell
-like function definition:
function RunWithProgressBar
{
Param(
[Parameter(Mandatory=$true, Position=0)]
[scriptblock]$functionToExecute,
[Parameter(Mandatory=$true, Position=1)]
[timespan]$EstimatedTime
)
....
}
Please consider to also change the rest of the script for readability.
To invoke a function with a progressbar, you first have to load the initializationScriptBlock
to the current runspace:
. $initializationScriptBlock
Now you are able to invoke the functions like this:
RunWithProgressBar $functions.BottleOfBeer (New-Timespan -Seconds 3)
RunWithProgressBar $functions.Install (New-Timespan -Seconds 30)
Your whole script now looks like this:
## Functions
$initializationScriptBlock = {
$functions = @{
Install = {
Sleep 5
<#cmd /c @"
"AutoHotkey112306_Install.exe" /S /D="%cd%"
"@#>
}
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
}
BeersOnTheWall = {
$NumBeers = $args[0]
for ($i=$NumBeers; $i -ge 0; $i--)
{
BottleOfBeer $i
}
}
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 RunWithProgressBar
{
Param(
[Parameter(Mandatory=$true, Position=0)]
[scriptblock]$functionToExecute,
[Parameter(Mandatory=$true, Position=1)]
[timespan]$EstimatedTime
)
$global:StartTime = Get-Date
$global:RunningTime = [System.Diagnostics.Stopwatch]::StartNew()
$global:EstimatedTime = $EstimatedTime
$timer = new-object timers.timer
$action = {DisplayProgress $(FormatDisplay $global:StartTime $global:RunningTime $global:EstimatedTime)}
$timer.Interval = 1000
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action $action | out-null
$timer.Start()
try {
$executingJob = start-Job -InitializationScript $initializationScriptBlock -ScriptBlock $functionToExecute
while ((Get-job $executingJob.Id).State -eq 'Running')
{
sleep 1;
}
} finally {
$timer.stop()
Unregister-Event thetimer
}
}
####END Progress Bar Stuff
## MAIN
. $initializationScriptBlock
RunWithProgressBar $functions.BottleOfBeer (New-Timespan -Seconds 3)
RunWithProgressBar $functions.Install (New-Timespan -Seconds 30)
RunWithProgressBar $functions.Uninstall (New-Timespan -Seconds 5)
RunWithProgressBar $functions.BeersOnTheWall (New-Timespan -Seconds 3)
Your script should work now even there are still a lot to refactor e. g. function names, scopes
Answer to your comment:
1.) You can use Receive-Job inside your loop to retrieve the output:
$executingJob = start-Job -InitializationScript $initializationScriptBlock -ScriptBlock $functionToExecute
while ((Get-job $executingJob.Id).State -eq 'Running')
{
Receive-Job $executingJob.Id
sleep 1;
}
Receive-Job $executingJob.Id
2.)
After you executed the $initializationScriptBlock
using:
. $initializationScriptBlock
You can use any functions:
& $functions.BottleOfBeer
Upvotes: 3