powershellpinvokeexiteventhandler

Is there a way to run code if someone tries exiting the program?


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?


Solution

  • 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:

    1. Store the code in a file or inline it in your PowerShell script itself, e.g.: $code = @'...'@.
    2. Add-Type the code with -PassThru that you can then pass-in to Import-Module -Assembly.
    3. Set the exit handler using the 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
    # ....