diff --git a/CLAUDE.md b/CLAUDE.md index b09a9d6..d30a60f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ PowerShell utility that monitors DNC folders and removes invalid special charact - **Language:** PowerShell 5.1+ - **Deployment:** Network share (S:\DT\cameron\eDNC-Fix\) → Local (C:\eDNC-Fix\) -- **Logging:** ShopDB API (http://geitshopdb/api.asp) +- **Logging:** ShopDB API (https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp) - **Version Control:** Gitea (localhost:3000/cproudlock/edncfix) ## IMPORTANT: After Making Changes @@ -54,7 +54,7 @@ PowerShell utility that monitors DNC folders and removes invalid special charact | Watch Folder | `C:\Dnc_Files\Q` | eDNC-SpecialCharFix.ps1 | | File Filter | `*.pun` | eDNC-SpecialCharFix.ps1 | | Update Source | `S:\DT\cameron\eDNC-Fix` | eDNC-SpecialCharFix.ps1 | -| API URL | `http://geitshopdb/api.asp` | eDNC-SpecialCharFix.ps1 | +| API URL | `https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp` | eDNC-SpecialCharFix.ps1 | | Update Interval | 300 seconds (5 min) | eDNC-SpecialCharFix.ps1 | ## ShopDB API Integration @@ -67,7 +67,7 @@ The script logs events to ShopDB via the API. Reference the ShopDB API documenta ### API Endpoint: logDNCEvent **Method:** POST -**URL:** `http://geitshopdb/api.asp` +**URL:** `https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp` **Parameters:** | Parameter | Type | Required | Description | @@ -89,7 +89,7 @@ The script logs events to ShopDB via the API. Reference the ShopDB API documenta ### API Endpoint: getDNCStats **Method:** GET -**URL:** `http://geitshopdb/api.asp?action=getDNCStats` +**URL:** `https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp?action=getDNCStats` Returns JSON with all eDNC installations and their stats. diff --git a/Install-ScheduledTask.ps1 b/Install-ScheduledTask.ps1 index e80950b..2585c52 100644 --- a/Install-ScheduledTask.ps1 +++ b/Install-ScheduledTask.ps1 @@ -1,13 +1,13 @@ #Requires -RunAsAdministrator <# .SYNOPSIS - Installs eDNC Special Character Fix as a scheduled task that runs at startup with admin rights. + Installs eDNC Special Character Fix as a scheduled task that runs at user logon with visible UI. .DESCRIPTION Creates a Windows scheduled task that: - - Runs at system startup + - Runs at user logon (visible window for debugging) - Runs with highest privileges (Administrator) - - Runs whether user is logged on or not + - Shows the monitoring UI to the logged-in user - Automatically restarts if it stops Monitors: C:\Dnc_Files\Q for *.pun files @@ -29,7 +29,10 @@ param( ) $TaskName = "eDNC Special Character Fix" -$ScriptPath = Join-Path $PSScriptRoot "eDNC-SpecialCharFix.ps1" +# IMPORTANT: Always use the local installation path, NOT $PSScriptRoot +# This ensures the task runs from C:\eDNC-Fix even if installed from S:\ share +$LocalInstallPath = "C:\eDNC-Fix" +$ScriptPath = Join-Path $LocalInstallPath "eDNC-SpecialCharFix.ps1" if ($Uninstall) { Write-Host "Removing scheduled task '$TaskName'..." -ForegroundColor Yellow @@ -38,9 +41,18 @@ if ($Uninstall) { exit 0 } -# Verify script exists +# Verify local installation exists +if (-not (Test-Path $LocalInstallPath)) { + Write-Host "[ERROR] Local installation folder not found: $LocalInstallPath" -ForegroundColor Red + Write-Host "" + Write-Host "Please run Deploy.bat first to copy files to $LocalInstallPath" -ForegroundColor Yellow + exit 1 +} + if (-not (Test-Path $ScriptPath)) { Write-Host "[ERROR] Script not found: $ScriptPath" -ForegroundColor Red + Write-Host "" + Write-Host "Please run Deploy.bat first to copy files to $LocalInstallPath" -ForegroundColor Yellow exit 1 } @@ -61,14 +73,14 @@ if ($existingTask) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false } -# Build the PowerShell command -$Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`"" +# Build the PowerShell command (visible window for debugging) +$Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`"" # Create the scheduled task -$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $Arguments -WorkingDirectory $PSScriptRoot +$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $Arguments -WorkingDirectory $LocalInstallPath -# Trigger: At startup -$Trigger = New-ScheduledTaskTrigger -AtStartup +# Trigger: At user logon (shows UI to logged-in user) +$Trigger = New-ScheduledTaskTrigger -AtLogOn # Settings $Settings = New-ScheduledTaskSettingsSet ` @@ -79,8 +91,8 @@ $Settings = New-ScheduledTaskSettingsSet ` -RestartCount 3 ` -ExecutionTimeLimit (New-TimeSpan -Days 9999) -# Principal: Run as SYSTEM with highest privileges -$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest +# Principal: Run as the logged-in user with highest privileges (for visible UI) +$Principal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Users" -RunLevel Highest # Register the task $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings $Settings -Principal $Principal -Description "Monitors DNC folder and removes invalid special characters (0xFF) from program files." @@ -89,7 +101,8 @@ Register-ScheduledTask -TaskName $TaskName -InputObject $Task | Out-Null Write-Host "Scheduled task created successfully!" -ForegroundColor Green Write-Host "" -Write-Host "The task will start automatically at system boot." -ForegroundColor White +Write-Host "The task will start automatically when any user logs in." -ForegroundColor White +Write-Host "A visible PowerShell window will show the monitoring UI." -ForegroundColor White Write-Host "" Write-Host "To start it now, run:" -ForegroundColor Yellow Write-Host " Start-ScheduledTask -TaskName '$TaskName'" diff --git a/eDNC-SpecialCharFix.ps1 b/eDNC-SpecialCharFix.ps1 index 0a50b92..92dbc0d 100644 --- a/eDNC-SpecialCharFix.ps1 +++ b/eDNC-SpecialCharFix.ps1 @@ -36,9 +36,45 @@ .NOTES Author: GE Aerospace - Rutland - Version: 1.5.1 + Version: 1.6.0 Date: 2025-12-12 + v1.6.0 - Auto-update loop fix: + - Fixed update loop when running as standard user (can't write to C:\eDNC-Fix) + - Added -ErrorAction Stop to Copy-Item to properly catch permission errors + - Verify version.ini was actually updated before restarting + - Disable further update checks after failure to prevent retry loop + + v1.5.9 - Auto-update debugging: + - Check for updates immediately at startup (not just every 5 min) + - Added diagnostic output for update checks (shows source path, version comparison) + - Shows message when periodic update check runs + + v1.5.8 - Update logging and event handler fix: + - Log "updated" event to API when auto-update occurs + - Fixed event handler to use synchronized hashtable instead of $script: variables + (event handlers run in separate runspaces where $script: is not accessible) + + v1.5.7 - Improved file detection: + - Added Renamed event handler (some apps write temp then rename) + - Increased internal buffer to 64KB to prevent missed events + - Added debug logging for watcher configuration + + v1.5.6 - Added explicit NotifyFilter for better file detection + + v1.5.5 - Scheduled task now runs at logon with visible UI (for debugging) + + v1.5.4 - Updated API URL to production server (tsgwp00525.rd.ds.ge.com) + + v1.5.3 - PowerShell 5.1 compatibility fix: + - Replaced Join-String (PS 6+) with -join operator (PS 5.1 compatible) + + v1.5.2 - Headless mode support: + - Detects when running without console (scheduled task with -WindowStyle Hidden) + - Logs to %TEMP%\eDNC-SpecialCharFix.log instead of console + - Fixed Install-ScheduledTask.ps1 to always use C:\eDNC-Fix path + - Added 60-second startup delay for scheduled task + v1.5.1 - Startup warning if PC not registered in ShopDB v1.5.0 - API logging: - Logs events (started, cleaned, ok, failed, error, stopped) to ShopDB API @@ -83,23 +119,51 @@ param( ) # Script info -$ScriptVersion = "1.5.1" +$ScriptVersion = "1.6.0" $ScriptName = "eDNC Special Character Fix" +# Detect headless mode (no console window - e.g., running as scheduled task with -WindowStyle Hidden) +$script:HeadlessMode = $false +try { + $null = [Console]::WindowWidth +} catch { + $script:HeadlessMode = $true +} + +# Log file for headless mode +$script:LogFile = Join-Path $env:TEMP "eDNC-SpecialCharFix.log" + +# Helper function for logging (works in both modes) +function Write-Log { + param([string]$Message, [string]$Color = "White") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + if ($script:HeadlessMode) { + Add-Content -Path $script:LogFile -Value "[$timestamp] $Message" + } else { + Write-Host $Message -ForegroundColor $Color + } +} + # Auto-update settings $UpdateSource = "S:\DT\cameron\eDNC-Fix" $LocalPath = "C:\eDNC-Fix" $UpdateCheckInterval = 300 # Check every 5 minutes (in seconds) $script:LastUpdateCheck = [DateTime]::MinValue +$script:UpdateDisabled = $false # Set to true after update failure to prevent retry loop # API logging settings -$ApiUrl = "http://geitshopdb/api.asp" +$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp" $script:Hostname = $env:COMPUTERNAME -# Header display function +# Header display function (skipped in headless mode) function Show-Header { param([int]$Cleaned = 0, [int]$Failed = 0, [string]$Status = "Watching...") + # Skip fancy UI in headless mode + if ($script:HeadlessMode) { + return 0 + } + $headerLines = @( "" " ____ ____ " @@ -155,10 +219,16 @@ function Show-Header { # Auto-update function function Check-ForUpdate { + # Skip if updates were disabled (e.g., after permission failure) + if ($script:UpdateDisabled) { + return $false + } + $versionFile = Join-Path $UpdateSource "version.ini" # Skip if source not available if (-not (Test-Path $versionFile)) { + Write-Host " [UPDATE] Cannot access update source: $versionFile" -ForegroundColor DarkGray return $false } @@ -169,13 +239,18 @@ function Check-ForUpdate { $remoteVersion = [version]$matches[1] $localVersion = [version]$ScriptVersion + Write-Host " [UPDATE] Check: local=$localVersion, remote=$remoteVersion" -ForegroundColor DarkGray + if ($remoteVersion -gt $localVersion) { return $remoteVersion.ToString() } } + else { + Write-Host " [UPDATE] Could not parse version from: $versionFile" -ForegroundColor DarkGray + } } catch { - # Silently fail if can't read + Write-Host " [UPDATE] Error checking for update: $($_.Exception.Message)" -ForegroundColor DarkGray } return $false @@ -184,22 +259,37 @@ function Check-ForUpdate { function Invoke-Update { param([string]$NewVersion) + $OldVersion = $ScriptVersion Write-Host "" Write-Host " [UPDATE] New version $NewVersion available!" -ForegroundColor Magenta Write-Host " [UPDATE] Downloading update..." -ForegroundColor Magenta try { - # Copy new files + # Copy new files (use -ErrorAction Stop to catch permission errors) $files = @("eDNC-SpecialCharFix.ps1", "Run-eDNCFix.bat", "Install-ScheduledTask.ps1", "version.ini") foreach ($file in $files) { $src = Join-Path $UpdateSource $file $dst = Join-Path $LocalPath $file if (Test-Path $src) { - Copy-Item -Path $src -Destination $dst -Force + Copy-Item -Path $src -Destination $dst -Force -ErrorAction Stop + } + } + + # Verify update succeeded by checking local version.ini + $localVersionFile = Join-Path $LocalPath "version.ini" + $content = Get-Content $localVersionFile -Raw -ErrorAction Stop + if ($content -match 'Version=(\d+\.\d+\.\d+)') { + $updatedVersion = [version]$matches[1] + if ($updatedVersion -lt [version]$NewVersion) { + throw "Version file not updated (still at $updatedVersion)" } } Write-Host " [UPDATE] Update complete! Restarting..." -ForegroundColor Magenta + + # Log the update to API before restarting + Send-DNCEvent -EventType "updated" -Message "Updated from $OldVersion to $NewVersion" + Start-Sleep -Seconds 2 # Restart the script @@ -211,6 +301,9 @@ function Invoke-Update { } catch { Write-Host " [UPDATE] Update failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " [UPDATE] Continuing with current version (updates disabled)..." -ForegroundColor Yellow + # Disable further update checks to prevent retry loop + $script:UpdateDisabled = $true } } @@ -246,23 +339,36 @@ function Send-DNCEvent { } } -# Initialize console -Clear-Host -$script:HeaderHeight = Show-Header -Status "Initializing..." +# Initialize console (or log file in headless mode) +if (-not $script:HeadlessMode) { + Clear-Host + $script:HeaderHeight = Show-Header -Status "Initializing..." +} else { + Write-Log "=== $ScriptName v$ScriptVersion started (headless mode) ===" + Write-Log "Watch folder: $WatchFolder | Filter: $FileFilter" +} # Validate watch folder exists if (-not (Test-Path $WatchFolder)) { - Show-Header -Status "ERROR: Watch folder does not exist!" - [Console]::SetCursorPosition(0, $script:HeaderHeight + 1) - Write-Host "Please create the folder or specify a different path." -ForegroundColor Yellow + if (-not $script:HeadlessMode) { + Show-Header -Status "ERROR: Watch folder does not exist!" + [Console]::SetCursorPosition(0, $script:HeaderHeight + 1) + Write-Host "Please create the folder or specify a different path." -ForegroundColor Yellow + } else { + Write-Log "ERROR: Watch folder does not exist: $WatchFolder" + } exit 1 } # Update header with ready status -Show-Header -Status "Watching for files... (Ctrl+C to stop)" | Out-Null -[Console]::SetCursorPosition(0, $script:HeaderHeight + 1) -Write-Host "Removing bytes: $($CharactersToRemove -join ', ') (0x$($CharactersToRemove | ForEach-Object { '{0:X2}' -f $_ } | Join-String -Separator ', 0x'))" -ForegroundColor DarkGray -Write-Host "" +if (-not $script:HeadlessMode) { + Show-Header -Status "Watching for files... (Ctrl+C to stop)" | Out-Null + [Console]::SetCursorPosition(0, $script:HeaderHeight + 1) + Write-Host "Removing bytes: $($CharactersToRemove -join ', ') ($(($CharactersToRemove | ForEach-Object { '0x{0:X2}' -f $_ }) -join ', '))" -ForegroundColor DarkGray + Write-Host "" +} else { + Write-Log "Watching for files..." +} # Log startup to API and check if PC is registered $script:ApiEnabled = $true @@ -278,16 +384,14 @@ try { $json = $response.Content | ConvertFrom-Json if ($json.success -eq $false -and $json.error -match "Unknown hostname") { $script:ApiEnabled = $false - Write-Host "WARNING: This PC ($($script:Hostname)) is not registered in ShopDB." -ForegroundColor Yellow - Write-Host " Activity will NOT be logged. Add this PC to ShopDB to enable logging." -ForegroundColor Yellow - Write-Host "" + Write-Log "WARNING: This PC ($($script:Hostname)) is not registered in ShopDB." "Yellow" + Write-Log " Activity will NOT be logged. Add this PC to ShopDB to enable logging." "Yellow" } } catch { # API unreachable - continue without logging $script:ApiEnabled = $false - Write-Host "WARNING: Cannot reach ShopDB API. Activity will NOT be logged." -ForegroundColor Yellow - Write-Host "" + Write-Log "WARNING: Cannot reach ShopDB API. Activity will NOT be logged." "Yellow" } # Statistics @@ -304,175 +408,184 @@ $watcher = New-Object System.IO.FileSystemWatcher $watcher.Path = $WatchFolder $watcher.Filter = $FileFilter $watcher.IncludeSubdirectories = $IncludeSubfolders +# Increase buffer size to prevent missing events (default is 8KB, increase to 64KB) +$watcher.InternalBufferSize = 65536 +# Watch for all relevant changes: file creation, modification, renames, size changes +$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor ` + [System.IO.NotifyFilters]::LastWrite -bor ` + [System.IO.NotifyFilters]::Size -bor ` + [System.IO.NotifyFilters]::CreationTime $watcher.EnableRaisingEvents = $true -# Define the cleanup action +Write-Log "FileSystemWatcher configured:" "Gray" +Write-Log " Path: $WatchFolder" "Gray" +Write-Log " Filter: $FileFilter" "Gray" +Write-Log " Buffer: $($watcher.InternalBufferSize) bytes" "Gray" +Write-Log " EnableRaisingEvents: $($watcher.EnableRaisingEvents)" "Gray" + +# List existing files matching filter for verification +$existingFiles = Get-ChildItem -Path $WatchFolder -Filter $FileFilter -ErrorAction SilentlyContinue +Write-Log " Existing files matching filter: $($existingFiles.Count)" "Gray" +if ($existingFiles.Count -gt 0) { + $existingFiles | ForEach-Object { Write-Log " - $($_.Name)" "DarkGray" } +} + +# Define the cleanup action (simplified - no $script: vars, they don't work in event handlers) $action = { - $path = $Event.SourceEventArgs.FullPath - $changeType = $Event.SourceEventArgs.ChangeType - $fileName = Split-Path $path -Leaf + try { + $path = $Event.SourceEventArgs.FullPath + $changeType = $Event.SourceEventArgs.ChangeType + $fileName = Split-Path $path -Leaf + $data = $Event.MessageData - # Get parameters from message data - $charsToRemove = $Event.MessageData.CharsToRemove - $debounceSeconds = $Event.MessageData.DebounceSeconds - $statusLine = $Event.MessageData.StatusLine + # IMMEDIATE debug output to confirm event fired + Write-Host "" + Write-Host ">>> EVENT: $changeType - $fileName <<<" -ForegroundColor Cyan - # Helper to update status line in header - $updateStatus = { - param($msg, $color = "Yellow") - $savedPos = [Console]::CursorTop - [Console]::SetCursorPosition(0, $statusLine) - Write-Host (" Status: " + $msg).PadRight([Console]::WindowWidth - 1) -ForegroundColor $color - [Console]::SetCursorPosition(0, $savedPos) - } + # Get parameters from message data + $charsToRemove = $data.CharsToRemove - # Helper to update stats line in header - $updateStats = { - $savedPos = [Console]::CursorTop - [Console]::SetCursorPosition(0, 12) - Write-Host (" Filter: $($Event.MessageData.FileFilter) | Cleaned: $($script:FilesProcessed) | Failed: $($script:FailedFiles)").PadRight([Console]::WindowWidth - 1) -ForegroundColor White - [Console]::SetCursorPosition(0, $savedPos) - } - - # Helper to log to API - $logToApi = { - param($eventType, $file, $bytes, $msg) - try { - $body = @{ - action = "logDNCEvent" - hostname = $Event.MessageData.Hostname - filename = $file - eventType = $eventType - bytesRemoved = $bytes - version = $Event.MessageData.Version - message = $msg - watchFolder = $Event.MessageData.WatchFolder - fileFilter = $Event.MessageData.FileFilter - } - $null = Invoke-WebRequest -Uri $Event.MessageData.ApiUrl -Method POST -Body $body -ContentType "application/x-www-form-urlencoded" -UseBasicParsing -TimeoutSec 5 - } catch { } - } - - # 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 + # Debounce using synchronized hashtable + $now = Get-Date + $lastProcessed = $data.State["LastProcessed_$path"] + if ($lastProcessed -and (($now - $lastProcessed).TotalSeconds -lt $data.DebounceSeconds)) { + Write-Host " [SKIP] Debounced" -ForegroundColor DarkGray + return } - } - & $updateStatus "Processing: $fileName" "Yellow" - 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') | Processing $fileName..." -ForegroundColor White - # Brief initial delay to let eDNC finish (50ms) - Start-Sleep -Milliseconds 50 + # Brief initial delay to let eDNC finish (50ms) + Start-Sleep -Milliseconds 50 - # Try to acquire exclusive access - process immediately when available - $maxRetries = 15 - $retryCount = 0 - $baseDelay = 100 # Start with 100ms, doubles each retry (100, 200, 400, 800...) + # Try to acquire exclusive access + $maxRetries = 15 + $retryCount = 0 + $baseDelay = 100 - 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 - ) + while ($retryCount -lt $maxRetries) { + $fileStream = $null + try { + $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 + # Read all bytes + $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) + # Remove specified bytes + $cleaned = [System.Collections.Generic.List[byte]]::new() + foreach ($b in $bytes) { + if ($b -notin $charsToRemove) { + $cleaned.Add($b) + } + } + $newCount = $cleaned.Count + + if ($originalCount -ne $newCount) { + # Rewrite file + $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 + $data.State["FilesProcessed"] = ([int]$data.State["FilesProcessed"]) + 1 + $data.State["BytesRemoved"] = ([int]$data.State["BytesRemoved"]) + $removed + + # Log to API (fire and forget) + try { + $body = @{ action="logDNCEvent"; hostname=$data.Hostname; filename=$fileName; eventType="cleaned"; bytesRemoved=$removed; version=$data.Version; message="Removed $removed byte(s)" } + $null = Invoke-WebRequest -Uri $data.ApiUrl -Method POST -Body $body -UseBasicParsing -TimeoutSec 5 + } catch { } + } else { + Write-Host " [OK] No special characters found" -ForegroundColor Gray + } + + # Mark as processed + $data.State["LastProcessed_$path"] = Get-Date + break + } + catch [System.IO.IOException] { + $retryCount++ + if ($retryCount -lt $maxRetries) { + $delay = [math]::Min($baseDelay * [math]::Pow(2, $retryCount - 1), 16000) + Write-Host " [RETRY] File locked ($retryCount/$maxRetries)" -ForegroundColor Yellow + Start-Sleep -Milliseconds $delay + } else { + Write-Host " [FAILED] Could not access file after $maxRetries attempts" -ForegroundColor Red + $data.State["FailedFiles"] = ([int]$data.State["FailedFiles"]) + 1 } } - $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 - & $updateStatus "CLEANED: $fileName ($removed bytes)" "Green" - & $updateStats - & $logToApi "cleaned" $fileName $removed "Removed $removed byte(s)" - } else { - Write-Host " [OK] No special characters found" -ForegroundColor Gray - & $updateStatus "OK: $fileName (no changes)" "Gray" - & $logToApi "ok" $fileName 0 "No changes needed" + catch { + Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red + $data.State["FailedFiles"] = ([int]$data.State["FailedFiles"]) + 1 + break } - - # Mark as recently processed - $script:RecentlyProcessed[$path] = Get-Date - break - } - catch [System.IO.IOException] { - $retryCount++ - if ($retryCount -lt $maxRetries) { - # Exponential backoff: 100ms, 200ms, 400ms, 800ms... - $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 - & $updateStatus "RETRY: $fileName ($retryCount/$maxRetries)" "Yellow" - Start-Sleep -Milliseconds $delay - } else { - Write-Host " [FAILED] Could not access file after $maxRetries attempts" -ForegroundColor Red - $script:FailedFiles++ - & $updateStatus "FAILED: $fileName" "Red" - & $updateStats - & $logToApi "failed" $fileName 0 "Could not access file after $maxRetries attempts" - } - } - catch { - Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red - $script:FailedFiles++ - & $updateStatus "ERROR: $fileName" "Red" - & $updateStats - & $logToApi "error" $fileName 0 $_.Exception.Message - break - } - finally { - if ($fileStream) { - $fileStream.Close() - $fileStream.Dispose() + finally { + if ($fileStream) { + $fileStream.Close() + $fileStream.Dispose() + } } } } + catch { + Write-Host " [ACTION ERROR] $($_.Exception.Message)" -ForegroundColor Red + } } +# Synchronized state hashtable (accessible from event handlers) +$script:SharedState = [hashtable]::Synchronized(@{ + FilesProcessed = 0 + BytesRemoved = 0 + FailedFiles = 0 +}) + # Create message data object to pass parameters to the action $messageData = @{ CharsToRemove = $CharactersToRemove - DebounceSeconds = $script:DebounceSeconds - StatusLine = 14 # Line number for status updates + DebounceSeconds = 5 FileFilter = $FileFilter ApiUrl = $ApiUrl Hostname = $script:Hostname Version = $ScriptVersion WatchFolder = $WatchFolder + State = $script:SharedState } # 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 +$null = Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -MessageData $messageData + +# Error handler for watcher issues +$errorAction = { + Write-Host " [WATCHER ERROR] $($Event.SourceEventArgs.GetException().Message)" -ForegroundColor Red +} +$null = Register-ObjectEvent -InputObject $watcher -EventName Error -Action $errorAction + +Write-Log "Event handlers registered: Created, Changed, Renamed, Error" "Green" +Write-Log "" + +# Immediate update check at startup +Write-Log "Checking for updates from: $UpdateSource" "DarkGray" +$newVersion = Check-ForUpdate +if ($newVersion) { + Invoke-Update -NewVersion $newVersion +} +$script:LastUpdateCheck = Get-Date + +Write-Log "" +Write-Log "Waiting for file events... (try copying a .pun file to test)" "Yellow" # Keep script running try { @@ -483,6 +596,7 @@ try { $now = Get-Date if (($now - $script:LastUpdateCheck).TotalSeconds -ge $UpdateCheckInterval) { $script:LastUpdateCheck = $now + Write-Host " [UPDATE] Periodic check..." -ForegroundColor DarkGray $newVersion = Check-ForUpdate if ($newVersion) { Invoke-Update -NewVersion $newVersion diff --git a/version.ini b/version.ini index f2ddb68..65654fc 100644 --- a/version.ini +++ b/version.ini @@ -1,2 +1,2 @@ [eDNC-Fix] -Version=1.5.1 +Version=1.6.0