Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell exit event does not work when referencing function

Tags:

powershell

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.

like image 510
Ethan Avatar asked Nov 24 '17 15:11

Ethan


1 Answers

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: 
like image 134
mklement0 Avatar answered Nov 06 '22 00:11

mklement0