mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
Fix AppX detection and add package restore support (#4842)
* Fix installed AppX package detection * Add AppX package restore workflow * Add AppX operation progress feedback * Fix AppX operation launch race * Handle AppX workflow failures
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
function Get-WinUtilInstalledAPPX {
|
||||
<#
|
||||
|
||||
.SYNOPSIS
|
||||
Gets the names of AppX packages installed for all users
|
||||
|
||||
#>
|
||||
|
||||
# AppX module auto-loading can leave PowerShell 7 dependent on a temporary Windows PowerShell
|
||||
# compatibility proxy. Run the query in Windows PowerShell 5.1 so it remains available after
|
||||
# those temporary proxy files are removed.
|
||||
$ps5Command = {
|
||||
Get-AppxPackage -AllUsers -ErrorAction Stop | Select-Object -ExpandProperty Name
|
||||
}
|
||||
|
||||
$packageOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$failureDetails = ($packageOutput | Out-String).Trim()
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "Failed to get installed AppX packages: $failureDetails"
|
||||
return @()
|
||||
}
|
||||
|
||||
return @($packageOutput)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
function Install-WinUtilAPPX {
|
||||
<#
|
||||
|
||||
.SYNOPSIS
|
||||
Registers a local AppX package or installs it from the Microsoft Store
|
||||
|
||||
.PARAMETER Name
|
||||
The AppX package name to install
|
||||
|
||||
.PARAMETER StoreId
|
||||
The optional Microsoft Store product ID used when no local manifest is available
|
||||
|
||||
#>
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name,
|
||||
|
||||
[string]$StoreId
|
||||
)
|
||||
|
||||
Write-WinUtilLog -Component "AppX" -Message "Installing AppX package: $Name"
|
||||
|
||||
# AppX and DISM cmdlets are more reliable in Windows PowerShell 5.1. Query both installed and
|
||||
# provisioned package metadata because either can expose a local manifest that can be registered.
|
||||
$ps5Command = {
|
||||
$packageName = $args[0]
|
||||
$manifestPaths = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
Get-AppxPackage -AllUsers -Name $packageName -ErrorAction SilentlyContinue |
|
||||
Sort-Object -Property Version -Descending |
|
||||
ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_.InstallLocation)) {
|
||||
$manifestPaths.Add((Join-Path $_.InstallLocation "AppxManifest.xml"))
|
||||
}
|
||||
}
|
||||
|
||||
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
|
||||
Where-Object DisplayName -EQ $packageName |
|
||||
ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_.InstallLocation)) {
|
||||
$manifestPaths.Add((Join-Path $_.InstallLocation "AppxManifest.xml"))
|
||||
}
|
||||
}
|
||||
|
||||
$manifestPath = $manifestPaths |
|
||||
Select-Object -Unique |
|
||||
Where-Object { Test-Path -LiteralPath $_ } |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($null -ne $manifestPath) {
|
||||
Add-AppxPackage -Register $manifestPath -DisableDevelopmentMode -ErrorAction Stop
|
||||
Write-Output $manifestPath
|
||||
}
|
||||
}
|
||||
|
||||
$manifestOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command -args $Name 2>&1
|
||||
if ($LASTEXITCODE -eq 0 -and $null -ne $manifestOutput) {
|
||||
$manifestPath = ($manifestOutput | Select-Object -Last 1).ToString().Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($manifestPath)) {
|
||||
Write-WinUtilLog -Component "AppX" -Message "Registered local AppX manifest for $Name`: $manifestPath"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$failureDetails = ($manifestOutput | Out-String).Trim()
|
||||
Write-WinUtilLog -Level "WARN" -Component "AppX" -Message "Local AppX registration failed for $Name`: $failureDetails"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($StoreId)) {
|
||||
$errorMessage = "Unable to install $Name because no local manifest or Microsoft Store ID is available."
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
|
||||
throw $errorMessage
|
||||
}
|
||||
|
||||
Write-WinUtilLog -Component "AppX" -Message "No usable local manifest found for $Name. Installing Microsoft Store product $StoreId."
|
||||
Install-WinUtilWinget
|
||||
Install-WinUtilProgramWinget -Action Install -Programs @("msstore:$StoreId")
|
||||
}
|
||||
@@ -53,8 +53,9 @@ function Remove-WinUtilProvisionedAPPX {
|
||||
$removalOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command -args $PackageList 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or $null -ne $removalOutput) {
|
||||
$failureDetails = ($removalOutput | Out-String).Trim()
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX provisioned package removal failed: $failureDetails"
|
||||
return
|
||||
$errorMessage = "AppX provisioned package removal failed: $failureDetails"
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
|
||||
throw $errorMessage
|
||||
}
|
||||
|
||||
Write-WinUtilLog -Component "AppX" -Message "AppX provisioned package removal completed."
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
function Set-WinUtilTweaksProgressIndicator {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shows, updates, or hides the window-level progress indicator used by the
|
||||
Tweaks and Undo workflows. It lives outside the TabControl, so unlike the
|
||||
Install tab's progress bar it stays visible no matter which tab is active.
|
||||
Shows, updates, or hides the window-level progress indicator used by long-running
|
||||
workflows such as Tweaks, Undo, and AppX management. It lives outside the TabControl,
|
||||
so unlike the Install tab's progress bar it stays visible no matter which tab is active.
|
||||
.PARAMETER Visible
|
||||
Whether the indicator should be shown or hidden.
|
||||
.PARAMETER Label
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
function Invoke-WPFAppxInstall {
|
||||
if ($sync.ProcessRunning) {
|
||||
Show-WinUtilMessage -Message "An AppX process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $sync.selectedAppx -or $sync.selectedAppx.Count -eq 0) {
|
||||
Show-WinUtilMessage -Message "No AppX Package selected" -Title "Error" -Button "OK" -Icon "Error"
|
||||
return
|
||||
}
|
||||
|
||||
$selected = @($sync.selectedAppx)
|
||||
$apps = $sync.configs.appxHashtable
|
||||
|
||||
$sync.ProcessRunning = $true
|
||||
Invoke-WPFRunspace -ParameterList @(("selected", $selected), ("apps", $apps)) -ScriptBlock {
|
||||
param($selected, $apps)
|
||||
|
||||
$totalPackages = @($selected).Count
|
||||
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
|
||||
|
||||
try {
|
||||
Write-WinUtilLog -Component "AppX" -Message "Starting AppX install for $totalPackages selected package(s)."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing AppX install (0/$totalPackages)" -Percent 0
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
|
||||
}
|
||||
|
||||
for ($index = 0; $index -lt $totalPackages; $index++) {
|
||||
$key = $selected[$index]
|
||||
$app = $apps[$key]
|
||||
$position = $index + 1
|
||||
$startPercent = [int](($index / $totalPackages) * 100)
|
||||
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installing $($app.Content) ($position/$totalPackages)" -Percent $startPercent
|
||||
}
|
||||
Write-Host "Installing $($app.Content)"
|
||||
Install-WinUtilAPPX -Name $app.PackageId -StoreId $app.StoreId
|
||||
|
||||
$completedPercent = [int](($position / $totalPackages) * 100)
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installed $($app.Content) ($position/$totalPackages)" -Percent $completedPercent
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "================================="
|
||||
Write-Host "-- AppX Install Finished ---"
|
||||
Write-Host "================================="
|
||||
Write-WinUtilLog -Component "AppX" -Message "AppX install finished."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX install finished" -Percent 100
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX install failed: $($_.Exception.Message)"
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX install failed" -Percent 100
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$sync.ProcessRunning = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,100 @@
|
||||
function Invoke-WPFAppxRemoval {
|
||||
if ($sync.ProcessRunning) {
|
||||
Show-WinUtilMessage -Message "An AppX process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $sync.selectedAppx -or $sync.selectedAppx.Count -eq 0) {
|
||||
Show-WinUtilMessage -Message "No AppX Package selected" -Title "Error" -Button "OK" -Icon "Error"
|
||||
return
|
||||
}
|
||||
|
||||
$selected = $sync.selectedAppx
|
||||
$selected = @($sync.selectedAppx)
|
||||
$apps = $sync.configs.appxHashtable
|
||||
|
||||
$sync.ProcessRunning = $true
|
||||
Invoke-WPFRunspace -ParameterList @(("selected", $selected), ("apps", $apps)) -ScriptBlock {
|
||||
param($selected, $apps)
|
||||
|
||||
$sync.ProcessRunning = $true
|
||||
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
|
||||
|
||||
$totalPackages = @($selected).Count
|
||||
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
|
||||
$packageList = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
foreach ($key in $selected) {
|
||||
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
||||
# Making sure Game Bar isn't running
|
||||
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
|
||||
Stop-Process -Name GameBarFTServer -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
# This stops annoying ms-gamebar popup when launching games.
|
||||
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
|
||||
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
|
||||
try {
|
||||
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $totalPackages selected package(s)."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing AppX removal (0/$totalPackages)" -Percent 0
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
|
||||
}
|
||||
|
||||
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
||||
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
|
||||
Stop-Process -Name dllhost -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||
for ($index = 0; $index -lt $totalPackages; $index++) {
|
||||
$key = $selected[$index]
|
||||
$app = $apps[$key]
|
||||
$position = $index + 1
|
||||
$startPercent = [int](($index / $totalPackages) * 90)
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removing $($app.Content) ($position/$totalPackages)" -Percent $startPercent
|
||||
}
|
||||
|
||||
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
||||
# Making sure Game Bar isn't running
|
||||
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
|
||||
Stop-Process -Name GameBarFTServer -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
# This stops annoying ms-gamebar popup when launching games.
|
||||
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
|
||||
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
|
||||
}
|
||||
|
||||
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
||||
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
|
||||
Stop-Process -Name dllhost -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host "Removing $($app.Content)"
|
||||
Write-WinUtilLog -Component "AppX" -Message "Removing $($app.Content) ($($app.PackageId))."
|
||||
Remove-WinUtilAPPX -Name $app.PackageId
|
||||
$packageList.Add($app.PackageId)
|
||||
|
||||
if ($key -eq "WPFAppxMSTeams") {
|
||||
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
||||
Write-WinUtilLog -Component "AppX" -Message "Uninstalling Microsoft Teams meeting add-in package."
|
||||
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
|
||||
}
|
||||
|
||||
$completedPercent = [int](($position / $totalPackages) * 90)
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removed $($app.Content) ($position/$totalPackages)" -Percent $completedPercent
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Removing $($apps[$key].Content)"
|
||||
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
|
||||
Remove-WinUtilAPPX -Name $apps[$key].PackageId
|
||||
$packageList.Add($apps[$key].PackageId)
|
||||
if ($packageList.Count -gt 0) {
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removing provisioned AppX packages" -Percent 90
|
||||
}
|
||||
Remove-WinUtilProvisionedAPPX -PackageList $packageList.ToArray()
|
||||
}
|
||||
|
||||
if ($key -eq "WPFAppxMSTeams") {
|
||||
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
||||
Write-WinUtilLog -Component "AppX" -Message "Uninstalling Microsoft Teams meeting add-in package."
|
||||
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
|
||||
Write-Host "================================="
|
||||
Write-Host "-- AppX Removal Finished ---"
|
||||
Write-Host "================================="
|
||||
Write-WinUtilLog -Component "AppX" -Message "AppX removal finished."
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX removal finished" -Percent 100
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
|
||||
}
|
||||
}
|
||||
|
||||
if ($packageList.Count -gt 0) {
|
||||
Remove-WinUtilProvisionedAPPX -PackageList $packageList.ToArray()
|
||||
catch {
|
||||
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX removal failed: $($_.Exception.Message)"
|
||||
if ($hasUI) {
|
||||
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX removal failed" -Percent 100
|
||||
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$sync.ProcessRunning = $false
|
||||
}
|
||||
|
||||
Write-Host "================================="
|
||||
Write-Host "-- AppX Removal Finished ---"
|
||||
Write-Host "================================="
|
||||
Write-WinUtilLog -Component "AppX" -Message "AppX removal finished."
|
||||
|
||||
$sync.ProcessRunning = $false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ function Invoke-WPFButton {
|
||||
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
|
||||
"WPFAppxRemoval" {Invoke-WPFTab "WPFTab6BT"}
|
||||
"WPFBackToTweaks" {Invoke-WPFTab "WPFTab2BT"}
|
||||
"WPFInstallSelectedAppx" {Invoke-WPFAppxInstall}
|
||||
"WPFRemoveSelectedAppx" {Invoke-WPFAppxRemoval}
|
||||
"WPFDefaultAppxSelection" {Invoke-WPFPresets "AppxDefault" -checkboxfilterpattern "WPFAppx*"}
|
||||
"WPFSelectAllAppx" {
|
||||
@@ -77,7 +78,7 @@ function Invoke-WPFButton {
|
||||
$sync.configs.appxHashtable.Keys | ForEach-Object {$sync.$_.IsChecked = $false}
|
||||
}
|
||||
"WPFGetInstalledAppx" {
|
||||
$installedAppxPackages = Get-AppxPackage -AllUsers | Select-Object -ExpandProperty Name
|
||||
$installedAppxPackages = Get-WinUtilInstalledAPPX
|
||||
foreach ($appx in $sync.configs.appxHashtable.GetEnumerator()) {
|
||||
if ($appx.Value.PackageId -in $installedAppxPackages) {
|
||||
$sync.$($appx.Key).IsChecked = $true
|
||||
|
||||
Reference in New Issue
Block a user