diff --git a/README.md b/README.md index ae0faa0..f4664df 100644 --- a/README.md +++ b/README.md @@ -111,18 +111,25 @@ Watching for files... Press Ctrl+C to stop ### "Watch folder does not exist" Create the folder or specify a different path with `-WatchFolder`. -### "File locked" errors -The script retries up to 3 times if a file is locked. If errors persist, the file may be in use by another application. +### "File locked" errors (v1.0.0) +If using v1.0.0, update to v1.1.0 for improved file locking handling with exponential backoff and stability checks. ### Script doesn't detect files - Verify the file extension matches `-FileFilter` - Check that `-IncludeSubfolders` is set correctly - Ensure the script has permissions to the folder +### "[WAIT] File still being written..." +This is normal behavior. The script waits for the file size to stabilize before processing, which means eDNC is still transferring the file. The script will automatically process it once the transfer completes. + +### "[RETRY] File locked..." +The script uses exponential backoff (500ms → 1s → 2s → 4s → up to 16s) for up to 10 attempts. If you see `[FAILED]` messages, eDNC may be holding the file longer than expected. + ## Version History | Version | Date | Changes | |---------|------|---------| +| 1.1.0 | 2025-12-12 | Improved file locking: stability check, exponential backoff, debouncing | | 1.0.0 | 2025-12-12 | Initial release | ## Author diff --git a/eDNC-SpecialCharFix.ps1 b/eDNC-SpecialCharFix.ps1 index 27324e3..d8a6fd7 100644 --- a/eDNC-SpecialCharFix.ps1 +++ b/eDNC-SpecialCharFix.ps1 @@ -36,9 +36,15 @@ .NOTES Author: GE Aerospace - Rutland - Version: 1.0.0 + Version: 1.1.0 Date: 2025-12-12 + v1.1.0 - Improved file locking handling: + - Wait for file to stabilize (size stops changing) + - Exponential backoff retry (500ms -> 16s) + - Up to 10 retry attempts + - Debouncing to prevent duplicate processing + Common problematic characters: - 0xFF (255) - Padding/fill character - 0x00 (0) - NULL character @@ -62,7 +68,7 @@ param( ) # Script info -$ScriptVersion = "1.0.0" +$ScriptVersion = "1.1.0" $ScriptName = "eDNC Special Character Fix" # Display banner @@ -92,6 +98,11 @@ Write-Host "" # Statistics $script:FilesProcessed = 0 $script:BytesRemoved = 0 +$script:FailedFiles = 0 + +# Debounce tracking - prevent processing same file multiple times +$script:RecentlyProcessed = @{} +$script:DebounceSeconds = 5 # Create file system watcher $watcher = New-Object System.IO.FileSystemWatcher @@ -106,31 +117,89 @@ $action = { $changeType = $Event.SourceEventArgs.ChangeType $fileName = Split-Path $path -Leaf - # Get characters to remove from the outer scope + # Get parameters from message data $charsToRemove = $Event.MessageData.CharsToRemove + $debounceSeconds = $Event.MessageData.DebounceSeconds + + # Debounce check - skip if we processed this file recently + $now = Get-Date + if ($script:RecentlyProcessed.ContainsKey($path)) { + $lastProcessed = $script:RecentlyProcessed[$path] + if (($now - $lastProcessed).TotalSeconds -lt $debounceSeconds) { + return # Skip duplicate event + } + } Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | $changeType | $fileName" -ForegroundColor White - # Wait for file to finish writing - Start-Sleep -Milliseconds 500 + # STEP 1: Wait for file to stabilize (size stops changing) + $stableChecks = 0 + $lastSize = -1 + $maxStableWait = 30 # Max 30 seconds waiting for file to stabilize - # Retry logic for locked files - $maxRetries = 3 + while ($stableChecks -lt $maxStableWait) { + Start-Sleep -Milliseconds 500 + try { + if (-not (Test-Path $path)) { + Write-Host " [SKIP] File no longer exists" -ForegroundColor Yellow + return + } + $currentSize = (Get-Item $path).Length + if ($currentSize -eq $lastSize -and $currentSize -gt 0) { + # File size hasn't changed - likely done writing + break + } + $lastSize = $currentSize + $stableChecks++ + if ($stableChecks % 4 -eq 0) { + Write-Host " [WAIT] File still being written... ($([math]::Round($stableChecks/2))s)" -ForegroundColor DarkGray + } + } + catch { + $stableChecks++ + } + } + + # STEP 2: Try to acquire exclusive access with exponential backoff + $maxRetries = 10 $retryCount = 0 + $baseDelay = 500 # Start with 500ms while ($retryCount -lt $maxRetries) { + $fileStream = $null try { - # Read file as bytes - $bytes = [System.IO.File]::ReadAllBytes($path) - $originalCount = $bytes.Count + # Try to open file with exclusive access - this confirms it's available + $fileStream = [System.IO.File]::Open( + $path, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::None + ) + + # Read all bytes while we have exclusive lock + $fileStream.Position = 0 + $bytes = New-Object byte[] $fileStream.Length + $null = $fileStream.Read($bytes, 0, $bytes.Length) + $originalCount = $bytes.Length # Remove specified bytes - $cleaned = $bytes | Where-Object { $_ -notin $charsToRemove } + $cleaned = [System.Collections.Generic.List[byte]]::new() + foreach ($b in $bytes) { + if ($b -notin $charsToRemove) { + $cleaned.Add($b) + } + } $newCount = $cleaned.Count # Only rewrite if we found characters to remove if ($originalCount -ne $newCount) { - [System.IO.File]::WriteAllBytes($path, [byte[]]$cleaned) + # Truncate and rewrite + $fileStream.Position = 0 + $fileStream.SetLength(0) + $cleanedArray = $cleaned.ToArray() + $fileStream.Write($cleanedArray, 0, $cleanedArray.Length) + $fileStream.Flush() + $removed = $originalCount - $newCount Write-Host " [CLEANED] Removed $removed byte(s)" -ForegroundColor Green $script:FilesProcessed++ @@ -138,27 +207,41 @@ $action = { } else { Write-Host " [OK] No special characters found" -ForegroundColor Gray } + + # Mark as recently processed + $script:RecentlyProcessed[$path] = Get-Date break } catch [System.IO.IOException] { $retryCount++ if ($retryCount -lt $maxRetries) { - Write-Host " [RETRY] File locked, waiting... ($retryCount/$maxRetries)" -ForegroundColor Yellow - Start-Sleep -Milliseconds 1000 + # Exponential backoff: 500ms, 1s, 2s, 4s, 8s, 16s... + $delay = [math]::Min($baseDelay * [math]::Pow(2, $retryCount - 1), 16000) + Write-Host " [RETRY] File locked, waiting $([math]::Round($delay/1000, 1))s... ($retryCount/$maxRetries)" -ForegroundColor Yellow + Start-Sleep -Milliseconds $delay } else { - Write-Host " [ERROR] Could not access file after $maxRetries attempts" -ForegroundColor Red + Write-Host " [FAILED] Could not access file after $maxRetries attempts" -ForegroundColor Red + $script:FailedFiles++ } } catch { Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red + $script:FailedFiles++ break } + finally { + if ($fileStream) { + $fileStream.Close() + $fileStream.Dispose() + } + } } } # Create message data object to pass parameters to the action $messageData = @{ CharsToRemove = $CharactersToRemove + DebounceSeconds = $script:DebounceSeconds } # Register event handlers @@ -183,6 +266,9 @@ finally { Write-Host "========================================" -ForegroundColor Cyan Write-Host " Files Cleaned: $script:FilesProcessed" Write-Host " Bytes Removed: $script:BytesRemoved" + if ($script:FailedFiles -gt 0) { + Write-Host " Failed Files: $script:FailedFiles" -ForegroundColor Red + } Write-Host "" Write-Host "Stopped watching folder" -ForegroundColor Yellow }