Home > Back-end >  PowerShell, [Console]::KeyAvailable generates error when running as a background task
PowerShell, [Console]::KeyAvailable generates error when running as a background task

Time:12-27

Instead of using pause in scripts, it is useful to me to make a script auto-exit after a set time. This is particularly useful to me as various of these scripts can run interactively or as a background task with Start-Process -WindowStyle Hidden where a pause would mean that process could hang around forever, but with the timer, even if it's in the background, it will time out. I was given this solution via this question: PowerShell, break out of a timer with custom keypress

$t = 8; Write-Host "Exiting in $t seconds (press any key to exit now)" -NoNewLine
for ($i=0; $i -le $t; $i  ) {
    Start-Sleep 1; Write-Host -NoNewLine "."
    if ([Console]::KeyAvailable) { 
        $key = [Console]::ReadKey($true).Key
        if ($key) { break }
    }
}

However, if I run this with Start-Process -WindowStyle Hidden in the background, PowerShell generates an error due to [Console]::KeyAvailable as there is no console available when running in the background.

Cannot see if a key has been pressed when either application does not have a 
console or when console input has been redirected from a file. Try 
Console.In.Peek.
At C:\Users\Boss\Install Chrome.ps1:36 char:78
  ... ep 1;  Write-Host -NoNewLine "."; if ([Console]::KeyAvailable) { $key ...
                                            ~~~~~~~~~~~~~~~~~~~~~~~
      CategoryInfo          : OperationStopped: (:) [], InvalidOperationExcept 
   ion
      FullyQualifiedErrorId : System.InvalidOperationException

• Is there some way that I can adjust the code I have such that it will not generate an error when run in the background, while still retaining the "press any key to exit now" option when run interactively?

• What does Try Console.In.Peek mean?

CodePudding user response:

Use a Runspace instead of Start-Process. Runspaces can be associated with your PSHost hence you would see both benefits, the output to your console and the ability to press any key to cancel your script.

try {
    $ttl = 20
    $iss = [initialsessionstate]::CreateDefault2()
    $rs  = [runspacefactory]::CreateRunspace($Host, $iss)
    $rs.Open()

    $ps  = [powershell]::Create().AddScript({
        param($ttl)

        Write-Host "Exiting in $ttl seconds (press any key to exit now)" -NoNewLine
        for ($i=0; $i -le $ttl; $i  ) {
            Start-Sleep 1; Write-Host -NoNewLine "."
            if ([Console]::KeyAvailable) {
                $key = [Console]::ReadKey($true).Key
                if ($key) { break }
            }
        }
        Write-Host "Finished!" -ForegroundColor Green

    }).AddParameter('ttl', $ttl)

    $ps.Runspace = $rs
    $async       = $ps.BeginInvoke()

    do {
        $id = [System.Threading.WaitHandle]::WaitAny($async.AsyncWaitHandle, 200)
    }
    while($id -eq [System.Threading.WaitHandle]::WaitTimeout)

    $ps.Stop()
    $ps.EndInvoke($async)
}
finally {
    $ps, $rs | ForEach-Object Dispose
}

CodePudding user response:

  • It is not Start-Process that is the problem:

    • When a console application is launched on Windows, such as powershell.exe, a console is always allocated - even if it is hidden (-WindowStyle Hidden)

    • Unless you use -RedirectStandardInput, that console's stdin stream ([Console]::In) is not redirected, as reflected in [Console]::IsInputRedirected returning $false.

    • Thus, your code would work: [Console]::KeyAvailable would always return $false, and the full timeout period would elapse.

  • However, you would see the problem with Start-Job:

    • The background job that is created uses a hidden PowerShell child process to run the code in the background, and the way that the calling PowerShell instance communicates with it is via stdin.

    • Thus, [Console]::KeyAvailable causes the statement-terminating error you saw, given that one the causes mentioned in the error message applies: stdin is redirected (but a console does exist).


Workarounds:

Simply make the use of [Console]::KeyAvailable conditional on [Console]::IsInputRedirected returning $false:

$t = 8; Write-Host "Exiting in $t seconds (press any key to exit now)" -NoNewLine
for ($i=0; $i -le $t; $i  ) {
    Start-Sleep 1; Write-Host -NoNewLine "."
    # Note the use of -not [Console]::IsInputRedirected
    if (-not [Console]::IsInputRedirected -and [Console]::KeyAvailable) { 
        $key = [Console]::ReadKey($true).Key
        if ($key) { break }
    }
}

Alternatively, use your code as-is and use a thread job instead, using Start-ThreadJob, which comes with PowerShell (Core) 7 and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser:

Start-ThreadJob { & 'C:\Users\Boss\Install Chrome.ps1' }

Thread jobs, if available, are generally preferable to regular, child-process-based background jobs, due to being faster and more light-weight.

Since the use of stdin only applies to inter-process communication, [Console]::KeyAvailable doesn't cause an error in a thread job, and - commendably - only pays attention to key strokes when the thread at hand is the foreground thread.

  • Related