1

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 $_
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
Rafal Rafal
  • 83
  • 2
  • 7

2 Answers2

1

{ ... } 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
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    @RafalRafal: I figured out why `$script:` doesn't work: the event handler runs in a _dynamic module_, and that module is only connected to the global scope, not to the caller's. – mklement0 Apr 06 '20 at 17:08
0

In this code, my script block "$action" is capable to overwrite my global parameter "terminateFlag" initialized in the main script. Many thanks for @mklement0 and link which he provided Click

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
    $global:terminateFlag = 0

    ### 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
                $global:terminateFlag = RunFunctions $path $changeType $logPath
            }

    ### DECIDE WHICH EVENTS SHOULD BE WATCHED
    Register-ObjectEvent $watcher "Created" -Action $action

    while ($true) {
        Write-Host "waiting for file..."
        if ($terminateFlag -eq 1) {
            Exit
        }
        Start-Sleep 10
    }
}
catch {
    Write-Host $_
}
Rafal Rafal
  • 83
  • 2
  • 7