Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell hanging due to Filesystemwatcher

We have a convoluted solution to some printing issues (caused by citrix and remote servers). Basically from the main server, we force push a pdf file to the remote pc and then have a powershell script which constantly runs on the remote pc to "catch" the file and push it to the local printer

This works "fine"

However we get random dropouts. The powershell script doesn't seem to have crashed because it's still running in windows but the actual action doesn't seem to be processing new files

I have done A LOT of reading today and there's mention of having to name and unregister events when you're done otherwise it can cause a buffer overflow issues and make powershell stop processing the action. But I'm unsure where it should actually go within the code. The idea is that this script will run permanently, so do we unregister or remove the event within the action itself or somewhere else?

I previous had A LOT of dummy logging going on within the action to try to find where it failed, but it seems to stop at different points without any justifiable reason (ie, it would fail at the command to find files, other times at the command to move etc etc)

### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "l:\files\cut"
$watcher.Filter = "*.pdf"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true  

### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
            $changeType = $Event.SourceEventArgs.ChangeType
            $scandir="l:\files\cut" 
            $scanbackdir="l:\files\cut\back"   
            $scanlogdir="l:\files\cut\log" 
            $sumatra="l:\SumatraPDF.exe"      
            $pdftoprint=""
            $printername= "MainLBL"

### Get the List of files in the Directory, print file, wait and then move file
Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % { 
$pdftoprint=$_ 
& $sumatra -silent $scandir\$pdftoprint -print-to $printername

sleep 3 

Move-Item -force $scandir\$pdftoprint $scanbackdir
 }
 } 

### Define what happens when script fails
$erroraction = {echo $(get-date) the process crashed | Out-File -Append l:\files\cut\log\errorlog.txt}    

### DECIDE WHICH EVENTS SHOULD BE WATCHED 
Register-ObjectEvent $watcher "Error" -Action $erroraction
Register-ObjectEvent $watcher "Created" -Action $action
while ($true) {sleep 5}
like image 283
Todd Querruel Avatar asked Apr 20 '26 13:04

Todd Querruel


2 Answers

If you want a script to run in the background, then look to PowerShell background jobs.

If you want a script to run permanently, then you want to make it a service ...

See these:

How to Create a User-Defined Service

How to run a PowerShell script as a Windows service

... or attach that to a scheduled task, that would restart it on reboots.

There are two ways to implement a FileSystemWatcher.

  • Synchronous
  • Asynchronous

A synchronous FileSystemWatcher, by it nature, when a change is detected, control is returned to your script so it can process the change. If another file change occurs while your script is no longer waiting for events, it gets lost. Hence leading to unexpected outcomes.

Using the FileSystemWatcher Asynchronously, it would continue to log new filesystem changes and process them once PowerShell is done processing previous changes.

* An Sample - Example Asynchronous FileSystemWatcher*

### New-FileSystemWatcherAsynchronous

# Set the folder target
$PathToMonitor = Read-Host -Prompt 'Enter a folder path'

$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path  = $PathToMonitor
$FileSystemWatcher.IncludeSubdirectories = $true

# Set emits events
$FileSystemWatcher.EnableRaisingEvents = $true

# Define change actions
$Action = {
    $details = $event.SourceEventArgs
    $Name = $details.Name
    $FullPath = $details.FullPath
    $OldFullPath = $details.OldFullPath
    $OldName = $details.OldName
    $ChangeType = $details.ChangeType
    $Timestamp = $event.TimeGenerated
    $text = "{0} was {1} at {2}" -f $FullPath, $ChangeType, $Timestamp

    Write-Host $text -ForegroundColor Green

    # Define change types
    switch ($ChangeType)
    {
        'Changed' { "CHANGE" }
        'Created' { "CREATED"}
        'Deleted' { "DELETED"
                    # Set time intensive handler
                    Write-Host "Deletion Started" -ForegroundColor Gray
                    Start-Sleep -Seconds 3    
                    Write-Warning -Message 'Deletion complete'
                  }
        'Renamed' { 
                    $text = "File {0} was renamed to {1}" -f $OldName, $Name
                    Write-Host $text -ForegroundColor Yellow
                  }
        default { Write-Host $_ -ForegroundColor Red -BackgroundColor White }
    }
}

# Set event handlers
$handlers = . {
    Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Changed -Action $Action -SourceIdentifier FSChange
    Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreate
    Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Deleted -Action $Action -SourceIdentifier FSDelete
    Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Renamed -Action $Action -SourceIdentifier FSRename
}

Write-Host "Watching for changes to $PathToMonitor" -ForegroundColor Cyan

try
{
    do
    {
        Wait-Event -Timeout 1
        Write-Host '.' -NoNewline

    } while ($true)
}
finally
{
    # End script actions + CTRL+C executes the remove event handlers
    Unregister-Event -SourceIdentifier FSChange
    Unregister-Event -SourceIdentifier FSCreate
    Unregister-Event -SourceIdentifier FSDelete
    Unregister-Event -SourceIdentifier FSRename

    # Remaining cleanup
    $handlers | 
    Remove-Job

    $FileSystemWatcher.EnableRaisingEvents = $false
    $FileSystemWatcher.Dispose()

    Write-Warning -Message 'Event Handler completed and disabled.'
}
like image 67
postanote Avatar answered Apr 22 '26 03:04

postanote


I have not encountered a script that will run permanently on windows. So with that in mind we take it for granted that some issue beyond your control such as the network or power or a system shutdown will occur. With that in mind we have a lifecycle for this script and everything should be properly cleaned up at the end. In this case we have while loop that should theoretically never end however if an exception is thrown it will end. Within the while loop if any of the events have been deregistered we can reregister them. If the watcher has been disposed we can recreate it and the events. If this really is mission critical code, then I would look at .net as an alternative with something like hangfire with nlog as a windows service.

### WRAP Everything in a try finally so we dispose of events 
try {
    ### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
    $watcherArgs = @{
        Path = "l:\files\cut"
        Filter = "*.pdf"
        IncludeSubdirectories = $false
        EnableRaisingEvents = $true  
    }
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $watcherArgs.Path
    $watcher.Filter = $watcherArgs.Filter
    $watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
    $watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents

    ### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
    $action = { $path = $Event.SourceEventArgs.FullPath
                $changeType = $Event.SourceEventArgs.ChangeType
                $scandir="l:\files\cut" 
                $scanbackdir="l:\files\cut\back"   
                $scanlogdir="l:\files\cut\log" 
                $sumatra="l:\SumatraPDF.exe"      
                $pdftoprint=""
                $printername= "MainLBL"

    ### Get the List of files in the Directory, print file, wait and then move file
    Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % { 
            $pdftoprint=$_ 
            if($LASTEXITCODE -ne 0) { 
                # Do something
                # Reset so we know when sumatra fails
                $LASTEXITCODE = 0
            }
            & $sumatra -silent $scandir\$pdftoprint -print-to $printername
            if($LASTEXITCODE -ne 0) { 
                # Do something to handle sumatra
            }
            sleep 3 

            # Split up copy and delete so we never loose files
            [system.io.file]::Copy("$scandir\$pdftoprint", "$scanbackdir", $true)
            [system.io.file]::Delete("$scandir\$pdftoprint")
        }
    } 

    ### Define what happens when script fails
    $erroraction = { 
        echo "$(get-date) the process crashed" | Out-File -Append "l:\files\cut\log\errorlog.txt"
    }    

    ### DECIDE WHICH EVENTS SHOULD BE WATCHED 
    $ErrorEvent  = Register-ObjectEvent $watcher "Error" -Action $erroraction
    $CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
    $ListOfEvents = @(
        $ErrorEvent  
        $CreatedEvent
    )

    while ($true) {

        $eventMissing = $false
        $ListOfEvents | % { 
            $e = $_

            if (!(Get-Event -SourceIdentifier $e.Name -ErrorAction SilentlyContinue)) {
                # Event does not exist 
                $eventMissing = $true
            }
        }

        if(!$watcher || $eventMissing -eq $true) {
            # deregister events 
            $ListOfEvents | % { 
                $e = $_
                try {
                    Unregister-Event -SourceIdentifier $e.Name
                } catch { 
                    # Do Nothing
                }
            }
            if($watcher) {
                $watcher.Dispose()
                $watcher = $null   
            } else {
                # Create watcher 
                $watcher = New-Object System.IO.FileSystemWatcher
                $watcher.Path = $watcherArgs.Path
                $watcher.Filter = $watcherArgs.Filter
                $watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
                $watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
                $ErrorEvent  = Register-ObjectEvent $watcher "Error" -Action $erroraction
                $CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
                $ListOfEvents = @(
                    $ErrorEvent  
                    $CreatedEvent
                )
            }

        }

        if ($watcher.EnableRaisingEvents -eq $false) {
            $watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
        }

        sleep 5
    }

} finally {
    $ListOfEvents | % { 
        $e = $_
        try {
            Unregister-Event -SourceIdentifier $e.Name
        } catch { 
            # Do Nothing
        }
    }
    if($watcher) {
        $watcher.Dispose();
    }
}
like image 21
lloyd Avatar answered Apr 22 '26 02:04

lloyd



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!