I’m messing with PowerShell scripting and wanted to know if this was possible. I know when you use try
/ finally
the code in the finally
block will execute even when Ctrl+C is typed. However, how can you run code it someone terminates it by closing the window? When I try try
/ finally
the code in the finally block never executes when exiting this way. Is this even possible?
Adding this as a new answer that can complement my previous one and doesn't require an orchestration script.
Thanks to zett42's research, it seems that SetConsoleCtrlHandler
function can be used to invoke handler when the PowerShell console is closed, you can see his attempt in this gist using a compiled handler. However, note, this approach does not work if the process is killed, i.e.: via Process.Kill()
or Stop-Process
.
After some testing it does seem to also work correctly passing a script block as your HandlerRoutine
callback function, however it requires a workaround using a new runspace, if you try to invoke the script block itself you'd get an exception stating that there is no available runspace. So, I've decided to add this cmdlet that you can compile ad-hoc with Add-Type
to set an exit handler:
using System.Collections.Generic;
using System.Management.Automation;
using System.Runtime.InteropServices;
public enum ConsoleCtrlEvent : uint
{
// A CTRL+C signal was received, typically from the user pressing Ctrl+C.
CTRL_C_EVENT = 0,
// A CTRL+BREAK signal was received, typically from the user pressing Ctrl+Break.
CTRL_BREAK_EVENT = 1,
// A signal that the console window is being closed.
CTRL_CLOSE_EVENT = 2,
// A signal that the user is logging off the system.
CTRL_LOGOFF_EVENT = 5,
// A signal that the system is shutting down.
CTRL_SHUTDOWN_EVENT = 6
}
[Cmdlet(VerbsCommon.Add, "ConsoleCtrlHandler")]
[OutputType(typeof(bool))]
public class ConsoleHelper : PSCmdlet
{
[DllImport("Kernel32")]
private static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
private delegate bool HandlerRoutine(ConsoleCtrlEvent ctrlType);
private static Dictionary<PowerShell, HandlerRoutine> s_handlers;
[Parameter(Mandatory = true, Position = 0)]
public ScriptBlock ScriptBlock { get; set; }
protected override void EndProcessing()
{
if (s_handlers == null)
{
s_handlers = new Dictionary<PowerShell, HandlerRoutine>();
}
PowerShell powershell = PowerShell
.Create(RunspaceMode.NewRunspace)
.AddScript(ScriptBlock.ToString());
s_handlers[powershell] = new HandlerRoutine(eventType =>
{
powershell.AddArgument(eventType).Invoke();
return false;
});
WriteObject(SetConsoleCtrlHandler(s_handlers[powershell], true));
}
}
The way to use it, if using Add-Type
versus pre-compile it:
$code = @'...'@
.Add-Type
the code with -PassThru
that you can then pass-in to Import-Module -Assembly
.Add-ConsoleCtrlHandler { ... }
cmdlet.In summary, in this example assumes the C# code is stored in a file:
# `-Raw` is important here, don't miss it!
$code = Get-Content -Path path\to\myCmdlet.cs -Raw
# compile it and import it as a module
Add-Type $code -PassThru | Import-Module -Assembly { $_.Assembly }
# set the handler
Add-ConsoleCtrlHandler {
param([ConsoleCtrlEvent] $ctrlType)
# here goes the code that should execute before powershell exits
}
# the actual code for your script goes here
# ....