Search code examples
.netpowershellloggingeventsfilesystemwatcher

Unable to find events being raised when log file changed


Im trying to use FileSystemWatcher to watch a file and check for changes, when a change occurs it should run aa command, this works when im saving the file in notepad, however not from the actual application that is making the log file. Ive seen posts in the past saying that it may be due to the application creating a copy of the log with the new data added then deletes the original and renames the copy, so I tried looking for delete, creation and rename events and still nothing.

This is the class that I've been using, it does have to be a derived class of FileSystemWatcher as there is other functionality that is to be included which is out of the scope of this question.

class LogWatcher : System.IO.FileSystemWatcher {
    [System.Management.Automation.Job[]] $Event
    [string] $LogName = 'Endpointlog.txt'
    [string] $LogFolder = ('C:\Users\' + $env:USERNAME + '\AppData\Roaming\<pathtologfolder>')
    LogWatcher () : base($this.LogFolder, $this.LogName) {
        $this.Event = (new-object System.Management.Automation.Job[] 4)
        $this.EnableRaisingEvents = $true;
        $fsaction = {
            param ( 
                [System.Object] $sender,
                [System.IO.FileSystemEventArgs] $e
            )
            $sender | fl
            $e | fl
        }
        $rnaction = {
            param ( 
                [System.Object] $sender,
                [System.IO.renamedEventArgs] $e
            )
            $sender | fl
            $e | fl
        }
        $this.NotifyFilter =[System.IO.NotifyFilters]::Attributes, [System.IO.NotifyFilters]::CreationTime,     [System.IO.NotifyFilters]::DirectoryName, [System.IO.NotifyFilters]::FileName, [System.IO.NotifyFilters]::LastAccess, [System.IO.NotifyFilters]::LastWrite, [System.IO.NotifyFilters]::Security, [System.IO.NotifyFilters]::Size 
        $this.Event[0] = register-ObjectEvent -InputObject $this -EventName changed -Action $fsaction
        $this.Event[1] = register-ObjectEvent -InputObject $this -EventName created -Action $fsaction
        $this.Event[2] = register-ObjectEvent -InputObject $this -EventName deleted -Action $fsaction
        $this.Event[3] = register-ObjectEvent -InputObject $this -EventName renamed -Action $rnaction
    }
}

And initialising it as. $lw = [LogWatcher]::new();

The log file is updated every couple seconds so I would expect that $lw.Event to show at least one Job that is running and not Not Started however I can still only get one Job to change and that is when I open the log and save it directly and as a note, notepad asks to save as rather than just save as normal which does still make me think that its being copied, deleted and copy renamed.

Is there any way that i can get any event to fire when the those switches are made?


Solution

  • The Action block of your events run in a different scope and all output produced in them is captured in the .Output property of each PSEventJob. For example, the .Output on index 0 would show you all output captured by Changed raised events, index 1 for Created events and so on:

    $lw.Event[0].Output
    

    If you're troubleshooting however, it's much easier to use Out-Host, then the output is sent directly to the console (Write-Host would work too):

    $fsaction = {
        $Sender | Format-List | Out-Host
        $EventArgs | Format-List | Out-Host
    }
    

    Worth noting here, in PowerShell, $Sender and $EventArgs are automatic variables, you don't need to declare them.

    I made a few changes to your class too that might help you simplify things:

    class LogWatcher : System.IO.FileSystemWatcher {
        [System.Management.Automation.Job[]] $Event
        [string] $LogName
        [string] $LogFolder
    
        LogWatcher([string] $folderPath, [string] $fileName) : base($folderPath, $fileName) {
            $this.LogFolder = $folderPath
            $this.LogName = $fileName
            $this.EnableRaisingEvents = $true
            # if you want to target all possibilities, this is easier ;)
            $this.NotifyFilter = [enum]::GetValues([System.IO.NotifyFilters])
    
            $this.Event = 'Changed', 'Created', 'Deleted', 'Renamed' | ForEach-Object {
                Register-ObjectEvent -InputObject $this -EventName $_ -Action {
                    $Sender | Format-List | Out-Host
                    $EventArgs | Format-List | Out-Host
    
                    # this is so that your watcher can keep track of the new file
                    # instead of watching a file that no longer exists
                    if ($EventArgs.ChangeType -eq 'Renamed') {
                        $sender.LogName = $sender.Filter = $EventArgs.Name
                    }
                }
            }
        }
    }
    

    Now for testing, this should work fine in both, Windows PowerShell 5.1 and PowerShell 7+. You should see all events being raised.

    $watcher = [LogWatcher]::new($pwd.Path, 'testingwatcher.txt')
    
    'Creating File...'
    Start-Sleep 1
    $file = New-Item 'testingwatcher.txt' -Force
    
    'Changing File...'
    Start-Sleep 1
    $file.LastWriteTime = [datetime]::Today
    
    'Renaming File...'
    Start-Sleep 1
    $file = $file | Rename-Item -NewName 'ihaveanewname.txt' -PassThru
    
    'Deleting File...'
    Start-Sleep 1
    $file.Delete()
    
    # Cleanup - probably your class should implement IDisposable ;)
    $watcher.Event.Name | Unregister-Event -SourceIdentifier { $_ }