2

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()
}

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the response. I've been unable to make this work, as it just prints a blank space (same problem I had before). Maybe I'm missing something. Only changes I made were the two code blocks you listed.
2

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.

3 Comments

Thanks for the response. The problem I'm trying to solve is that sometimes the FileSystemWatcher triggers twice in a row for the same new file. I thought maybe I'll use a hashtable to store the filenames and timestamps, then use that to prevent the double up that sometimes occurs. But I've been unsuccessful unless I make the hashtable a global variable.
@xeric080 - updated to use -MessageData parameter.
Cool idea, thanks for your help!

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.