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

@@ -111,18 +111,25 @@ Watching for files... Press Ctrl+C to stop
### "Watch folder does not exist" ### "Watch folder does not exist"
Create the folder or specify a different path with `-WatchFolder`. Create the folder or specify a different path with `-WatchFolder`.
### "File locked" errors ### "File locked" errors (v1.0.0)
The script retries up to 3 times if a file is locked. If errors persist, the file may be in use by another application. 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 ### Script doesn't detect files
- Verify the file extension matches `-FileFilter` - Verify the file extension matches `-FileFilter`
- Check that `-IncludeSubfolders` is set correctly - Check that `-IncludeSubfolders` is set correctly
- Ensure the script has permissions to the folder - 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 History
| Version | Date | Changes | | 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 | | 1.0.0 | 2025-12-12 | Initial release |
## Author ## Author

View File

@@ -36,9 +36,15 @@
.NOTES .NOTES
Author: GE Aerospace - Rutland Author: GE Aerospace - Rutland
Version: 1.0.0 Version: 1.1.0
Date: 2025-12-12 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: Common problematic characters:
- 0xFF (255) - Padding/fill character - 0xFF (255) - Padding/fill character
- 0x00 (0) - NULL character - 0x00 (0) - NULL character
@@ -62,7 +68,7 @@ param(
) )
# Script info # Script info
$ScriptVersion = "1.0.0" $ScriptVersion = "1.1.0"
$ScriptName = "eDNC Special Character Fix" $ScriptName = "eDNC Special Character Fix"
# Display banner # Display banner
@@ -92,6 +98,11 @@ Write-Host ""
# Statistics # Statistics
$script:FilesProcessed = 0 $script:FilesProcessed = 0
$script:BytesRemoved = 0 $script:BytesRemoved = 0
$script:FailedFiles = 0
# Debounce tracking - prevent processing same file multiple times
$script:RecentlyProcessed = @{}
$script:DebounceSeconds = 5
# Create file system watcher # Create file system watcher
$watcher = New-Object System.IO.FileSystemWatcher $watcher = New-Object System.IO.FileSystemWatcher
@@ -106,31 +117,89 @@ $action = {
$changeType = $Event.SourceEventArgs.ChangeType $changeType = $Event.SourceEventArgs.ChangeType
$fileName = Split-Path $path -Leaf $fileName = Split-Path $path -Leaf
# Get characters to remove from the outer scope # Get parameters from message data
$charsToRemove = $Event.MessageData.CharsToRemove $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 Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | $changeType | $fileName" -ForegroundColor White
# Wait for file to finish writing # STEP 1: Wait for file to stabilize (size stops changing)
Start-Sleep -Milliseconds 500 $stableChecks = 0
$lastSize = -1
$maxStableWait = 30 # Max 30 seconds waiting for file to stabilize
# Retry logic for locked files while ($stableChecks -lt $maxStableWait) {
$maxRetries = 3 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 $retryCount = 0
$baseDelay = 500 # Start with 500ms
while ($retryCount -lt $maxRetries) { while ($retryCount -lt $maxRetries) {
$fileStream = $null
try { try {
# Read file as bytes # Try to open file with exclusive access - this confirms it's available
$bytes = [System.IO.File]::ReadAllBytes($path) $fileStream = [System.IO.File]::Open(
$originalCount = $bytes.Count $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 # 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 $newCount = $cleaned.Count
# Only rewrite if we found characters to remove # Only rewrite if we found characters to remove
if ($originalCount -ne $newCount) { 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 $removed = $originalCount - $newCount
Write-Host " [CLEANED] Removed $removed byte(s)" -ForegroundColor Green Write-Host " [CLEANED] Removed $removed byte(s)" -ForegroundColor Green
$script:FilesProcessed++ $script:FilesProcessed++
@@ -138,27 +207,41 @@ $action = {
} else { } else {
Write-Host " [OK] No special characters found" -ForegroundColor Gray Write-Host " [OK] No special characters found" -ForegroundColor Gray
} }
# Mark as recently processed
$script:RecentlyProcessed[$path] = Get-Date
break break
} }
catch [System.IO.IOException] { catch [System.IO.IOException] {
$retryCount++ $retryCount++
if ($retryCount -lt $maxRetries) { if ($retryCount -lt $maxRetries) {
Write-Host " [RETRY] File locked, waiting... ($retryCount/$maxRetries)" -ForegroundColor Yellow # Exponential backoff: 500ms, 1s, 2s, 4s, 8s, 16s...
Start-Sleep -Milliseconds 1000 $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 { } 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 { catch {
Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red
$script:FailedFiles++
break break
} }
finally {
if ($fileStream) {
$fileStream.Close()
$fileStream.Dispose()
}
}
} }
} }
# Create message data object to pass parameters to the action # Create message data object to pass parameters to the action
$messageData = @{ $messageData = @{
CharsToRemove = $CharactersToRemove CharsToRemove = $CharactersToRemove
DebounceSeconds = $script:DebounceSeconds
} }
# Register event handlers # Register event handlers
@@ -183,6 +266,9 @@ finally {
Write-Host "========================================" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Files Cleaned: $script:FilesProcessed" Write-Host " Files Cleaned: $script:FilesProcessed"
Write-Host " Bytes Removed: $script:BytesRemoved" Write-Host " Bytes Removed: $script:BytesRemoved"
if ($script:FailedFiles -gt 0) {
Write-Host " Failed Files: $script:FailedFiles" -ForegroundColor Red
}
Write-Host "" Write-Host ""
Write-Host "Stopped watching folder" -ForegroundColor Yellow Write-Host "Stopped watching folder" -ForegroundColor Yellow
} }