258 lines
9.7 KiB
PowerShell
258 lines
9.7 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Remove the ClickOnce app "Quality Management (g12361)" for the current user.
|
|
|
|
.DESCRIPTION
|
|
- Detects HKCU Uninstall entries and Start Menu .appref-ms for "Quality Management (g12361)".
|
|
- If UninstallString exists (publisher enabled ARP), runs it.
|
|
- Force-removes: Start Menu shortcut(s), app cache under %LOCALAPPDATA%\Apps\2.0,
|
|
and the HKCU Uninstall key.
|
|
- Dry-run by default. Use -Execute to apply changes.
|
|
|
|
.EXITCODES
|
|
0 = Success (removed or nothing to do)
|
|
2 = Not found
|
|
4 = Partial cleanup
|
|
|
|
.NOTES
|
|
Run in the user's context (same user who installed the ClickOnce app).
|
|
#>
|
|
|
|
[CmdletBinding(SupportsShouldProcess)]
|
|
param(
|
|
[switch]$Execute,
|
|
[switch]$PurgeAllClickOnceCache
|
|
)
|
|
|
|
# ---- Constants ----
|
|
# Exact target
|
|
$DisplayNameTarget = 'Quality Management (g12361)'
|
|
|
|
# ---- Logging ----
|
|
function Write-Log { param([string]$Message,[string]$Level="INFO")
|
|
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
Write-Host "[$ts][$Level] $Message"
|
|
}
|
|
|
|
# ---- Helpers ----
|
|
function Get-ClickOnceUninstallEntries {
|
|
$base = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
|
|
if (-not (Test-Path $base)) { return @() }
|
|
Get-ChildItem $base | ForEach-Object {
|
|
$k = $_.PsPath
|
|
$p = Get-ItemProperty -Path $k -ErrorAction SilentlyContinue
|
|
if ($p -and $p.DisplayName -and ($p.DisplayName -like "*$DisplayNameTarget*")) {
|
|
$ver = try { [version]$p.DisplayVersion } catch { [version]'0.0.0.0' }
|
|
[pscustomobject]@{
|
|
KeyPath = $k
|
|
DisplayName = $p.DisplayName
|
|
Publisher = $p.Publisher
|
|
UninstallString = $p.UninstallString
|
|
URLUpdateInfo = $p.URLUpdateInfo
|
|
DisplayVersion = $p.DisplayVersion
|
|
VerObj = $ver
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Select-BestEntry {
|
|
param($Entries)
|
|
if (-not $Entries -or $Entries.Count -eq 0) { return $null }
|
|
$Entries |
|
|
Sort-Object `
|
|
@{ Expression = { $_.DisplayName.Length }; Descending = $true },
|
|
@{ Expression = { $_.VerObj }; Descending = $true } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
function Find-ApprefShortcut {
|
|
$paths = @(
|
|
"$env:APPDATA\Microsoft\Windows\Start Menu\Programs",
|
|
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs"
|
|
) | Select-Object -Unique
|
|
|
|
foreach ($root in $paths) {
|
|
if (-not (Test-Path $root)) { continue }
|
|
# 1) Name match
|
|
$byName = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.BaseName -like "*$DisplayNameTarget*" } |
|
|
Select-Object -First 1
|
|
if ($byName) { return $byName }
|
|
|
|
# 2) Content match (sometimes name differs)
|
|
$byContent = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue |
|
|
Where-Object {
|
|
try { (Get-Content -LiteralPath $_.FullName -Raw -ErrorAction Stop) -match [regex]::Escape($DisplayNameTarget) }
|
|
catch { $false }
|
|
} | Select-Object -First 1
|
|
if ($byContent) { return $byContent }
|
|
}
|
|
}
|
|
|
|
function Read-ApprefDetails {
|
|
param([string]$ApprefPath)
|
|
$raw = Get-Content -LiteralPath $ApprefPath -Raw -ErrorAction SilentlyContinue
|
|
if (-not $raw) { return $null }
|
|
# Handles quoted/unquoted:
|
|
# deploymentProvider url="https://example/app.application" OR without quotes
|
|
$pattern = 'deploymentProvider\s+url\s*=\s*"?([^"\r\n]+)"?'
|
|
$m = [regex]::Match($raw, $pattern, 'IgnoreCase')
|
|
$url = if ($m.Success) { $m.Groups[1].Value } else { $null }
|
|
[pscustomobject]@{ ApprefPath = $ApprefPath; DeploymentUrl = $url }
|
|
}
|
|
|
|
function Remove-ClickOnceCacheForApp {
|
|
param([string]$NameHint, [string]$UrlHint)
|
|
|
|
$cacheRoot = Join-Path $env:LOCALAPPDATA 'Apps\2.0'
|
|
if (-not (Test-Path $cacheRoot)) {
|
|
Write-Log "ClickOnce cache root not found: $cacheRoot" "WARN"
|
|
return $true
|
|
}
|
|
|
|
$patterns = @()
|
|
if ($NameHint) { $patterns += ($NameHint -replace '[^a-zA-Z0-9]+','') }
|
|
if ($UrlHint) { try { $uri = [uri]$UrlHint; if ($uri.Host) { $patterns += ($uri.Host -replace '[^a-zA-Z0-9]+','') } } catch {} }
|
|
if (-not $patterns -or $patterns.Count -eq 0) { $patterns = @('quality','management','g12361') }
|
|
|
|
Write-Log "Scanning ClickOnce cache at $cacheRoot for patterns: $($patterns -join ', ')"
|
|
$dirs = Get-ChildItem -Path $cacheRoot -Recurse -Directory -Force -ErrorAction SilentlyContinue
|
|
$targets = New-Object System.Collections.Generic.HashSet[string]
|
|
foreach ($d in $dirs) {
|
|
$n = ($d.FullName -replace '[^a-zA-Z0-9]','')
|
|
foreach ($p in $patterns) {
|
|
if ($p -and $n -match [regex]::Escape($p)) { [void]$targets.Add($d.FullName); break }
|
|
}
|
|
}
|
|
|
|
if ($targets.Count -eq 0) {
|
|
Write-Log "No app-specific cache paths matched. (Cache structure varies by build.)" "WARN"
|
|
return $true
|
|
}
|
|
|
|
$allOk = $true
|
|
foreach ($t in $targets) {
|
|
if (Test-Path $t) {
|
|
if ($PSCmdlet.ShouldProcess($t, "Remove")) {
|
|
try { Remove-Item -LiteralPath $t -Recurse -Force -ErrorAction Stop; Write-Log "Removed $t" }
|
|
catch { Write-Log "Failed to remove $t : $($_.Exception.Message)" "WARN"; $allOk = $false }
|
|
} else {
|
|
Write-Log "Would remove $t (WhatIf)"
|
|
}
|
|
}
|
|
}
|
|
return $allOk
|
|
}
|
|
|
|
function Invoke-RegisteredUninstall {
|
|
param($Entry)
|
|
if (-not $Entry.UninstallString) { return $true } # nothing to invoke; treat as neutral
|
|
|
|
Write-Log "Attempting registered uninstall: $($Entry.UninstallString)"
|
|
try {
|
|
$file,$args = $null,$null
|
|
if ($Entry.UninstallString.StartsWith('"')) {
|
|
$end = $Entry.UninstallString.IndexOf('"',1)
|
|
if ($end -lt 1) { throw "Malformed quoted path in UninstallString." }
|
|
$file = $Entry.UninstallString.Substring(1, $end-1)
|
|
$args = $Entry.UninstallString.Substring($end+1).Trim()
|
|
} else {
|
|
$parts = $Entry.UninstallString.Split(' ',2)
|
|
$file = $parts[0]
|
|
$args = if ($parts.Count -gt 1) { $parts[1] } else { "" }
|
|
}
|
|
if ($PSCmdlet.ShouldProcess($file, "Start-Process $args")) {
|
|
$p = Start-Process -FilePath $file -ArgumentList $args -PassThru -WindowStyle Hidden
|
|
$p.WaitForExit()
|
|
Write-Log "Registered uninstall exited with code $($p.ExitCode)"
|
|
return ($p.ExitCode -eq 0)
|
|
} else {
|
|
Write-Log "Would run registered uninstall (WhatIf)"
|
|
return $true
|
|
}
|
|
} catch {
|
|
Write-Log "Registered uninstall failed: $($_.Exception.Message)" "WARN"
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# ---- Main ----
|
|
$partial = $false
|
|
$notFound = $false
|
|
$doIt = $Execute.IsPresent
|
|
if (-not $doIt) { $WhatIfPreference = $true; Write-Log "Dry run. Pass -Execute to perform removal." "WARN" }
|
|
|
|
Write-Log "Targeting ClickOnce app '$DisplayNameTarget' for user '$env:USERNAME'..."
|
|
|
|
$entries = @(Get-ClickOnceUninstallEntries)
|
|
$entry = Select-BestEntry -Entries $entries
|
|
if (-not $entry) { Write-Log "No HKCU ARP entry matched '$DisplayNameTarget'." "WARN" }
|
|
|
|
$shortcut = Find-ApprefShortcut
|
|
$urlHint = $null
|
|
if ($shortcut) {
|
|
Write-Log "Found .appref-ms shortcut: $($shortcut.FullName)"
|
|
$appref = Read-ApprefDetails -ApprefPath $shortcut.FullName
|
|
if ($appref -and $appref.DeploymentUrl) {
|
|
Write-Log "Deployment URL (hint for cache cleanup): $($appref.DeploymentUrl)"
|
|
$urlHint = $appref.DeploymentUrl
|
|
}
|
|
} else {
|
|
Write-Log "No .appref-ms shortcut found in Start Menu paths." "WARN"
|
|
}
|
|
|
|
if (-not $entry -and -not $shortcut) {
|
|
Write-Log "No matching ClickOnce install detected."
|
|
$notFound = $true
|
|
}
|
|
|
|
# 1) Graceful uninstall if available
|
|
if ($entry) {
|
|
Write-Log "HKCU Uninstall match: $($entry.DisplayName) Version: $($entry.DisplayVersion)"
|
|
$ok = Invoke-RegisteredUninstall -Entry $entry
|
|
if (-not $ok) { $partial = $true }
|
|
}
|
|
|
|
# 2) Force-remove: Start Menu shortcut(s), cache, ARP key
|
|
if ($shortcut) {
|
|
if ($PSCmdlet.ShouldProcess($shortcut.FullName, "Remove .appref-ms shortcut")) {
|
|
try { Remove-Item -LiteralPath $shortcut.FullName -Force -ErrorAction Stop; Write-Log "Removed shortcut: $($shortcut.FullName)" }
|
|
catch { Write-Log "Failed to remove shortcut: $($_.Exception.Message)" "WARN"; $partial = $true }
|
|
} else {
|
|
Write-Log "Would remove shortcut: $($shortcut.FullName) (WhatIf)"
|
|
}
|
|
}
|
|
|
|
$cacheOk = Remove-ClickOnceCacheForApp -NameHint $DisplayNameTarget -UrlHint $urlHint
|
|
if (-not $cacheOk) { $partial = $true }
|
|
|
|
if ($entry) {
|
|
if ($PSCmdlet.ShouldProcess($entry.KeyPath, "Remove HKCU Uninstall entry")) {
|
|
try { Remove-Item -LiteralPath $entry.KeyPath -Recurse -Force -ErrorAction Stop; Write-Log "Removed HKCU uninstall entry." }
|
|
catch { Write-Log "Failed to remove HKCU uninstall entry: $($_.Exception.Message)" "WARN"; $partial = $true }
|
|
} else {
|
|
Write-Log "Would remove HKCU uninstall entry at $($entry.KeyPath) (WhatIf)"
|
|
}
|
|
}
|
|
|
|
# 3) Optional: purge entire user ClickOnce cache
|
|
if ($PurgeAllClickOnceCache) {
|
|
Write-Log "Purging entire ClickOnce cache for this user (dfshim)..." "WARN"
|
|
if ($PSCmdlet.ShouldProcess("dfshim", "CleanOnlineAppCache")) {
|
|
try {
|
|
Start-Process -FilePath "rundll32.exe" -ArgumentList "dfshim.dll,ShArpMaintain CleanOnlineAppCache" -Wait -WindowStyle Hidden
|
|
Write-Log "dfshim cache purge attempted."
|
|
} catch {
|
|
Write-Log "dfshim purge failed: $($_.Exception.Message)" "WARN"; $partial = $true
|
|
}
|
|
} else {
|
|
Write-Log "Would purge dfshim cache (WhatIf)"
|
|
}
|
|
}
|
|
|
|
Write-Log "Done."
|
|
if ($partial) { exit 4 }
|
|
elseif ($notFound) { exit 2 }
|
|
else { exit 0 }
|