Files
edncfix/eDNC-SpecialCharFix.ps1
cproudlock dbcff19b27 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>
2025-12-12 08:33:29 -05:00

275 lines
9.5 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
eDNC Special Character Fix - Real-time file watcher to strip invalid characters from DNC files.
.DESCRIPTION
Monitors a specified folder for DNC program files (.pun, .nc, etc.) and automatically
removes special characters (0xFF and others) that cause issues with CNC machines.
Some legacy DNC systems and communication protocols add padding or termination bytes
that modern CNC controllers cannot process correctly.
.PARAMETER WatchFolder
The folder to monitor for DNC files. Default: C:\Dnc_Files\Q
.PARAMETER FileFilter
File pattern to watch. Default: *.pun
.PARAMETER IncludeSubfolders
Whether to watch subfolders. Default: $true
.PARAMETER CharactersToRemove
Array of byte values to strip from files. Default: @(255) for 0xFF
.EXAMPLE
.\eDNC-SpecialCharFix.ps1
Runs with default settings, watching C:\Dnc_Files\Q for *.pun files
.EXAMPLE
.\eDNC-SpecialCharFix.ps1 -WatchFolder "D:\DNC\Programs" -FileFilter "*.nc"
Watches D:\DNC\Programs for .nc files
.EXAMPLE
.\eDNC-SpecialCharFix.ps1 -CharactersToRemove @(255, 0, 26)
Removes 0xFF, NULL, and SUB (Ctrl+Z) characters
.NOTES
Author: GE Aerospace - Rutland
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
- 0x1A (26) - SUB/Ctrl+Z (EOF marker in some systems)
- 0x7F (127) - DEL character
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$WatchFolder = "C:\Dnc_Files\Q",
[Parameter()]
[string]$FileFilter = "*.pun",
[Parameter()]
[bool]$IncludeSubfolders = $true,
[Parameter()]
[int[]]$CharactersToRemove = @(255)
)
# Script info
$ScriptVersion = "1.1.0"
$ScriptName = "eDNC Special Character Fix"
# Display banner
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " $ScriptName v$ScriptVersion" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Validate watch folder exists
if (-not (Test-Path $WatchFolder)) {
Write-Host "[ERROR] Watch folder does not exist: $WatchFolder" -ForegroundColor Red
Write-Host "Please create the folder or specify a different path." -ForegroundColor Yellow
exit 1
}
# Display configuration
Write-Host "Configuration:" -ForegroundColor Yellow
Write-Host " Watch Folder: $WatchFolder"
Write-Host " File Filter: $FileFilter"
Write-Host " Subfolders: $IncludeSubfolders"
Write-Host " Removing bytes: $($CharactersToRemove -join ', ') (0x$($CharactersToRemove | ForEach-Object { '{0:X2}' -f $_ } | Join-String -Separator ', 0x'))"
Write-Host ""
Write-Host "Watching for files... Press Ctrl+C to stop" -ForegroundColor Green
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
$watcher.Path = $WatchFolder
$watcher.Filter = $FileFilter
$watcher.IncludeSubdirectories = $IncludeSubfolders
$watcher.EnableRaisingEvents = $true
# Define the cleanup action
$action = {
$path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$fileName = Split-Path $path -Leaf
# 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
# STEP 1: Wait for file to stabilize (size stops changing)
$stableChecks = 0
$lastSize = -1
$maxStableWait = 30 # Max 30 seconds waiting for file to stabilize
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 {
# 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 = [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) {
# 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++
$script:BytesRemoved += $removed
} 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) {
# 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 " [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
$null = Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -MessageData $messageData
$null = Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -MessageData $messageData
# Keep script running
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
# Cleanup on exit
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Get-EventSubscriber | Unregister-Event
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Session Summary" -ForegroundColor Cyan
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
}