# Monitoring Folders for File Changes

With a FileSystemWatcher, you can monitor folders for file changes and respond immediately when changes are detected. This way, you can create “drop” folders and respond to log file changes.

With a FileSystemWatcher, you can monitor folders for file changes and respond immediately when changes are detected. This way, you can create “drop” folders and respond to log file changes.

The FileSystemWatcher object can monitor files or folders and notify PowerShell when changes occur. It can monitor a single folder or include all subfolders, and there is a variety of filters.

## Synchronously or Asynchronously?

You can invoke the FileSystemWatcher in two ways:

• Simple Mode: In synchronous mode, you ask the FileSystemWatcher to wait for a single change. This blocks PowerShell until either the change occurs or a timeout is reached. This approachis very simple to implement however there is a chance to miss change events when they occur in rapid succession.
• Advanced Mode: In asynchronous mode, the FileSystemWatcher does not block PowerShell. Instead, whenever a change occurs, an event is fired, and your script can respond to the events. This way, you cannot miss change events because the FileSystemWatcher is constantly monitoring. However, responding to events is not trivial in a single-threaded environment like PowerShell.

## Simple Mode (synchronous)

This is straight-forward: the script below monitors your desktop and all of its subfolders for new files and for deletion of files. Whenever a change is detected, Invoke-SomeAction is called.

# find the path to the desktop folder:
$desktop = [Environment]::GetFolderPath('Desktop') # specify the path to the folder you want to monitor:$Path = $desktop # specify which files you want to monitor$FileFilter = '*'

# specify whether you want to monitor subfolders as well:
$IncludeSubfolders =$true

# specify the file or folder properties you want to monitor:
$AttributeFilter = [IO.NotifyFilters]::FileName, [IO.NotifyFilters]::LastWrite # specify the type of changes you want to monitor:$ChangeTypes = [System.IO.WatcherChangeTypes]::Created, [System.IO.WatcherChangeTypes]::Deleted

# specify the maximum time (in milliseconds) you want to wait for changes:
$Timeout = 1000 # define a function that gets called for every change: function Invoke-SomeAction { param ( [Parameter(Mandatory)] [System.IO.WaitForChangedResult]$ChangeInformation
)

Write-Warning 'Change detected:'
$ChangeInformation | Out-String | Write-Host -ForegroundColor DarkYellow } # use a try...finally construct to release the # filesystemwatcher once the loop is aborted # by pressing CTRL+C try { Write-Warning "FileSystemWatcher is monitoring$Path"

# create a filesystemwatcher object
$watcher = New-Object -TypeName IO.FileSystemWatcher -ArgumentList$Path, $FileFilter -Property @{ IncludeSubdirectories =$IncludeSubfolders
NotifyFilter = $AttributeFilter } # start monitoring manually in a loop: do { # wait for changes for the specified timeout # IMPORTANT: while the watcher is active, PowerShell cannot be stopped # so it is recommended to use a timeout of 1000ms and repeat the # monitoring in a loop. This way, you have the chance to abort the # script every second.$result = $watcher.WaitForChanged($ChangeTypes, $Timeout) # if there was a timeout, continue monitoring: if ($result.TimedOut) { continue }

Invoke-SomeAction -Change $result # the loop runs forever until you hit CTRL+C } while ($true)
}
finally
{
# release the watcher and free its memory:
NotifyFilter = $AttributeFilter }  ## Advanced Mode (asynchonous) If you expect changes to happen in rapid succession or even simultaneously, you can use the FileSystemWatcher in asynchronous mode: the FileSystemWatcher now works in the background and no longer blocks PowerShell. Instead, whenever a change occurs, an event is raised. So with this approach, you get a queue and won’t miss any change. On the back side, this approach has two challenges: • Handling Events: since PowerShell is single-threaded by nature, it is not trivial to respond to events, and even more cumbersome to debug event handler code. • Keeping PowerShell running: ironically, because the FileSystemWatcher now no longer blocks PowerShell, this leads to another problem. You need to keep PowerShell waiting for events but you cannot use Start-Sleep or and endless loop because as long as PowerShell is busy - and it is considered busy even if it sleeps - no events can be handled. ### Implementation The script below does the exact same thing as the synchronous version from above, only it is event-based and won’t miss any events anymore: # find the path to the desktop folder:$desktop = [Environment]::GetFolderPath('Desktop')

# specify the path to the folder you want to monitor:
$Path =$desktop

# specify which files you want to monitor
$FileFilter = '*' # specify whether you want to monitor subfolders as well:$IncludeSubfolders = $true # specify the file or folder properties you want to monitor:$AttributeFilter = [IO.NotifyFilters]::FileName, [IO.NotifyFilters]::LastWrite

try
{
$watcher = New-Object -TypeName System.IO.FileSystemWatcher -Property @{ Path =$Path
Filter = $FileFilter IncludeSubdirectories =$IncludeSubfolders
NotifyFilter = $AttributeFilter } # define the code that should execute when a change occurs:$action = {
# the code is receiving this to work with:

# change type information:
$details =$event.SourceEventArgs
$Name =$details.Name
$FullPath =$details.FullPath
$OldFullPath =$details.OldFullPath
$OldName =$details.OldName

# type of change:
$ChangeType =$details.ChangeType

# when the change occured:
$Timestamp =$event.TimeGenerated

# save information to a global variable for testing purposes
# so you can examine it later
# MAKE SURE YOU REMOVE THIS IN PRODUCTION!
$global:all =$details

# now you can define some action to take based on the
# details about the change event:

# let's compose a message:
$text = "{0} was {1} at {2}" -f$FullPath, $ChangeType,$Timestamp
Write-Host ""
Write-Host $text -ForegroundColor DarkYellow # you can also execute code based on change type here: switch ($ChangeType)
{
'Changed'  { "CHANGE" }
'Created'  { "CREATED"}
'Deleted'  { "DELETED"
# to illustrate that ALL changes are picked up even if
# handling an event takes a lot of time, we artifically
# extend the time the handler needs whenever a file is deleted
Write-Host "Deletion Handler Start" -ForegroundColor Gray
Start-Sleep -Seconds 4
Write-Host "Deletion Handler End" -ForegroundColor Gray
}
'Renamed'  {
# this executes only when a file was renamed
$text = "File {0} was renamed to {1}" -f$OldName, $Name Write-Host$text -ForegroundColor Yellow
}

# any unhandled change types surface here:
default   { Write-Host $_ -ForegroundColor Red -BackgroundColor White } } } # subscribe your event handler to all event types that are # important to you. Do this as a scriptblock so all returned # event handlers can be easily stored in$handlers:
$handlers = . { Register-ObjectEvent -InputObject$watcher -EventName Changed  -Action $action Register-ObjectEvent -InputObject$watcher -EventName Created  -Action $action Register-ObjectEvent -InputObject$watcher -EventName Deleted  -Action $action Register-ObjectEvent -InputObject$watcher -EventName Renamed  -Action $action } # monitoring starts now:$watcher.EnableRaisingEvents = $true Write-Host "Watching for changes to$Path"

# since the FileSystemWatcher is no longer blocking PowerShell
# we need a way to pause PowerShell while being responsive to
# incoming events. Use an endless loop to keep PowerShell busy:
do
{
# Wait-Event waits for a second and stays responsive to events
# Start-Sleep in contrast would NOT work and ignore incoming events
Wait-Event -Timeout 1

# write a dot to indicate we are still monitoring:
Write-Host "." -NoNewline

} while ($true) } finally { # this gets executed when user presses CTRL+C: # stop monitoring$watcher.EnableRaisingEvents = $false # remove the event handlers$handlers | ForEach-Object {
Unregister-Event -SourceIdentifier $_.Name } # event handlers are technically implemented as a special kind # of background job, so remove the jobs now:$handlers | Remove-Job

# properly dispose the FileSystemWatcher:
$watcher.Dispose() Write-Warning "Event Handler disabled, monitoring ends." }  When you run this script, PowerShell emits a dot every second indicating that monitoring is active. When you now create, delete, rename, or change a file on your desktop or an of its subfolders, the event handler in$action triggers.

To illustrate that you won’t miss any event anymore, create a number of files on your desktop. Each time you create a new file, the appropriate events trigger. Next, select all the files you just created, and delete them all.

With the synchronous approach, the FileSystemWatcher would only pick up the first deletion. With the new asynchronous approach, you don’t miss anything. In fact, the event handler in $action purposely adds a Start-Sleep whenever a Delete event comes in, simulating a lengthy action script. Still, all deleted files are picked up. PowerShell can’t process events simultaneously, but it queues up all events and processes them one by one. Press CTRL+C to abort monitoring. The script turns off monitoring, removes all event handlers, and properly disposes the FileSystemWatcher. The event handler scriptblock in$action defined a global variable $all with the event information submitted by the newest event: # save information to a global variable for testing purposes # so you can examine it later # MAKE SURE YOU REMOVE THIS IN PRODUCTION!$global:all = $details  Now that the script is stopped, you can examine$all to see the information passed from the event to your event handler code:

Of course your event handler code needs to know details about the event it should handle. So within the event handler scriptblock, there is one special variable: $event: Property Purpose MessageData can be set from Register-ObjectEvent via -MessageData Sender The object that emitted the event. In our example the FileSystemWatcher SourceEventArgs The information submitted by the event. In our example the information about the file change. This is the same information that was returned by WaitForChanged() in the synchronous example. SourceArgs An array with the Sender and SourceEventArgs SourceIdentifier can be set from Register-ObjectEvent via -SourceIdentifier TimeGenerated Time and Date when the event was fired #### Registering Event Handlers For event handler code to become active, it needs to be attached to an event emitted by an object. This is done by Register-ObjectEvent. In our example, the FileSystemWatcher fires four different events: PS>$watcher = New-Object -TypeName System.IO.FileSystemWatcher

PS> $watcher | Get-Member -MemberType Event TypeName: System.IO.FileSystemWatcher Name MemberType Definition ---- ---------- ---------- Changed Event System.IO.FileSystemEventHandler Changed(System.Object, ... Created Event System.IO.FileSystemEventHandler Created(System.Object, ... Deleted Event System.IO.FileSystemEventHandler Deleted(System.Object, ... Disposed Event System.EventHandler Disposed(System.Object, System.Event... Error Event System.IO.ErrorEventHandler Error(System.Object, System.... Renamed Event System.IO.RenamedEventHandler Renamed(System.Object, Sys...  The script assigns the same event handler to all four events: $handlers = . {
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action$Action -MessageData 1
Register-ObjectEvent -InputObject $watcher -EventName Created -Action$Action -MessageData 2
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action$Action -MessageData 3
Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action$Action -MessageData 4
}


Register-ObjectEvent takes the event handler scriptblock and creates and returns a special type of background job from it that now listens to the event.

Since we need to keep track of the returned background jobs so we can later remove them, the script uses a scriptblock to call all four Register-ObjectEvent commands and runs the scriptblock dot-sourced. That’s a clever trick to store the return values of all four calls as an array in $handlers. #### Enabling Events Once the event handlers are in place, the FileSystemWatcher can be allowed to fire events: $watcher.EnableRaisingEvents = $true  Whenever an event is raised, the registered event handler will now pick it up. Technically, the event handler runs as a special type of background job. In contrast to normal background jobs, it shares the runspace with your script and does not run in a separate PowerShell instance. So event handlers share the runspace with your script. That’s good because your event handler can share all variables with your script and also output information into the shell. It’s also bad because PowerShell by nature is single-threaded, so event handlers will only run when the script is currently idle or else be queued. ### Cleaning Up An endless loop keeps PowerShell busy while waiting for events, so to end monitoring, press CTRL+C. These are the clean-up steps to end eventing: • Tell the FileSystemWatcher to no longer emit events by setting EnableRisingEvents to$false
• Call Unregister-Event for each registered event handler, and use the automatically assigned names from the background jobs stored in $handlers • Call Remove-Job to remove all background jobs in$handlers
• Dispose the FileSystemWatcher object.