v1.1.0: Improved file locking handling for eDNC transfers

- Wait for file to stabilize (size stops changing) before processing
- Exponential backoff retry: 500ms -> 1s -> 2s -> up to 16s
- Up to 10 retry attempts (was 3)
- Debouncing to prevent duplicate processing of same file
- Use exclusive file lock during read/write for atomic operations
- Track failed file count in session summary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2025-12-12 08:33:29 -05:00
parent dc80aceafb
commit dbcff19b27
2 changed files with 110 additions and 17 deletions

View File

@@ -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
}