Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Private/Test-AppxInstalledForAnyUser.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
function Test-AppxInstalledForAnyUser {
<#
.SYNOPSIS
True when an MSIX package is actually installed for at least one user.

.DESCRIPTION
Get-AppxPackage -AllUsers lists packages that are merely Staged on the
machine as well as ones a user has installed, so presence in that list
is not evidence of installation.

Removal is also asynchronous: a package stays listed for a period after
Remove-AppxPackage returns. Callers should therefore poll this over a
settle window rather than treat a single immediate result as final.

PackageUserInformation carries the per-user InstallState, which is the
distinction that matters. Where it is unavailable - not elevated, or an
older build - this falls back to a plain Get-AppxPackage, which lists
only what is installed for the caller.
#>
[CmdletBinding()]
param(
[Parameter(ParameterSetName = 'ByObject')]
$Package,

[Parameter(ParameterSetName = 'ByName')]
[string]$PackageFullName
)

if (-not $Package -and $PackageFullName) {
# Name is the segment before the first underscore of the full name, and
# filtering on it avoids enumerating every package on the machine.
$ShortName = $PackageFullName.Split('_')[0]
try {
$Package = Get-AppxPackage -AllUsers -Name $ShortName -ErrorAction Stop |
Where-Object { $_.PackageFullName -eq $PackageFullName } |
Select-Object -First 1
} catch { }

if (-not $Package) {
try {
$Package = Get-AppxPackage -Name $ShortName -ErrorAction SilentlyContinue |
Where-Object { $_.PackageFullName -eq $PackageFullName } |
Select-Object -First 1
} catch { }
}
}

# Not present at all, so certainly not installed.
if (-not $Package) { return $false }

$Info = $null
try { $Info = $Package.PackageUserInformation } catch { }

if ($Info) {
foreach ($User in $Info) {
if ("$($User.InstallState)" -eq 'Installed') { return $true }
}
return $false
}

try {
$Mine = Get-AppxPackage -ErrorAction SilentlyContinue |
Where-Object { $_.PackageFullName -eq $Package.PackageFullName }
return [bool]$Mine
} catch {
return $false
}
}
105 changes: 97 additions & 8 deletions Public/Uninstall-TecharyApp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,115 @@ function Uninstall-TecharyApp {
# 2. IF NOT IN REGISTRY, CHECK MSIX (Modern Apps)
if (-not $App) {
Write-PackagerLog -Message "Not found in Registry. Checking Modern Apps (MSIX)..."
$MsixResults = Get-AppxPackage -Name "*$Name*" -ErrorAction SilentlyContinue

if ($MsixResults) {
# FIX: Handle cases where multiple apps match (Array vs Single Object)
# SYSTEM has essentially no packages of its own, so a plain
# Get-AppxPackage run from an RMM found nothing and reported the app as
# absent. -AllUsers is what makes this work in the context the module is
# actually driven from. It needs elevation, so fall back without it.
$IsElevated = $false
try {
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$IsElevated = (New-Object Security.Principal.WindowsPrincipal($Identity)).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
} catch {}

$MsixResults = @()
if ($IsElevated) {
# Filtered to packages actually installed for someone. -AllUsers also
# lists packages that are merely Staged on the machine, and a staged
# package remains listed after a successful removal, so an unfiltered
# list makes a completed uninstall look like it did nothing.
try { $MsixResults = @(Get-AppxPackage -AllUsers -Name "*$Name*" -ErrorAction Stop |
Where-Object { Test-AppxInstalledForAnyUser -Package $_ }) }
catch {
Write-PackagerLog -Message "Could not enumerate packages for all users ($($_.Exception.Message)). Falling back to the current user." -Severity Warning
$MsixResults = @(Get-AppxPackage -Name "*$Name*" -ErrorAction SilentlyContinue)
}
} else {
Write-PackagerLog -Message "Not elevated, so only the current user's packages are visible." -Severity Warning
$MsixResults = @(Get-AppxPackage -Name "*$Name*" -ErrorAction SilentlyContinue)
}

# A provisioned package is what seeds new user profiles. Leaving it in
# place meant a removed app reappeared for the next user who signed in.
$Provisioned = @()
if ($IsElevated) {
try {
$Provisioned = @(Get-AppxProvisionedPackage -Online -ErrorAction Stop |
Where-Object { $_.DisplayName -like "*$Name*" })
} catch {
Write-PackagerLog -Message "Could not enumerate provisioned packages: $($_.Exception.Message)" -Severity Warning
}
}

if ($MsixResults.Count -gt 0 -or $Provisioned.Count -gt 0) {
# Handle cases where multiple apps match (Array vs Single Object)
foreach ($Package in $MsixResults) {
Write-PackagerLog -Message "Found Modern App: $($Package.Name)"
Write-PackagerLog -Message "Found Modern App: $($Package.Name) ($($Package.PackageFullName))"

if ($WhatIf) {
$Scope = if ($IsElevated) { "for all users" } else { "for the current user only" }
Write-Host "[WhatIf] Would remove $Scope`: $($Package.PackageFullName)" -ForegroundColor Yellow
continue
}

try {
if ($IsElevated) {
Remove-AppxPackage -Package $Package.PackageFullName -AllUsers -ErrorAction Stop
$Scope = "for all users"
} else {
Remove-AppxPackage -Package $Package.PackageFullName -ErrorAction Stop
$Scope = "for the current user"
}

# Confirm rather than infer, but give it time to settle.
# Removal completes asynchronously: the package is still
# listed for a short period after Remove-AppxPackage returns,
# so an immediate check reports a false failure.
$Deadline = (Get-Date).AddSeconds(30)
$StillThere = $true
while ($StillThere -and (Get-Date) -lt $Deadline) {
$StillThere = Test-AppxInstalledForAnyUser -PackageFullName $Package.PackageFullName
if ($StillThere) { Start-Sleep -Seconds 2 }
}

if ($StillThere) {
# Not asserted as a failure: removal may still be pending.
# Reported so it is visible rather than assumed successful.
Write-PackagerLog -Message "Removed $($Package.Name) $Scope, but it is still registered after 30s. Removal may be pending a reboot or sign-out." -Severity Warning
} else {
Write-PackagerLog -Message "Success: Removed $($Package.Name) $Scope, confirmed."
}
}
catch {
# -AllUsers is unsupported on some builds. A per-user removal
# still beats reporting an outright failure.
Write-PackagerLog -Message "All-users removal failed for $($Package.Name): $($_.Exception.Message). Retrying for the current user." -Severity Warning
try {
Remove-AppxPackage -Package $Package.PackageFullName -ErrorAction Stop
Write-PackagerLog -Message "Success: Removed $($Package.Name) for the current user."
}
catch {
Write-PackagerLog -Message "Failed to remove $($Package.Name): $($_.Exception.Message)" -Severity Error
}
}
}

foreach ($Prov in $Provisioned) {
if ($WhatIf) {
Write-Host "[WhatIf] Would remove: $($Package.PackageFullName)" -ForegroundColor Yellow
Write-Host "[WhatIf] Would deprovision: $($Prov.PackageName)" -ForegroundColor Yellow
continue
}

try {
Remove-AppxPackage -Package $Package.PackageFullName -ErrorAction Stop
Write-PackagerLog -Message "Success: Removed $($Package.Name)"
Remove-AppxProvisionedPackage -Online -PackageName $Prov.PackageName -ErrorAction Stop | Out-Null
Write-PackagerLog -Message "Deprovisioned $($Prov.DisplayName), so it will not return for new users."
}
catch {
Write-PackagerLog -Message "Failed to remove $($Package.Name): $_" -Severity Error
Write-PackagerLog -Message "Failed to deprovision $($Prov.DisplayName): $($_.Exception.Message)" -Severity Error
}
}

return
}

Expand Down