The use of .NET classes can extend PowerShell's reach. In this article I will be looking at the System.IO.FileSystemWatcher .NET class and how you can use it to raise notifications on files and directories.
Contents of this article

The use of .NET classes can extend PowerShell's reach. In this article I will be looking at the System.IO.FileSystemWatcher .NET class and how you can use it to raise notifications on files and directories.

PowerShell doesn't do restrictions, and when you can't achieve something one way, there is most probably another way with PowerShell's extension ability. To use an example of notifying about file system changes, we can delve into .NET classes.

I'm going to show you how to initialize an instance of the System.IO.FileSystemWatcher class and subscribe to the events generated by a Microsoft .NET Framework object. By using a PowerShell function, I've created a way to simplify this process even further.

Before we go into the function, let's initialize an instance of the class and look at some of the properties. If you are using an older version of PowerShell, use the New-Object cmdlet; otherwise, use the static method new:

$fsw = [System.IO.FileSystemWatcher]::new()
Or:
$fsw = New-Object -TypeName System.IO.FileSystemWatcher

Viewing the initialized instance shows the properties:

FileSystemWatcher properties on a new instance

FileSystemWatcher properties on a new instance

The properties to look at are NotifyFilter, Filter, EnableRaisingEvents, Path, and IncludeSubdirectories.

The NotifyFilter property specifies changes to watch for in a file or folder. Using Get-Member on our instance shows NotifyFilter takes its values from the .NET class System.IO.NotifyFilters.

$fsw | Get-Member -Name NotifyFilter
NotifyFilter properties view by Get Member

NotifyFilter properties view by Get Member

Digging a little deeper, we see the base type on this .NET class is an Enum:

[System.IO.NotifyFilters].BaseType

NotifyFilter basetype of enum

NotifyFilter basetype of enum

By using the Enum type, we can view the values of 'System.IO.NotifyFilters':

[Enum]::GetNames('System.IO.NotifyFilters')

Enum of NotifyFilter properties

Enum of NotifyFilter properties

You can use more than one of these values when defining your NotifyFilter.

The other properties are straightforward to populate:

Path sets the directory to watch.

The Filter property takes a string, so you can define which files to watch in a directory.

IncludeSubdirectories takes a Boolean value to indicate whether to monitor subdirectories.

EnableRaisingEvents also takes a Boolean value to indicate enablement of the watcher. To be able to subscribe to the events generated by .NET Framework objects, you will need to use the Register-ObjectEvent cmdlet. For now, I will go over the function to help pull all of this together, which will include a further look into Register-ObjectEvent.

Invoke-FileSystemWatcher ^

I will start with the advanced parameters for the function. This is how they look:

function Invoke-FileSystemWatcher {
    [CmdletBinding()]
    param (
        [Parameter()]
        [System.IO.FileInfo] $Path,
        [Parameter()]
        [String] $filter = '*.*',
        [Parameter()]
        [System.IO.NotifyFilters] $NotifyFilters,
        [parameter()]
        [Switch] $Recurse,
        [parameter()]
        [Switch] $EnableRaisingEvents,
        [Parameter()]
        [EventName] $EventName,
        [Parameter()]
        [psobject] $MessageData,
        [Parameter()]
        [scriptblock] $Action
    )

The first five parameters relate to setting up the System.IO.FileSystemWatcher .NET class. The System.IO.NotifyFilters parameter can take more than one option. Defining the type allows for tab completion, which extends to more than one value. So by tabbing the first value, add a comma, then you'll be able to tab through the next. The Recurse switch will define a true or false value depending on whether you use it, which will then apply to IncludeSubdirectories.

To help add an EventName, I created an Enum. The Enum looks like this:

enum EventName {
    Created
    Changed
    Deleted
    Renamed
}

This is a handy feature added in PowerShell version 5. (You could create one before PowerShell 5 with C# code, if required.)

Sure, I could have used a ValidateSet, but it introduces another aspect of the PowerShell language. I can tab through the options like any Enum type. The MessageData parameter is part of the Register-ObjectEvent cmdlet. The description from the online help does a good job on describing its use:

Specifies any additional data to be associated with this event subscription. The value of this parameter appears in the MessageData property of all events associated with this subscription.

This can be useful, and I will provide an example at the end.

The Action parameter is optional and specifies commands to handle the event in the form of a script block. The commands in the action script block run when raising the event as opposed to sending to the event queue.

Here is the rest of the function:

$watcher = [System.IO.FileSystemWatcher]::new()
    Write-Verbose -Message "[PROCESS] Building FileSystemWatcher with parameters"
    # Build the FileSystemWatcher class
    Switch ($PSBoundParameters) {
        { $_.ContainsKey('Path') } {
            $watcher.Path = $Path.FullName
        }
        { $_.ContainsKey('Filter') } {
            $watcher.Filter = $filter
        }
        { $_.ContainsKey('NotifyFilters') } {
            $watcher.NotifyFilter = $NotifyFilters
        }
        { $_.ContainsKey('Recurse') } {
            $watcher.IncludeSubdirectories = $Recurse.ToBool()
        }
        { $_.ContainsKey('Recurse') } {
            $watcher.EnableRaisingEvents = $EnableRaisingEvents.ToBool()
        }
    }
    Write-Verbose -Message "[PROCESS] Built FileSystemWatcher, registering Event"
    # Register the Event
    $ObjectEvent = @{
        InputObject      = $watcher
        EventName        = $EventName
        SourceIdentifier = "Watching: $($Path.FullName)"
        Action           = $Action
        MessageData      = $MessageData
    }
    Register-ObjectEvent @ObjectEvent
    Write-Verbose -Message "[PROCESS] Event now registered"
}

The first part of the code is to create the instance of the .NET class, System.IO.FileSystemWatcher.

The next is to parse $PSBoundParmameters through a switch statement with each parameter value added to the properties of the instance of the FileSystemWatcher. Finally, using the splatting technique, we build the subscriber for the event and parse to Register-ObjectEvent.

I shall create an example now that will display alerts to the screen when a folder reaches a size limit. For readability, I've splatted the parameters:

$watcherEvent = @{
    Path                = 'C:\temp'
    filter              = "*.*"
    NotifyFilters       = 'LastWrite', 'DirectoryName'
    Recurse             = $true
    MessageData         = "8mb"
    EnableRaisingEvents = $true
    EventName           = 'Changed'
    Action              = {
        $Folder = (Get-ChildItem $Sender.Path -Recurse |
                Measure-Object -Property Length -Sum -ErrorAction Stop).Sum
        if ($Folder -ge $Event.MessageData) {
            $limitOver = '{0:N2}MB' -f (($Folder - $Event.MessageData) / 1mb)

            $message = "[WARNING] [{0}] {1} has exceeded the limit of {2} by {3}" -f
            $Event.TimeGenerated,
            $Sender.Path,
            $Event.MessageData,
            $limitOver
            $message | Out-Host
        }
    }
}

Before invoking this, you will notice a few variables in the Action script block parameter you might not be aware of. $Event and $Sender are automatic variables available in the event handler script block. They help provide information to us about the event. There are a few more, so it's worth reading about_automatic_variables to start with.

Let's now run the command:

Invoke-FileSystemWatcher @watcherEvent

Copying a file to C:\Temp takes me over the 8 MB limit I set in the MessageData parameter, which generates an alert:

Event subscriber alert to screen

Event subscriber alert to screen

Summary ^

Using the System.IO.FileSystemWatcher .NET class is a great way to get real-time event notifications. The ability to write code in the Action script block gives great flexibility on alert notifications. Instead of writing to the host, you could send an email, write to a log file, or perform other actions. Using the function should simplify the creation of the FileSystemWatcher to get you creating your own file and directory event notifications. The function and examples are up on my Git page.

13 Comments
  1. DanJ 3 years ago

    I'm trying to monitor a folder for files being created over a certain size (so I can move them to different folder). It seems your code is close to what I'm looking for, but I'm not sure how to get the size of the newly created file. I thought it would be $Event.SourceEventArgs.Size, but that seems to return nothing. Any suggestions?

  2. Author

    Hi Danj,

    Are you looking to monitor a single file in a folder or several files? Sounds like you will need to tweak the 'NotifyFilter'. Have a look at the MSDN documentation, they list the available options, https://msdn.microsoft.com/en-us/library/system.io.notifyfilters(v=vs.110).aspx. Try using 'Size' with 'FileName' for the 'NotifiyFilter'.
    When building the watcher event, add a value to the 'MessageData' parameter of the size you want to alert on. Then, like on my example, you will be able to work with '$Event.MessageData' to alert on the size.
    One thing to note, you will need to either use 'Write-Host' or pipe to 'Out-Host' to display a value from the 'Action' scriptblock parameter. Hope this helps!

    Graham

  3. Cyrill 3 years ago

    Hi Graham,

    Nice idea and code. I wonder whether there is any opportunity to see user information in the subscription - to know who is the author of the corresponding change?

    Thx!

  4. Author

    Hi Cyrill

    Thank you. This information should be present in the event written which can be viewed from the Event Viewer.

  5. Mateusz 3 years ago

    2 days ago
    Hello Graham,

    Thank you very much for all information. Great job!

    My question is, could you be more spesify about how to find who made changes in our directory? Maybe there will be also option to find the IP address?
    To be honest I stuck with my script and I have no idea how to find who modyfi file without audit option.

    Best Regards!

    btw
    I'm powershell noob 😀

  6. Mateusz 3 years ago

    here is my script:

    $FileSystemWatcher = New-Object System.IO.FileSystemWatcher
    $FileSystemWatcher.Path = "c:\temp"
    $FileSystemWatcher.IncludeSubdirectories = "True"
    # Tworzenie plikow
    $Created = Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action {

    $Object = write-host "Object created: $($Event.SourceEventArgs.FullPath , $Event.TimeGenerated , $env:username)"
    }

    Hope you will be able to help me

  7. Jeff 3 years ago

    So I have this script, which is using the older language, but is not throwing errors and seems do to at least, half of the job. After reading this, I discovered, I was using older language but it seemed like all I really needed to do was add one parameter. But it is still not working fully. It runs the process, but doesn't seem to trigger the action when files are changed in the directory being watched.

    Any ideas?:

    $folder = 'C:\Windows\System32\ias' # Enter the root path you want to monitor.
    $filter = '*.*' # You can enter a wildcard filter here.

    $fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property @{IncludeSubdirectories = $false;EnableRaisingEvents = $true;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}

    Register-ObjectEvent $fsw Changed -SourceIdentifier FileChanged -Action {
    $name = $Event.SourceEventArgs.Name
    $changeType = $Event.SourceEventArgs.ChangeType
    $timeStamp = $Event.TimeGenerated
    Invoke-Item (start powershell ((Split-Path $MyInvocation.InvocationName) + "\NPSMasterExport.ps1"))
    Write-Host "The file '$name' was $changeType at $timeStamp" -fore red
    Out-File -FilePath c:\scripts\filechange\outlog.txt -Append -InputObject "The file '$name' was $changeType at $timeStamp"}

  8. Mateusz 3 years ago

    Hello,

    To clarify- There is no option in Powershell to check WHO made changes in our monitored folder or file without turning on the auditing option in Windows

    Very sad...

  9. Author

    If you can find out who last wrote to a file from the filesystem then you can do this from powershell 😊
    Once you have worked this out, plug it into the file watcher. The file watcher is alerting you on certain changes.

  10. Mateusz 3 years ago

    Hello Graham,

    Thank you for the clue. I found someting interesting. Two options how to check what you talked about:
    1.

    Open C:\Users\user name\AppData\Roaming\Microsoft\Windows\Recent Items, and see if the file is listed there.
    2.
    Open regedit.exe, and navigate to the HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs section. You will see values, structured by extension, that have recently been opened by the 'current user' (last 10 files of that extension).

    Note that option 1 will only list files that have been modified, whereas option 2 will also list files that have been opened and closed, without editing or saving the file.

    And now is the main question, how to implement this solution using PowerShell to Windows Server and track changes in folders 🙂
    Right now I have no idea how to do this

    Maybe next clue Graham, please?

  11. Without file auditing turned on, you'll never know who wrote to the file *for sure*.  The Recent Files entry is interesting, but would not be reliable.  It won't pick up any changes done at the command line, or remotely.  You have to rely on auditing.  With the auditing turned on, then you will need to query the Security log on the system that you are monitoring the file on.

    David F.

  12. martin 2 years ago

    Hi, I want to filter by 2 file extensions but I can't get it to work, 

    what is the best way to approach that?

  13. Try changing the hashtable filter entry from filter = "*.*" to filter = '*.pdf' (or whatever extension) to verify the filtering works. Then once that works, then the correct way to handle this is to set up multiple watchers - one for each of the file extensions.

    https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.filter?redirectedfrom=MSDN&view=netframework-4.8#System_IO_FileSystemWatcher_Filter

    David F. 

Leave a reply

Please enclose code in pre tags

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2021

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account