PowerShell : Using Try / Finally block to catch a Ctrl-C and cleanup

Original article can be found here: http://sushihangover.blogspot.com/2012/03/powershell-using-try-finally-block-to.html

 

There are times that you may need to stop a script at a certain code section and still have the script do some additional work. I prefer using Ctrl-C (abort) since most shell based users know the basic Ctrl keystrokes (ie: Ctrl-S to start/stop console output, ....). If I am doing processing that the user can abort, but I need to clean up the current state due to the user abort, then a little .Net Console help is needed along with a try / finally block.

If you are a basic scripter and not a software developer then you might not be aware of the try / catch / finally blocks that are available in PowerShell 2.0+. For a quick review of how they work visit the Scripting Guy's blog on the subject.

The reason for using a try / finally block is in order to monitor the console for the user pressing a Ctrl-C and NOT totally aborting the current script, you need to flag Ctrl-C as normal input ([console]::TreatControlCAsInput = $true). Now if the user presses Ctrl-C you can capture it and break/abort the current script process and continue running the rest of the script. Everything seems fine right? Well not quite, the current PowerShell session is now only looking a Ctrl-C as a standard keyboard input, and you can not use it to abort any future scripts, commands or cmdlets. You need to reset the console to view Ctrl-C inputs as an abort again ([console]::TreatControlCAsInput = $false).

Doing this is perfect in the 'finally' block. We will execute our processing code and Ctrl-C monitoring in the 'try' block and no matter if the user presses Ctrl-C or not, the 'finally' block will get executed and thus our console input monitoring will be reset properly.
 

try {
    [console]::TreatControlCAsInput = $true

    # Do some processing and monitor for Ctrl-C 

    }
} finally {

   # No matter what the user did, reset the console to process Ctrl-C inputs 'normally'
    [console]::TreatControlCAsInput = $false
}



Try / Catch / Finally blocks are great for areas that you are going to 'try' to do, possibly 'catch' some errors that might occur and 'finally', do something at the end (pull the logs of that remote server, close a CSV file properly, release those COM objects, delete those video conversion temp files, etc....).

So in this basic example, I am going to put the computer into hibernation in X number of minutes but allow the user to stop that hibernation using Ctrl-C. I tend to throw this at the end of a lot of personal scripts that are long running but wish to be ECO friendly and shutdown the computer when done. This allows me to do that but abort the shutdown if I wish...

Note: I do not use Stop-Computer as there are no built options that will hibernation or sleep.

<#
.NOTES
Copyright 2012 Robert Nees
Licensed under the Apache License, Version 2.0 (the "License");
http://sushihangover.blogspot.com
.SYNOPSIS
Shutdown the computer with a Ctrl-C break
.DESCRIPTION
This scripts provides a visual countdown bar till computer is shutdown/rebooted/hibernate and 
you can use Ctrl-C to break this countdown
.EXAMPLE
Do-Shutdown.ps1 30
.LINK
http://sushihangover.blogspot.com
#>
Param (
        [parameter(
            parametersetname="All",
            mandatory=$true,
            position=1)]
            [Alias("min")]
            [int]$minutes,
        [parameter(
            parametersetname="All",
            mandatory=$false,
            position=2)]
            [string]$type = 'h',
        [parameter(
            parametersetname="All",
            mandatory=$false)]
            [switch]$whatif,
        [parameter(
            parametersetname="All",
            mandatory=$false)]
            [switch]$silent
)
$timeStep = 5
try {
    [console]::TreatControlCAsInput = $true
    for ($minutesLeft = $minutes - 1; $minutesLeft -ge 0 ; $minutesLeft--) {
        if (!$silent.IsPresent) {
            [System.Console]::Beep()
        }
        Write-Progress -Id 1 -Activity "Press Ctrl-C to Terminate Shutdown" -status "Shutdown in $minutesLeft minutes" -percentComplete (($minutesLeft / ($minutes)) * 100)
        for ($secondsLeft = 60; $secondsLeft -gt 0 ; $secondsLeft = $secondsLeft - $timeStep) {
            Write-Progress -Id 2 -ParentID 1 -Activity " " -status "+ $secondsLeft Seconds" -percentComplete (($secondsLeft / 60) * 100)
            Start-Sleep -s $timeStep
            if ([console]::KeyAvailable) {
                $key = [system.console]::readkey($true)
                if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
                    $breaking = $true
                    break
                }
            }
        }
    }
    if (!$breaking) {
        if ($whatif.IsPresent) {
            write-host 'Whatif: shutdown /' + $type
        } else {
            shutdown /$type
        }
    }
} finally {
    [console]::TreatControlCAsInput = $false
}

 

Trap Control C Function and Test Functions code Samples:

 

function Trap-CtrlC {
   ## Stops Ctrl+C from exiting this function
   [console]::TreatControlCAsInput = $true
   ## And you have to check every keystroke to see if it's a Ctrl+C
   ## As far as I can tell, for CtrlC the VirtualKeyCode will be 67 and
   ## The ControlKeyState will include either "LeftCtrlPressed" or "RightCtrlPressed"
   ## Either of which will -match "CtrlPressed"
   ## But the simplest thing to do is just compare Character = [char]3
   if ($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character))
   {
      throw (new-object ExecutionEngineException "Ctrl+C Pressed")
   }
}
 
function Test-CtrlCIntercept {
   Trap-CtrlC  # Call Trap-CtrlC right away to turn on TreatControlCAsInput
   ## Do your work ...
   while($true) {
      $i = ($i+1)%16
      Trap-CtrlC ## Constantly check ...
      write-host (Get-Date) -fore ([ConsoleColor]$i) -NoNewline
      foreach($sleep in 1..4) {
         Trap-CtrlC ## Constantly check ...
         sleep -milli 500; ## Do a few things ...
         Write-Host "." -fore ([ConsoleColor]$i) -NoNewline
      }
      Write-Host
   }
   
   trap [ExecutionEngineException] {
      Write-Host "Exiting now, don't try to stop me...." -Background DarkRed
      continue # Be careful to do the right thing here (or just don't do anything)
   }
}
 
 
 
## Another way to do the same thing without an external function ....
## Don't use this way unless your loop is really tight ...
## If you use this and hit CTRL+C right after a timestamp is printed,
## you'll notice the 2 second delay (compared with above)
function Test-CtrlCIntercept {
   ## Stops Ctrl+C from exiting this function
   [console]::TreatControlCAsInput = $true
   ## Do your work here ...
   while($true) {
      $i = ($i+1)%16
      write-host (Get-Date) -fore ([ConsoleColor]$i)
      sleep 2;
      ## You have to be constantly checking for KeyAvailable
      ## And you have to check every keystroke to see if it's a Ctrl+C
      ## As far as I can tell, for CtrlC the VirtualKeyCode will be 67 and
      ## The ControlKeyState will include either "LeftCtrlPressed" or "RightCtrlPressed"
      ## Either of which will -match "CtrlPressed"
      ## But the simplest thing to do is just compare Character = [char]3
      if ($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character))
      {
         Write-Host "Exiting now, don't try to stop me...." -Background DarkRed
         break;
      }
   }
}