Skip to content

Instantly share code, notes, and snippets.

@Seekatar
Created August 4, 2022 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Seekatar/6aea0a5f80821a4fabcfe787fe2f3353 to your computer and use it in GitHub Desktop.
Save Seekatar/6aea0a5f80821a4fabcfe787fe2f3353 to your computer and use it in GitHub Desktop.
PowerShell functions for watching for file changes
<#
.SYNOPSIS
Watch a file for changes
.PARAMETER File
File to watch, may be a folder with a mask
.EXAMPLE
$watch = Start-WatchFile .\src\test\hras\hra-heart-health.json -ChangeScript {param($ChangeType,$FullPath) Write-Host "$FullPath Changed" }
# do something else
Stop-WatchFile $watch
Start watching a file in the background (no -Wait), then later stop watching
.EXAMPLE
Start-WatchFile .\src\test\hras\hra-heart-health.json -ChangeScript {param($ChangeType,$FullPath) Write-Host "$FullPath Changed" } -Wait
Watch a file until Ctrl+C pressed
.NOTES
Based on https://powershell.one/tricks/filesystem/filesystemwatcher
#>
function Start-WatchFile {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript({ Test-Path $_ -PathType Leaf })]
[string] $File,
[Parameter(Mandatory)]
[scriptblock] $ChangeScript,
[switch] $IncludeSubfolders,
[switch] $Wait
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$File = Convert-Path $File
$handlers = $null
# specify the path to the folder you want to monitor:
$Path = (Split-Path $File -parent)
# specify which files you want to monitor
$FileFilter = (Split-Path $File -leaf)
# specify the file or folder properties you want to monitor:
$AttributeFilter = [IO.NotifyFilters]::LastWrite
try {
$watcher = New-Object -TypeName System.IO.FileSystemWatcher -Property @{
Path = $Path
Filter = $FileFilter
IncludeSubdirectories = [bool]$IncludeSubfolders
NotifyFilter = $AttributeFilter
}
$VarName = [guid]::NewGuid().ToString()
Set-Variable -Name $VarName -Value $null -Scope Global
# define the code that should execute when a change occurs:
$action = {
# the code is receiving this to work with:
try {
# change type information:
$details = $event.SourceEventArgs
$Name = $details.Name
$FullPath = $details.FullPath
$OldFullPath = $details.OldFullPath
$OldName = $details.OldName
$MessageData = $event.MessageData
# type of change:
$ChangeType = $details.ChangeType
# when the change occurred:
$Timestamp = $event.TimeGenerated
# handle double calls
$prev = Get-Variable -Name $MessageData.VarName -Scope Global
if ($prev -and ($prev.FullPath -eq $details.FullPath) -and (($Timestamp - $prev.TimeStamp).TotalSeconds -lt .2 )) {
return
}
Set-Variable -Name $MessageData.VarName -Value @{FullPath = $FullPath; Timestamp = $Timestamp } -Scope Global
if ($global:fw_test) {
$global:fw_lastEvent = $event
}
Invoke-Command -ScriptBlock $MessageData.ChangeScript -ArgumentList $ChangeType, $FullPath, $OldFullPath
} catch {
Write-Warning "Error in watcher action: $_`n$($_.ScriptStackTrace)"
}
}
# 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:
$messageData = @{
ChangeScript = $ChangeScript
VarName = $VarName
}
$handlers = . {
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -MessageData $messageData
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -MessageData $messageData
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action -MessageData $messageData
Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -MessageData $messageData
}
# monitoring starts now:
$watcher.EnableRaisingEvents = $true
Write-Verbose "Watching for changes to $File"
if ($Wait) {
# 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)
} else {
Write-Information "Call Stop-Watcher with the return value" -InformationAction Continue
$global:fw_watchStatus = @{
Watcher = $watcher
Handlers = $handlers
}
return $global:fw_watchStatus
}
} catch {
Write-Error $_
} finally {
if ($Wait) {
# this gets executed when user presses CTRL+C:
Stop-WatchFile $global:fw_watchStatus
}
}
}
<#
.SYNOPSIS
Stop watching a file started with Start-WatchFile
.PARAMETER WatchInfo
Value returned from Start-WatchFile without -Wait
.EXAMPLE
$watch = Start-WatchFile .\src\test\hras\hra-heart-health.json -ChangeScript {param($ChangeType,$FullPath) Write-Host "$FullPath Changed" }
# do something else
Stop-WatchFile $watch
Start watching a file in the background (no -Wait), then later stop watching
#>
function Stop-WatchFile {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[HashTable] $WatchInfo
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# stop monitoring
$WatchInfo.watcher.EnableRaisingEvents = $false
if ($WatchInfo.handlers) {
# remove the event handlers
$WatchInfo.handlers | ForEach-Object {
Unregister-Event -SourceIdentifier $_.Name
}
# event handlers are technically implemented as a special kind
# of background job, so remove the jobs now:
$WatchInfo.handlers | Remove-Job
}
# properly dispose the FileSystemWatcher:
$WatchInfo.watcher.Dispose()
Write-Verbose "`nMonitoring ended."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment