Share via


How to handle close event of PowerShell window if user clicks on Close('X') button

Question

Thursday, December 1, 2011 11:14 AM

I want to run some code before PowerShell window is closed. For this I tried:

PS > register-engineevent PowerShell.Exiting -action {get-process | out-file c:\work\powershellexiteventcalled.txt}

It works fine if user closes PowerShell window using exit command (i.e. exit <ENTER>). But it does not work if user closes it by clicking on Close ('X') button on top right.

I could not find any way to handle this. I also tried to do it the following way, but this does not work either:

PS > $query = "Select * from __InstanceDeletionEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name='powershell.exe'"

PS > Register-WmiEvent -Query $query -Action {get-process | out-file c:\work\powershellexiteventcalled.txt}

 

Please guide how I can achieve this task.

All replies (17)

Friday, December 2, 2011 9:44 AM

Hello TeamPowerShell,

 

Good question! I never think it before. I did some research, and found that if we want to capture the console close event in C#, we can use DomainUnload event under System.AppDomain.CurrentDomain. I tried following code in my PowerShell console, but it didn't work. I am a PowerShell MVP who focus on system management. So, it seems like we have to wait a DEV to give us some ideas.

 

$appCurrentDomain = [System.AppDomain]::CurrentDomain
$psEvent = Register-ObjectEvent -Action {Out-File -FilePath "D:\Scripts\output.txt"} `
-InputObject $appCurrentDomain -EventName DomainUnload -SourceIdentifier App.DomainUnload

Best Regards,
Huajun Gu

 

 

 

After I watched the video "Richard St. John's 8 secrets of success" on TED.com, I learned this: "Being good at your job is not enough, you should be damn good at it."


Friday, December 2, 2011 11:24 AM

Hello Huajun Gu,

Thanks for the response. You are right, the code u posted should work because as per Microsoft "DomainUnload occurs when an AppDomain is about to be unloaded."

 

I tried

> Register-ObjectEvent -Action {get-process | out-file c:\work\testfile.txt} -InputObject $appCurrentDomain -EventName AssemblyLoad

> Import-Module C:\MyPSAsm.dll

 

This works. But again, DomainUnloadevent does not work when i close the window ( or even type exit <ENTER> for that matter).

 

I hope some developer helps with a relevant solution soon.


Friday, December 2, 2011 2:46 PM

It seems like we might be able to use PowerShell's ability to compile and run C# with the method discussed here. However I'm still a newb when it comes using Add-Type to integrate other languages into Powershell and I've not been able to figure it out. 


Friday, December 2, 2011 2:51 PM

I think the problem is that by the time this gets kicked off, its already

been removed in the 'shutdown' process, so you'd have to find something that

is triggered but allows code to run... not really sure what that event is

though.. I've heard this come up before and have never seen an answer to it.

 

 

Justin Rich
http://jrich523.wordpress.com
Please remember to mark the replies as answers if they help and unmark them if they provide no help.


Friday, December 2, 2011 6:45 PM

The following code demonstrates that we can catch the Close event and execute some code, but I've only figured out how to get it to call a C# handler function, not a PowerShell function. I suspect it's possible but I don't know enough to do it. 

I can't figure out how to pass a PowerShell function to the SetConsoleCtrlHandler routine.  I think you might be able to use a technique similar to the one discussed here - getting the PowerShell function cast as a delegate - but I've not been able to get it to work.  The code below at least shows how we can get a C# function called when the Close box is clicked.  Maybe someone else can figure out how to get it calling a PS function.

 

$code = @"
        public static void SetHandler()
        {
            SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
        }

        private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
        {
            // Put your own handler here
            switch (ctrlType)
            {
                case CtrlTypes.CTRL_C_EVENT:
                    Console.WriteLine("CTRL+C received!");
                    break;
 
                case CtrlTypes.CTRL_BREAK_EVENT:
                    Console.WriteLine("CTRL+BREAK received!");
                    break;
 
                case CtrlTypes.CTRL_CLOSE_EVENT:
                    Console.WriteLine("Program being closed!");
                    break;

                case CtrlTypes.CTRL_LOGOFF_EVENT:
                case CtrlTypes.CTRL_SHUTDOWN_EVENT:
                    Console.WriteLine("User is logging off!");
                    break;
            }
            return true;
        }
 
        [DllImport("Kernel32")]
        public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);
 
        // A delegate type to be used as the handler routine
        // for SetConsoleCtrlHandler.
        public delegate bool HandlerRoutine(CtrlTypes CtrlType);
 
        // An enumerated type for the control messages
        // sent to the handler routine.
        public enum CtrlTypes
        {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT,
            CTRL_CLOSE_EVENT,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT
        }
"@
$test = Add-Type -memberDefinition $code -Name "Testing" -Namespace ConsoleTest -passthru
[ConsoleTest.Testing]::SetHandler()

By the way, if ConsoleCtrlCheck() returns true, once the routine executes and writes to the console, Windows notices the program doesn't close and asks if you want to force it closed. If you say no, pressing Enter at the console Window returns a new prompt.  If ConsoleCtrlCheck returns false, the console exits as soon as the handler finishes executing.

Anyway, I hope someone else can assist here. I'm already in way over my head as is! :)

 

 

 


Friday, December 2, 2011 7:41 PM

the only thing that HAS to be in the add-type is the pinvoke stuff

(setConsoleCtrlHandler)

the problem with that is it requires a HandlerRoutine to be defined, which

is what is used to register the event. since that HAS to be in the C# side

of things, you cant run powershell from there.

 

the post you provided would work for any .NET event that required a delegate

but since it’s a win32 api, your sort of forced in to using c# (any .net

lang)

 

now, take this all with a grain of salt, my .net/win32 knowledge isnt the

best. I have a decent understanding of delegates but not enough to say for

sure..

 

I'd say you would need someone with extensive .net development skills and a

solid understanding of PowerShell... there are a few people around that have

that, but even still, this isnt a simple task.

 

 

Justin Rich
http://jrich523.wordpress.com
Please remember to mark the replies as answers if they help and unmark them if they provide no help.


Friday, December 2, 2011 10:14 PM

I can't help but think one could use a technique like that discussed in this article or this one, but so far my best efforts have resulted in a PowerShell crash when the Close button is clicked. But hey, that's something. I at least know it's trying to do something with the callback I've setup. :)


Monday, December 5, 2011 11:50 AM

Thanks wtgreen & jrich for the input.

Using the following code changes in 'case CtrlTypes.CTRL_C_EVENT' I am able to call 'Get-Date' or other cmdlets. But still it does not solve the problem, as I want to run a custom cmdlet which is in the loaded module (using Import-Module abd.dll).

If I use custom cmdlet, it say it is not recognised namespace r command. Help :)

 

$code = @"
        public static void SetHandler()
        {
            SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
        }

        private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
        {
            // Put your own handler here
            switch (ctrlType)
            {
                case CtrlTypes.CTRL_C_EVENT:
                    Console.WriteLine("CTRL+C received!");

            System.Management.Automation.Runspaces.Runspace runSpace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace();
                runSpace.Open();
       
                System.Management.Automation.Runspaces.Pipeline pipeline = runSpace.CreatePipeline();
                System.Management.Automation.Runspaces.Command getProcess = new System.Management.Automation.Runspaces.Command("Get-Date");
                pipeline.Commands.Add(getProcess);

                System.Collections.ObjectModel.Collection<System.Management.Automation.PSObject> output = pipeline.Invoke();

            foreach(object date in output)
            {
               Console.WriteLine(date.ToString());
            }
                    break;
 
                case CtrlTypes.CTRL_BREAK_EVENT:
                    Console.WriteLine("CTRL+BREAK received!");
                    break;
 
                case CtrlTypes.CTRL_CLOSE_EVENT:
                    Console.WriteLine("Program being closed!");
            System.IO.FileStream fs =  System.IO.File.OpenWrite(@"C:\WORK\HandlerCodeRun.txt");
                fs.WriteByte(1);
                fs.Close();
                    break;

                case CtrlTypes.CTRL_LOGOFF_EVENT:
                case CtrlTypes.CTRL_SHUTDOWN_EVENT:
                    Console.WriteLine("User is logging off!");
                    break;
            }
            return true;
        }
 
        [DllImport("Kernel32")]
        public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);
 
        // A delegate type to be used as the handler routine
        // for SetConsoleCtrlHandler.
        public delegate bool HandlerRoutine(CtrlTypes CtrlType);
 
        // An enumerated type for the control messages
        // sent to the handler routine.
        public enum CtrlTypes
        {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT,
            CTRL_CLOSE_EVENT,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT
        }
"@
$test = Add-Type -memberDefinition $code -Name "Testing" -Namespace ConsoleTest -passthru
[ConsoleTest.Testing]::SetHandler()


Monday, December 5, 2011 1:09 PM

wow, so you realize that what you've done is load powershell, inject C#

code, and in that c# code you've created a powershell instance (basically

running another powershell) which will have no access to the original

powershell context.

 

Is that really what you're looking for?

 

as far as loading modules from a runspace take a look at this

 

http://stackoverflow.com/questions/6266108/powershell-how-to-import-module-in-a-runspace

 

 

Justin Rich
http://jrich523.wordpress.com
Please remember to mark the replies as answers if they help and unmark them if they provide no help.


Tuesday, December 6, 2011 4:45 AM

No jrich. that is exactly the issue. I want to run the custom made cmdlet (from within injected C# code) from the current PowerShell context itself. As I am very new to PowerShell I am not able to find out the way.

 

Let me try explain the exact situation.

I have a .Net library (lets say abc.dll) which defines custom cmdlet (say e.g. My-CustomCmdlet).

I have loaded the library into PowerShell instance using 'Import-Module ...abc.dll'. Now, I want to make sure, if the user exits Powershell by clicking the close button on top (or with exit command), my cmdlet 'My-CustomCmdlet' should be run before exiting (for releasing unmanaged resources).

That is what I am looking the solution for. Can you please help?


Tuesday, December 6, 2011 12:59 PM

try this in your module,

 

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { <unload code> }

 

you arent so much worries about powershell closing as you are the module

being unloaded, you want to make sure its cleanly removed and there are

built in methods for that.

 

 

Justin Rich
http://jrich523.wordpress.com
Please remember to mark the replies as answers if they help and unmark them if they provide no help.


Thursday, December 8, 2011 5:00 AM

you are right jrich,

I just want to make sure that my cmdlet is executed before Powershell is closed.

The approach you just mentioned has the same drawback. Below text is from Powershell Cookbook.

" Beware of using this technique for extremely sensitive cleanup requirements. If the user simply exits the PowerShell window, the OnRemove event is not processed. If this is a concern, register for the PowerShell.Exiting engine event and remove your module from there:
Register-EngineEvent PowerShell.Exiting { Remove-Module TidyModule }

For PowerShell to handle this event, the user must use the exit keyword to close the session, rather than the X button at the top right of the console window "

So we are back to step 1: my cmdlet will not be executed if user clicks X button to close the window. :(


Thursday, December 8, 2011 11:46 AM

Hi guys,

I have found a way to achieve this.

 

$code = @"
        using System;
        using System.Runtime.InteropServices;
        using System.Management.Automation;
        using System.Management.Automation.Runspaces;
       
        namespace MyNamespace
        {
            public static class MyClass
            {
                public static Runspace defaultRunSpace;
               
                public static void SetHandler(Runspace defRunSpace)
                {
                    defaultRunSpace = defRunSpace;
                    SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
                }

                private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
                {
                    switch (ctrlType)
                    {   
                        case CtrlTypes.CTRL_CLOSE_EVENT:
                           
                            if (defaultRunSpace.RunspaceStateInfo.State == RunspaceState.Opened)
                            {
                                PowerShell powershell = PowerShell.Create();
                                powershell.Runspace = defaultRunSpace;

                                using (powershell)
                                {
                                    powershell.Stop();
                                    System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));

                                    powershell.AddCommand("Clear-Host");
                                    powershell.Invoke();
                                }
                                powershell = null;
                            }
                            break;
                    }
                    return true;
                }
        
                [DllImport("Kernel32")]
                public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);
        
                // A delegate type to be used as the handler routine
                // for SetConsoleCtrlHandler.
                public delegate bool HandlerRoutine(CtrlTypes CtrlType);
        
                // An enumerated type for the control messages
                // sent to the handler routine.
                public enum CtrlTypes
                {
                    CTRL_C_EVENT = 0,
                    CTRL_BREAK_EVENT,
                    CTRL_CLOSE_EVENT,
                    CTRL_LOGOFF_EVENT = 5,
                    CTRL_SHUTDOWN_EVENT
                }
            }
        }
"@

$text = Add-Type  -TypeDefinition $code -Language CSharp
$rs = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace
[MyNamespace.MyClass]::SetHandler($rs)

 

BUT THERE'S STILL AN ISSUE.

This way to attach event handler (as posted by  wtgreen) works on closing Powershell by clicking 'X'. But if I run any cmdlet on console, then it crashes the event handler on whatever event ( close, ctrl+c, etc.). Am i missing something?


Thursday, December 8, 2011 1:39 PM

honestly, I think you'd need to attach a debugger to this or talk to someone

who really knows how powershell works, my gut is telling me that one of two

things is happening (mostly one since the ctrl+c doesn’t work)...

 

when you hit ctrl+c and it kicks off the event handler and goes in to c#

world and puts powershell on hold (single thread) and then in the c# world

you are trying to get powershell to do something, but it cant because its

waiting on your c# code to finish and hand back control...

 

I looked on Connect for a bug on this and didn’t find one so I created it

 

https://connect.microsoft.com/PowerShell/feedback/details/712486/v3-ctp1-powershell-exiting-engine-event-not-triggered-when-you-x-window

 

I tested this with V3 CTP1 and it doesn’t work..

 

I would imagine there has to be a way for them to capture this.

 

 

Justin Rich
http://jrich523.wordpress.com
Please remember to mark the replies as answers if they help and unmark them if they provide no help.


Wednesday, October 24, 2012 9:43 AM | 1 vote

Hello TeamBSA,

I'm sure you have found solution and everything is OK now, just wanted to share for others searchers that in PS v3 "Register-EngineEvent PowerShell.Exiting" works like a charm with both"exit" and "X button" actions.

Good luck!


Wednesday, June 18, 2014 11:27 AM

Not really a solution but you can add lines after showDialog() so when the form will be closed those lines are executed.

[void]$objForm.ShowDialog()$config.startWindowPosition = $objForm.location.x, $objForm.location.y
set-configXML

Thursday, April 25, 2019 6:22 PM

Thank you!!!!!!!! @Stas.N