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"
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

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
}