2

I have an ObjectEvent that sees a new file getting created and then tries to move the file.

It works... except when it goes to move the file, the file is still open, so the Move-Item fails.

So I suppose there are two possible paths... I am open to either (or both!)

First, how would my ObjectEvent fire only after the file is closed? Current objectevent:

Register-ObjectEvent $Watcher -EventName Created -SourceIdentifier FileCreated -Action 

Second, is it possible for MoveItem to sit and keep trying for 5 seconds or so before failing? Current Move-Item call:

Move-Item $path -Destination $destination -Force -Verbose
Jonesome Reinstate Monica
  • 6,618
  • 11
  • 65
  • 112
  • 1
    Can you please share other parts of the code, e.g. the `$Watcher` and such? – Zafer Balkan Dec 26 '21 at 22:13
  • @ZaferBalkan This is a stand alone question, on specifically how to either watch for a file close event , or to make Move-Item work in this situation. – Jonesome Reinstate Monica Dec 27 '21 at 03:05
  • If the file is still in use and considering the file is created by an external process, you could monitor the process until it's closed and then move the file or you could implement a try catch block on your move file to retry on timed intervals, you can see a retry example [here](https://stackoverflow.com/a/70430436/15339544). – Santiago Squarzon Dec 27 '21 at 03:43

1 Answers1

1

The FileSystemWatcher class have a few events available. Changed, Created, Deleted, Error and Renamed. There is nothing there to indicate if the file was recently unlocked by the system. So there is no direct way to fire your event specifically after the file was closed.

What I would do personally to make that the files are moved properly would be to delay the move to X ms after the file creation event is fired.

Here's a summary of how that would work.

Prerequisites

  • A Filewatcher, which detects the file being created
  • A timer, which will move the files after their creation
  • A queue, on which the Filewatcher enqueue items being created and on which the timer dequeue items that need processing. It is what will communicate the information between the two.

Flow

  • A file is created
  • The Filewatcher created event trigger and add the item in the queue, then start the timer.
  • The timer elapsed event trigger, stop itself and process the queue. If any items remains in the queue, it restart itself.

Here's a base version of all of that.

$Params = @{
    # Path we are watching
    WatchPath         = 'C:\temp\11\test'
    # Path we are moving stuff to.
    DestinationPath   = 'C:\temp\11'
    # Stop after X attempts
    MoveAttempts      = 5
    # Timer heartbeat
    MoveAttemptsDelay = 1000 
}

# Create watcher
$Watcher = [System.IO.FileSystemWatcher]::new()
$Watcher.Path = $Params.WatchPath
$Watcher.EnableRaisingEvents = $true    

# Create Timer - stopped state
$WatchTimer = [System.Timers.Timer]::new()
$WatchTimer.Interval = 1000

#Create WatchQueue (Each items will be FullPath,Timestamp,MoveAttempts)
$WatchQueue = [System.Collections.Generic.Queue[psobject]]::new()

$FileWatcherReg = @{
    InputObject      = $Watcher
    EventName        = 'Created'
    SourceIdentifier = 'FileCreated'
    MessageData      = @{WatchQueue = $WatchQueue; Timer = $WatchTimer }
    Action           = {
        if ($null -ne $event) {
            $Queue = $Event.MessageData.WatchQueue
            $Timer = $Event.MessageData.Timer
            $Queue.Enqueue([PSCustomObject]@{
                    FullPath     = $Event.SourceArgs.FullPath
                    TimeStamp    = $Event.TimeGenerated
                    MoveAttempts = 0
                })
            
            # We only start the timer if it is not already counting down
            # We also don't want to start the timer if item is not 1 since this mean 
            # the timer logic is already running.
            if ($Queue.Count -eq 1 -and ! $Timer.Enabled) { $Timer.Start() }
        }
    }
   
}

$TimerReg = @{
    InputObject      = $WatchTimer
    EventName        = 'Elapsed'
    Sourceidentifier = 'WatchTimerElapsed'
    MessageData      = @{WatchQueue = $WatchQueue; ConfigParams = $Params }
    Action           = {
        $Queue = $Event.MessageData.WatchQueue
        $ConfigParams = $Event.MessageData.ConfigParams
        $Event.Sender.Stop()
        $SkipItemsCount = 0
        while ($Queue.Count -gt 0 + $SkipItemsCount ) {
            $Item = $Queue.Dequeue()
            $ItemName = Split-Path $item.FullPath -Leaf

            while ($Item.MoveAttempts -lt $ConfigParams.MoveAttempts) {
                try {
                    Write-Host 'test'
                    $Item.MoveAttempts += 1
                    Move-Item -Path $Item.FullPath -Destination "$($ConfigParams.DestinationPath)\$ItemName" -ErrorAction Stop
                    break
                }
                catch {
                    $ex = $_.Exception
                    if ( $Item.MoveAttempts -eq 5) {
                        # Do something about it... Log / Warn / etc...
                        Write-warning "Move attempts: $($ConfigParams.MoveAttempts)"
                        Write-Warning "FilePath: $($Item.FullPath)"
                        Write-Warning $ex
                        continue
                    }
                    else {
                        
                        $Queue.Enqueue($Item)
                        $SkipItemsCount += 1
                    }
                    
                }
            }
            # If we skipped any items, we don't want to dequeue until 0 anymore but rather we will stop
            if ($SkipItemsCount -gt 0){
                 $Event.Sender.Start()
            }
        }
    }

}

# ObjectEvent for FileWatcher
Register-ObjectEvent @FileWatcherReg 
# ObjectEvent for Timer which process stuff in a delayed fashion
Register-ObjectEvent @TimerReg

while ($true) {
    Start-Sleep -Milliseconds 100
}

# Unregister events at the end 
Unregister-Event -SourceIdentifier FileCreated | Out-Null # Will fail first time since never registered
Unregister-Event -SourceIdentifier WatchTimerElapsed | Out-Null
Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39