如何在执行静默安装时异步 运行 进度条?

How to run a progress bar asynchronously while performing silent installs?

目标:

我想在等待静默安装时异步 运行 进度条(显示已用 time/estimated 时间)。例如,

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)

问题

虽然上述实现 运行 是预期的,但只要 RunWithProgressBar 的有效负载参数是安装,更新进度条的事件就会停止触发。

我在找什么:

如何修改我当前的实现以每秒更新进度条,即使在执行安装时也是如此?

我觉得你正试图在你的 RunWithProgressBar 函数中 重新发明轮子 :

 $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)
  }

这基本上是一个计时器并且可以重构:

$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()

但是,要解决您的问题,您可以在作业中执行任务并等待它们完成。

与其创建函数 public,不如考虑 中定义它们 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
        }
    }
}

现在,代替 Invoke-Expression $Payload,您通过传递 initializationScriptBlock 和实际函数 $functionToExecute 作为要执行的 scriptblock 来开始一项新工作:

$executingJob =  start-Job -InitializationScript $initializationScriptBlock -ScriptBlock $functionToExecute 
while ((Get-job $executingJob.Id).State -eq 'Running')
{
    sleep 1;
}

您的 RunWithProgressBar 函数现在使用类似 PowerShell 的函数定义:

function RunWithProgressBar 
{
    Param(
        [Parameter(Mandatory=$true, Position=0)]
        [scriptblock]$functionToExecute,

        [Parameter(Mandatory=$true, Position=1)]
        [timespan]$EstimatedTime
    )
....
}

请考虑更改脚本的其余部分以提高 可读性

要调用带有进度条的函数,您首先必须加载 initializationScriptBlock 到当前运行空间:

. $initializationScriptBlock

现在您可以像这样调用函数了:

RunWithProgressBar $functions.BottleOfBeer (New-Timespan -Seconds 3)
RunWithProgressBar $functions.Install (New-Timespan -Seconds 30)

你的整个脚本现在看起来像这样:

## 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)

你的脚本现在应该可以工作了,即使有还有很多东西需要重构 e。 G。函数名称、作用域

回复您的评论:

1.) 您可以在循环中使用 Receive-Job 来检索输出:

$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.) 使用

执行 $initializationScriptBlock
. $initializationScriptBlock

您可以使用任何功能:

& $functions.BottleOfBeer

虽然提供的解决方案赢得了赏金,但这并不是我最终所做的。这是 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)
}