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:
11
README.md
11
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user