v1.6.0: Switch to user-logon task, migrate API to tsgwp00525 FQDN
- Install-ScheduledTask.ps1: run at user logon with visible UI (was startup with no UI), hardcode install path to C:\eDNC-Fix - Migrate ShopDB API URL from http://geitshopdb/api.asp to https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp in code and docs - eDNC-SpecialCharFix.ps1: ~250 line rework (see diff) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ PowerShell utility that monitors DNC folders and removes invalid special charact
|
|||||||
|
|
||||||
- **Language:** PowerShell 5.1+
|
- **Language:** PowerShell 5.1+
|
||||||
- **Deployment:** Network share (S:\DT\cameron\eDNC-Fix\) → Local (C:\eDNC-Fix\)
|
- **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)
|
- **Version Control:** Gitea (localhost:3000/cproudlock/edncfix)
|
||||||
|
|
||||||
## IMPORTANT: After Making Changes
|
## 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 |
|
| Watch Folder | `C:\Dnc_Files\Q` | eDNC-SpecialCharFix.ps1 |
|
||||||
| File Filter | `*.pun` | eDNC-SpecialCharFix.ps1 |
|
| File Filter | `*.pun` | eDNC-SpecialCharFix.ps1 |
|
||||||
| Update Source | `S:\DT\cameron\eDNC-Fix` | 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 |
|
| Update Interval | 300 seconds (5 min) | eDNC-SpecialCharFix.ps1 |
|
||||||
|
|
||||||
## ShopDB API Integration
|
## ShopDB API Integration
|
||||||
@@ -67,7 +67,7 @@ The script logs events to ShopDB via the API. Reference the ShopDB API documenta
|
|||||||
### API Endpoint: logDNCEvent
|
### API Endpoint: logDNCEvent
|
||||||
|
|
||||||
**Method:** POST
|
**Method:** POST
|
||||||
**URL:** `http://geitshopdb/api.asp`
|
**URL:** `https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp`
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
| Parameter | Type | Required | Description |
|
| 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
|
### API Endpoint: getDNCStats
|
||||||
|
|
||||||
**Method:** GET
|
**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.
|
Returns JSON with all eDNC installations and their stats.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#Requires -RunAsAdministrator
|
#Requires -RunAsAdministrator
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.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
|
.DESCRIPTION
|
||||||
Creates a Windows scheduled task that:
|
Creates a Windows scheduled task that:
|
||||||
- Runs at system startup
|
- Runs at user logon (visible window for debugging)
|
||||||
- Runs with highest privileges (Administrator)
|
- 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
|
- Automatically restarts if it stops
|
||||||
|
|
||||||
Monitors: C:\Dnc_Files\Q for *.pun files
|
Monitors: C:\Dnc_Files\Q for *.pun files
|
||||||
@@ -29,7 +29,10 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$TaskName = "eDNC Special Character Fix"
|
$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) {
|
if ($Uninstall) {
|
||||||
Write-Host "Removing scheduled task '$TaskName'..." -ForegroundColor Yellow
|
Write-Host "Removing scheduled task '$TaskName'..." -ForegroundColor Yellow
|
||||||
@@ -38,9 +41,18 @@ if ($Uninstall) {
|
|||||||
exit 0
|
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)) {
|
if (-not (Test-Path $ScriptPath)) {
|
||||||
Write-Host "[ERROR] Script not found: $ScriptPath" -ForegroundColor Red
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +73,14 @@ if ($existingTask) {
|
|||||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build the PowerShell command
|
# Build the PowerShell command (visible window for debugging)
|
||||||
$Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
|
$Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
|
||||||
|
|
||||||
# Create the scheduled task
|
# 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: At user logon (shows UI to logged-in user)
|
||||||
$Trigger = New-ScheduledTaskTrigger -AtStartup
|
$Trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
$Settings = New-ScheduledTaskSettingsSet `
|
$Settings = New-ScheduledTaskSettingsSet `
|
||||||
@@ -79,8 +91,8 @@ $Settings = New-ScheduledTaskSettingsSet `
|
|||||||
-RestartCount 3 `
|
-RestartCount 3 `
|
||||||
-ExecutionTimeLimit (New-TimeSpan -Days 9999)
|
-ExecutionTimeLimit (New-TimeSpan -Days 9999)
|
||||||
|
|
||||||
# Principal: Run as SYSTEM with highest privileges
|
# Principal: Run as the logged-in user with highest privileges (for visible UI)
|
||||||
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
$Principal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Users" -RunLevel Highest
|
||||||
|
|
||||||
# Register the task
|
# 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."
|
$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 "Scheduled task created successfully!" -ForegroundColor Green
|
||||||
Write-Host ""
|
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 ""
|
||||||
Write-Host "To start it now, run:" -ForegroundColor Yellow
|
Write-Host "To start it now, run:" -ForegroundColor Yellow
|
||||||
Write-Host " Start-ScheduledTask -TaskName '$TaskName'"
|
Write-Host " Start-ScheduledTask -TaskName '$TaskName'"
|
||||||
|
|||||||
@@ -36,9 +36,45 @@
|
|||||||
|
|
||||||
.NOTES
|
.NOTES
|
||||||
Author: GE Aerospace - Rutland
|
Author: GE Aerospace - Rutland
|
||||||
Version: 1.5.1
|
Version: 1.6.0
|
||||||
Date: 2025-12-12
|
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.1 - Startup warning if PC not registered in ShopDB
|
||||||
v1.5.0 - API logging:
|
v1.5.0 - API logging:
|
||||||
- Logs events (started, cleaned, ok, failed, error, stopped) to ShopDB API
|
- Logs events (started, cleaned, ok, failed, error, stopped) to ShopDB API
|
||||||
@@ -83,23 +119,51 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Script info
|
# Script info
|
||||||
$ScriptVersion = "1.5.1"
|
$ScriptVersion = "1.6.0"
|
||||||
$ScriptName = "eDNC Special Character Fix"
|
$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
|
# Auto-update settings
|
||||||
$UpdateSource = "S:\DT\cameron\eDNC-Fix"
|
$UpdateSource = "S:\DT\cameron\eDNC-Fix"
|
||||||
$LocalPath = "C:\eDNC-Fix"
|
$LocalPath = "C:\eDNC-Fix"
|
||||||
$UpdateCheckInterval = 300 # Check every 5 minutes (in seconds)
|
$UpdateCheckInterval = 300 # Check every 5 minutes (in seconds)
|
||||||
$script:LastUpdateCheck = [DateTime]::MinValue
|
$script:LastUpdateCheck = [DateTime]::MinValue
|
||||||
|
$script:UpdateDisabled = $false # Set to true after update failure to prevent retry loop
|
||||||
|
|
||||||
# API logging settings
|
# API logging settings
|
||||||
$ApiUrl = "http://geitshopdb/api.asp"
|
$ApiUrl = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp"
|
||||||
$script:Hostname = $env:COMPUTERNAME
|
$script:Hostname = $env:COMPUTERNAME
|
||||||
|
|
||||||
# Header display function
|
# Header display function (skipped in headless mode)
|
||||||
function Show-Header {
|
function Show-Header {
|
||||||
param([int]$Cleaned = 0, [int]$Failed = 0, [string]$Status = "Watching...")
|
param([int]$Cleaned = 0, [int]$Failed = 0, [string]$Status = "Watching...")
|
||||||
|
|
||||||
|
# Skip fancy UI in headless mode
|
||||||
|
if ($script:HeadlessMode) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
$headerLines = @(
|
$headerLines = @(
|
||||||
""
|
""
|
||||||
" ____ ____ "
|
" ____ ____ "
|
||||||
@@ -155,10 +219,16 @@ function Show-Header {
|
|||||||
|
|
||||||
# Auto-update function
|
# Auto-update function
|
||||||
function Check-ForUpdate {
|
function Check-ForUpdate {
|
||||||
|
# Skip if updates were disabled (e.g., after permission failure)
|
||||||
|
if ($script:UpdateDisabled) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
$versionFile = Join-Path $UpdateSource "version.ini"
|
$versionFile = Join-Path $UpdateSource "version.ini"
|
||||||
|
|
||||||
# Skip if source not available
|
# Skip if source not available
|
||||||
if (-not (Test-Path $versionFile)) {
|
if (-not (Test-Path $versionFile)) {
|
||||||
|
Write-Host " [UPDATE] Cannot access update source: $versionFile" -ForegroundColor DarkGray
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +239,18 @@ function Check-ForUpdate {
|
|||||||
$remoteVersion = [version]$matches[1]
|
$remoteVersion = [version]$matches[1]
|
||||||
$localVersion = [version]$ScriptVersion
|
$localVersion = [version]$ScriptVersion
|
||||||
|
|
||||||
|
Write-Host " [UPDATE] Check: local=$localVersion, remote=$remoteVersion" -ForegroundColor DarkGray
|
||||||
|
|
||||||
if ($remoteVersion -gt $localVersion) {
|
if ($remoteVersion -gt $localVersion) {
|
||||||
return $remoteVersion.ToString()
|
return $remoteVersion.ToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Write-Host " [UPDATE] Could not parse version from: $versionFile" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# Silently fail if can't read
|
Write-Host " [UPDATE] Error checking for update: $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||||
}
|
}
|
||||||
|
|
||||||
return $false
|
return $false
|
||||||
@@ -184,22 +259,37 @@ function Check-ForUpdate {
|
|||||||
function Invoke-Update {
|
function Invoke-Update {
|
||||||
param([string]$NewVersion)
|
param([string]$NewVersion)
|
||||||
|
|
||||||
|
$OldVersion = $ScriptVersion
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " [UPDATE] New version $NewVersion available!" -ForegroundColor Magenta
|
Write-Host " [UPDATE] New version $NewVersion available!" -ForegroundColor Magenta
|
||||||
Write-Host " [UPDATE] Downloading update..." -ForegroundColor Magenta
|
Write-Host " [UPDATE] Downloading update..." -ForegroundColor Magenta
|
||||||
|
|
||||||
try {
|
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")
|
$files = @("eDNC-SpecialCharFix.ps1", "Run-eDNCFix.bat", "Install-ScheduledTask.ps1", "version.ini")
|
||||||
foreach ($file in $files) {
|
foreach ($file in $files) {
|
||||||
$src = Join-Path $UpdateSource $file
|
$src = Join-Path $UpdateSource $file
|
||||||
$dst = Join-Path $LocalPath $file
|
$dst = Join-Path $LocalPath $file
|
||||||
if (Test-Path $src) {
|
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
|
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
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
# Restart the script
|
# Restart the script
|
||||||
@@ -211,6 +301,9 @@ function Invoke-Update {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Host " [UPDATE] Update failed: $($_.Exception.Message)" -ForegroundColor Red
|
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
|
# Initialize console (or log file in headless mode)
|
||||||
Clear-Host
|
if (-not $script:HeadlessMode) {
|
||||||
$script:HeaderHeight = Show-Header -Status "Initializing..."
|
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
|
# Validate watch folder exists
|
||||||
if (-not (Test-Path $WatchFolder)) {
|
if (-not (Test-Path $WatchFolder)) {
|
||||||
Show-Header -Status "ERROR: Watch folder does not exist!"
|
if (-not $script:HeadlessMode) {
|
||||||
[Console]::SetCursorPosition(0, $script:HeaderHeight + 1)
|
Show-Header -Status "ERROR: Watch folder does not exist!"
|
||||||
Write-Host "Please create the folder or specify a different path." -ForegroundColor Yellow
|
[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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update header with ready status
|
# Update header with ready status
|
||||||
Show-Header -Status "Watching for files... (Ctrl+C to stop)" | Out-Null
|
if (-not $script:HeadlessMode) {
|
||||||
[Console]::SetCursorPosition(0, $script:HeaderHeight + 1)
|
Show-Header -Status "Watching for files... (Ctrl+C to stop)" | Out-Null
|
||||||
Write-Host "Removing bytes: $($CharactersToRemove -join ', ') (0x$($CharactersToRemove | ForEach-Object { '{0:X2}' -f $_ } | Join-String -Separator ', 0x'))" -ForegroundColor DarkGray
|
[Console]::SetCursorPosition(0, $script:HeaderHeight + 1)
|
||||||
Write-Host ""
|
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
|
# Log startup to API and check if PC is registered
|
||||||
$script:ApiEnabled = $true
|
$script:ApiEnabled = $true
|
||||||
@@ -278,16 +384,14 @@ try {
|
|||||||
$json = $response.Content | ConvertFrom-Json
|
$json = $response.Content | ConvertFrom-Json
|
||||||
if ($json.success -eq $false -and $json.error -match "Unknown hostname") {
|
if ($json.success -eq $false -and $json.error -match "Unknown hostname") {
|
||||||
$script:ApiEnabled = $false
|
$script:ApiEnabled = $false
|
||||||
Write-Host "WARNING: This PC ($($script:Hostname)) is not registered in ShopDB." -ForegroundColor Yellow
|
Write-Log "WARNING: This PC ($($script:Hostname)) is not registered in ShopDB." "Yellow"
|
||||||
Write-Host " Activity will NOT be logged. Add this PC to ShopDB to enable logging." -ForegroundColor Yellow
|
Write-Log " Activity will NOT be logged. Add this PC to ShopDB to enable logging." "Yellow"
|
||||||
Write-Host ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
# API unreachable - continue without logging
|
# API unreachable - continue without logging
|
||||||
$script:ApiEnabled = $false
|
$script:ApiEnabled = $false
|
||||||
Write-Host "WARNING: Cannot reach ShopDB API. Activity will NOT be logged." -ForegroundColor Yellow
|
Write-Log "WARNING: Cannot reach ShopDB API. Activity will NOT be logged." "Yellow"
|
||||||
Write-Host ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Statistics
|
# Statistics
|
||||||
@@ -304,175 +408,184 @@ $watcher = New-Object System.IO.FileSystemWatcher
|
|||||||
$watcher.Path = $WatchFolder
|
$watcher.Path = $WatchFolder
|
||||||
$watcher.Filter = $FileFilter
|
$watcher.Filter = $FileFilter
|
||||||
$watcher.IncludeSubdirectories = $IncludeSubfolders
|
$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
|
$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 = {
|
$action = {
|
||||||
$path = $Event.SourceEventArgs.FullPath
|
try {
|
||||||
$changeType = $Event.SourceEventArgs.ChangeType
|
$path = $Event.SourceEventArgs.FullPath
|
||||||
$fileName = Split-Path $path -Leaf
|
$changeType = $Event.SourceEventArgs.ChangeType
|
||||||
|
$fileName = Split-Path $path -Leaf
|
||||||
|
$data = $Event.MessageData
|
||||||
|
|
||||||
# Get parameters from message data
|
# IMMEDIATE debug output to confirm event fired
|
||||||
$charsToRemove = $Event.MessageData.CharsToRemove
|
Write-Host ""
|
||||||
$debounceSeconds = $Event.MessageData.DebounceSeconds
|
Write-Host ">>> EVENT: $changeType - $fileName <<<" -ForegroundColor Cyan
|
||||||
$statusLine = $Event.MessageData.StatusLine
|
|
||||||
|
|
||||||
# Helper to update status line in header
|
# Get parameters from message data
|
||||||
$updateStatus = {
|
$charsToRemove = $data.CharsToRemove
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper to update stats line in header
|
# Debounce using synchronized hashtable
|
||||||
$updateStats = {
|
$now = Get-Date
|
||||||
$savedPos = [Console]::CursorTop
|
$lastProcessed = $data.State["LastProcessed_$path"]
|
||||||
[Console]::SetCursorPosition(0, 12)
|
if ($lastProcessed -and (($now - $lastProcessed).TotalSeconds -lt $data.DebounceSeconds)) {
|
||||||
Write-Host (" Filter: $($Event.MessageData.FileFilter) | Cleaned: $($script:FilesProcessed) | Failed: $($script:FailedFiles)").PadRight([Console]::WindowWidth - 1) -ForegroundColor White
|
Write-Host " [SKIP] Debounced" -ForegroundColor DarkGray
|
||||||
[Console]::SetCursorPosition(0, $savedPos)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
& $updateStatus "Processing: $fileName" "Yellow"
|
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Processing $fileName..." -ForegroundColor White
|
||||||
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | $changeType | $fileName" -ForegroundColor White
|
|
||||||
|
|
||||||
# Brief initial delay to let eDNC finish (50ms)
|
# Brief initial delay to let eDNC finish (50ms)
|
||||||
Start-Sleep -Milliseconds 50
|
Start-Sleep -Milliseconds 50
|
||||||
|
|
||||||
# Try to acquire exclusive access - process immediately when available
|
# Try to acquire exclusive access
|
||||||
$maxRetries = 15
|
$maxRetries = 15
|
||||||
$retryCount = 0
|
$retryCount = 0
|
||||||
$baseDelay = 100 # Start with 100ms, doubles each retry (100, 200, 400, 800...)
|
$baseDelay = 100
|
||||||
|
|
||||||
while ($retryCount -lt $maxRetries) {
|
while ($retryCount -lt $maxRetries) {
|
||||||
$fileStream = $null
|
$fileStream = $null
|
||||||
try {
|
try {
|
||||||
# Try to open file with exclusive access - this confirms it's available
|
$fileStream = [System.IO.File]::Open(
|
||||||
$fileStream = [System.IO.File]::Open(
|
$path,
|
||||||
$path,
|
[System.IO.FileMode]::Open,
|
||||||
[System.IO.FileMode]::Open,
|
[System.IO.FileAccess]::ReadWrite,
|
||||||
[System.IO.FileAccess]::ReadWrite,
|
[System.IO.FileShare]::None
|
||||||
[System.IO.FileShare]::None
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# Read all bytes while we have exclusive lock
|
# Read all bytes
|
||||||
$fileStream.Position = 0
|
$fileStream.Position = 0
|
||||||
$bytes = New-Object byte[] $fileStream.Length
|
$bytes = New-Object byte[] $fileStream.Length
|
||||||
$null = $fileStream.Read($bytes, 0, $bytes.Length)
|
$null = $fileStream.Read($bytes, 0, $bytes.Length)
|
||||||
$originalCount = $bytes.Length
|
$originalCount = $bytes.Length
|
||||||
|
|
||||||
# Remove specified bytes
|
# Remove specified bytes
|
||||||
$cleaned = [System.Collections.Generic.List[byte]]::new()
|
$cleaned = [System.Collections.Generic.List[byte]]::new()
|
||||||
foreach ($b in $bytes) {
|
foreach ($b in $bytes) {
|
||||||
if ($b -notin $charsToRemove) {
|
if ($b -notin $charsToRemove) {
|
||||||
$cleaned.Add($b)
|
$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
|
catch {
|
||||||
|
Write-Host " [ERROR] $($_.Exception.Message)" -ForegroundColor Red
|
||||||
# Only rewrite if we found characters to remove
|
$data.State["FailedFiles"] = ([int]$data.State["FailedFiles"]) + 1
|
||||||
if ($originalCount -ne $newCount) {
|
break
|
||||||
# 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"
|
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
# Mark as recently processed
|
if ($fileStream) {
|
||||||
$script:RecentlyProcessed[$path] = Get-Date
|
$fileStream.Close()
|
||||||
break
|
$fileStream.Dispose()
|
||||||
}
|
}
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
# Create message data object to pass parameters to the action
|
||||||
$messageData = @{
|
$messageData = @{
|
||||||
CharsToRemove = $CharactersToRemove
|
CharsToRemove = $CharactersToRemove
|
||||||
DebounceSeconds = $script:DebounceSeconds
|
DebounceSeconds = 5
|
||||||
StatusLine = 14 # Line number for status updates
|
|
||||||
FileFilter = $FileFilter
|
FileFilter = $FileFilter
|
||||||
ApiUrl = $ApiUrl
|
ApiUrl = $ApiUrl
|
||||||
Hostname = $script:Hostname
|
Hostname = $script:Hostname
|
||||||
Version = $ScriptVersion
|
Version = $ScriptVersion
|
||||||
WatchFolder = $WatchFolder
|
WatchFolder = $WatchFolder
|
||||||
|
State = $script:SharedState
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register event handlers
|
# Register event handlers
|
||||||
$null = Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -MessageData $messageData
|
$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 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
|
# Keep script running
|
||||||
try {
|
try {
|
||||||
@@ -483,6 +596,7 @@ try {
|
|||||||
$now = Get-Date
|
$now = Get-Date
|
||||||
if (($now - $script:LastUpdateCheck).TotalSeconds -ge $UpdateCheckInterval) {
|
if (($now - $script:LastUpdateCheck).TotalSeconds -ge $UpdateCheckInterval) {
|
||||||
$script:LastUpdateCheck = $now
|
$script:LastUpdateCheck = $now
|
||||||
|
Write-Host " [UPDATE] Periodic check..." -ForegroundColor DarkGray
|
||||||
$newVersion = Check-ForUpdate
|
$newVersion = Check-ForUpdate
|
||||||
if ($newVersion) {
|
if ($newVersion) {
|
||||||
Invoke-Update -NewVersion $newVersion
|
Invoke-Update -NewVersion $newVersion
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
[eDNC-Fix]
|
[eDNC-Fix]
|
||||||
Version=1.5.1
|
Version=1.6.0
|
||||||
|
|||||||
Reference in New Issue
Block a user