I'm trying to understand and figure out how I can pass a variable into a scriptblock. In my below example script, when a new file is dropped into the monitored folder it executes the $action script block. But the $test1 variable just shows up blank. Only way I can make it work is by making it a global variable, but I don't really want to do that.
I've looked into this some and I'm more confused than when I started. Can anyone help me out or point me in the right direction to understand this?
$PathToMonitor = "\\path\to\folder"
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $PathToMonitor
$FileSystemWatcher.Filter = "*.*"
$FileSystemWatcher.IncludeSubdirectories = $false
$FileSystemWatcher.EnableRaisingEvents = $true
$test1 = "Test variable"
$Action = {
Write-Host "$test1"
}
$handlers = . {
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreateConsumer
}
try {
do {
Wait-Event -Timeout 5
} while ($true)
}
finally {
Unregister-Event -SourceIdentifier FSCreateConsumer
$handlers | Remove-Job
$FileSystemWatcher.EnableRaisingEvents = $false
$FileSystemWatcher.Dispose()
}
CodePudding user response:
Hidden away in the documentation for Register-ObjectEvent, way down in the -Action
parameter description is this little tidbit:
The value of the Action parameter can include the $Event, $EventSubscriber, $Sender, $EventArgs, and $Args automatic variables. These variables provide information about the event to the Action script block. For more information, see about_Automatic_Variables.
What this means is PowerShell automatically creates some variables that you can use inside the event handler scriptblock and it populates them when the event is triggered - for example:
$Action = {
write-host ($Sender | format-list * | out-string)
write-host ($EventArgs | format-list * | out-string)
}
When you create a file in the watched folder you'll see some output like this:
NotifyFilter : FileName, DirectoryName, LastWrite
Filters : {*}
EnableRaisingEvents : True
Filter : *
IncludeSubdirectories : False
InternalBufferSize : 8192
Path : c:\temp\scratch
Site :
SynchronizingObject :
Container :
ChangeType : Created
FullPath : c:\temp\scratch\New Text Document (3).txt
Name : New Text Document (3).txt
If these contain the information you're after then you don't actually need to pass any parameters into the scriptblock yourself :-).
Update
If you still need to pass your own variables into the event you can use the -MessageData
parameter of Register-ObjectEvent
to be able to access it as $Event.MessageData
inside your event scriptblock - for example:
$Action = {
write-host ($EventArgs | format-list * | out-string)
write-host "messagedata before = "
write-host ($Event.MessageData | ConvertTo-Json)
$Event.MessageData.Add($EventArgs.FullPath, $true)
write-host "messagedata after = "
write-host ($Event.MessageData | ConvertTo-Json)
}
$messageData = @{ };
$handlers = . {
# note the -MessageData parameter
Register-ObjectEvent `
-InputObject $FileSystemWatcher `
-EventName Created `
-Action $Action `
-MessageData $messageData `
-SourceIdentifier FSCreateConsumer
}
which will output something like this when the event triggers:
ChangeType : Created
FullPath : c:\temp\scratch\New Text Document (16).txt
Name : New Text Document (16).txt
messagedata before =
{}
messagedata after =
{
"c:\\temp\\scratch\\New Text Document (16).txt": true
}
$messageData
is technically still a global variable but your $Action
doesn't need to know about it anymore as it takes a reference from the $Event
.
Note you'll need to use a mutable data structure if you want to persist changes - you can't just assign a new value to $Event.MessageData
, and it'll possibly need to be thread-safe as well.
CodePudding user response:
The event action block runs on a background thread and can't resolve $test1
when dispatched.
One workaround is to explicitly read from and write to a globally-scoped variable (eg. Write-Host $global:test1
), but a better solution is to ensure the $Action
block "remembers" the value of $test1
for later - something we can accomplish with a closure.
We'll need to reorganize the code slightly for this, so start by replacing the $test1
string literal with a synchronized hashtable:
$test1 = [hashtable]::Synchronized(@{
Value = "Test variable"
})
This will allow us to do 2 things:
- we can modify the string value without changing the identity of the object stored in
$test1
, - string value can be modified by multiple background threads without any race conditions occuring
Now we just need to create the closure from the $Action
block:
$Action = {
Write-Host $test1.Value
}.GetNewClosure()
This will bind the value of $test1
(the reference to the synchronized hashtable we just created on the line above) to the $Action
block, and it will therefore "remember" that $test1
resolves to the hashtable rather than attempt (and fail) to resolve it at runtime.