【问题标题】:How to run a progress bar asynchronously while performing silent installs?如何在执行静默安装时异步运行进度条?
【发布时间】:2016-10-16 10:46:07
【问题描述】:

目标:

我想在等待静默安装时异步运行进度条(显示经过时间/估计时间)。例如,

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 的有效负载参数是安装,更新进度条的事件就会停止触发。

我在寻找什么:

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

【问题讨论】:

  • 您希望进度条以何种方式更新?系统无法预测安装何时完成,也不知道安装程序将处理哪些项目。
  • 作为人类,您会根据您认为需要多长时间做出判断。前任。 Notepadd++ 应该在 5 分钟内安装。该脚本将在多台相同类型的机器上运行;从第一次运行就很容易找到合理的估计值。

标签: powershell asynchronous progress


【解决方案1】:

我觉得您正在尝试在您的 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()

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

与其将函数公开,不如考虑将它们定义在一个 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

【讨论】:

  • 不是我想要的(但很有帮助)。 2个问题:1)如何修改上述内容以在作业进行时显示作业输出?这甚至可能吗? 2) 有什么方法可以在initializationscriptblock 函数属性之外使用函数(或将它们包装在其中)?
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-04
    • 2022-11-10
    • 1970-01-01
    • 1970-01-01
    • 2022-10-05
    相关资源
    最近更新 更多