PowerShell: Limitations of ScriptBlock in Register-ObjectEvent

5,362

You're running into two different PowerShell surprises.

First: scoping. When you change a variable from inside your event handler, you're only setting a new variable that only exists inside that block. As a simple test case, typing this at the console produces 1:

$a = 1; {$a = 2}.Invoke(); $a

To specify that you're assigning to the global scope, prepend global: to the variable name. This prints 2:

$a = 1; {$global:a = 2}.Invoke(); $a

Second: event output redirection. When you produce results from an event handler, it actually gets saved in a job object. To print to the console, use Write-Host instead of (the implicit) Write-Output. For example:

Write-Host $lasttimefilecreated

You might consider using a timer instead of a loop, like so:

$timer = New-Object System.Timers.Timer
$timer.Interval = 5000
$timer.Enabled = $true
Register-ObjectEvent $timer 'Elapsed' -Action {
    If ($global:logflag -eq 0) {
        $global:logflag = 1
        Add-content "C:\Log.txt" -value "$(Get-Date)"
        Write-Host "Log.txt file created/modified at $(Get-Date)"
    }
}
$timer.AutoReset = $true
$timer.Start()
while ($true) {Start-Sleep -Seconds 5}

The final loop there just prevents the script from exiting. Timers would continue to run if it did stop, but it looks a little weird.

Share:
5,362

Related videos on Youtube

skyfx
Author by

skyfx

Updated on September 18, 2022

Comments

  • skyfx
    skyfx over 1 year

    I am trying to build a PowerShell script to write to a text file if no new file has been generated in a Windows folder (C:\Test) after a certain period of time. As a foundation, I am using the script shown here. Below is my modified script.

    As you can see, I added an IF-ELSE statement that checks if the last file creation is more than 10 seconds ago. I also modified $action to assign the current timestamp to $lasttimefilecreated and reset $logflag. However, it appears that $action does not perform either of those two things. The Add-content "C:\FileCreated.txt" line executes successfully and a file is created, but the values of $lasttimefilecreated and $logflag remain unchanged and are not printed to the console. Is there any other way to accomplish this? I reviewed the Microsoft article on the Register-ObjectEvent object, but could not figure out why the script block I am assigning to $action does not fully work. Appreciate your help!

    ### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = "C:\Test"
    $watcher.Filter = "*.*"
    $watcher.IncludeSubdirectories = $true
    $watcher.EnableRaisingEvents = $true  
    
    ### INITIALIZE VARIABLES
    $lasttimefilecreated = $(Get-Date)
    $logflag = 0
    $action = { $lasttimefilecreated = $(Get-Date)
                "$lasttimefilecreated"
                $logflag = 0
                "$logflag"
                $path = $Event.SourceEventArgs.FullPath
                $changeType = $Event.SourceEventArgs.ChangeType
                $logline = "$(Get-Date), $changeType, $path"
                Add-content "C:\FileCreated.txt" -value $logline
              }
    
    ### DECIDE WHICH EVENTS SHOULD BE WATCHED + LOOP INDEFINITELY THROUGH IF-ELSE LOGIC  
    $created = Register-ObjectEvent $watcher "Created" -Action $action
    while ($true) {
            if ((New-TimeSpan -Start $lasttimefilecreated –End $(Get-Date)).Seconds -gt 10 -and $logflag -eq 0) {
            $logflag = 1
            Add-content "C:\Log.txt" -value "$(Get-Date)"
            "Log.txt file created/modified at $(Get-Date)"
            }
            else {sleep 5}}
    
  • skyfx
    skyfx over 7 years
    Thanks for your input, Ben. Adding "Write-Host" worked :). However, adding "$script:" in front of the variables in the event handler does not result in changes to the variables to be reflected outside of the event handler. In other words, after the event handler exists, the values of the variables outside of the event handler are unchanged. How can I change the scope of the variable assignments within in the event handler to be "global"?
  • Ben N
    Ben N over 7 years
    @skyfx Ah, after more experimentation, it looks like you need the global scope instead. I also added a demonstration of a timer object.
  • skyfx
    skyfx over 7 years
    That's interesting. I read this article on scoping, and it seems to agree with your original suggestion of using scope script. Perhaps there is something special about Register-ObjectEvent's event handler?! That aside, what's the benefit of using a timer vs. a loop? And could you clarify what you meant by, "Timers would continue to run if it did stop, but it looks a little weird"? In the other post I linked to, they had a separate script to unregister the event (e.g., Unregister-Event $created.Id). Is that needed for the timer too?
  • Ben N
    Ben N over 7 years
    @skyfx I'm not sure, but global: works for me while script: doesn't. (I made sure to always refer to each variable with the global scope.) I suspect there's something even more special about event handlers. Using a timer removes the need to deal with TimeSpan instances. If you don't have a loop, the script will exit but if PowerShell keeps running, the timer will keep firing and its output will get printed to wherever your cursor happens to be at the time. You don't have to manually unregister the event because everything will be torn down when the PowerShell process exits.
  • skyfx
    skyfx over 7 years
    Understood. Unfortunately, I'm still encountering issues, even using global scope. If I debug the script using an elevated PowerShell ISE instance, it behaves as expected. However, if I launch the script from an elevated PowerShell console, it doesn't -- after the event handler exists, the values of the variables are still unchanged. I know this because I added statements at the beginning of the WHILE loop to print the values of the variables. Another thing I noticed when I debugged in ISE is that the event handler action runs twice. Any idea what's going on here?
  • Ben N
    Ben N over 7 years
    @skyfx Make sure you always refer to and assign the variables using the global: scope if you use that scope at all. As for duplicate event handlers, every time you run the script, a new file system watcher is created with a new event handler. Those won't be removed until manually stopped or automatically destroyed by the process exiting.
  • skyfx
    skyfx over 7 years
    Got it. I had to put $global: in front of the lasttimefilecreated and logflag variables throughout the script. To your second point, what exactly constitutes the "process exiting"? Assuming I launched the script from the PowerShell console, would I just need to close that console, or if I launched it from the ISE, just close the ISE? Is there any way to view what event handlers are still active?
  • Ben N
    Ben N over 7 years
    @skyfx Yes, you'd need to close the PowerShell console or, for the ISE, probably exit that program. (I don't use the ISE, so I'm not sure.)