Uninstall-ANWS.ps1
|
function Uninstall-ANWS { <# .SYNOPSIS Uninstalls the ActiveNet Workstation Service with hardened cleanup and corruption recovery. .DESCRIPTION Natively uninstalls the ActiveNet Workstation Service (ANWS) across Windows PowerShell 5.1 and PowerShell 7+ without external subprocess dependencies. Detects product registrations via both 64-bit and WOW6432Node uninstall registry hives. Includes automated self-healing for common MSI Error 1603 conditions by recreating service stubs if a prior corrupted uninstallation deleted the Windows service while leaving MSI metadata intact. Gracefully stops services, terminates locking Java/ActiveNet processes, executes silent MSI removal, and validates full post-uninstallation cleanup of orphaned services, registry keys, directories, and desktop shortcuts. .EXAMPLE PS> Uninstall-ANWS .EXAMPLE PS> Uninstall-ActiveNet -Verbose #> [CmdletBinding()] [Alias('Uninstall-ActiveNet')] param() $serviceName = 'activenetworkstationservice' $displayName = 'ActiveNet Workstation Service' $installPath = if (${env:ProgramFiles(x86)}) { Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath $displayName } else { Join-Path -Path $env:ProgramFiles -ChildPath $displayName } $msiLog = Join-Path -Path $env:TEMP -ChildPath 'ANWS_msi_uninstall.log' Write-Verbose "Beginning uninstallation workflow for $displayName..." # Step 1: Detect installation in registry (64-bit and WOW6432Node) $regPaths = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) $installedEntries = Get-ItemProperty -Path $regPaths -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and ($_.DisplayName -like "*$displayName*" -or $_.DisplayName -like "*ActiveNet Workstation*") } $productCodes = [System.Collections.Generic.List[string]]::new() foreach ($entry in $installedEntries) { Write-Verbose "Detected registry registration: '$($entry.DisplayName)' at $($entry.PSPath)" if ($entry.InstallLocation -and (Test-Path -Path $entry.InstallLocation)) { $installPath = $entry.InstallLocation } # Check for GUID product code in PSChildName or UninstallString if ($entry.PSChildName -match '^\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}$') { if (-not $productCodes.Contains($entry.PSChildName)) { $productCodes.Add($entry.PSChildName) } } elseif ($entry.UninstallString -match '\{[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}') { if (-not $productCodes.Contains($matches[0])) { $productCodes.Add($matches[0]) } } } # Step 2: Handle corrupted state (MSI registered but Windows service missing -> causes MSI Error 1603) $serviceExists = Get-Service -Name $serviceName -ErrorAction SilentlyContinue if ($productCodes.Count -gt 0 -and -not $serviceExists) { Write-Verbose "MSI product registered but Windows service '$serviceName' is missing. Recreating service stub to prevent MSI 1603..." $stubBinary = Join-Path -Path $installPath -ChildPath 'ActiveNet.ServiceContainer.exe' try { New-Service -Name $serviceName -DisplayName $displayName -BinaryPathName "`"$stubBinary`"" -StartupType Disabled -ErrorAction SilentlyContinue | Out-Null Write-Verbose "Dummy service stub successfully staged." } catch { Write-Verbose "Unable to create dummy service stub: $($_.Exception.Message)" } } # Step 3: Pre-uninstall process & service termination if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { Write-Verbose "Stopping service '$serviceName'..." Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue } Write-Verbose "Terminating potential locking processes..." $processNames = @('ActiveNet.ServiceContainer', 'ANWorkstationConfigurationUI', 'java', 'javaw') foreach ($pName in $processNames) { $foundProcs = Get-Process -Name $pName -ErrorAction SilentlyContinue foreach ($proc in $foundProcs) { $shouldKill = $false if ($pName -in @('java', 'javaw')) { try { $pPath = $proc.MainModule.FileName if ($pPath -and ($pPath -like "*$installPath*" -or $pPath -like "*ActiveNet*")) { $shouldKill = $true } } catch { # If process path cannot be inspected, kill if associated with ActiveNet directory $shouldKill = $true } } else { $shouldKill = $true } if ($shouldKill) { Write-Verbose "Terminating locking process '$($proc.ProcessName)' (PID: $($proc.Id))..." Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } } } # Step 4: Silent MSI uninstallation if ($productCodes.Count -gt 0) { foreach ($code in $productCodes) { Write-Verbose "Executing MSI uninstallation for ProductCode '$code'..." $msiArgs = @('/x', $code, '/qn', '/norestart', '/l*v', "`"$msiLog`"") $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru -NoNewWindow Write-Verbose "msiexec /x exited with code $($proc.ExitCode)." } } else { Write-Verbose "No active MSI ProductCode entries detected in registry. Proceeding to post-uninstall cleanup..." } # Step 5: Post-uninstall double check & purge # 5a. Remove service via sc.exe if still present if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { Write-Verbose "Service '$serviceName' still registered post-MSI. Deleting via sc.exe..." & sc.exe delete $serviceName 2>&1 | Out-Null } # 5b. Purge orphaned registry service key $serviceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" if (Test-Path -Path $serviceRegPath) { Write-Verbose "Purging orphaned service registry key at '$serviceRegPath'..." Remove-Item -Path $serviceRegPath -Recurse -Force -ErrorAction SilentlyContinue } # 5c. Wait for file handles to release and remove installation directory Start-Sleep -Seconds 2 if (Test-Path -Path $installPath) { Write-Verbose "Removing residual installation folder at '$installPath'..." Remove-Item -Path $installPath -Recurse -Force -ErrorAction SilentlyContinue } # 5d. Clean up desktop shortcuts and batch files $targetShortcutDirs = @( "$env:PUBLIC\Desktop", [Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('CommonDesktopDirectory') ) | Select-Object -Unique $cleanupFiles = @('Restart ActiveNet.lnk', 'restartanws.bat', 'ActiveNet.url') foreach ($dir in $targetShortcutDirs) { if (Test-Path -Path $dir) { foreach ($file in $cleanupFiles) { $itemPath = Join-Path -Path $dir -ChildPath $file if (Test-Path -Path $itemPath) { Write-Verbose "Removing shortcut/script file at '$itemPath'..." Remove-Item -Path $itemPath -Force -ErrorAction SilentlyContinue } } } } Write-Verbose "ActiveNet Workstation Service uninstallation and cleanup completed." } |