My problem is fairly simple: I'm trying to execute a file on a timer elapsed event. I use a closure to preserve the value of my variable. But I think I struggle to understand what is the lifetime of each object.
$file = $params.FilePath
$action = {& $file }.GetNewClosure()
$action.Invoke() # no problem here
$event = Register-ObjectEvent -InputObject $timer -EventName elapsed `
-Action $action ; # When this event trigger $file is empty
Note: the whole thing is in a loop But I did check it also happens without ?
foreach ($params in $JOBS) {...}
Use -MessageData
parameter instead of a closure to preserve the reference of the iterated item. Then in your action block you can find that reference in $Event.MessageData
, a simple example:
$timer = [System.Timers.Timer]::new()
$timer.Interval = 1000
$timer.AutoReset = $false
$timer.Start()
$evts = foreach ($i in 0..10) {
$registerObjectEventSplat = @{
InputObject = $timer
EventName = 'Elapsed'
MessageData = $i
Action = { Write-Host $Event.MessageData }
}
Register-ObjectEvent @registerObjectEventSplat
}
Start-Sleep 1
$evts | Unregister-Event -SourceIdentifier { $_.Name }
$timer.Dispose()
If you want to understand why the new closure doesn't work, in theory it should, closures in PowerShell, are essentially a new temporary dynamic module where the script block local variables are stored in its execution context, taking this simple example:
$scriptblocks = foreach ($i in 0..10) {
{ $i }.GetNewClosure()
}
$scriptblocks | ForEach-Object { & $_ }
# Outputs: 0 to 10
However, if you inspect the source code you would see that the cmdlet first creates a new PSEventSubscriber
before registering the new job that will act as our callback, see EventManager.cs#L427-L435
. And, if we further inspect the code for PSEventSubscriber
, we can see that this class also creates a new temporary module and calls .NewBoundScriptBlock(..)
, and when this happens all the context from our previous script block (the closure one) is lost which explains the issue. Demo isn't exactly the same as in the source (overloads used there are internal and would require reflection making this answer too complicated) but serves the demo purpose, see ModuleIntrinsics.cs#L257-L261
.
$scriptblocks = foreach ($i in 0..10) {
$closure = { $i }.GetNewClosure()
$module = [psmoduleinfo]::new($true)
$module.NewBoundScriptBlock($closure)
}
$scriptblocks | ForEach-Object { & $_ }
# Outputs: 10, only the last reference to `$i` is preserved