Fixing the repos
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
User-scope removal of the ClickOnce app "BPC Q-Pulse 7.1" without admin rights.
|
||||
|
||||
.DESCRIPTION
|
||||
- Detects HKCU Uninstall and the user's Start Menu/Desktop .appref-ms for "BPC Q-Pulse 7.1".
|
||||
- If UninstallString exists (HKCU ARP), runs it (user-context).
|
||||
- Force-removes: user's Start Menu + user Desktop shortcut(s), user ClickOnce cache (%LOCALAPPDATA%\Apps\2.0),
|
||||
and HKCU Uninstall key(s) — all user-scope.
|
||||
- Purges ClickOnce Deployment hives in HKCU that reference the app/URL (user-scope).
|
||||
- Public Desktop shortcut is report-only (no delete) to avoid admin rights.
|
||||
- Dry-run by default; use -Execute to apply.
|
||||
- -DetectOnly returns 0 if nothing is found (inverse detect), 1 if found.
|
||||
|
||||
.EXITCODES
|
||||
0 = Success (removed or nothing to do) / DetectOnly: nothing found
|
||||
1 = DetectOnly: something found
|
||||
2 = Not found (normal flow)
|
||||
4 = Partial cleanup
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[switch]$Execute,
|
||||
[switch]$PurgeAllClickOnceCache, # user-safe, optional
|
||||
[switch]$DetectOnly,
|
||||
[string]$LogPath # passed in from SYSTEM script (e.g., C:\Windows\Logs\Software\UserCleanup-*.log)
|
||||
)
|
||||
|
||||
# --- Severity helper: map strings to PSADT numeric codes (1=Info, 2=Warning, 3=Error) ---
|
||||
function Convert-Severity {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Level
|
||||
)
|
||||
switch ($Level.ToUpperInvariant()) {
|
||||
'INFORMATION' { return 1 }
|
||||
'INFO' { return 1 }
|
||||
'WARNING' { return 2 }
|
||||
'ERROR' { return 3 }
|
||||
default { return 1 } # Fallback to Information
|
||||
}
|
||||
}
|
||||
|
||||
# --- Minimal logger if PSADT isn't loaded in this user process ---
|
||||
# Note: This shim accepts the numeric Severity (UInt32) to match PSADT v4.
|
||||
if (-not (Get-Command Write-ADTLogEntry -ErrorAction SilentlyContinue)) {
|
||||
function Write-ADTLogEntry {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Message,
|
||||
[Parameter(Mandatory)][uint32]$Severity # 1=Info, 2=Warning, 3=Error
|
||||
)
|
||||
$sevLabel = switch ($Severity) { 1 { 'Information' } 2 { 'Warning' } 3 { 'Error' } default { 'Information' } }
|
||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
$line = "[$ts][$sevLabel] $Message"
|
||||
if ($LogPath) {
|
||||
try { Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 } catch { Write-Output $line }
|
||||
} else {
|
||||
Write-Output $line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Targets (user-scope only) ----
|
||||
$DisplayNameTarget = 'BPC Q-Pulse 7.1' # For HKCU ARP & manifests
|
||||
$ShortcutTarget = 'BPC Q-Pulse 7.1.appref-ms' # Exact filename for Start Menu/Desktop
|
||||
|
||||
# ---- Helpers (HKCU + user profile only) ----
|
||||
function Get-ClickOnceUninstallEntries {
|
||||
$base = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
|
||||
if (-not (Test-Path $base)) { return @() }
|
||||
Get-ChildItem $base -ErrorAction SilentlyContinue | 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-UserStartMenuApprefShortcuts {
|
||||
$root = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
|
||||
if (-not (Test-Path $root)) { return @() }
|
||||
|
||||
$found = @()
|
||||
$hits = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ieq $ShortcutTarget }
|
||||
if ($hits) { $found += $hits }
|
||||
|
||||
# Fallback: content match for renamed shortcuts
|
||||
$all = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue
|
||||
foreach ($f in $all) {
|
||||
try {
|
||||
$raw = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop
|
||||
if ($raw -like "*$DisplayNameTarget*") {
|
||||
if (-not ($found.FullName -contains $f.FullName)) { $found += $f }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return $found
|
||||
}
|
||||
|
||||
function Find-DesktopApprefShortcuts {
|
||||
# User desktops only + report-only Public Desktop (no delete)
|
||||
$desktops = @(
|
||||
(Join-Path $env:USERPROFILE 'Desktop')
|
||||
)
|
||||
if ($env:OneDrive) { $desktops += (Join-Path $env:OneDrive 'Desktop') }
|
||||
try {
|
||||
$odDirs = Get-ChildItem -Path $env:USERPROFILE -Directory -Filter 'OneDrive*' -ErrorAction SilentlyContinue
|
||||
foreach ($d in $odDirs) { $desktops += (Join-Path $d.FullName 'Desktop') }
|
||||
} catch {}
|
||||
# Add Public Desktop only for reporting
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$desktopsForDelete = $desktops | Sort-Object -Unique
|
||||
$desktopsForReport = $desktopsForDelete + $publicDesktop
|
||||
|
||||
$report = @()
|
||||
foreach ($desk in ($desktopsForReport | Sort-Object -Unique)) {
|
||||
if (-not (Test-Path $desk)) { continue }
|
||||
$hits = Get-ChildItem -LiteralPath $desk -Filter '*.appref-ms' -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ieq $ShortcutTarget -or $_.Name -like "*$DisplayNameTarget*.appref-ms" }
|
||||
if ($hits) { $report += $hits }
|
||||
}
|
||||
|
||||
# Return both sets (user-delete set separately calculated at call site)
|
||||
return [pscustomobject]@{
|
||||
Report = $report
|
||||
DeleteCandidatesRootPaths = $desktopsForDelete
|
||||
}
|
||||
}
|
||||
|
||||
function Read-ApprefDetails {
|
||||
param([string]$ApprefPath)
|
||||
$raw = Get-Content -LiteralPath $ApprefPath -Raw -ErrorAction SilentlyContinue
|
||||
if (-not $raw) { return $null }
|
||||
$m = [regex]::Match($raw, 'deploymentProvider\s+url\s*=\s*["'']?([^"''\r\n]+)["'']?', 'IgnoreCase')
|
||||
$url = if ($m.Success) { $m.Groups[1].Value } else { $null }
|
||||
[pscustomobject]@{ ApprefPath = $ApprefPath; DeploymentUrl = $url }
|
||||
}
|
||||
|
||||
# Manifest-guided cache removal (user-scope)
|
||||
function Remove-ClickOnceCacheForApp {
|
||||
param(
|
||||
[string]$NameHint,
|
||||
[string]$UrlHint
|
||||
)
|
||||
|
||||
$cacheRoot = Join-Path $env:LOCALAPPDATA 'Apps\2.0'
|
||||
if (-not (Test-Path $cacheRoot)) {
|
||||
Write-ADTLogEntry -Message "ClickOnce cache root not found: $cacheRoot" -Severity (Convert-Severity 'Information')
|
||||
return $true
|
||||
}
|
||||
|
||||
$hostToken = $null
|
||||
if ($UrlHint) {
|
||||
try {
|
||||
$u = [uri]$UrlHint
|
||||
if ($u.Host) { $hostToken = $u.Host.ToLowerInvariant() }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$manifestPatterns = @('*.application','*.manifest')
|
||||
$topFoldersToDelete = New-Object System.Collections.Generic.HashSet[string]
|
||||
|
||||
Write-ADTLogEntry -Message "Scanning manifests under $cacheRoot for app references…" -Severity (Convert-Severity 'Information')
|
||||
|
||||
Get-ChildItem -Path $cacheRoot -Recurse -Include $manifestPatterns -File -Force -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
$f = $_.FullName
|
||||
$raw = $null
|
||||
try { $raw = Get-Content -LiteralPath $f -Raw -ErrorAction Stop } catch { return }
|
||||
if (-not $raw) { return }
|
||||
|
||||
$hit = $false
|
||||
if ($hostToken -and ($raw.ToLowerInvariant() -like "*$hostToken*")) { $hit = $true }
|
||||
elseif ($NameHint -and ($raw -like "*$NameHint*")) { $hit = $true }
|
||||
|
||||
if ($hit) {
|
||||
# climb to first-level child of Apps\2.0 and remove that
|
||||
$parent = Split-Path $f -Parent
|
||||
while ($parent -and (Split-Path $parent -Parent) -and ((Split-Path $parent -Parent) -ne $cacheRoot)) {
|
||||
$parent = Split-Path $parent -Parent
|
||||
}
|
||||
if ($parent -and (Split-Path $parent -Parent) -eq $cacheRoot) {
|
||||
[void]$topFoldersToDelete.Add($parent)
|
||||
} else {
|
||||
[void]$topFoldersToDelete.Add((Split-Path $f -Parent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($topFoldersToDelete.Count -eq 0) {
|
||||
# Conservative fallback tokens, still user-scope only
|
||||
$fallbackTokens = @('bpc','qpulse','q','pulse','71')
|
||||
Write-ADTLogEntry -Message "No manifest-linked folders found; falling back to token match on folder names." -Severity (Convert-Severity 'Warning')
|
||||
Get-ChildItem -Path $cacheRoot -Recurse -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$n = ($_.FullName -replace '[^a-zA-Z0-9]','').ToLowerInvariant()
|
||||
foreach ($t in $fallbackTokens) {
|
||||
if ($t -and $n -like "*$t*") { [void]$topFoldersToDelete.Add($_.FullName); break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($topFoldersToDelete.Count -eq 0) {
|
||||
Write-ADTLogEntry -Message "No candidate ClickOnce cache folders matched." -Severity (Convert-Severity 'Warning')
|
||||
return $true
|
||||
}
|
||||
|
||||
$ok = $true
|
||||
foreach ($t in $topFoldersToDelete) {
|
||||
if ($PSCmdlet.ShouldProcess($t, "Remove ClickOnce cache (top hashed folder)")) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $t -Recurse -Force -ErrorAction Stop
|
||||
Write-ADTLogEntry -Message "Removed cache folder: $t" -Severity (Convert-Severity 'Information')
|
||||
} catch {
|
||||
Write-ADTLogEntry -Message "Failed to remove cache folder: $t : $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
|
||||
$ok = $false
|
||||
}
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would remove cache folder: $t (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
return $ok
|
||||
}
|
||||
|
||||
# Purge ClickOnce Deployment hives (HKCU only)
|
||||
function Remove-ClickOnceDeploymentRegistryRefs {
|
||||
param(
|
||||
[string]$NameHint,
|
||||
[string]$UrlHint
|
||||
)
|
||||
|
||||
$roots = @(
|
||||
'HKCU:\Software\Classes\Software\Microsoft\Windows\CurrentVersion\Deployment',
|
||||
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Deployment'
|
||||
)
|
||||
|
||||
$tokens = @()
|
||||
if ($NameHint) { $tokens += $NameHint }
|
||||
if ($UrlHint) { $tokens += $UrlHint }
|
||||
if ($tokens.Count -eq 0) { return $true }
|
||||
|
||||
$removedAny = $false
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
Write-ADTLogEntry -Message "Scanning ClickOnce deployment hive: $root" -Severity (Convert-Severity 'Information')
|
||||
|
||||
$keys = Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue
|
||||
foreach ($k in $keys) {
|
||||
$hit = $false
|
||||
try {
|
||||
$props = Get-ItemProperty -Path $k.PSPath -ErrorAction SilentlyContinue
|
||||
if ($props) {
|
||||
foreach ($p in $props.PSObject.Properties) {
|
||||
$val = [string]$p.Value
|
||||
foreach ($t in $tokens) {
|
||||
if ($t -and $val -and ($val -like "*$t*")) { $hit = $true; break }
|
||||
}
|
||||
if ($hit) { break }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if ($hit) {
|
||||
if ($PSCmdlet.ShouldProcess($k.PSPath, "Remove ClickOnce deployment key (HKCU)")) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $k.PSPath -Recurse -Force -ErrorAction Stop
|
||||
Write-ADTLogEntry -Message "Removed deployment key: $($k.PSPath)" -Severity (Convert-Severity 'Information')
|
||||
$removedAny = $true
|
||||
} catch {
|
||||
Write-ADTLogEntry -Message "Failed to remove deployment key: $($k.PSPath) : $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
|
||||
}
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would remove deployment key: $($k.PSPath) (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
$removedAny = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $removedAny
|
||||
}
|
||||
|
||||
function Invoke-RegisteredUninstall {
|
||||
param($Entry)
|
||||
if (-not $Entry.UninstallString) { return $true }
|
||||
|
||||
Write-ADTLogEntry -Message "Attempting registered uninstall (user-context): $($Entry.UninstallString)" -Severity (Convert-Severity 'Information')
|
||||
try {
|
||||
$cmd = $Entry.UninstallString.Trim()
|
||||
$file = $null; $args = ""
|
||||
if ($cmd.StartsWith('"')) {
|
||||
$end = $cmd.IndexOf('"',1)
|
||||
if ($end -lt 1) { throw "Malformed quoted path in UninstallString." }
|
||||
$file = $cmd.Substring(1, $end-1)
|
||||
$args = $cmd.Substring($end+1).Trim()
|
||||
} else {
|
||||
$m = [regex]::Match($cmd, '^\S+')
|
||||
$file = $m.Value
|
||||
$args = $cmd.Substring($file.Length).Trim()
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($file, "Start-Process $args")) {
|
||||
$p = Start-Process -FilePath $file -ArgumentList $args -PassThru -WindowStyle Hidden
|
||||
$p.WaitForExit()
|
||||
Write-ADTLogEntry -Message "Registered uninstall exited with code $($p.ExitCode)" -Severity (Convert-Severity 'Information')
|
||||
return ($p.ExitCode -eq 0)
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would run registered uninstall (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-ADTLogEntry -Message "Registered uninstall failed: $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# ---- Main (user-scope only) ----
|
||||
$partial = $false
|
||||
$notFound = $false
|
||||
if (-not $Execute) { $WhatIfPreference = $true; Write-ADTLogEntry -Message "Dry run. Pass -Execute to perform removal." -Severity (Convert-Severity 'Warning') }
|
||||
|
||||
Write-ADTLogEntry -Message "Targeting ClickOnce app '$DisplayNameTarget' (shortcut '$ShortcutTarget') for user '$env:USERNAME'..." -Severity (Convert-Severity 'Information')
|
||||
|
||||
$entries = @(Get-ClickOnceUninstallEntries)
|
||||
$bestEntry = Select-BestEntry -Entries $entries
|
||||
if (-not $bestEntry) { Write-ADTLogEntry -Message "No HKCU ARP entry matched '$DisplayNameTarget'." -Severity (Convert-Severity 'Information') }
|
||||
|
||||
$userStartMenu = @(Find-UserStartMenuApprefShortcuts)
|
||||
if ($userStartMenu.Count -gt 0) {
|
||||
Write-ADTLogEntry -Message ("User Start Menu shortcut(s): " + ($userStartMenu.FullName -join '; ')) -Severity (Convert-Severity 'Information')
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "No user Start Menu .appref-ms found for '$ShortcutTarget'." -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
|
||||
$deskObj = Find-DesktopApprefShortcuts
|
||||
$deskShortcuts = @($deskObj.Report)
|
||||
if ($deskShortcuts.Count -gt 0) {
|
||||
Write-ADTLogEntry -Message ("Desktop shortcuts (report-only; includes Public): " + ($deskShortcuts.FullName -join '; ')) -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
|
||||
# Deployment URL hint from any user Start Menu shortcut
|
||||
$urlHint = $null
|
||||
if ($userStartMenu.Count -gt 0) {
|
||||
$appref = Read-ApprefDetails -ApprefPath $userStartMenu[0].FullName
|
||||
if ($appref -and $appref.DeploymentUrl) {
|
||||
$urlHint = $appref.DeploymentUrl
|
||||
Write-ADTLogEntry -Message "Deployment URL hint: $urlHint" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
|
||||
# Inverse detect
|
||||
if (-not $bestEntry -and $userStartMenu.Count -eq 0 -and $deskShortcuts.Count -eq 0) {
|
||||
Write-ADTLogEntry -Message "No matching ClickOnce install or shortcuts detected (user-scope)." -Severity (Convert-Severity 'Information')
|
||||
if ($DetectOnly) { Write-ADTLogEntry -Message "DetectOnly: nothing found -> 0" -Severity (Convert-Severity 'Information'); exit 0 }
|
||||
$notFound = $true
|
||||
} elseif ($DetectOnly) {
|
||||
Write-ADTLogEntry -Message "DetectOnly: found something -> 1" -Severity (Convert-Severity 'Information')
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Graceful uninstall (HKCU ARP)
|
||||
if ($bestEntry) {
|
||||
Write-ADTLogEntry -Message "HKCU Uninstall match: $($bestEntry.DisplayName) Version: $($bestEntry.DisplayVersion)" -Severity (Convert-Severity 'Information')
|
||||
if (-not (Invoke-RegisteredUninstall -Entry $bestEntry)) { $partial = $true }
|
||||
}
|
||||
|
||||
# Remove user Start Menu shortcuts (user-scope)
|
||||
foreach ($sc in $userStartMenu) {
|
||||
if ($PSCmdlet.ShouldProcess($sc.FullName, "Remove user .appref-ms shortcut")) {
|
||||
try { Remove-Item -LiteralPath $sc.FullName -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed: $($sc.FullName)" -Severity (Convert-Severity 'Information') }
|
||||
catch { Write-ADTLogEntry -Message "Failed to remove $($sc.FullName): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would remove: $($sc.FullName) (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
|
||||
# Remove user Desktop shortcuts only under user-owned desktops (no Public Desktop)
|
||||
foreach ($desk in ($deskObj.DeleteCandidatesRootPaths | Sort-Object -Unique)) {
|
||||
if (-not (Test-Path $desk)) { continue }
|
||||
$hits = Get-ChildItem -LiteralPath $desk -Filter '*.appref-ms' -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -ieq $ShortcutTarget -or $_.Name -like "*$DisplayNameTarget*.appref-ms" }
|
||||
foreach ($h in $hits) {
|
||||
if ($PSCmdlet.ShouldProcess($h.FullName, "Remove user Desktop .appref-ms shortcut")) {
|
||||
try { Remove-Item -LiteralPath $h.FullName -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed: $($h.FullName)" -Severity (Convert-Severity 'Information') }
|
||||
catch { Write-ADTLogEntry -Message "Failed to remove $($h.FullName): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would remove: $($h.FullName) (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Remove ALL matching HKCU ARP entries
|
||||
$allArp = @(Get-ClickOnceUninstallEntries)
|
||||
foreach ($arp in $allArp) {
|
||||
if ($PSCmdlet.ShouldProcess($arp.KeyPath, "Remove HKCU Uninstall entry")) {
|
||||
try { Remove-Item -LiteralPath $arp.KeyPath -Recurse -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed HKCU ARP: $($arp.DisplayName)" -Severity (Convert-Severity 'Information') }
|
||||
catch { Write-ADTLogEntry -Message "Failed to remove HKCU ARP at $($arp.KeyPath): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would remove HKCU ARP at $($arp.KeyPath) (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
|
||||
# ClickOnce deployment hives cleanup (HKCU)
|
||||
$depCleaned = Remove-ClickOnceDeploymentRegistryRefs -NameHint $DisplayNameTarget -UrlHint $urlHint
|
||||
if (-not $depCleaned) { Write-ADTLogEntry -Message "No deployment keys matched (may already be clean)." -Severity (Convert-Severity 'Information') }
|
||||
|
||||
# Cache cleanup (manifest-guided; %LOCALAPPDATA%\Apps\2.0)
|
||||
$cacheOk = Remove-ClickOnceCacheForApp -NameHint $DisplayNameTarget -UrlHint $urlHint
|
||||
if (-not $cacheOk) { $partial = $true }
|
||||
|
||||
# Optional: dfshim cache purge (user-context safe)
|
||||
if ($PurgeAllClickOnceCache) {
|
||||
Write-ADTLogEntry -Message "Purging user ClickOnce cache via dfshim (user-context)..." -Severity (Convert-Severity 'Warning')
|
||||
if ($PSCmdlet.ShouldProcess("rundll32.exe", "dfshim.dll,ShArpMaintain CleanOnlineAppCache")) {
|
||||
try {
|
||||
Start-Process -FilePath "rundll32.exe" -ArgumentList "dfshim.dll,ShArpMaintain CleanOnlineAppCache" -Wait -WindowStyle Hidden
|
||||
Write-ADTLogEntry -Message "dfshim cache purge attempted." -Severity (Convert-Severity 'Information')
|
||||
} catch {
|
||||
Write-ADTLogEntry -Message "dfshim cache purge failed: $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
|
||||
$partial = $true
|
||||
}
|
||||
} else {
|
||||
Write-ADTLogEntry -Message "Would purge dfshim cache (WhatIf)" -Severity (Convert-Severity 'Information')
|
||||
}
|
||||
}
|
||||
|
||||
Write-ADTLogEntry -Message ("Summary (user-scope): ARP entries removed=" + $allArp.Count +
|
||||
"; User StartMenu removed=" + $userStartMenu.Count +
|
||||
"; Desktop shortcuts reported (incl. Public)=" + $deskShortcuts.Count +
|
||||
"; Cache removal OK=" + $cacheOk) -Severity (Convert-Severity 'Information')
|
||||
|
||||
if ($partial) { exit 4 }
|
||||
elseif ($notFound) { exit 2 }
|
||||
else { exit 0 }
|
||||
257
Q-Pulse to QMS Migration/1.0/Files/QMS_ClickOnce_Uninstall.ps1
Normal file
257
Q-Pulse to QMS Migration/1.0/Files/QMS_ClickOnce_Uninstall.ps1
Normal file
@@ -0,0 +1,257 @@
|
||||
<#
|
||||
.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 }
|
||||
BIN
Q-Pulse to QMS Migration/1.0/Files/Thumbs.db
Normal file
BIN
Q-Pulse to QMS Migration/1.0/Files/Thumbs.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user