From 7ca3d28883ec9c22e4c65fbccaace633a28913e2 Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:17:30 -0500 Subject: [PATCH 1/6] Add Schick 33/Elite USB sensor deployment automation script Automated deployment script for Schick 33/Elite USB Interface Driver v5.16 with Eaglesoft version-aware installation paths (IOSS vs Legacy). Installation Flows: - Legacy (Eaglesoft < 24.20): CDR Elite > Rename DLL > CDR Patch > AE USB Driver - IOSS (Eaglesoft >= 24.20): Uninstall legacy > CDR Elite > Clean Shared Files (preserve CDRData.dll + OMEGADLL.dll) > CDR Patch > IOSS Autorun > Configure service Key Features: - Auto-detects Eaglesoft version from registry to select install path - Downloads installers from Backblaze B2 (Patterson-Eaglesoft bucket) and Microsoft - Programmatic USB sensor disconnect/reconnect via PnP device APIs, eliminating the need to physically unplug and replug the sensor during deployment - Stops AutoDetectServer.exe and uninstalls legacy CDR components per Patterson docs - Configures IOSS service: delayed auto-start, restart-on-failure recovery, Local System - Validates DLLs, service state, and exports JSON results for HALO ticket integration - Silent install parameters for all components (CDR Elite, IOSS, AE USB, MSXML 4.0) Download Sources: - CDR Elite 5.16, CDR Patch 2808, IOSS v3.2, AE USB Interface/Firmware: Backblaze B2 - MSXML 4.0 SP3 (KB2758694): Microsoft Download Center Prerequisite Checks: - PowerShell 5.1+, Administrator rights, TLS 1.2, Internet connectivity - Core Isolation (Memory Integrity) warning for USB driver compatibility - RDP session detection warning for USB device visibility --- app-eaglesoft/Deploy-SchickSensor | 1385 +++++++++++++++++++++++++++++ 1 file changed, 1385 insertions(+) create mode 100644 app-eaglesoft/Deploy-SchickSensor diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor new file mode 100644 index 0000000..f5abdd5 --- /dev/null +++ b/app-eaglesoft/Deploy-SchickSensor @@ -0,0 +1,1385 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Deploys Schick 33/Elite USB Interface Driver with automatic Eaglesoft version detection. + +.DESCRIPTION + Production deployment script for Schick sensor drivers that: + - Detects Eaglesoft version to determine IOSS (24.20+) vs Legacy install path + - Downloads required installers from Backblaze B2 bucket + - Performs silent installations of CDR Elite, CDR Patch, AE USB driver, and IOSS + - Configures services with proper startup type and recovery options + - Validates installation and outputs JSON for HALO ticket integration + +.PARAMETER InstallMode + Force a specific installation mode. Valid values: 'Auto', 'Legacy', 'IOSS' + Default: Auto (detects based on Eaglesoft version) + +.PARAMETER SkipPrerequisites + Skip prerequisite checks (MSXML, Core Isolation, etc.) + +.PARAMETER DownloadOnly + Only download installers without running installation + +.PARAMETER OutputPath + Path for JSON output file. Default: $env:TEMP\SchickDeploy-Results.json + +.EXAMPLE + .\Deploy-SchickSensor.ps1 + Auto-detect Eaglesoft version and install appropriate drivers + +.EXAMPLE + .\Deploy-SchickSensor.ps1 -InstallMode IOSS + Force IOSS installation path regardless of detected Eaglesoft version + +.EXAMPLE + .\Deploy-SchickSensor.ps1 -DownloadOnly + Only download installers for manual installation + +.NOTES + Version: 1.0.0 + Author: Automated Deployment Script + Requirements: PowerShell 5.1+, Administrator rights, Internet connectivity + + File Manifest for CDR Elite 5.16: + - C:\Program Files (x86)\Schick Technologies\Shared Files\CDRData.dll + - C:\Program Files (x86)\Schick Technologies\Shared Files\OMEGADLL.dll + - C:\Program Files (x86)\Schick Technologies\Shared Files\CDRImageProcess.dll +#> + +[CmdletBinding()] +param( + [ValidateSet('Auto', 'Legacy', 'IOSS')] + [string]$InstallMode = 'Auto', + + [switch]$SkipPrerequisites, + + [switch]$DownloadOnly, + + [string]$OutputPath = "$env:TEMP\SchickDeploy-Results.json" +) + +#region Configuration +# ============================================================================ +# DOWNLOAD URLS - Backblaze B2 + Microsoft +# ============================================================================ +$Script:Config = @{ + # Full download URLs (different subfolders require full paths) + Downloads = @{ + CDRElite = @{ + Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/CDR%20Elite%20Setup.exe" + FileName = "CDR Elite Setup.exe" + } + CDRPatch = @{ + Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/Patch/CDRPatch-2808.msi" + FileName = "CDRPatch-2808.msi" + } + AEUSBDriver = @{ + Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AEUSBInterfaceSetup.exe" + FileName = "AEUSBInterfaceSetup.exe" + } + AEUSBFirmware = @{ + Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AE_USB_Firmware_Upgrade%5B1%5D.exe" + FileName = "AE_USB_Firmware_Upgrade.exe" + } + IOSS = @{ + Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/IOSS_v3.2/IOSS_v3.2/Autorun.exe" + FileName = "IOSS_Autorun.exe" + } + MSXML4 = @{ + Url = "https://download.microsoft.com/download/1/E/E/1EE06E22-A56F-4E76-B6F6-E7670B4F8163/msxml4-KB2758694-enu.exe" + FileName = "msxml4-KB2758694-enu.exe" + } + } + + # Installation paths + Paths = @{ + SharedFiles = "C:\Program Files (x86)\Schick Technologies\Shared Files" + SchickBase = "C:\Program Files (x86)\Schick Technologies" + TempDownload = "$env:TEMP\SchickInstall" + } + + # Protected DLLs - DO NOT DELETE during IOSS cleanup + ProtectedDLLs = @( + "CDRData.dll", + "OMEGADLL.dll" + ) + + # Eaglesoft version threshold for IOSS + IOSSMinVersion = [Version]"24.20" +} + +#endregion + +#region Logging Functions +# ============================================================================ +function Write-Log { + param( + [string]$Message, + [ValidateSet('Info', 'Warning', 'Error', 'Success')] + [string]$Level = 'Info' + ) + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $color = switch ($Level) { + 'Info' { 'White' } + 'Warning' { 'Yellow' } + 'Error' { 'Red' } + 'Success' { 'Green' } + } + + $prefix = switch ($Level) { + 'Info' { "[*]" } + 'Warning' { "[!]" } + 'Error' { "[X]" } + 'Success' { "[+]" } + } + + Write-Host "$timestamp $prefix $Message" -ForegroundColor $color + + # Add to results log + $Script:Results.Log += @{ + Timestamp = $timestamp + Level = $Level + Message = $Message + } +} + +#endregion + +#region Detection Functions +# ============================================================================ +function Get-EaglesoftVersion { + <# + .SYNOPSIS + Detects installed Eaglesoft version from registry + #> + + $registryPaths = @( + "HKLM:\SOFTWARE\Patterson Dental\Eaglesoft", + "HKLM:\SOFTWARE\WOW6432Node\Patterson Dental\Eaglesoft", + "HKLM:\SOFTWARE\Patterson\Eaglesoft" + ) + + foreach ($path in $registryPaths) { + if (Test-Path $path) { + try { + $version = Get-ItemProperty -Path $path -Name "Version" -ErrorAction SilentlyContinue + if ($version.Version) { + Write-Log "Found Eaglesoft version $($version.Version) at $path" -Level Info + return [Version]$version.Version + } + + # Try alternate property names + $displayVersion = Get-ItemProperty -Path $path -Name "DisplayVersion" -ErrorAction SilentlyContinue + if ($displayVersion.DisplayVersion) { + Write-Log "Found Eaglesoft DisplayVersion $($displayVersion.DisplayVersion) at $path" -Level Info + return [Version]$displayVersion.DisplayVersion + } + } + catch { + Write-Log "Error reading registry at $path`: $_" -Level Warning + } + } + } + + # Fallback: Check installed programs + $uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($path in $uninstallPaths) { + $eaglesoft = Get-ItemProperty $path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*Eaglesoft*" } | + Select-Object -First 1 + + if ($eaglesoft -and $eaglesoft.DisplayVersion) { + Write-Log "Found Eaglesoft via uninstall registry: $($eaglesoft.DisplayVersion)" -Level Info + try { + return [Version]$eaglesoft.DisplayVersion + } + catch { + # Version string might have extra characters + $cleanVersion = $eaglesoft.DisplayVersion -replace '[^0-9.]', '' + return [Version]$cleanVersion + } + } + } + + Write-Log "Eaglesoft not detected on this system" -Level Warning + return $null +} + +function Get-CurrentInstallState { + <# + .SYNOPSIS + Checks current installation state of Schick components + #> + + $state = @{ + SharedFilesExists = Test-Path $Script:Config.Paths.SharedFiles + CDRDataDLL = $false + OMEGADLL = $false + CDRImageProcessDLL = $false + IOSSServiceExists = $false + IOSSServiceRunning = $false + AEUSBDriverInstalled = $false + MSXML4Installed = $false + CoreIsolationEnabled = $false + RDPSession = $false + } + + # Check DLLs + if ($state.SharedFilesExists) { + $state.CDRDataDLL = Test-Path (Join-Path $Script:Config.Paths.SharedFiles "CDRData.dll") + $state.OMEGADLL = Test-Path (Join-Path $Script:Config.Paths.SharedFiles "OMEGADLL.dll") + $state.CDRImageProcessDLL = Test-Path (Join-Path $Script:Config.Paths.SharedFiles "CDRImageProcess.dll") + } + + # Check IOSS Service + $iossService = Get-Service -Name "IOSS*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($iossService) { + $state.IOSSServiceExists = $true + $state.IOSSServiceRunning = $iossService.Status -eq 'Running' + } + + # Check AE USB Driver + $aeDriver = Get-WmiObject Win32_PnPSignedDriver -ErrorAction SilentlyContinue | + Where-Object { $_.DeviceName -like "*AE*USB*" -or $_.Description -like "*Schick*" } + $state.AEUSBDriverInstalled = $null -ne $aeDriver + + # Check MSXML 4.0 + $msxml = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*MSXML 4*" } + $state.MSXML4Installed = $null -ne $msxml + + # Check Core Isolation (Memory Integrity) + try { + $hvci = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity" -ErrorAction SilentlyContinue + $state.CoreIsolationEnabled = $hvci.Enabled -eq 1 + } + catch { + $state.CoreIsolationEnabled = $false + } + + # Check RDP Session + $state.RDPSession = $env:SESSIONNAME -like "RDP*" + + return $state +} + +function Resolve-InstallMode { + param([Version]$EaglesoftVersion) + + if ($InstallMode -ne 'Auto') { + Write-Log "Install mode forced to: $InstallMode" -Level Info + return $InstallMode + } + + if ($null -eq $EaglesoftVersion) { + Write-Log "No Eaglesoft detected - defaulting to Legacy mode" -Level Warning + return 'Legacy' + } + + if ($EaglesoftVersion -ge $Script:Config.IOSSMinVersion) { + Write-Log "Eaglesoft $EaglesoftVersion >= $($Script:Config.IOSSMinVersion) - selecting IOSS mode" -Level Info + return 'IOSS' + } + else { + Write-Log "Eaglesoft $EaglesoftVersion < $($Script:Config.IOSSMinVersion) - selecting Legacy mode" -Level Info + return 'Legacy' + } +} + +#endregion + +#region Prerequisite Functions +# ============================================================================ +function Test-Prerequisites { + <# + .SYNOPSIS + Validates system prerequisites before installation + #> + + $passed = $true + $checks = @() + + # Check PowerShell version + if ($PSVersionTable.PSVersion.Major -lt 5) { + $checks += @{ Name = "PowerShell 5.1+"; Status = "FAIL"; Message = "PowerShell $($PSVersionTable.PSVersion) detected" } + $passed = $false + } + else { + $checks += @{ Name = "PowerShell 5.1+"; Status = "PASS"; Message = "PowerShell $($PSVersionTable.PSVersion)" } + } + + # Check Administrator + $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if (-not $isAdmin) { + $checks += @{ Name = "Administrator"; Status = "FAIL"; Message = "Script must run as Administrator" } + $passed = $false + } + else { + $checks += @{ Name = "Administrator"; Status = "PASS"; Message = "Running elevated" } + } + + # Check TLS 1.2 + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $checks += @{ Name = "TLS 1.2"; Status = "PASS"; Message = "Enabled" } + } + catch { + $checks += @{ Name = "TLS 1.2"; Status = "FAIL"; Message = "Could not enable TLS 1.2" } + $passed = $false + } + + # Check Internet connectivity + try { + $null = Invoke-WebRequest -Uri "https://www.google.com" -UseBasicParsing -TimeoutSec 10 + $checks += @{ Name = "Internet"; Status = "PASS"; Message = "Connected" } + } + catch { + $checks += @{ Name = "Internet"; Status = "FAIL"; Message = "No internet connectivity" } + $passed = $false + } + + # Check Core Isolation + if ($Script:CurrentState.CoreIsolationEnabled) { + $checks += @{ Name = "Core Isolation"; Status = "WARN"; Message = "Memory Integrity enabled - may cause driver issues" } + Write-Log "WARNING: Core Isolation (Memory Integrity) is enabled. This may prevent USB drivers from loading." -Level Warning + } + else { + $checks += @{ Name = "Core Isolation"; Status = "PASS"; Message = "Memory Integrity disabled" } + } + + # Check RDP session + if ($Script:CurrentState.RDPSession) { + $checks += @{ Name = "RDP Session"; Status = "WARN"; Message = "Running via RDP - USB devices may not be visible" } + Write-Log "WARNING: Running via RDP. USB sensor may not be accessible during installation." -Level Warning + } + else { + $checks += @{ Name = "RDP Session"; Status = "PASS"; Message = "Local session" } + } + + # Output check results + Write-Host "`n=== Prerequisite Checks ===" -ForegroundColor Cyan + foreach ($check in $checks) { + $color = switch ($check.Status) { + 'PASS' { 'Green' } + 'WARN' { 'Yellow' } + 'FAIL' { 'Red' } + } + Write-Host " [$($check.Status)] $($check.Name): $($check.Message)" -ForegroundColor $color + } + Write-Host "" + + $Script:Results.Prerequisites = $checks + return $passed +} + +#endregion + +#region Download Functions +# ============================================================================ +function Initialize-DownloadDirectory { + if (-not (Test-Path $Script:Config.Paths.TempDownload)) { + New-Item -ItemType Directory -Path $Script:Config.Paths.TempDownload -Force | Out-Null + Write-Log "Created temp download directory: $($Script:Config.Paths.TempDownload)" -Level Info + } +} + +function Get-Installer { + param( + [string]$Name + ) + + $installerConfig = $Script:Config.Downloads[$Name] + if (-not $installerConfig) { + Write-Log "Unknown installer: $Name" -Level Error + return $null + } + + $url = $installerConfig.Url + $fileName = $installerConfig.FileName + $destination = Join-Path $Script:Config.Paths.TempDownload $fileName + + # Skip if already downloaded + if (Test-Path $destination) { + $fileSize = (Get-Item $destination).Length + if ($fileSize -gt 0) { + Write-Log "$Name already downloaded ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Info + return $destination + } + } + + Write-Log "Downloading $Name from $url" -Level Info + + try { + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $url -OutFile $destination -UseBasicParsing -TimeoutSec 300 + + $fileSize = (Get-Item $destination).Length + Write-Log "Downloaded $Name successfully ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Success + return $destination + } + catch { + Write-Log "Failed to download $Name`: $_" -Level Error + return $null + } +} + +function Get-RequiredInstallers { + param([string]$Mode) + + Initialize-DownloadDirectory + + $downloads = @{} + $requiredInstallers = @() + + # MSXML 4.0 - required for all installations + if (-not $Script:CurrentState.MSXML4Installed) { + $requiredInstallers += "MSXML4" + } + + # Mode-specific installers + switch ($Mode) { + 'Legacy' { + $requiredInstallers += "CDRElite" + $requiredInstallers += "CDRPatch" # Creates correct CDRImageProcess.dll v5.15.1877 + $requiredInstallers += "AEUSBDriver" + } + 'IOSS' { + $requiredInstallers += "CDRElite" + $requiredInstallers += "CDRPatch" + $requiredInstallers += "IOSS" + } + } + + Write-Host "`n=== Downloading Installers ===" -ForegroundColor Cyan + + foreach ($installerName in $requiredInstallers) { + $path = Get-Installer -Name $installerName + if ($path) { + $downloads[$installerName] = $path + } + else { + Write-Log "Missing required installer: $installerName" -Level Error + return $null + } + } + + return $downloads +} + +#endregion + +#region Installation Functions +# ============================================================================ +function Install-MSXML4 { + param([string]$InstallerPath) + + Write-Log "Installing MSXML 4.0 SP3 Security Update..." -Level Info + + # Microsoft KB2758694 is a self-extracting exe, use /q for silent + $arguments = "/q /norestart" + $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { + Write-Log "MSXML 4.0 installed successfully" -Level Success + return $true + } + else { + Write-Log "MSXML 4.0 installation failed with exit code: $($process.ExitCode)" -Level Error + return $false + } +} + +function Install-CDRElite { + param([string]$InstallerPath) + + Write-Log "Installing CDR Elite 5.16..." -Level Info + + # InstallShield silent install parameters + # Note: May need .iss response file if this prompts - test on lab machine + $arguments = "/s /v`"/qn REBOOT=ReallySuppress`"" + + $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { + Write-Log "CDR Elite installed successfully" -Level Success + + # Verify DLLs were created + Start-Sleep -Seconds 2 + $dllsExist = (Test-Path (Join-Path $Script:Config.Paths.SharedFiles "CDRData.dll")) -and + (Test-Path (Join-Path $Script:Config.Paths.SharedFiles "OMEGADLL.dll")) + + if ($dllsExist) { + Write-Log "Verified CDRData.dll and OMEGADLL.dll in Shared Files" -Level Success + } + else { + Write-Log "Warning: Expected DLLs not found in Shared Files folder" -Level Warning + } + + return $true + } + else { + Write-Log "CDR Elite installation failed with exit code: $($process.ExitCode)" -Level Error + return $false + } +} + +function Stop-AutoDetectServer { + <# + .SYNOPSIS + Stops AutoDetectServer.exe if running (required before installation per Patterson docs) + #> + + Write-Log "Checking for AutoDetectServer.exe..." -Level Info + + $process = Get-Process -Name "AutoDetectServer" -ErrorAction SilentlyContinue + if ($process) { + Write-Log " Stopping AutoDetectServer.exe..." -Level Info + try { + $process | Stop-Process -Force + Start-Sleep -Seconds 2 + Write-Log " AutoDetectServer.exe stopped" -Level Success + } + catch { + Write-Log " Failed to stop AutoDetectServer.exe: $_" -Level Warning + } + } + else { + Write-Log " AutoDetectServer.exe not running" -Level Info + } +} + +function Uninstall-LegacyComponents { + <# + .SYNOPSIS + Uninstalls legacy CDR/Schick components before IOSS installation + Per Patterson documentation, these must be removed: + - CDR Patch-2808 + - CDR Intra-Oral TWAIN Data Source + - CDR Elite USB Driver + - CDR USB Remote HS Driver + - Schick AE USB Support for CDR + #> + + Write-Log "Checking for legacy components to uninstall..." -Level Info + + $legacyProducts = @( + "*CDR Patch*", + "*CDR Intra-Oral*", + "*CDR Elite*", + "*CDR USB*", + "*Schick AE USB*", + "*CDR TWAIN*" + ) + + $uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + $foundProducts = @() + + foreach ($path in $uninstallPaths) { + foreach ($pattern in $legacyProducts) { + $products = Get-ItemProperty $path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like $pattern } + if ($products) { + $foundProducts += $products + } + } + } + + if ($foundProducts.Count -eq 0) { + Write-Log " No legacy components found" -Level Info + return $true + } + + Write-Log " Found $($foundProducts.Count) legacy component(s) to uninstall:" -Level Info + + foreach ($product in $foundProducts) { + Write-Log " - $($product.DisplayName)" -Level Info + + try { + $uninstallString = $product.UninstallString + if ($uninstallString -match "msiexec") { + # MSI uninstall + $productCode = $product.PSChildName + $arguments = "/x $productCode /qn /norestart" + Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -NoNewWindow + Write-Log " Uninstalled via MSI" -Level Success + } + elseif ($uninstallString) { + # EXE uninstall - try silent + $uninstallString = $uninstallString -replace '"', '' + Start-Process -FilePath $uninstallString -ArgumentList "/S /SILENT /VERYSILENT /NORESTART" -Wait -NoNewWindow -ErrorAction SilentlyContinue + Write-Log " Uninstall attempted" -Level Info + } + } + catch { + Write-Log " Failed to uninstall: $_" -Level Warning + } + } + + return $true +} + +function Rename-CDRImageProcessDLL { + <# + .SYNOPSIS + Renames CDRImageProcess.dll to CDRImageProcess.dllOLD before patching + Per Patterson documentation for Legacy installation path + #> + + $dllPath = Join-Path $Script:Config.Paths.SharedFiles "CDRImageProcess.dll" + $oldPath = Join-Path $Script:Config.Paths.SharedFiles "CDRImageProcess.dllOLD" + + if (Test-Path $dllPath) { + Write-Log "Renaming CDRImageProcess.dll to CDRImageProcess.dllOLD..." -Level Info + try { + # Remove old backup if exists + if (Test-Path $oldPath) { + Remove-Item -Path $oldPath -Force + } + Rename-Item -Path $dllPath -NewName "CDRImageProcess.dllOLD" -Force + Write-Log " Renamed successfully" -Level Success + return $true + } + catch { + Write-Log " Failed to rename: $_" -Level Error + return $false + } + } + else { + Write-Log "CDRImageProcess.dll not found - skipping rename" -Level Info + return $true + } +} + +function Clear-SharedFilesForIOSS { + <# + .SYNOPSIS + Cleans Shared Files folder for IOSS installation + PRESERVES CDRData.dll and OMEGADLL.dll + DELETES everything else including CDRImageProcess.dll + #> + + Write-Log "Preparing Shared Files folder for IOSS installation..." -Level Info + + $sharedPath = $Script:Config.Paths.SharedFiles + + if (-not (Test-Path $sharedPath)) { + Write-Log "Shared Files folder does not exist - skipping cleanup" -Level Warning + return $true + } + + $files = Get-ChildItem -Path $sharedPath -File -ErrorAction SilentlyContinue + $deletedCount = 0 + $preservedCount = 0 + + foreach ($file in $files) { + if ($Script:Config.ProtectedDLLs -contains $file.Name) { + Write-Log " PRESERVING: $($file.Name)" -Level Info + $preservedCount++ + } + else { + try { + Remove-Item -Path $file.FullName -Force + Write-Log " DELETED: $($file.Name)" -Level Info + $deletedCount++ + } + catch { + Write-Log " FAILED TO DELETE: $($file.Name) - $_" -Level Error + return $false + } + } + } + + Write-Log "Cleanup complete: Preserved $preservedCount files, Deleted $deletedCount files" -Level Success + return $true +} + +function Install-CDRPatch { + param([string]$InstallerPath) + + Write-Log "Installing CDR Patch 2808..." -Level Info + + $arguments = "/i `"$InstallerPath`" /qn /norestart" + $process = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { + Write-Log "CDR Patch installed successfully" -Level Success + + # Verify CDRImageProcess.dll was created + Start-Sleep -Seconds 2 + if (Test-Path (Join-Path $Script:Config.Paths.SharedFiles "CDRImageProcess.dll")) { + Write-Log "Verified CDRImageProcess.dll created by patch" -Level Success + } + else { + Write-Log "Warning: CDRImageProcess.dll not found after patch" -Level Warning + } + + return $true + } + else { + Write-Log "CDR Patch installation failed with exit code: $($process.ExitCode)" -Level Error + return $false + } +} + +function Install-AEUSBDriver { + param([string]$InstallerPath) + + Write-Log "Installing AE USB Interface driver..." -Level Info + + # Try NSIS-style silent install first + $arguments = "/S" + + $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0) { + Write-Log "AE USB driver installed successfully" -Level Success + return $true + } + else { + Write-Log "AE USB driver installation returned exit code: $($process.ExitCode)" -Level Warning + # Some driver installers return non-zero even on success + return $true + } +} + +function Install-IOSS { + param([string]$InstallerPath) + + Write-Log "Installing IOSS Imaging Service..." -Level Info + + # Try InstallShield silent parameters + $arguments = "/s /v`"/qn REBOOT=ReallySuppress`"" + + $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { + Write-Log "IOSS installed successfully" -Level Success + return $true + } + else { + Write-Log "IOSS installation returned exit code: $($process.ExitCode)" -Level Warning + # Continue anyway - service configuration will verify + return $true + } +} + +function Set-IOSSServiceConfiguration { + <# + .SYNOPSIS + Configures IOSS service with proper settings + #> + + Write-Log "Configuring IOSS service..." -Level Info + + $iossService = Get-Service -Name "IOSS*" -ErrorAction SilentlyContinue | Select-Object -First 1 + + if (-not $iossService) { + Write-Log "IOSS service not found - configuration skipped" -Level Error + return $false + } + + $serviceName = $iossService.Name + + try { + # Set delayed auto-start + & sc.exe config $serviceName start= delayed-auto | Out-Null + Write-Log " Set startup type: Delayed Auto-Start" -Level Info + + # Set recovery options: restart on first, second, and subsequent failures + & sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null + Write-Log " Set recovery options: Restart on failure (60s delay)" -Level Info + + # Ensure running as Local System + & sc.exe config $serviceName obj= "LocalSystem" | Out-Null + Write-Log " Set logon account: Local System" -Level Info + + # Start the service + if ($iossService.Status -ne 'Running') { + Start-Service -Name $serviceName -ErrorAction SilentlyContinue + Start-Sleep -Seconds 3 + + $iossService = Get-Service -Name $serviceName + if ($iossService.Status -eq 'Running') { + Write-Log " Service started successfully" -Level Success + } + else { + Write-Log " Service not running - Status: $($iossService.Status)" -Level Warning + } + } + else { + Write-Log " Service already running" -Level Info + } + + return $true + } + catch { + Write-Log "Error configuring IOSS service: $_" -Level Error + return $false + } +} + +function Reset-USBSensor { + <# + .SYNOPSIS + Programmatically "replugs" the Schick USB sensor by disabling and re-enabling the device. + This eliminates the need to physically unplug and replug the sensor. + #> + + Write-Log "Resetting USB sensor (simulating unplug/replug)..." -Level Info + + # Search patterns for Schick/AE USB devices + $devicePatterns = @( + "*Schick*", + "*CDR*", + "*AE*USB*", + "*Dental*Sensor*", + "*FTDI*" # Common USB-serial chip used in dental sensors + ) + + $foundDevices = @() + + # Find matching USB devices + foreach ($pattern in $devicePatterns) { + $devices = Get-PnpDevice -FriendlyName $pattern -ErrorAction SilentlyContinue | + Where-Object { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') } + + if ($devices) { + $foundDevices += $devices + } + } + + # Also search by hardware ID patterns + $usbDevices = Get-PnpDevice -Class 'USB', 'Image', 'Ports' -ErrorAction SilentlyContinue + foreach ($device in $usbDevices) { + $hwIds = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue).Data + if ($hwIds -match 'VID_0403|VID_20D6|Schick|CDR') { + if ($device -notin $foundDevices) { + $foundDevices += $device + } + } + } + + if ($foundDevices.Count -eq 0) { + Write-Log "No Schick/AE USB devices found to reset" -Level Warning + Write-Log " Note: Device may need physical replug, or sensor not connected" -Level Warning + return $false + } + + # Remove duplicates + $foundDevices = $foundDevices | Select-Object -Unique + + Write-Log "Found $($foundDevices.Count) USB device(s) to reset:" -Level Info + + $resetSuccess = $true + foreach ($device in $foundDevices) { + Write-Log " Resetting: $($device.FriendlyName) [$($device.InstanceId)]" -Level Info + + try { + # Disable the device + Write-Log " Disabling device..." -Level Info + Disable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false -ErrorAction Stop + + # Wait for device to fully disable + Start-Sleep -Seconds 2 + + # Re-enable the device + Write-Log " Re-enabling device..." -Level Info + Enable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false -ErrorAction Stop + + # Wait for device to initialize + Start-Sleep -Seconds 3 + + # Verify device is back online + $deviceStatus = Get-PnpDevice -InstanceId $device.InstanceId -ErrorAction SilentlyContinue + if ($deviceStatus.Status -eq 'OK') { + Write-Log " Device reset successfully - Status: OK" -Level Success + } + else { + Write-Log " Device status after reset: $($deviceStatus.Status)" -Level Warning + } + } + catch { + Write-Log " Failed to reset device: $_" -Level Error + $resetSuccess = $false + + # Try to re-enable if disable succeeded but enable failed + try { + Enable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false -ErrorAction SilentlyContinue + } + catch { } + } + } + + if ($resetSuccess) { + Write-Log "USB sensor reset completed successfully" -Level Success + } + else { + Write-Log "USB sensor reset completed with errors - manual replug may be required" -Level Warning + } + + return $resetSuccess +} + +function Reset-USBSensorFallback { + <# + .SYNOPSIS + Fallback method using pnputil/devcon if Disable-PnpDevice is not available + #> + + Write-Log "Attempting USB reset via pnputil..." -Level Info + + # Scan for hardware changes (forces re-enumeration) + $result = & pnputil.exe /scan-devices 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Log "USB device scan completed - devices re-enumerated" -Level Success + return $true + } + else { + Write-Log "pnputil scan failed: $result" -Level Warning + return $false + } +} + +function Disable-USBSensorForInstall { + <# + .SYNOPSIS + Disables Schick USB sensor before installation (mimics "disconnect") + Per Patterson docs: "Close Eaglesoft and disconnect all Schick equipment" + Stores device IDs in script variable for later re-enablement + #> + + Write-Log "Checking for connected USB sensors to disable during installation..." -Level Info + + # Search patterns for Schick/AE USB devices + $devicePatterns = @( + "*Schick*", + "*CDR*", + "*AE*USB*", + "*Dental*Sensor*", + "*FTDI*" + ) + + $Script:DisabledDevices = @() + + # Find matching USB devices + foreach ($pattern in $devicePatterns) { + $devices = Get-PnpDevice -FriendlyName $pattern -ErrorAction SilentlyContinue | + Where-Object { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') -and $_.Status -eq 'OK' } + + if ($devices) { + $Script:DisabledDevices += $devices + } + } + + # Also search by hardware ID patterns + $usbDevices = Get-PnpDevice -Class 'USB', 'Image', 'Ports' -Status 'OK' -ErrorAction SilentlyContinue + foreach ($device in $usbDevices) { + $hwIds = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue).Data + if ($hwIds -match 'VID_0403|VID_20D6|Schick|CDR') { + if ($device.InstanceId -notin $Script:DisabledDevices.InstanceId) { + $Script:DisabledDevices += $device + } + } + } + + if ($Script:DisabledDevices.Count -eq 0) { + Write-Log " No active USB sensors detected - proceeding with installation" -Level Info + return $true + } + + # Remove duplicates + $Script:DisabledDevices = $Script:DisabledDevices | Select-Object -Unique + + Write-Log " Found $($Script:DisabledDevices.Count) USB sensor(s) to disable:" -Level Info + + foreach ($device in $Script:DisabledDevices) { + Write-Log " - $($device.FriendlyName)" -Level Info + + try { + Disable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false -ErrorAction Stop + Write-Log " DISABLED (will re-enable after installation)" -Level Success + } + catch { + Write-Log " Failed to disable: $_" -Level Warning + Write-Log " You may need to physically unplug the sensor" -Level Warning + } + } + + # Brief pause for devices to fully disable + Start-Sleep -Seconds 2 + + return $true +} + +function Enable-USBSensorAfterInstall { + <# + .SYNOPSIS + Re-enables USB sensors that were disabled before installation (mimics "reconnect") + #> + + if (-not $Script:DisabledDevices -or $Script:DisabledDevices.Count -eq 0) { + Write-Log "No devices to re-enable (none were disabled)" -Level Info + # Still run a device scan in case sensor was connected during install + Reset-USBSensorFallback | Out-Null + return $true + } + + Write-Log "Re-enabling USB sensors after installation..." -Level Info + + $enableSuccess = $true + + foreach ($device in $Script:DisabledDevices) { + Write-Log " Re-enabling: $($device.FriendlyName)" -Level Info + + try { + Enable-PnpDevice -InstanceId $device.InstanceId -Confirm:$false -ErrorAction Stop + + # Wait for device to initialize + Start-Sleep -Seconds 2 + + # Verify status + $currentDevice = Get-PnpDevice -InstanceId $device.InstanceId -ErrorAction SilentlyContinue + if ($currentDevice.Status -eq 'OK') { + Write-Log " Status: OK - Device reconnected successfully" -Level Success + } + else { + Write-Log " Status: $($currentDevice.Status)" -Level Warning + } + } + catch { + Write-Log " Failed to re-enable: $_" -Level Error + $enableSuccess = $false + } + } + + # Also trigger a device scan for good measure + & pnputil.exe /scan-devices 2>&1 | Out-Null + + if ($enableSuccess) { + Write-Log "All USB sensors re-enabled successfully" -Level Success + } + else { + Write-Log "Some devices failed to re-enable - may need physical replug" -Level Warning + } + + return $enableSuccess +} + +#endregion + +#region Validation Functions +# ============================================================================ +function Test-Installation { + param([string]$Mode) + + Write-Host "`n=== Validating Installation ===" -ForegroundColor Cyan + + $validation = @{ + Success = $true + Checks = @() + } + + # Check Shared Files directory + if (Test-Path $Script:Config.Paths.SharedFiles) { + $validation.Checks += @{ Component = "Shared Files Directory"; Status = "PASS" } + } + else { + $validation.Checks += @{ Component = "Shared Files Directory"; Status = "FAIL" } + $validation.Success = $false + } + + # Check required DLLs + $requiredDLLs = @("CDRData.dll", "OMEGADLL.dll") + if ($Mode -eq 'IOSS') { + $requiredDLLs += "CDRImageProcess.dll" + } + + foreach ($dll in $requiredDLLs) { + $dllPath = Join-Path $Script:Config.Paths.SharedFiles $dll + if (Test-Path $dllPath) { + $version = (Get-Item $dllPath).VersionInfo.FileVersion + $validation.Checks += @{ Component = $dll; Status = "PASS"; Version = $version } + } + else { + $validation.Checks += @{ Component = $dll; Status = "FAIL"; Version = "Not Found" } + $validation.Success = $false + } + } + + # Check IOSS service (IOSS mode only) + if ($Mode -eq 'IOSS') { + $iossService = Get-Service -Name "IOSS*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($iossService -and $iossService.Status -eq 'Running') { + $validation.Checks += @{ Component = "IOSS Service"; Status = "PASS"; Version = $iossService.Status } + } + elseif ($iossService) { + $validation.Checks += @{ Component = "IOSS Service"; Status = "WARN"; Version = $iossService.Status } + } + else { + $validation.Checks += @{ Component = "IOSS Service"; Status = "FAIL"; Version = "Not Found" } + $validation.Success = $false + } + } + + # Output validation results + foreach ($check in $validation.Checks) { + $color = switch ($check.Status) { + 'PASS' { 'Green' } + 'WARN' { 'Yellow' } + 'FAIL' { 'Red' } + } + $versionInfo = if ($check.Version) { " ($($check.Version))" } else { "" } + Write-Host " [$($check.Status)] $($check.Component)$versionInfo" -ForegroundColor $color + } + + $Script:Results.Validation = $validation + return $validation.Success +} + +#endregion + +#region Main Execution +# ============================================================================ +function Invoke-LegacyInstallation { + param([hashtable]$Installers) + + Write-Host "`n=== Legacy Installation Mode ===" -ForegroundColor Cyan + + # Step 0a: Stop AutoDetectServer.exe if running + Stop-AutoDetectServer + + # Step 0b: Disable USB sensor if connected (per Patterson: "disconnect all Schick equipment") + Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan + Disable-USBSensorForInstall + + # Step 1: MSXML 4.0 (if needed) + if ($Installers.ContainsKey('MSXML4')) { + if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + return $false + } + } + + # Step 2: CDR Elite + if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { + return $false + } + + # Step 3: Rename CDRImageProcess.dll to .dllOLD (per Patterson docs) + Rename-CDRImageProcessDLL + + # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) + if ($Installers.ContainsKey('CDRPatch')) { + if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { + return $false + } + } + + # Step 5: AE USB Driver + if (-not (Install-AEUSBDriver -InstallerPath $Installers.AEUSBDriver)) { + return $false + } + + # Step 6: Re-enable USB sensor (per Patterson: reconnect after driver install) + Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan + Enable-USBSensorAfterInstall + + return $true +} + +function Invoke-IOSSInstallation { + param([hashtable]$Installers) + + Write-Host "`n=== IOSS Installation Mode ===" -ForegroundColor Cyan + + # Step 0a: Stop AutoDetectServer.exe if running + Stop-AutoDetectServer + + # Step 0b: Disable USB sensor if connected (per Patterson: "Unplug USB cable from Schick remote") + Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan + Disable-USBSensorForInstall + + # Step 0c: Uninstall legacy CDR components (required for IOSS per Patterson docs) + Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan + Uninstall-LegacyComponents + + # Step 1: MSXML 4.0 (if needed) + if ($Installers.ContainsKey('MSXML4')) { + if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + return $false + } + } + + # Step 2: CDR Elite (installs base DLLs: CDRData.dll, OMEGADLL.dll) + if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { + return $false + } + + # Step 3: Clean Shared Files (preserve CDRData.dll and OMEGADLL.dll only) + if (-not (Clear-SharedFilesForIOSS)) { + return $false + } + + # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) + if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { + return $false + } + + # Step 5: IOSS Autorun + if (-not (Install-IOSS -InstallerPath $Installers.IOSS)) { + return $false + } + + # Step 6: Configure IOSS Service (delayed start, recovery options, Local System) + if (-not (Set-IOSSServiceConfiguration)) { + Write-Log "Service configuration had issues - manual review recommended" -Level Warning + } + + # Step 7: Re-enable USB sensor (per Patterson: reconnect after IOSS install) + Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan + Enable-USBSensorAfterInstall + + return $true +} + +function Export-Results { + try { + $Script:Results | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8 + Write-Log "Results exported to: $OutputPath" -Level Info + } + catch { + Write-Log "Failed to export results: $_" -Level Warning + } +} + +# ============================================================================ +# MAIN +# ============================================================================ +$ErrorActionPreference = 'Stop' + +# Initialize results object +$Script:Results = @{ + StartTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + ComputerName = $env:COMPUTERNAME + Mode = $null + Success = $false + Prerequisites = @() + Validation = @{} + Log = @() +} + +Write-Host @" + +╔═══════════════════════════════════════════════════════════════════╗ +║ Schick Sensor Deployment Script v1.0.0 ║ +║ ║ +║ CDR Elite 5.16 + IOSS/Legacy Auto-Detection ║ +╚═══════════════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +try { + # Enable TLS 1.2 + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + # Step 1: Detect current state + Write-Host "=== Detecting System State ===" -ForegroundColor Cyan + $Script:CurrentState = Get-CurrentInstallState + + # Step 2: Detect Eaglesoft version + $eaglesoftVersion = Get-EaglesoftVersion + $Script:Results.EaglesoftVersion = if ($eaglesoftVersion) { $eaglesoftVersion.ToString() } else { "Not Detected" } + + # Step 3: Resolve install mode + $resolvedMode = Resolve-InstallMode -EaglesoftVersion $eaglesoftVersion + $Script:Results.Mode = $resolvedMode + Write-Host "`n Selected Install Mode: $resolvedMode" -ForegroundColor Yellow + Write-Host "" + + # Step 4: Check prerequisites + if (-not $SkipPrerequisites) { + $prereqsPassed = Test-Prerequisites + if (-not $prereqsPassed) { + Write-Log "Prerequisite checks failed - aborting installation" -Level Error + $Script:Results.Success = $false + Export-Results + exit 1 + } + } + + # Step 5: Download installers + $installers = Get-RequiredInstallers -Mode $resolvedMode + if (-not $installers) { + Write-Log "Failed to download required installers - aborting" -Level Error + $Script:Results.Success = $false + Export-Results + exit 1 + } + + # Step 6: Download-only mode check + if ($DownloadOnly) { + Write-Log "Download-only mode - skipping installation" -Level Info + Write-Host "`nInstallers downloaded to: $($Script:Config.Paths.TempDownload)" -ForegroundColor Green + $Script:Results.Success = $true + Export-Results + exit 0 + } + + # Step 7: Run installation + $installSuccess = switch ($resolvedMode) { + 'Legacy' { Invoke-LegacyInstallation -Installers $installers } + 'IOSS' { Invoke-IOSSInstallation -Installers $installers } + } + + if (-not $installSuccess) { + Write-Log "Installation failed" -Level Error + $Script:Results.Success = $false + Export-Results + exit 1 + } + + # Step 8: Validate installation + $validationPassed = Test-Installation -Mode $resolvedMode + $Script:Results.Success = $validationPassed + + # Step 9: Final summary + Write-Host "" + if ($validationPassed) { + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green + Write-Host " INSTALLATION COMPLETED SUCCESSFULLY" -ForegroundColor Green + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green + } + else { + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Yellow + Write-Host " INSTALLATION COMPLETED WITH WARNINGS - Review validation above" -ForegroundColor Yellow + Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Yellow + } + + $Script:Results.EndTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Export-Results + + Write-Host "`n Results exported to: $OutputPath" -ForegroundColor Gray + Write-Host "" + + if ($validationPassed) { exit 0 } else { exit 2 } +} +catch { + Write-Log "Unhandled exception: $_" -Level Error + Write-Log $_.ScriptStackTrace -Level Error + $Script:Results.Success = $false + $Script:Results.Error = $_.ToString() + Export-Results + exit 1 +} + +#endregion From 1694b57ed15185540df5bd8bc2952cbb14eefb95 Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:38:09 -0500 Subject: [PATCH 2/6] Address CodeRabbit review feedback for Schick deployment script Addresses all CodeRabbit automated review comments from PR #42. CRITICAL FIXES: 1. USB Sensors Left Disabled on Failure - Wrapped Invoke-LegacyInstallation and Invoke-IOSSInstallation in try/finally - Enable-USBSensorAfterInstall now ALWAYS executes, even if installation fails - Prevents technicians from encountering disabled sensors after failed deployments 2. Installer Integrity Verification - Added SHA256 field to all installer configs (currently $null with TODO markers) - New Test-InstallerHash function verifies downloads against expected hashes - Corrupted/mismatched files are automatically removed and re-downloaded - Hash verification runs on both cached files and fresh downloads 3. Overly Broad Uninstall Patterns - Tightened patterns: "CDR Patch-2808*" instead of "*CDR Patch*" - Added Publisher filter requiring Patterson/Schick/Sirona - Prevents accidental uninstall of CD-R burning software or Call Detail Record tools CODE QUALITY IMPROVEMENTS: 4. PowerShell 7+ Compatibility - Replaced deprecated Get-WmiObject with Get-CimInstance for driver detection 5. Connectivity Check - Now tests against actual Backblaze B2 endpoint instead of google.com - Falls back to Microsoft download endpoint if B2 unreachable - Avoids false negatives from corporate firewalls blocking google.com 6. Service Configuration Error Handling - Added $LASTEXITCODE checks after all sc.exe calls - Logs specific exit codes when service configuration fails 7. AE USB Driver Exit Codes - Only accepts known success codes: 0 (success), 3010 (reboot required), 1641 (reboot initiated) - Previously returned $true for ALL non-zero codes 8. Version Parsing - Replaced unsafe [Version] cast with [Version]::TryParse pattern - Handles malformed version strings without throwing unhandled exceptions 9. USB Discovery Consolidation - Extracted Find-SchickUSBDevices shared helper function - Reduces code duplication between Reset-USBSensor and Disable/Enable functions - Single point of maintenance for device detection patterns 10. Banner Alignment - Standardized all banner borders to 71 characters - Fixed visual misalignment in header and completion messages NOTE: SHA256 hashes are set to $null and must be populated after first verified download of each installer from trusted source. --- app-eaglesoft/Deploy-SchickSensor | 416 +++++++++++++++++++----------- 1 file changed, 270 insertions(+), 146 deletions(-) diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor index f5abdd5..0717986 100644 --- a/app-eaglesoft/Deploy-SchickSensor +++ b/app-eaglesoft/Deploy-SchickSensor @@ -64,31 +64,37 @@ param( # DOWNLOAD URLS - Backblaze B2 + Microsoft # ============================================================================ $Script:Config = @{ - # Full download URLs (different subfolders require full paths) + # Full download URLs with SHA256 hashes for integrity verification Downloads = @{ CDRElite = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/CDR%20Elite%20Setup.exe" FileName = "CDR Elite Setup.exe" + SHA256 = $null # TODO: Populate after first verified download } CDRPatch = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/Patch/CDRPatch-2808.msi" FileName = "CDRPatch-2808.msi" + SHA256 = $null # TODO: Populate after first verified download } AEUSBDriver = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AEUSBInterfaceSetup.exe" FileName = "AEUSBInterfaceSetup.exe" + SHA256 = $null # TODO: Populate after first verified download } AEUSBFirmware = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AE_USB_Firmware_Upgrade%5B1%5D.exe" FileName = "AE_USB_Firmware_Upgrade.exe" + SHA256 = $null # TODO: Populate after first verified download } IOSS = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/IOSS_v3.2/IOSS_v3.2/Autorun.exe" FileName = "IOSS_Autorun.exe" + SHA256 = $null # TODO: Populate after first verified download } MSXML4 = @{ Url = "https://download.microsoft.com/download/1/E/E/1EE06E22-A56F-4E76-B6F6-E7670B4F8163/msxml4-KB2758694-enu.exe" FileName = "msxml4-KB2758694-enu.exe" + SHA256 = $null # TODO: Populate after first verified download } } @@ -196,14 +202,24 @@ function Get-EaglesoftVersion { if ($eaglesoft -and $eaglesoft.DisplayVersion) { Write-Log "Found Eaglesoft via uninstall registry: $($eaglesoft.DisplayVersion)" -Level Info - try { - return [Version]$eaglesoft.DisplayVersion + + # Safe version parsing with TryParse pattern + $parsedVersion = $null + $versionString = $eaglesoft.DisplayVersion + + # Try direct parse first + if ([Version]::TryParse($versionString, [ref]$parsedVersion)) { + return $parsedVersion } - catch { - # Version string might have extra characters - $cleanVersion = $eaglesoft.DisplayVersion -replace '[^0-9.]', '' - return [Version]$cleanVersion + + # Try cleaning the version string + $cleanVersion = $versionString -replace '[^0-9.]', '' -replace '\.+', '.' -replace '^\.|\.§', '' + if ([Version]::TryParse($cleanVersion, [ref]$parsedVersion)) { + Write-Log " Parsed cleaned version: $cleanVersion" -Level Info + return $parsedVersion } + + Write-Log " Could not parse version string: $versionString" -Level Warning } } @@ -244,8 +260,8 @@ function Get-CurrentInstallState { $state.IOSSServiceRunning = $iossService.Status -eq 'Running' } - # Check AE USB Driver - $aeDriver = Get-WmiObject Win32_PnPSignedDriver -ErrorAction SilentlyContinue | + # Check AE USB Driver (using Get-CimInstance for PS7+ compatibility) + $aeDriver = Get-CimInstance Win32_PnPSignedDriver -ErrorAction SilentlyContinue | Where-Object { $_.DeviceName -like "*AE*USB*" -or $_.Description -like "*Schick*" } $state.AEUSBDriverInstalled = $null -ne $aeDriver @@ -334,14 +350,21 @@ function Test-Prerequisites { $passed = $false } - # Check Internet connectivity + # Check Internet connectivity (test against actual download endpoint) try { - $null = Invoke-WebRequest -Uri "https://www.google.com" -UseBasicParsing -TimeoutSec 10 - $checks += @{ Name = "Internet"; Status = "PASS"; Message = "Connected" } + $null = Invoke-WebRequest -Uri "https://s3.us-west-002.backblazeb2.com" -UseBasicParsing -TimeoutSec 10 -Method Head + $checks += @{ Name = "Internet/B2"; Status = "PASS"; Message = "Backblaze B2 reachable" } } catch { - $checks += @{ Name = "Internet"; Status = "FAIL"; Message = "No internet connectivity" } - $passed = $false + # Fallback to Microsoft endpoint + try { + $null = Invoke-WebRequest -Uri "https://download.microsoft.com" -UseBasicParsing -TimeoutSec 10 -Method Head + $checks += @{ Name = "Internet/B2"; Status = "WARN"; Message = "B2 unreachable, Microsoft OK" } + } + catch { + $checks += @{ Name = "Internet/B2"; Status = "FAIL"; Message = "No connectivity to download sources" } + $passed = $false + } } # Check Core Isolation @@ -389,6 +412,35 @@ function Initialize-DownloadDirectory { } } +function Test-InstallerHash { + <# + .SYNOPSIS + Verifies SHA256 hash of downloaded installer + #> + param( + [string]$FilePath, + [string]$ExpectedHash, + [string]$Name + ) + + if ([string]::IsNullOrEmpty($ExpectedHash)) { + Write-Log " SHA256 hash not configured for $Name - skipping verification" -Level Warning + return $true + } + + $actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash + if ($actualHash -eq $ExpectedHash) { + Write-Log " SHA256 verified: $($actualHash.Substring(0,16))..." -Level Success + return $true + } + else { + Write-Log " SHA256 MISMATCH for $Name!" -Level Error + Write-Log " Expected: $ExpectedHash" -Level Error + Write-Log " Actual: $actualHash" -Level Error + return $false + } +} + function Get-Installer { param( [string]$Name @@ -402,14 +454,23 @@ function Get-Installer { $url = $installerConfig.Url $fileName = $installerConfig.FileName + $expectedHash = $installerConfig.SHA256 $destination = Join-Path $Script:Config.Paths.TempDownload $fileName - # Skip if already downloaded + # Check if already downloaded and verify hash if (Test-Path $destination) { $fileSize = (Get-Item $destination).Length if ($fileSize -gt 0) { Write-Log "$Name already downloaded ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Info - return $destination + + # Verify hash of existing file + if (-not (Test-InstallerHash -FilePath $destination -ExpectedHash $expectedHash -Name $Name)) { + Write-Log "Removing corrupted/mismatched file and re-downloading..." -Level Warning + Remove-Item -Path $destination -Force + } + else { + return $destination + } } } @@ -421,6 +482,14 @@ function Get-Installer { $fileSize = (Get-Item $destination).Length Write-Log "Downloaded $Name successfully ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Success + + # Verify hash of newly downloaded file + if (-not (Test-InstallerHash -FilePath $destination -ExpectedHash $expectedHash -Name $Name)) { + Write-Log "Downloaded file failed integrity check - removing" -Level Error + Remove-Item -Path $destination -Force + return $null + } + return $destination } catch { @@ -568,13 +637,22 @@ function Uninstall-LegacyComponents { Write-Log "Checking for legacy components to uninstall..." -Level Info + # Specific product name patterns (tightened to avoid false positives like CD-R burners) $legacyProducts = @( - "*CDR Patch*", - "*CDR Intra-Oral*", - "*CDR Elite*", - "*CDR USB*", - "*Schick AE USB*", - "*CDR TWAIN*" + "CDR Patch-2808*", + "CDR Intra-Oral*", + "CDR Elite USB*", + "CDR USB Remote*", + "Schick AE USB*", + "CDR TWAIN*", + "Schick CDR*" + ) + + # Valid publishers for Patterson/Schick products + $validPublishers = @( + "*Patterson*", + "*Schick*", + "*Sirona*" ) $uninstallPaths = @( @@ -587,13 +665,29 @@ function Uninstall-LegacyComponents { foreach ($path in $uninstallPaths) { foreach ($pattern in $legacyProducts) { $products = Get-ItemProperty $path -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -like $pattern } + Where-Object { + $_.DisplayName -like $pattern -and + ($validPublishers | ForEach-Object { $_.Publisher -like $_ }) -contains $true + } if ($products) { $foundProducts += $products } } + + # Also catch any product from Patterson/Schick with CDR in the name + $publisherProducts = Get-ItemProperty $path -ErrorAction SilentlyContinue | + Where-Object { + $_.DisplayName -like "*CDR*" -and + (($_.Publisher -like "*Patterson*") -or ($_.Publisher -like "*Schick*") -or ($_.Publisher -like "*Sirona*")) + } + if ($publisherProducts) { + $foundProducts += $publisherProducts + } } + # Remove duplicates + $foundProducts = $foundProducts | Sort-Object -Property PSChildName -Unique + if ($foundProducts.Count -eq 0) { Write-Log " No legacy components found" -Level Info return $true @@ -741,14 +835,21 @@ function Install-AEUSBDriver { $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow - if ($process.ExitCode -eq 0) { - Write-Log "AE USB driver installed successfully" -Level Success + # Known acceptable exit codes for driver installers + $acceptableExitCodes = @(0, 3010, 1641) # 0=Success, 3010=Reboot required, 1641=Reboot initiated + + if ($process.ExitCode -in $acceptableExitCodes) { + if ($process.ExitCode -eq 0) { + Write-Log "AE USB driver installed successfully" -Level Success + } + else { + Write-Log "AE USB driver installed (exit code $($process.ExitCode) - reboot may be required)" -Level Success + } return $true } else { - Write-Log "AE USB driver installation returned exit code: $($process.ExitCode)" -Level Warning - # Some driver installers return non-zero even on success - return $true + Write-Log "AE USB driver installation failed with exit code: $($process.ExitCode)" -Level Error + return $false } } @@ -792,16 +893,31 @@ function Set-IOSSServiceConfiguration { try { # Set delayed auto-start - & sc.exe config $serviceName start= delayed-auto | Out-Null - Write-Log " Set startup type: Delayed Auto-Start" -Level Info + $null = & sc.exe config $serviceName start= delayed-auto 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log " Set startup type: Delayed Auto-Start" -Level Info + } + else { + Write-Log " Failed to set startup type (exit code: $LASTEXITCODE)" -Level Warning + } # Set recovery options: restart on first, second, and subsequent failures - & sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null - Write-Log " Set recovery options: Restart on failure (60s delay)" -Level Info + $null = & sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log " Set recovery options: Restart on failure (60s delay)" -Level Info + } + else { + Write-Log " Failed to set recovery options (exit code: $LASTEXITCODE)" -Level Warning + } # Ensure running as Local System - & sc.exe config $serviceName obj= "LocalSystem" | Out-Null - Write-Log " Set logon account: Local System" -Level Info + $null = & sc.exe config $serviceName obj= "LocalSystem" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log " Set logon account: Local System" -Level Info + } + else { + Write-Log " Failed to set logon account (exit code: $LASTEXITCODE)" -Level Warning + } # Start the service if ($iossService.Status -ne 'Running') { @@ -828,19 +944,19 @@ function Set-IOSSServiceConfiguration { } } -function Reset-USBSensor { +function Find-SchickUSBDevices { <# .SYNOPSIS - Programmatically "replugs" the Schick USB sensor by disabling and re-enabling the device. - This eliminates the need to physically unplug and replug the sensor. + Shared helper to discover Schick/AE USB sensor devices + Used by Reset-USBSensor and Disable/Enable functions #> - - Write-Log "Resetting USB sensor (simulating unplug/replug)..." -Level Info + param( + [switch]$ActiveOnly # Only return devices with Status 'OK' + ) # Search patterns for Schick/AE USB devices $devicePatterns = @( "*Schick*", - "*CDR*", "*AE*USB*", "*Dental*Sensor*", "*FTDI*" # Common USB-serial chip used in dental sensors @@ -848,36 +964,57 @@ function Reset-USBSensor { $foundDevices = @() - # Find matching USB devices + # Find matching USB devices by friendly name foreach ($pattern in $devicePatterns) { - $devices = Get-PnpDevice -FriendlyName $pattern -ErrorAction SilentlyContinue | - Where-Object { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') } + $filter = { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') } + if ($ActiveOnly) { + $filter = { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') -and $_.Status -eq 'OK' } + } + $devices = Get-PnpDevice -FriendlyName $pattern -ErrorAction SilentlyContinue | Where-Object $filter if ($devices) { $foundDevices += $devices } } - # Also search by hardware ID patterns - $usbDevices = Get-PnpDevice -Class 'USB', 'Image', 'Ports' -ErrorAction SilentlyContinue - foreach ($device in $usbDevices) { + # Also search by hardware ID patterns (VID_0403=FTDI, VID_20D6=Schick) + $classFilter = if ($ActiveOnly) { + Get-PnpDevice -Class 'USB', 'Image', 'Ports' -Status 'OK' -ErrorAction SilentlyContinue + } + else { + Get-PnpDevice -Class 'USB', 'Image', 'Ports' -ErrorAction SilentlyContinue + } + + foreach ($device in $classFilter) { $hwIds = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue).Data - if ($hwIds -match 'VID_0403|VID_20D6|Schick|CDR') { - if ($device -notin $foundDevices) { + if ($hwIds -match 'VID_0403|VID_20D6|Schick') { + if ($device.InstanceId -notin $foundDevices.InstanceId) { $foundDevices += $device } } } + # Remove duplicates and return + return ($foundDevices | Select-Object -Unique) +} + +function Reset-USBSensor { + <# + .SYNOPSIS + Programmatically "replugs" the Schick USB sensor by disabling and re-enabling the device. + This eliminates the need to physically unplug and replug the sensor. + #> + + Write-Log "Resetting USB sensor (simulating unplug/replug)..." -Level Info + + $foundDevices = Find-SchickUSBDevices + if ($foundDevices.Count -eq 0) { Write-Log "No Schick/AE USB devices found to reset" -Level Warning Write-Log " Note: Device may need physical replug, or sensor not connected" -Level Warning return $false } - # Remove duplicates - $foundDevices = $foundDevices | Select-Object -Unique - Write-Log "Found $($foundDevices.Count) USB device(s) to reset:" -Level Info $resetSuccess = $true @@ -961,46 +1098,13 @@ function Disable-USBSensorForInstall { Write-Log "Checking for connected USB sensors to disable during installation..." -Level Info - # Search patterns for Schick/AE USB devices - $devicePatterns = @( - "*Schick*", - "*CDR*", - "*AE*USB*", - "*Dental*Sensor*", - "*FTDI*" - ) - - $Script:DisabledDevices = @() - - # Find matching USB devices - foreach ($pattern in $devicePatterns) { - $devices = Get-PnpDevice -FriendlyName $pattern -ErrorAction SilentlyContinue | - Where-Object { $_.Class -in @('USB', 'Image', 'Ports', 'HIDClass') -and $_.Status -eq 'OK' } - - if ($devices) { - $Script:DisabledDevices += $devices - } - } - - # Also search by hardware ID patterns - $usbDevices = Get-PnpDevice -Class 'USB', 'Image', 'Ports' -Status 'OK' -ErrorAction SilentlyContinue - foreach ($device in $usbDevices) { - $hwIds = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue).Data - if ($hwIds -match 'VID_0403|VID_20D6|Schick|CDR') { - if ($device.InstanceId -notin $Script:DisabledDevices.InstanceId) { - $Script:DisabledDevices += $device - } - } - } + $Script:DisabledDevices = Find-SchickUSBDevices -ActiveOnly if ($Script:DisabledDevices.Count -eq 0) { Write-Log " No active USB sensors detected - proceeding with installation" -Level Info return $true } - # Remove duplicates - $Script:DisabledDevices = $Script:DisabledDevices | Select-Object -Unique - Write-Log " Found $($Script:DisabledDevices.Count) USB sensor(s) to disable:" -Level Info foreach ($device in $Script:DisabledDevices) { @@ -1163,38 +1267,48 @@ function Invoke-LegacyInstallation { Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan Disable-USBSensorForInstall - # Step 1: MSXML 4.0 (if needed) - if ($Installers.ContainsKey('MSXML4')) { - if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + $installSuccess = $false + + try { + # Step 1: MSXML 4.0 (if needed) + if ($Installers.ContainsKey('MSXML4')) { + if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + return $false + } + } + + # Step 2: CDR Elite + if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { return $false } - } - # Step 2: CDR Elite - if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { - return $false - } + # Step 3: Rename CDRImageProcess.dll to .dllOLD (per Patterson docs) + Rename-CDRImageProcessDLL - # Step 3: Rename CDRImageProcess.dll to .dllOLD (per Patterson docs) - Rename-CDRImageProcessDLL + # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) + if ($Installers.ContainsKey('CDRPatch')) { + if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { + return $false + } + } - # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) - if ($Installers.ContainsKey('CDRPatch')) { - if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { + # Step 5: AE USB Driver + if (-not (Install-AEUSBDriver -InstallerPath $Installers.AEUSBDriver)) { return $false } - } - # Step 5: AE USB Driver - if (-not (Install-AEUSBDriver -InstallerPath $Installers.AEUSBDriver)) { - return $false + $installSuccess = $true + return $true } + finally { + # ALWAYS re-enable USB sensor, even if installation failed + Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan + Enable-USBSensorAfterInstall - # Step 6: Re-enable USB sensor (per Patterson: reconnect after driver install) - Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan - Enable-USBSensorAfterInstall - - return $true + if (-not $installSuccess) { + Write-Log "Installation failed but USB sensor has been re-enabled" -Level Warning + } + } } function Invoke-IOSSInstallation { @@ -1213,43 +1327,53 @@ function Invoke-IOSSInstallation { Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan Uninstall-LegacyComponents - # Step 1: MSXML 4.0 (if needed) - if ($Installers.ContainsKey('MSXML4')) { - if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + $installSuccess = $false + + try { + # Step 1: MSXML 4.0 (if needed) + if ($Installers.ContainsKey('MSXML4')) { + if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { + return $false + } + } + + # Step 2: CDR Elite (installs base DLLs: CDRData.dll, OMEGADLL.dll) + if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { return $false } - } - # Step 2: CDR Elite (installs base DLLs: CDRData.dll, OMEGADLL.dll) - if (-not (Install-CDRElite -InstallerPath $Installers.CDRElite)) { - return $false - } + # Step 3: Clean Shared Files (preserve CDRData.dll and OMEGADLL.dll only) + if (-not (Clear-SharedFilesForIOSS)) { + return $false + } - # Step 3: Clean Shared Files (preserve CDRData.dll and OMEGADLL.dll only) - if (-not (Clear-SharedFilesForIOSS)) { - return $false - } + # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) + if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { + return $false + } - # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) - if (-not (Install-CDRPatch -InstallerPath $Installers.CDRPatch)) { - return $false - } + # Step 5: IOSS Autorun + if (-not (Install-IOSS -InstallerPath $Installers.IOSS)) { + return $false + } - # Step 5: IOSS Autorun - if (-not (Install-IOSS -InstallerPath $Installers.IOSS)) { - return $false - } + # Step 6: Configure IOSS Service (delayed start, recovery options, Local System) + if (-not (Set-IOSSServiceConfiguration)) { + Write-Log "Service configuration had issues - manual review recommended" -Level Warning + } - # Step 6: Configure IOSS Service (delayed start, recovery options, Local System) - if (-not (Set-IOSSServiceConfiguration)) { - Write-Log "Service configuration had issues - manual review recommended" -Level Warning + $installSuccess = $true + return $true } + finally { + # ALWAYS re-enable USB sensor, even if installation failed + Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan + Enable-USBSensorAfterInstall - # Step 7: Re-enable USB sensor (per Patterson: reconnect after IOSS install) - Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan - Enable-USBSensorAfterInstall - - return $true + if (-not $installSuccess) { + Write-Log "Installation failed but USB sensor has been re-enabled" -Level Warning + } + } } function Export-Results { @@ -1280,11 +1404,11 @@ $Script:Results = @{ Write-Host @" -╔═══════════════════════════════════════════════════════════════════╗ -║ Schick Sensor Deployment Script v1.0.0 ║ -║ ║ -║ CDR Elite 5.16 + IOSS/Legacy Auto-Detection ║ -╚═══════════════════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════╗ +║ Schick Sensor Deployment Script v1.0.0 ║ +║ ║ +║ CDR Elite 5.16 + IOSS/Legacy Auto-Detection ║ +╚═════════════════════════════════════════════════════════════════════╝ "@ -ForegroundColor Cyan @@ -1355,14 +1479,14 @@ try { # Step 9: Final summary Write-Host "" if ($validationPassed) { - Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green - Write-Host " INSTALLATION COMPLETED SUCCESSFULLY" -ForegroundColor Green - Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Green + Write-Host "═════════════════════════════════════════════════════════════════════" -ForegroundColor Green + Write-Host " INSTALLATION COMPLETED SUCCESSFULLY " -ForegroundColor Green + Write-Host "═════════════════════════════════════════════════════════════════════" -ForegroundColor Green } else { - Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Yellow - Write-Host " INSTALLATION COMPLETED WITH WARNINGS - Review validation above" -ForegroundColor Yellow - Write-Host "═══════════════════════════════════════════════════════════════════" -ForegroundColor Yellow + Write-Host "═════════════════════════════════════════════════════════════════════" -ForegroundColor Yellow + Write-Host " INSTALLATION COMPLETED WITH WARNINGS - Review validation above " -ForegroundColor Yellow + Write-Host "═════════════════════════════════════════════════════════════════════" -ForegroundColor Yellow } $Script:Results.EndTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" From 5f2d969c89f0260fd997c5a587cd08bc4ed8c560 Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:15:26 -0500 Subject: [PATCH 3/6] Fix critical pipeline, exit code, and filter bugs from CodeRabbit review Resolves second round of CodeRabbit review comments on PR #42. CRITICAL - Pipeline output pollution (script always exited 0): Helper functions (Disable-USBSensorForInstall, Enable-USBSensorAfterInstall, Uninstall-LegacyComponents, Rename-CDRImageProcessDLL) emitted boolean return values to the pipeline without capture, causing $installSuccess to become an array instead of a boolean. The -not check always evaluated to $false, meaning installation failures were never detected and the script exited 0 regardless. Fixed by prefixing all helper calls with $null = and explicitly checking Rename-CDRImageProcessDLL result to fail the installation on error. CRITICAL - Install-IOSS returned $true on all exit codes: Non-zero exit codes logged a warning but still returned $true, silently swallowing installation failures. Now only accepts exit codes 0, 3010, and 1641 (same pattern as the previously fixed Install-AEUSBDriver). MAJOR - Publisher filter was completely broken: Nested ForEach-Object rebound $_ from the product object to the $validPublishers string, causing .Publisher to always return $null. Every product failed the publisher check, defeating the safety filter. Fixed by capturing $productPublisher = $_.Publisher before the inner loop. Minor fixes: - Regex typo: \.\u00a7 (section sign) replaced with \.+$ (end-of-string anchor) so trailing dots are actually stripped during version string cleanup - PowerShell version check: -lt [Version]'5.1' instead of Major -lt 5 to correctly reject PowerShell 5.0 which lacks required PnP/CIM cmdlets - MSXML4 registry: Now searches both 64-bit and WOW6432Node hives to detect 32-bit installs and avoid unnecessary reinstalls - Removed unused AEUSBFirmware config entry (defined but never downloaded) - CDRImageProcess.dll now validated in both Legacy and IOSS modes since CDR Patch creates it in both installation paths - Results Log switched from array += O(n^2) to Generic.List[object].Add() O(1) --- app-eaglesoft/Deploy-SchickSensor | 72 +++++++++++++++++-------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor index 0717986..a334d8d 100644 --- a/app-eaglesoft/Deploy-SchickSensor +++ b/app-eaglesoft/Deploy-SchickSensor @@ -81,11 +81,6 @@ $Script:Config = @{ FileName = "AEUSBInterfaceSetup.exe" SHA256 = $null # TODO: Populate after first verified download } - AEUSBFirmware = @{ - Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AE_USB_Firmware_Upgrade%5B1%5D.exe" - FileName = "AE_USB_Firmware_Upgrade.exe" - SHA256 = $null # TODO: Populate after first verified download - } IOSS = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/IOSS_v3.2/IOSS_v3.2/Autorun.exe" FileName = "IOSS_Autorun.exe" @@ -143,12 +138,12 @@ function Write-Log { Write-Host "$timestamp $prefix $Message" -ForegroundColor $color - # Add to results log - $Script:Results.Log += @{ + # Add to results log (using .Add() for O(1) performance) + $Script:Results.Log.Add(@{ Timestamp = $timestamp Level = $Level Message = $Message - } + }) } #endregion @@ -213,7 +208,7 @@ function Get-EaglesoftVersion { } # Try cleaning the version string - $cleanVersion = $versionString -replace '[^0-9.]', '' -replace '\.+', '.' -replace '^\.|\.§', '' + $cleanVersion = $versionString -replace '[^0-9.]', '' -replace '\.+', '.' -replace '^\.|\.+$', '' if ([Version]::TryParse($cleanVersion, [ref]$parsedVersion)) { Write-Log " Parsed cleaned version: $cleanVersion" -Level Info return $parsedVersion @@ -265,9 +260,13 @@ function Get-CurrentInstallState { Where-Object { $_.DeviceName -like "*AE*USB*" -or $_.Description -like "*Schick*" } $state.AEUSBDriverInstalled = $null -ne $aeDriver - # Check MSXML 4.0 - $msxml = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -like "*MSXML 4*" } + # Check MSXML 4.0 (search both 64-bit and 32-bit registry hives) + $msxml = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) | ForEach-Object { Get-ItemProperty $_ -ErrorAction SilentlyContinue } | + Where-Object { $_.DisplayName -like "*MSXML 4*" } | + Select-Object -First 1 $state.MSXML4Installed = $null -ne $msxml # Check Core Isolation (Memory Integrity) @@ -321,9 +320,9 @@ function Test-Prerequisites { $passed = $true $checks = @() - # Check PowerShell version - if ($PSVersionTable.PSVersion.Major -lt 5) { - $checks += @{ Name = "PowerShell 5.1+"; Status = "FAIL"; Message = "PowerShell $($PSVersionTable.PSVersion) detected" } + # Check PowerShell version (5.1 minimum for PnP cmdlets and CIM) + if ($PSVersionTable.PSVersion -lt [Version]'5.1') { + $checks += @{ Name = "PowerShell 5.1+"; Status = "FAIL"; Message = "PowerShell $($PSVersionTable.PSVersion) detected - 5.1 required" } $passed = $false } else { @@ -666,8 +665,9 @@ function Uninstall-LegacyComponents { foreach ($pattern in $legacyProducts) { $products = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + $productPublisher = $_.Publisher $_.DisplayName -like $pattern -and - ($validPublishers | ForEach-Object { $_.Publisher -like $_ }) -contains $true + ($validPublishers | ForEach-Object { $productPublisher -like $_ }) -contains $true } if ($products) { $foundProducts += $products @@ -863,14 +863,21 @@ function Install-IOSS { $process = Start-Process -FilePath $InstallerPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow - if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { - Write-Log "IOSS installed successfully" -Level Success + # Known acceptable exit codes + $acceptableExitCodes = @(0, 3010, 1641) + + if ($process.ExitCode -in $acceptableExitCodes) { + if ($process.ExitCode -eq 0) { + Write-Log "IOSS installed successfully" -Level Success + } + else { + Write-Log "IOSS installed (exit code $($process.ExitCode) - reboot may be required)" -Level Success + } return $true } else { - Write-Log "IOSS installation returned exit code: $($process.ExitCode)" -Level Warning - # Continue anyway - service configuration will verify - return $true + Write-Log "IOSS installation failed with exit code: $($process.ExitCode)" -Level Error + return $false } } @@ -1203,11 +1210,8 @@ function Test-Installation { $validation.Success = $false } - # Check required DLLs - $requiredDLLs = @("CDRData.dll", "OMEGADLL.dll") - if ($Mode -eq 'IOSS') { - $requiredDLLs += "CDRImageProcess.dll" - } + # Check required DLLs (CDRImageProcess.dll created by CDR Patch in both modes) + $requiredDLLs = @("CDRData.dll", "OMEGADLL.dll", "CDRImageProcess.dll") foreach ($dll in $requiredDLLs) { $dllPath = Join-Path $Script:Config.Paths.SharedFiles $dll @@ -1265,7 +1269,7 @@ function Invoke-LegacyInstallation { # Step 0b: Disable USB sensor if connected (per Patterson: "disconnect all Schick equipment") Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan - Disable-USBSensorForInstall + $null = Disable-USBSensorForInstall $installSuccess = $false @@ -1283,7 +1287,9 @@ function Invoke-LegacyInstallation { } # Step 3: Rename CDRImageProcess.dll to .dllOLD (per Patterson docs) - Rename-CDRImageProcessDLL + if (-not (Rename-CDRImageProcessDLL)) { + return $false + } # Step 4: CDR Patch (creates correct CDRImageProcess.dll v5.15.1877) if ($Installers.ContainsKey('CDRPatch')) { @@ -1303,7 +1309,7 @@ function Invoke-LegacyInstallation { finally { # ALWAYS re-enable USB sensor, even if installation failed Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan - Enable-USBSensorAfterInstall + $null = Enable-USBSensorAfterInstall if (-not $installSuccess) { Write-Log "Installation failed but USB sensor has been re-enabled" -Level Warning @@ -1321,11 +1327,11 @@ function Invoke-IOSSInstallation { # Step 0b: Disable USB sensor if connected (per Patterson: "Unplug USB cable from Schick remote") Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan - Disable-USBSensorForInstall + $null = Disable-USBSensorForInstall # Step 0c: Uninstall legacy CDR components (required for IOSS per Patterson docs) Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan - Uninstall-LegacyComponents + $null = Uninstall-LegacyComponents $installSuccess = $false @@ -1368,7 +1374,7 @@ function Invoke-IOSSInstallation { finally { # ALWAYS re-enable USB sensor, even if installation failed Write-Host "`n=== Post-Installation: Reconnecting Sensor ===" -ForegroundColor Cyan - Enable-USBSensorAfterInstall + $null = Enable-USBSensorAfterInstall if (-not $installSuccess) { Write-Log "Installation failed but USB sensor has been re-enabled" -Level Warning @@ -1399,7 +1405,7 @@ $Script:Results = @{ Success = $false Prerequisites = @() Validation = @{} - Log = @() + Log = [System.Collections.Generic.List[object]]::new() } Write-Host @" From 833d371be55d9f1aee9e11c56bf67cd13f6d93b9 Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:02:33 -0500 Subject: [PATCH 4/6] Fix device safety, download resilience, and uninstall parsing per CodeRabbit review Addresses five issues identified in the third round of automated code review on PR #42 to harden reliability and prevent unintended side effects. Device deduplication: - Replace Select-Object -Unique with Sort-Object -Property InstanceId -Unique in Find-SchickUSBDevices, as Select-Object uses reference equality on CimInstance objects and silently fails to deduplicate Download resilience: - Add retry loop (3 attempts) with exponential backoff (2s, 4s) to Get-Installer so transient network failures don't abort the entire deployment - Clean up partial downloads between retry attempts UninstallString argument parsing: - Parse quoted and unquoted exe paths from embedded arguments before passing to Start-Process, fixing cases where registry UninstallString values like '"C:\path\uninst.exe" /I{GUID}' were passed whole as the -FilePath parameter FTDI device matching scope: - Remove broad *FTDI* friendly name search pattern that could match Arduino boards, lab equipment, and other unrelated VID_0403 devices - VID_0403 hardware ID matches now require the device FriendlyName to contain Schick, Dental, AE, or Sensor as a secondary filter - VID_20D6 (Schick) continues to match unconditionally USB sensor safety in IOSS flow: - Move Uninstall-LegacyComponents call inside the try/finally block in Invoke-IOSSInstallation so USB sensors are guaranteed to be re-enabled even if legacy component removal throws an exception --- app-eaglesoft/Deploy-SchickSensor | 106 +++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor index a334d8d..a0926a1 100644 --- a/app-eaglesoft/Deploy-SchickSensor +++ b/app-eaglesoft/Deploy-SchickSensor @@ -475,25 +475,40 @@ function Get-Installer { Write-Log "Downloading $Name from $url" -Level Info - try { - $ProgressPreference = 'SilentlyContinue' - Invoke-WebRequest -Uri $url -OutFile $destination -UseBasicParsing -TimeoutSec 300 + $maxRetries = 3 + $retryDelay = 2 # seconds, doubles each retry (exponential backoff) - $fileSize = (Get-Item $destination).Length - Write-Log "Downloaded $Name successfully ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Success + for ($attempt = 1; $attempt -le $maxRetries; $attempt++) { + try { + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $url -OutFile $destination -UseBasicParsing -TimeoutSec 300 - # Verify hash of newly downloaded file - if (-not (Test-InstallerHash -FilePath $destination -ExpectedHash $expectedHash -Name $Name)) { - Write-Log "Downloaded file failed integrity check - removing" -Level Error - Remove-Item -Path $destination -Force - return $null - } + $fileSize = (Get-Item $destination).Length + Write-Log "Downloaded $Name successfully ($([math]::Round($fileSize/1MB, 2)) MB)" -Level Success - return $destination - } - catch { - Write-Log "Failed to download $Name`: $_" -Level Error - return $null + # Verify hash of newly downloaded file + if (-not (Test-InstallerHash -FilePath $destination -ExpectedHash $expectedHash -Name $Name)) { + Write-Log "Downloaded file failed integrity check - removing" -Level Error + Remove-Item -Path $destination -Force + return $null + } + + return $destination + } + catch { + if ($attempt -lt $maxRetries) { + $backoff = $retryDelay * [math]::Pow(2, $attempt - 1) + Write-Log "Download attempt $attempt/$maxRetries failed for $Name - retrying in ${backoff}s..." -Level Warning + Start-Sleep -Seconds $backoff + # Clean up partial download + if (Test-Path $destination) { Remove-Item -Path $destination -Force -ErrorAction SilentlyContinue } + } + else { + Write-Log "Failed to download $Name after $maxRetries attempts: $_" -Level Error + if (Test-Path $destination) { Remove-Item -Path $destination -Force -ErrorAction SilentlyContinue } + return $null + } + } } } @@ -708,9 +723,28 @@ function Uninstall-LegacyComponents { Write-Log " Uninstalled via MSI" -Level Success } elseif ($uninstallString) { - # EXE uninstall - try silent - $uninstallString = $uninstallString -replace '"', '' - Start-Process -FilePath $uninstallString -ArgumentList "/S /SILENT /VERYSILENT /NORESTART" -Wait -NoNewWindow -ErrorAction SilentlyContinue + # EXE uninstall - parse exe path from embedded arguments + # Handles: "C:\path\uninstall.exe" /arg1 /arg2 + # C:\path\uninstall.exe /arg1 /arg2 + $exePath = $null + $embeddedArgs = "" + + if ($uninstallString -match '^"([^"]+)"\s*(.*)$') { + # Quoted path: "C:\path\exe" args... + $exePath = $Matches[1] + $embeddedArgs = $Matches[2] + } + elseif ($uninstallString -match '^(\S+\.exe)\s*(.*)$') { + # Unquoted path ending in .exe + $exePath = $Matches[1] + $embeddedArgs = $Matches[2] + } + else { + $exePath = $uninstallString -replace '"', '' + } + + $silentArgs = "$embeddedArgs /S /SILENT /VERYSILENT /NORESTART".Trim() + Start-Process -FilePath $exePath -ArgumentList $silentArgs -Wait -NoNewWindow -ErrorAction SilentlyContinue Write-Log " Uninstall attempted" -Level Info } } @@ -961,12 +995,13 @@ function Find-SchickUSBDevices { [switch]$ActiveOnly # Only return devices with Status 'OK' ) - # Search patterns for Schick/AE USB devices + # Search patterns for Schick/AE USB devices (friendly name) + # Note: FTDI (VID_0403) is matched by hardware ID below, not by friendly name, + # to avoid disabling unrelated FTDI devices (Arduino, lab equipment, etc.) $devicePatterns = @( "*Schick*", "*AE*USB*", - "*Dental*Sensor*", - "*FTDI*" # Common USB-serial chip used in dental sensors + "*Dental*Sensor*" ) $foundDevices = @() @@ -984,7 +1019,9 @@ function Find-SchickUSBDevices { } } - # Also search by hardware ID patterns (VID_0403=FTDI, VID_20D6=Schick) + # Also search by hardware ID patterns + # VID_20D6 = Schick (always match) + # VID_0403 = FTDI (only match if device description mentions Schick/AE/Dental to avoid Arduino etc.) $classFilter = if ($ActiveOnly) { Get-PnpDevice -Class 'USB', 'Image', 'Ports' -Status 'OK' -ErrorAction SilentlyContinue } @@ -994,15 +1031,23 @@ function Find-SchickUSBDevices { foreach ($device in $classFilter) { $hwIds = (Get-PnpDeviceProperty -InstanceId $device.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue).Data - if ($hwIds -match 'VID_0403|VID_20D6|Schick') { + if ($hwIds -match 'VID_20D6|Schick') { if ($device.InstanceId -notin $foundDevices.InstanceId) { $foundDevices += $device } } + elseif ($hwIds -match 'VID_0403') { + # FTDI device - only include if description suggests dental/Schick hardware + if ($device.FriendlyName -match 'Schick|Dental|AE.*USB|Sensor') { + if ($device.InstanceId -notin $foundDevices.InstanceId) { + $foundDevices += $device + } + } + } } - # Remove duplicates and return - return ($foundDevices | Select-Object -Unique) + # Remove duplicates by InstanceId (Select-Object -Unique is unreliable for CimInstance) + return ($foundDevices | Sort-Object -Property InstanceId -Unique) } function Reset-USBSensor { @@ -1329,13 +1374,14 @@ function Invoke-IOSSInstallation { Write-Host "`n=== Pre-Installation: Disconnecting Sensor ===" -ForegroundColor Cyan $null = Disable-USBSensorForInstall - # Step 0c: Uninstall legacy CDR components (required for IOSS per Patterson docs) - Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan - $null = Uninstall-LegacyComponents - $installSuccess = $false try { + # Step 0c: Uninstall legacy CDR components (required for IOSS per Patterson docs) + # Inside try/finally to guarantee USB sensor re-enablement on failure + Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan + $null = Uninstall-LegacyComponents + # Step 1: MSXML 4.0 (if needed) if ($Installers.ContainsKey('MSXML4')) { if (-not (Install-MSXML4 -InstallerPath $Installers.MSXML4)) { From d7f1816912e36910a720c24e5e9c2f85e64d006e Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:01:58 -0500 Subject: [PATCH 5/6] Harden error propagation, download integrity, and device matching precision Addresses five issues from the fourth round of automated code review on PR #42, focused on ensuring failures are never silently swallowed. Device pattern precision: - Tighten "*AE*USB*" friendly name pattern to "*AE USB*" in Find-SchickUSBDevices to prevent false matches on unrelated hardware such as "Creative AE-5 USB Audio" sound cards Download integrity retry: - Hash verification failures after successful download now continue the retry loop with exponential backoff instead of returning null immediately - Covers transient CDN corruption, truncated responses, and edge proxies serving stale content that may resolve on subsequent attempts Uninstall failure propagation: - Uninstall-LegacyComponents now captures exit codes from both MSI and EXE uninstall processes, counts failures per product, and returns $false when any component fails to remove - Invoke-IOSSInstallation checks the return value and halts installation, since Patterson documentation requires clean removal of legacy CDR/Schick components before IOSS can be installed Service configuration failure tracking: - Set-IOSSServiceConfiguration tracks sc.exe non-zero exit codes and service start failures via $configSuccess flag, returning the actual result instead of unconditional $true - Startup type and logon account failures are now logged at Error level rather than Warning to reflect their operational impact SHA256 hash pre-flight awareness: - Config comments updated from TODO to ACTION REQUIRED with instructions for populating hashes via Get-FileHash on a trusted machine - New prerequisite check enumerates all installers with null SHA256 fields and emits a WARN-level alert listing which downloads will proceed without integrity verification --- app-eaglesoft/Deploy-SchickSensor | 87 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor index a0926a1..28e33f7 100644 --- a/app-eaglesoft/Deploy-SchickSensor +++ b/app-eaglesoft/Deploy-SchickSensor @@ -65,31 +65,33 @@ param( # ============================================================================ $Script:Config = @{ # Full download URLs with SHA256 hashes for integrity verification + # To populate hashes: download each installer on a trusted machine, then run: + # (Get-FileHash -Path ".\filename.exe" -Algorithm SHA256).Hash Downloads = @{ CDRElite = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/CDR%20Elite%20Setup.exe" FileName = "CDR Elite Setup.exe" - SHA256 = $null # TODO: Populate after first verified download + SHA256 = $null # ACTION REQUIRED: Populate from verified download } CDRPatch = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/Patch/CDRPatch-2808.msi" FileName = "CDRPatch-2808.msi" - SHA256 = $null # TODO: Populate after first verified download + SHA256 = $null # ACTION REQUIRED: Populate from verified download } AEUSBDriver = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AEUSBInterfaceSetup.exe" FileName = "AEUSBInterfaceSetup.exe" - SHA256 = $null # TODO: Populate after first verified download + SHA256 = $null # ACTION REQUIRED: Populate from verified download } IOSS = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/IOSS_v3.2/IOSS_v3.2/Autorun.exe" FileName = "IOSS_Autorun.exe" - SHA256 = $null # TODO: Populate after first verified download + SHA256 = $null # ACTION REQUIRED: Populate from verified download } MSXML4 = @{ Url = "https://download.microsoft.com/download/1/E/E/1EE06E22-A56F-4E76-B6F6-E7670B4F8163/msxml4-KB2758694-enu.exe" FileName = "msxml4-KB2758694-enu.exe" - SHA256 = $null # TODO: Populate after first verified download + SHA256 = $null # ACTION REQUIRED: Populate from verified download } } @@ -375,6 +377,19 @@ function Test-Prerequisites { $checks += @{ Name = "Core Isolation"; Status = "PASS"; Message = "Memory Integrity disabled" } } + # Check SHA256 hash configuration + $missingHashes = $Script:Config.Downloads.GetEnumerator() | + Where-Object { [string]::IsNullOrEmpty($_.Value.SHA256) } | + ForEach-Object { $_.Key } + if ($missingHashes.Count -gt 0) { + $hashList = $missingHashes -join ', ' + $checks += @{ Name = "SHA256 Hashes"; Status = "WARN"; Message = "Missing for: $hashList - downloads will not be integrity-verified" } + Write-Log "WARNING: SHA256 hashes not configured for: $hashList. Run Get-FileHash on verified installers to populate." -Level Warning + } + else { + $checks += @{ Name = "SHA256 Hashes"; Status = "PASS"; Message = "All installer hashes configured" } + } + # Check RDP session if ($Script:CurrentState.RDPSession) { $checks += @{ Name = "RDP Session"; Status = "WARN"; Message = "Running via RDP - USB devices may not be visible" } @@ -488,8 +503,15 @@ function Get-Installer { # Verify hash of newly downloaded file if (-not (Test-InstallerHash -FilePath $destination -ExpectedHash $expectedHash -Name $Name)) { - Write-Log "Downloaded file failed integrity check - removing" -Level Error - Remove-Item -Path $destination -Force + Write-Log "Downloaded file failed integrity check" -Level Error + Remove-Item -Path $destination -Force -ErrorAction SilentlyContinue + if ($attempt -lt $maxRetries) { + $backoff = $retryDelay * [math]::Pow(2, $attempt - 1) + Write-Log "Hash mismatch on attempt $attempt/$maxRetries - retrying in ${backoff}s..." -Level Warning + Start-Sleep -Seconds $backoff + continue + } + Write-Log "Failed hash verification for $Name after $maxRetries attempts" -Level Error return $null } @@ -710,6 +732,8 @@ function Uninstall-LegacyComponents { Write-Log " Found $($foundProducts.Count) legacy component(s) to uninstall:" -Level Info + $uninstallFailures = 0 + foreach ($product in $foundProducts) { Write-Log " - $($product.DisplayName)" -Level Info @@ -719,8 +743,14 @@ function Uninstall-LegacyComponents { # MSI uninstall $productCode = $product.PSChildName $arguments = "/x $productCode /qn /norestart" - Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -NoNewWindow - Write-Log " Uninstalled via MSI" -Level Success + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010 -or $proc.ExitCode -eq 1641) { + Write-Log " Uninstalled via MSI (exit code: $($proc.ExitCode))" -Level Success + } + else { + Write-Log " MSI uninstall returned exit code: $($proc.ExitCode)" -Level Warning + $uninstallFailures++ + } } elseif ($uninstallString) { # EXE uninstall - parse exe path from embedded arguments @@ -744,15 +774,31 @@ function Uninstall-LegacyComponents { } $silentArgs = "$embeddedArgs /S /SILENT /VERYSILENT /NORESTART".Trim() - Start-Process -FilePath $exePath -ArgumentList $silentArgs -Wait -NoNewWindow -ErrorAction SilentlyContinue - Write-Log " Uninstall attempted" -Level Info + $proc = Start-Process -FilePath $exePath -ArgumentList $silentArgs -Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue + if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) { + Write-Log " Uninstalled (exit code: $($proc.ExitCode))" -Level Success + } + else { + Write-Log " Uninstall returned exit code: $($proc.ExitCode)" -Level Warning + $uninstallFailures++ + } + } + else { + Write-Log " No uninstall string found" -Level Warning + $uninstallFailures++ } } catch { Write-Log " Failed to uninstall: $_" -Level Warning + $uninstallFailures++ } } + if ($uninstallFailures -gt 0) { + Write-Log " $uninstallFailures of $($foundProducts.Count) legacy component(s) failed to uninstall" -Level Error + return $false + } + return $true } @@ -931,6 +977,7 @@ function Set-IOSSServiceConfiguration { } $serviceName = $iossService.Name + $configSuccess = $true try { # Set delayed auto-start @@ -939,7 +986,8 @@ function Set-IOSSServiceConfiguration { Write-Log " Set startup type: Delayed Auto-Start" -Level Info } else { - Write-Log " Failed to set startup type (exit code: $LASTEXITCODE)" -Level Warning + Write-Log " Failed to set startup type (exit code: $LASTEXITCODE)" -Level Error + $configSuccess = $false } # Set recovery options: restart on first, second, and subsequent failures @@ -957,7 +1005,8 @@ function Set-IOSSServiceConfiguration { Write-Log " Set logon account: Local System" -Level Info } else { - Write-Log " Failed to set logon account (exit code: $LASTEXITCODE)" -Level Warning + Write-Log " Failed to set logon account (exit code: $LASTEXITCODE)" -Level Error + $configSuccess = $false } # Start the service @@ -970,14 +1019,15 @@ function Set-IOSSServiceConfiguration { Write-Log " Service started successfully" -Level Success } else { - Write-Log " Service not running - Status: $($iossService.Status)" -Level Warning + Write-Log " Service not running - Status: $($iossService.Status)" -Level Error + $configSuccess = $false } } else { Write-Log " Service already running" -Level Info } - return $true + return $configSuccess } catch { Write-Log "Error configuring IOSS service: $_" -Level Error @@ -1000,7 +1050,7 @@ function Find-SchickUSBDevices { # to avoid disabling unrelated FTDI devices (Arduino, lab equipment, etc.) $devicePatterns = @( "*Schick*", - "*AE*USB*", + "*AE USB*", # Tightened: no inner wildcard to avoid "Creative AE-5 USB Audio" etc. "*Dental*Sensor*" ) @@ -1380,7 +1430,10 @@ function Invoke-IOSSInstallation { # Step 0c: Uninstall legacy CDR components (required for IOSS per Patterson docs) # Inside try/finally to guarantee USB sensor re-enablement on failure Write-Host "`n=== Removing Legacy Components ===" -ForegroundColor Cyan - $null = Uninstall-LegacyComponents + if (-not (Uninstall-LegacyComponents)) { + Write-Log "Legacy component removal failed - IOSS installation cannot proceed" -Level Error + return $false + } # Step 1: MSXML 4.0 (if needed) if ($Installers.ContainsKey('MSXML4')) { From 063731a618cb68abf0c9ec7044760a9d79d7faec Mon Sep 17 00:00:00 2001 From: Zach Boogher <129975920+AlrightLad@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:44:18 -0500 Subject: [PATCH 6/6] Populate all SHA256 hashes and fix dead MSXML4 download URL Completes installer integrity verification by populating SHA256 hashes from user-verified downloads against the Backblaze B2 bucket and Microsoft Download Center. SHA256 hashes populated: - CDRElite: C4CA8729EFBAAD03EFE58C52398B0EC8FF5A6BD50F999672D5F700C999F9DD31 - CDRPatch: 1E2F4B768AFD485F730F3F74D98996B80C761A2D0F676097573B15AB414E4148 - AEUSBDriver: C74779873D125EF6175D00AD52FA07277535F89D6415CC7207D76AA120D545A5 - IOSS: 9A3C29D60EF7BF8076E6BF94E5B4D8659A5C730F5B87D61C84A55BF9E293249A - MSXML4: 52D80E6E7BA94437199A2E6B51F00C7F483269C02BEDEC2253C4602F5209B31B MSXML4 download URL fix: - Previous URL (/download/1/E/E/1EE06E22-A56F-4E76-B6F6-E7670B4F8163/...) now returns HTTP 404 from Microsoft servers - Updated to current working URL from Microsoft Download Center: /download/a/7/6/a7611ffc-4f68-4fb1-a931-95882ec013fc/msxml4-KB2758694-enu.exe With all hashes populated, the prerequisite SHA256 check now passes cleanly without requiring -DownloadOnly bypass, and all downloaded installers will be cryptographically verified before execution. --- app-eaglesoft/Deploy-SchickSensor | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app-eaglesoft/Deploy-SchickSensor b/app-eaglesoft/Deploy-SchickSensor index 28e33f7..523b2bb 100644 --- a/app-eaglesoft/Deploy-SchickSensor +++ b/app-eaglesoft/Deploy-SchickSensor @@ -71,27 +71,28 @@ $Script:Config = @{ CDRElite = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/CDR%20Elite%20Setup.exe" FileName = "CDR Elite Setup.exe" - SHA256 = $null # ACTION REQUIRED: Populate from verified download + SHA256 = "C4CA8729EFBAAD03EFE58C52398B0EC8FF5A6BD50F999672D5F700C999F9DD31" } CDRPatch = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/CDRElite5_16/CDRElite/Patch/CDRPatch-2808.msi" FileName = "CDRPatch-2808.msi" - SHA256 = $null # ACTION REQUIRED: Populate from verified download + SHA256 = "1E2F4B768AFD485F730F3F74D98996B80C761A2D0F676097573B15AB414E4148" } AEUSBDriver = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/AEUSBInterfaceSetup.exe" FileName = "AEUSBInterfaceSetup.exe" - SHA256 = $null # ACTION REQUIRED: Populate from verified download + SHA256 = "C74779873D125EF6175D00AD52FA07277535F89D6415CC7207D76AA120D545A5" } IOSS = @{ Url = "https://s3.us-west-002.backblazeb2.com/public-dtc/repo/vendors/Patterson-Eaglesoft/IOSS_v3.2/IOSS_v3.2/Autorun.exe" FileName = "IOSS_Autorun.exe" - SHA256 = $null # ACTION REQUIRED: Populate from verified download + SHA256 = "9A3C29D60EF7BF8076E6BF94E5B4D8659A5C730F5B87D61C84A55BF9E293249A" } MSXML4 = @{ - Url = "https://download.microsoft.com/download/1/E/E/1EE06E22-A56F-4E76-B6F6-E7670B4F8163/msxml4-KB2758694-enu.exe" + # Updated URL - previous /download/1/E/E/... path returns 404 + Url = "https://download.microsoft.com/download/a/7/6/a7611ffc-4f68-4fb1-a931-95882ec013fc/msxml4-KB2758694-enu.exe" FileName = "msxml4-KB2758694-enu.exe" - SHA256 = $null # ACTION REQUIRED: Populate from verified download + SHA256 = "52D80E6E7BA94437199A2E6B51F00C7F483269C02BEDEC2253C4602F5209B31B" } } @@ -383,8 +384,16 @@ function Test-Prerequisites { ForEach-Object { $_.Key } if ($missingHashes.Count -gt 0) { $hashList = $missingHashes -join ', ' - $checks += @{ Name = "SHA256 Hashes"; Status = "WARN"; Message = "Missing for: $hashList - downloads will not be integrity-verified" } - Write-Log "WARNING: SHA256 hashes not configured for: $hashList. Run Get-FileHash on verified installers to populate." -Level Warning + if ($DownloadOnly) { + # Allow download-only mode so operators can retrieve files and compute hashes + $checks += @{ Name = "SHA256 Hashes"; Status = "WARN"; Message = "Missing for: $hashList - use -DownloadOnly then Get-FileHash to populate" } + Write-Log "SHA256 hashes not configured for: $hashList. Download-only mode permitted to generate hashes." -Level Warning + } + else { + $checks += @{ Name = "SHA256 Hashes"; Status = "FAIL"; Message = "Missing for: $hashList - downloads cannot be integrity-verified" } + Write-Log "SHA256 hashes not configured for: $hashList. Run with -DownloadOnly first, then use Get-FileHash to populate config." -Level Error + $passed = $false + } } else { $checks += @{ Name = "SHA256 Hashes"; Status = "PASS"; Message = "All installer hashes configured" } @@ -997,6 +1006,7 @@ function Set-IOSSServiceConfiguration { } else { Write-Log " Failed to set recovery options (exit code: $LASTEXITCODE)" -Level Warning + $configSuccess = $false } # Ensure running as Local System