Search code examples
powershelleventsfilesystemwatcher

PowerShell. Can't get variable from {}


In the code below there is a parameter named "$action" = {...}. Inside the brackets there are some variables ($path, $logPath, etc.) How can I access variables from this parameter outside of the braces "{}"? I need result of one of my functions("RunFunctions") for a while loop. Or if it's some stupid question please can you point me to the place where I can find some information about this {} notation? Thanks.

try {
    ### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = "C:\Testers\ResetOptimisation\tracking"
    $watcher.Filter = "user_name*.*"
    $watcher.IncludeSubdirectories = $true
    $watcher.EnableRaisingEvents = $true

    ### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
    $action = { $path = $Event.SourceEventArgs.FullPath
                $logPath = "C:\Testers\ResetOptimisation\output\log.txt"
                $changeType = $Event.SourceEventArgs.ChangeType
                $logline = "$(Get-Date), $changeType, $path"
                Add-content $logPath -value $logline
                $terminateFlag = RunFunctions $path $changeType $logPath
            }

    ### DECIDE WHICH EVENTS SHOULD BE WATCHED
    Register-ObjectEvent $watcher "Created" -Action $action
    Register-ObjectEvent $watcher "Changed" -Action $action
    Register-ObjectEvent $watcher "Deleted" -Action $action
    Register-ObjectEvent $watcher "Renamed" -Action $action
    while ($true) {
        Write-Host $teminateFlag
        if ($teminateFlag -eq 1) {
            Exit
        }
        Start-Sleep 3
    }
}
catch {
    Write-Host $_
}

Solution

  • { ... } is a script block (literal) - a reusable piece of PowerShell code that you can execute on demand (like a function pointer or delegate in other languages).

    By passing such a block, stored in variable $action, to Register-ObjectEvent -Action, PowerShell invokes it whenever the event of interest fires, and does so in a dynamic module, whose scope is entirely separate from the caller's.

    Therefore, your calling code doesn't see the variables created inside the block, as they are local to that block.

    For more information about scopes in PowerShell, see the bottom section of this answer.

    While PowerShell generally lets you create and modify variables in other scopes as well, if you take explicit action, this is not an option with Register-ObjectEvent -Action, because a dynamic module's scope fundamentally doesn't have access to the caller's scope, only to the global scope.

    # !! This works, but is ill-advised.
    $global:terminateFlag = RunFunctions $path $changeType $logPath
    

    However, using the global scope is best avoided, because global variables linger even after the script exits (they are session-global).

    The better solution is to:

    • make the action script block output a value for the caller.

    • have the caller receive that output via Receive-Job, using the event job that
      Register-ObjectEvent -Action returns.

    Here's a simplified, self-contained example that demonstrates the technique:

    It sets up a watcher, attaches an event handler, and creates a file that triggers a watcher event.

    try {
    
      # Specify the target folder: the system's temp folder in this example.
      $dir = (Get-Item -EA Ignore temp:).FullName; if (-not $dir) { $dir = $env:TEMP }
    
      # Create and initialize the watcher.
      $watcher = [System.IO.FileSystemWatcher] @{
        Filter                = '*.tmp'
        Path                  = $dir
      }
    
      # Define the action script block (event handler).
      $action = {
        # Print the event data to the host.
        Write-Host "Event raised:`n$($EventArgs | Format-List | Out-String)"
        $terminateFlag = $true
        # *Return* a value that indicates whether watching should be stopped.
        # This value is later retrieved via Receive-Job.
        return $terminateFlag
      }
    
      # Subscribe to the watcher's Created events, which returns an event job.
      # This indefinitely running job receives the output from the -Action script
      # block whenever the latter is called after an event fires.
      $eventJob = Register-ObjectEvent $watcher Created -Action $action
    
      # Start watching:
      # Note: Not strictly necessary, because, curiously, 
      #       Register-ObjectEvent has aleady done this for us.
      $watcher.EnableRaisingEvents = $true 
    
      # Using an aux. background job, create a sample file that will trigger the 
      # watcher, after a delay.
      $tempFile = Join-Path $dir "$PID.tmp"
      $auxJob = Start-Job { Start-Sleep 3; 'hi' > $using:tempFile }
    
      Write-Host "Watching $dir for creation of $($watcher.Filter) files..."
    
      # Wait in a loop until the action block is run in response to an event and
      # produces $true as output to signal the intent to terminate, via Receive-Job.
      while ($true -ne (Receive-Job $eventJob)) {
        write-host . -NoNewline
        Start-Sleep -Milliseconds 500  # sleep a little
      }
    
    }
    finally {
      # Clean up.
      # Dispose of the watcher.
      $watcher.Dispose() 
      # Remove the event job (and with it the event subscription).
      $eventJob | Remove-Job -Force 
      # Clean up the helper job.
      Remove-Job -ea Ignore -Force $auxJob 
      # Remove the temp. file
      Remove-Item -ea Ignore $tempFile
    }