Currently I am building the prototype of a PowerShell-based automation solution. From the high level perspective it will work like a Print Spooler meaning that some kind of watchdog listens in the background to a particular directory for incoming files in order to process them or rather pass them to a processing engine.
With PowerShell 2.0 it is an easy task to leverage the .NET Framework’s FileSystemWatcher Class to establish such a file system watchdog:
PS C:\Scripts> $fsw = New-Object System.IO.FileSystemWatcher
PS C:\Scripts> $fsw
NotifyFilter : FileName, DirectoryName, LastWrite
EnableRaisingEvents : False
Filter : *.*
IncludeSubdirectories : False
InternalBufferSize : 8192
Path :
Site :
SynchronizingObject :
Container :
PS C:\Scripts>
Obviously, a freshly initialized instance of System.IO.FileSystemWatcher is very roughly prepared to watch for file system changes. Five properties are preset, namely NotifyFilter (specifies the changes to watch for in a file system object), EnableRaisingEvents (indicates whether events are raised or not – yes, we want!), Filter (specifies what files are monitored), IncludeSubdirectories (indicates whether subdirectories are monitored or not), and InternalBufferSize. (Please look in the MSDN for more info on the FileSystemWatcher class.)
So, how about a PowerShell function to initialize a .NET FileSystemWatcher object with proper values for Patch, Filter, NotifyFilter, IncludeSubdirectories, and EnableRaisingEvents? Here we go!
function New-FSWatcher
{
<#
.SYNOPSIS
Initializes a .NET file system watcher
.DESCRIPTION
Initializes a .NET file system watcher, given the specified directory and optionally the type of files to monitor.
.PARAMETER Path
Specifies the path of an existing directory to watch.
.PARAMETER Filter
Determines what files are monitored in Path. (Defaults to *.*)
.PARAMETER NotifyFilter
Specifies the types of changes to watch for. Valid types are FileName (default), DirectoryName (default), Attributes, Size, LastWrite (default), LastAccess (default), CreationTime, and Security.
.PARAMETER Recurse
Indicates whether subdirectories within the Path should be monitored.
.OUTPUTS
Returns a .NET FileSystemWatcher object
.EXAMPLE
PS C:\> $fsw = New-FSWatcher -Path 'C:\Temp'
This command initializes a file system watcher for *.* in C:\Temp.
.EXAMPLE
PS C:\> $fswxml = New-FSWatcher C:\Temp *.xml -Recurse
This command initializes a file system watcher for *.xml in C:\Temp including its subdirectories.
.LINK
Start-FSWatcher
Register-FSWatcherEventHandler
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[String]
$Path
,
[Parameter(Mandatory=$false, Position=1)]
[String]
$Filter = '*.*'
,
[Parameter(Mandatory=$false)]
[System.IO.NotifyFilters]
$NotifyFilter = ('FileName','LastWrite','LastAccess')
,
[Switch]
$Recurse
)
if ($PSCmdlet.ShouldProcess("$Path\$Filter", "Initialize FileSystemWatcher"))
{
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $Path
$FileSystemWatcher.Filter = $Filter
$FileSystemWatcher.NotifyFilter = $NotifyFilter
if ($Recurse)
{
$FileSystemWatcher.IncludeSubdirectories = $true
}
$FileSystemWatcher.EnableRaisingEvents = $true
$FileSystemWatcher
}
}
That’s only half the battle. With New-FSWatcher you are able to initialize a FileSystemWatcher. But how to start monitoring?
If you look at the methods of System.IO.FileSystemWatcher you will sooner or later discover WaitForChanged(), "a synchronous method that returns a structure that contains specific information on the change that occurred, given the type of change you want to monitor and the time (in milliseconds) to wait before timing out".
So, with WaitForChanged() it is perfectly possible to stop script processing for a given amount of maximum time in order to wait for a file. Apart from the fact that I have a watchdog in mind that should wait for file system events in the background, I will provide a function to leverage a synchronous (foreground) wait for files anyways:
function Start-FSWatcher
{
<#
.SYNOPSIS
Listens the file system changes for a given amount of time.
.DESCRIPTION
Uses a previously configured FileSystemWatcher to listen for changes in a directory using a synchronous method that returns specific information on each change that occurred, given the type of change you want to monitor and the time to wait before timing out.
.PARAMETER FileSystemWatcher
Specifies a FileSystemWatcher object.
.PARAMETER Type
Specifies the type of change you want to monitor. Valid type are All (default), Changed, Created, Deleted, Disposed, Error, and Renamed.
.PARAMETER TimeOut
Specifies the timeout in ms. (Defaults to 10000)
.PARAMETER Infinite
Specifies to watch infinitely. (Not recommended. Consider registering events instead.)
.OUTPUTS
A structure containing specific information on each change that occurred.
.NOTES
Hidden files are NOT ignored.
.EXAMPLE
PS C:\> $fsw = New-FSWatcher -Path 'C:\Temp' -Filter 'jobfinished.txt'
PS C:\> Start-FSWatcher -FSW $fsw
These two commands initialize and use a FileSystemWather for C:\Temp. The watcher listens 10 seconds (default) for any changes in the directory.
.EXAMPLE
PS C:\> New-FSWatcher C:\Temp *.xml | Start-FSWatcher -Infinite
This pipeline shows how to start an infinite FileSystemWatcher for any change on any XML file in C:\Temp.
.LINK
New-FSWatcher
Register-FSWatcherEventHandler
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[Alias("FSW")]
[ValidateNotNullOrEmpty()]
[System.IO.FileSystemWatcher]
$FileSystemWatcher
,
[Parameter(Mandatory=$false)]
[ValidateSet('All','Changed', 'Created', 'Deleted', 'Disposed', 'Error', 'Renamed')]
[String]
$Type = 'All'
,
[Parameter(Mandatory=$false)]
[Int]
$TimeOut = 10000
,
[Switch]
$Infinite
)
if ($PSCmdlet.ShouldProcess("$($FileSystemWatcher.Path)", "WaitForChanged($Type, $TimeOut)"))
{
do
{
$FileSystemChange = $FileSystemWatcher.WaitforChanged($Type, $TimeOut)
if (!$FileSystemChange.TimedOut)
{
$FileSystemChange
}
}
while ($Infinite)
}
}
Please note that Start-FSWatcher will stop monitoring the file system as soon as the first change occurs that matches the FileSystemWatcher object’s configuration (unless the Infinite switch has been specified which causes the function to invoke the WaitForChanges() method again and again till the end of time). If you want to wait for a particular file you can define a FileSystemWatcher object and setup its Filter property to match the exact file name. Therefore, concerning a specific file name to be monitored, there’s no need to change the function.
To return to square one finally: how to define the file system watcher event handlers that will be fired on change, creation, deletion, or renaming of a file or directory? This is really straightforward. Basically you need to use the Register-ObjectEvent Cmdlet with a previously initialized FileSystemWatcher object, the event to act on, and the action to be executed on that event. Although it seems to be overkill, I’ll provide a third function, Register-FSWatcherEventHandler, that helps to define a proper event action for a file system watcher:
function Register-FSWatcherEventHandler
{
<#
.SYNOPSIS
Registers a FileSystemWatcher event handler.
.DESCRIPTION
Specifies what is done when a file is changed, created, deleted, or renamed.
.PARAMETER FileSystemWatcher
Specifies a FileSystemWatcher object
.PARAMETER EventName
Specifies the type of event. Valid events are Changed, Created, Deleted, Disposed, Error, and Renamed.
.PARAMETER EventAction
Specifies what is done.
.EXAMPLE
PS C:\Scripts> $fsw = New-FSWatcher -Path 'C:\Temp'
PS C:\Scripts> Register-FSWatcherEventHandler $fsw 'Created' -Action {
>> Remove-Item $($eventArgs.FullPath) -Confirm
}
These two commands initialize a FileSystemWather for C:\Temp and registers an event handler which will offer to delete any created file.
.LINK
New-FSWatcher
Start-FSWatcher
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param (
[Parameter(Mandatory=$true, Position=0)]
[Alias("FSW")]
[ValidateNotNullOrEmpty()]
[System.IO.FileSystemWatcher]
$FileSystemWatcher
,
[Parameter(Mandatory=$true, Position=1)]
[ValidateSet('Changed', 'Created', 'Deleted', 'Disposed', 'Error', 'Renamed')]
[String]
$EventName
,
[Parameter(Mandatory=$true, Position=2)]
[Alias('Action')]
[Scriptblock]
$EventAction
)
if ($PSCmdlet.ShouldProcess("$($FileSystemWatcher.Path)", "Register Event Handler (for File $EventName)"))
{
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName $EventName -Action $EventAction
}
}
Have fun!
Pit