How do I detect Ctrl-C in PowerShell in a responsive way?

269 Views Asked by At

When I break out of a PowerShell 7 script using Ctrl-C I'll get any jobs running, left running. I might also leave files I've been working with in state that is not desirable.

I did find a way to detect the key press, but either it's consuming to much CPU or is very unresponsive (or both).

See

When using the example below, the detection of Ctrl-C is very sluggish or will consume a lot of CPU resources (or both).

[Console]::TreatControlCAsInput = $true
sleep 1
$Host.UI.RawUI.FlushInputBuffer()

Start-ThreadJob {# Do something
  sleep 5
} -Name 'SomeJob'

Start-ThreadJob {# Do another thing
  sleep 5
} -Name 'OtherJob'

while (Get-Job | where State -ne 'Stopped') {# Run the main loop

  if (# Some key was pressed
    $Host.UI.RawUI.KeyAvailable -and
    ($Key = $Host.UI.RawUI.ReadKey("AllowCtrlC,NoEcho,IncludeKeyUp"))
  ) {
    if ([int]$Key.Character -eq 3) {# Ctrl-C pressed
      Get-Job | Remove-Job -Force

      [Console]::TreatControlCAsInput = $false

      return

    }#end Ctrl-C pressed

  }#end Some key was pressed

  # sleep -Milliseconds 200
  $Host.UI.RawUI.FlushInputBuffer()
  
}

Is there a good way to increase the responsiveness in the key detection?

2

There are 2 best solutions below

0
Dennis On BEST ANSWER

I went with the solution of only stopping the jobs.

Get-Job | Remove-Job -Force # Clean-up jobs from previous runs
try {
    $jJobs = @(
        Start-ThreadJob {
            Start-Sleep 10
        } -Name 'SomeJob'

        Start-ThreadJob {
            Start-Sleep 10
        } -Name 'OtherJob'
    )

    $Jobs | Receive-Job -Keep -Wait # Keep output for trouble shooting
}
# this block will always run, even on CTRL+C
finally {
    $Jobs | Stop-Job
}

Get-Job # should not contain any running jobs
6
Santiago Squarzon On

You're trying to solve the problem the wrong way, the finally block is designed specifically for this. See Freeing resources using finally.

Regarding high CPU and memory consumption with your current code, you already have the answer except that it should be uncommented (sleep -Milliseconds 200).

try {
    $jobs = @(
        Start-ThreadJob {
            Start-Sleep 10
        } -Name 'SomeJob'

        Start-ThreadJob {
            Start-Sleep 10
        } -Name 'OtherJob'
    )

    $jobs | Receive-Job -Wait -AutoRemoveJob
}
# this block will always run, even on CTRL+C
finally {
    $jobs | Remove-Job -Force
}

Get-Job # should be empty