6

I have a .ps1 script that needs some code to execute for cleanup purposes once the PowerShell session ends. Simplest reproduction of the problem I am having:

function ExitLogic(){
     Write-Host 'closing'
}
Write-Host 'started'
Register-EngineEvent `
    -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) `
    -Action { ExitLogic }

The ExitLogic never happens. Not if I manually use the exit command within my PowerShell session, not if I click the X window button, not if I run PowerShell within a cmd.exe... I'm at a loss. But, if I change the Action parameter from referencing ExitLogic as a function to just Write-Host 'inline closing' then it does work.

4
  • 1
    Why don't you use a Finally {} block? Commented Nov 24, 2017 at 16:02
  • 1
    Write-Host is not a good evidence. Use [System.Console]::Beep(). Commented Nov 24, 2017 at 16:33
  • @TheIncorrigible1 I didn't include this detail, but my use case is eventually in a module. Need something automatic when things are getting closed out. Commented Nov 25, 2017 at 5:26
  • 1
    @Reza thanks for that suggestion! I forget about console beeps. That would be much easier for detecting it, I was resorting to launching PowerShell via a CMD prompt where I could then exit without the original window closing and I would be able to see text written to host. Commented Nov 25, 2017 at 5:29

1 Answer 1

8

tl;dr

Depending on how your script is invoked, the -Action script block may not see your ExitLogic function - to ensure that it is visible there, define the function in the global scope.


General points:

  • *.ps1 files do NOT run in a child process, so exiting the script is NOT tantamount to exiting the PowerShell engine as a whole.

  • scripts by default run in a child scope, so functions defined therein are only in scope while the script is running.

  • Only functions defined in the global scope can be referenced in an -Action script block, because it runs in a dynamic module (an in-memory module, which, like persisted modules, sees only definitions from the global scope).

  • By the time the -Action script block executes, much of regular PowerShell functionality is no longer available.Written as of v6.2.0

    • Notably, PowerShell's own output streams cannot be used anymore - regular output and error messages no longer print.

    • However, you can use Write-Host to produce display output (though an outside caller will receive that via stdout), but note that if the engine exiting also closes the current console window, you won't even get to see that. A Read-Host command can delay the closing.

    • It seems that only commands from the Microsoft.PowerShell.Utility module are available, whereas all other modules have already been unloaded - see this GitHub issue.

  • Important: The event handler only gets to run if PowerShell itself exits the session (whether normally or via a script-terminating error triggered with throw) - if you terminate PowerShell indirectly by closing the console / terminal window, the event handler won't run - see this answer for details.

Your specific case:

  • Unless you happen to call your script file via the -File parameter (as opposed to -Command) of the PowerShell CLI (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+), the -Action script block does not see the ExitLogic function, because it is then only defined in the script file's scope, not the global one.

    • One way to make ExitLogic available in the global scope is to "dot-source" your script (e.g., . .\script.ps1), but note that only works from the global scope (this is essentially what the -File CLI parameter does - it dot-sources the script file in the global scope); more work is needed to add a function to the global scope from a child scope or module scope.

The following snippet demonstrates this:

Let's assume the existence of script .\script.ps1 with the following content:

function ExitLogic {
  Write-Host 'closing'
}

Write-Host 'started'

$null = Register-EngineEvent `
 -SourceIdentifier PowerShell.Exiting `
 -Action { 
   try { ExitLogic } catch { Write-Host $_ }
   Read-Host -Prompt 'Press ENTER to exit' 
 }

Note: As Get-Help Register-EngineEvent states, the following -SourceIdentifier values are supported: PowerShell.Exiting, PowerShell.OnIdle, and PowerShell.OnScriptBlockInvoke, which correspond to the value of the enum-like [System.Management.Automation.PSEngineEvent] class; using the latter explicitly (as in the OP) gives you more type safety, but is also more cumbersome to type.

Caveat: The following sample commands exit the running PowerShell session; it is only the Read-Host command that keeps the window open, allowing you to inspect the event handler's Write-Host output; on pressing Enter, the window closes.

  • Direct invocation (the equivalent of powershell.exe -Command "& .\script.ps1 ..." from the outside):
# Due to direct invocation, the -Action block does NOT see ExitLogic()
PS> .\script.ps1; exit
started
The term 'ExitLogic' is not recognized as the name of a cmdlet, function, 
script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Press ENTER to exit: 
  • Dot-Sourced invocation (the equivalent of calling powershell.exe -File script.ps1 ... from the outside):
# Thanks to dot-sourcing in the global scope, the -Action block DOES see ExitLogic()
PS> . .\script.ps1; exit
started
closing
Press ENTER to exit: 
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the very thorough answer! Now I need to figure out the implications of this within a module... That's where all of this stemmed from. I'm trying to integrate a .net logging library and have my module running a .ps1 file as part of module initialization that sets up the logging and then after any processing that utilizes the module completes I need to close and flush the logging. Perhaps the dot sourcing will work here, or the global option.
@Ethan: Glad to hear it was helpful. Unfortunately, dot-sourcing only loads into the current scope, so if you dot-source from within a module, the resulting definitions will not be in the global scope (I've updated the answer to make that clearer). You can either explicitly create a duplicate of the on-exit function in the global scope or pass its body (script block) directly to -Action (assuming no further dependencies on custom functions). If you're having difficulty with this, I suggest you ask a new question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.