mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-09 17:41:14 +10:00
fix: resolve silent failures in AppX removal and PS7 DISM bugs (#4822)
* fix: resolve silent failures in AppX removal and PS7 DISM bugs
- Fixed a bug where Remove-AppxPackage failed silently in PowerShell 7 due to pipeline binding issues. We now explicitly loop through Get-AppxPackage results and pass PackageFullName directly.
- Added wildcard matching to Get-AppxPackage and Get-AppxProvisionedPackage to reliably locate packages regardless of short name vs family name variations.
- Offloaded Get-AppxProvisionedPackage and Remove-AppxProvisionedPackage execution to Windows PowerShell 5.1 (powershell.exe) to bypass PowerShell 7 DISM COM exceptions ("Class not registered") and performance hangs.
- Deduplicated AppX removal logic by calling Remove-WinUtilAPPX directly from Invoke-WPFAppxRemoval instead of copying the implementation.
* refactor(Remove-WinUtilAPPX): use script block to improve readability
* feat: enhance AppX removal process and add provisioned package removal function
* fix(tests): update AppX removal tests
This also adds tests for the new Remove-WinUtilProvisionedAPPX functionality
* Move AppX removal under Tweaks
* Address AppX removal review feedback
* Complete AppX removal error handling
* Prevent AppX cleanup confirmation prompts
---------
Co-authored-by: Chris Titus <contact@christitus.com>
This commit is contained in:
@@ -78,6 +78,7 @@ function Invoke-WinUtilTweaks {
|
|||||||
$sync.configs.tweaks.$CheckBox.appx | ForEach-Object {
|
$sync.configs.tweaks.$CheckBox.appx | ForEach-Object {
|
||||||
Remove-WinUtilAPPX -Name $psitem
|
Remove-WinUtilAPPX -Name $psitem
|
||||||
}
|
}
|
||||||
|
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
|
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
|
||||||
|
|||||||
@@ -17,7 +17,20 @@ function Remove-WinUtilAPPX {
|
|||||||
|
|
||||||
Write-Host "Removing $Name"
|
Write-Host "Removing $Name"
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
|
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
|
||||||
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
|
|
||||||
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
|
# We explicitly loop through packages instead of using the pipeline because PowerShell 7 pipeline binding
|
||||||
|
# for Remove-AppxPackage fails silently, and Get-AppxPackage -AllUsers returns duplicate objects for each user profile.
|
||||||
|
$pkgs = Get-AppxPackage "*$Name*" -AllUsers | Sort-Object -Property PackageFullName -Unique
|
||||||
|
if ($null -ne $pkgs) {
|
||||||
|
foreach ($pkg in $pkgs) {
|
||||||
|
try {
|
||||||
|
Remove-AppxPackage -Package $pkg.PackageFullName -AllUsers -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "Failed to remove AppX package $($pkg.PackageFullName): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
|
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
function Remove-WinUtilProvisionedAPPX {
|
||||||
|
<#
|
||||||
|
|
||||||
|
.SYNOPSIS
|
||||||
|
Removes all AppX provisioned packages that match the given names
|
||||||
|
|
||||||
|
.PARAMETER PackageList
|
||||||
|
An array of names of the APPX packages to remove
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
Remove-WinUtilProvisionedAPPX -PackageList @("Microsoft.Microsoft3DViewer", "Microsoft.WindowsCalculator")
|
||||||
|
|
||||||
|
#>
|
||||||
|
param (
|
||||||
|
[string[]]$PackageList
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $PackageList -or $PackageList.Count -eq 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nRemoving provisioned packages..."
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "Removing AppX provisioned packages: $($PackageList -join ', ')"
|
||||||
|
|
||||||
|
# DISM cmdlets like Get-AppxProvisionedPackage often fail with "Class not registered" or hang in PowerShell 7.
|
||||||
|
# We shell out to Windows PowerShell 5.1 (powershell.exe) to reliably remove the provisioned packages.
|
||||||
|
$ps5Command = {
|
||||||
|
$pkgs = $args
|
||||||
|
$provisionedPackages = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
|
||||||
|
$failures = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
foreach ($Package in $pkgs) {
|
||||||
|
$provs = $provisionedPackages |
|
||||||
|
Where-Object DisplayName -Like "*$Package*"
|
||||||
|
|
||||||
|
if ($null -ne $provs) {
|
||||||
|
foreach ($prov in $provs) {
|
||||||
|
try {
|
||||||
|
Remove-AppxProvisionedPackage -Online -PackageName $prov.PackageName -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$failures.Add("Failed to remove provisioned AppX package $($prov.PackageName): $($_.Exception.Message)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failures.Count -gt 0) {
|
||||||
|
throw ($failures -join [Environment]::NewLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-WinUtilLog -Component "AppX" -Message "AppX provisioned package removal completed."
|
||||||
|
}
|
||||||
@@ -13,11 +13,13 @@ function Invoke-WPFAppxRemoval {
|
|||||||
$sync.ProcessRunning = $true
|
$sync.ProcessRunning = $true
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
|
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
|
||||||
|
|
||||||
|
$packageList = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
foreach ($key in $selected) {
|
foreach ($key in $selected) {
|
||||||
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
|
||||||
# Making sure Game Bar isn't running
|
# Making sure Game Bar isn't running
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
|
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
|
||||||
Stop-Process -Name GameBarFTServer
|
Stop-Process -Name GameBarFTServer -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# This stops annoying ms-gamebar popup when launching games.
|
# This stops annoying ms-gamebar popup when launching games.
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
|
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
|
||||||
@@ -25,14 +27,14 @@ function Invoke-WPFAppxRemoval {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
|
||||||
# i hope your having fun reading this
|
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
|
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
|
||||||
Stop-Process -Name dllhost
|
Stop-Process -Name dllhost -Force -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Removing $($apps[$key].Content)"
|
Write-Host "Removing $($apps[$key].Content)"
|
||||||
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
|
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
|
||||||
Get-AppxPackage -Name $apps[$key].PackageId -AllUsers | Remove-AppxPackage -AllUsers
|
Remove-WinUtilAPPX -Name $apps[$key].PackageId
|
||||||
|
$packageList.Add($apps[$key].PackageId)
|
||||||
|
|
||||||
if ($key -eq "WPFAppxMSTeams") {
|
if ($key -eq "WPFAppxMSTeams") {
|
||||||
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
|
||||||
@@ -41,6 +43,10 @@ function Invoke-WPFAppxRemoval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($packageList.Count -gt 0) {
|
||||||
|
Remove-WinUtilProvisionedAPPX -PackageList $packageList.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
Write-Host "-- AppX Removal Finished ---"
|
Write-Host "-- AppX Removal Finished ---"
|
||||||
Write-Host "================================="
|
Write-Host "================================="
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ function Invoke-WPFButton {
|
|||||||
"WPFUpdatessecurity" {Invoke-WPFUpdatessecurity}
|
"WPFUpdatessecurity" {Invoke-WPFUpdatessecurity}
|
||||||
"WPFGetInstalled" {Invoke-WPFGetInstalled -CheckBox "winget"}
|
"WPFGetInstalled" {Invoke-WPFGetInstalled -CheckBox "winget"}
|
||||||
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
|
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
|
||||||
|
"WPFAppxRemoval" {Invoke-WPFTab "WPFTab6BT"}
|
||||||
|
"WPFBackToTweaks" {Invoke-WPFTab "WPFTab2BT"}
|
||||||
"WPFRemoveSelectedAppx" {Invoke-WPFAppxRemoval}
|
"WPFRemoveSelectedAppx" {Invoke-WPFAppxRemoval}
|
||||||
"WPFDefaultAppxSelection" {Invoke-WPFPresets "AppxDefault" -checkboxfilterpattern "WPFAppx*"}
|
"WPFDefaultAppxSelection" {Invoke-WPFPresets "AppxDefault" -checkboxfilterpattern "WPFAppx*"}
|
||||||
"WPFSelectAllAppx" {
|
"WPFSelectAllAppx" {
|
||||||
|
|||||||
@@ -19,13 +19,12 @@ function Invoke-WPFTab {
|
|||||||
$tabNumber = [int]($ClickedTab -replace "WPFTab","" -replace "BT","") - 1
|
$tabNumber = [int]($ClickedTab -replace "WPFTab","" -replace "BT","") - 1
|
||||||
|
|
||||||
$filter = Get-WinUtilVariables -Type ToggleButton | Where-Object {$psitem -like "WPFTab?BT"}
|
$filter = Get-WinUtilVariables -Type ToggleButton | Where-Object {$psitem -like "WPFTab?BT"}
|
||||||
|
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
|
||||||
($sync.GetEnumerator()).where{$psitem.Key -in $filter} | ForEach-Object {
|
($sync.GetEnumerator()).where{$psitem.Key -in $filter} | ForEach-Object {
|
||||||
if ($ClickedTab -ne $PSItem.name) {
|
if ($ClickedTab -ne $PSItem.name) {
|
||||||
$sync[$PSItem.Name].IsChecked = $false
|
$sync[$PSItem.Name].IsChecked = $false
|
||||||
} else {
|
} else {
|
||||||
$sync["$ClickedTab"].IsChecked = $true
|
$sync["$ClickedTab"].IsChecked = $true
|
||||||
$tabNumber = [int]($ClickedTab-replace "WPFTab","" -replace "BT","") - 1
|
|
||||||
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
|
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
|
||||||
|
|||||||
+113
-21
@@ -5,8 +5,21 @@
|
|||||||
BeforeAll {
|
BeforeAll {
|
||||||
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||||
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
|
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
|
||||||
|
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1")
|
||||||
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
|
||||||
|
|
||||||
|
$tokens = $null
|
||||||
|
$parseErrors = $null
|
||||||
|
$provisionedSourcePath = Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1"
|
||||||
|
$provisionedSourceAst = [System.Management.Automation.Language.Parser]::ParseFile($provisionedSourcePath, [ref]$tokens, [ref]$parseErrors)
|
||||||
|
$ps5CommandAssignment = $provisionedSourceAst.Find({
|
||||||
|
param($node)
|
||||||
|
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
|
||||||
|
$node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
|
||||||
|
$node.Left.VariablePath.UserPath -eq "ps5Command"
|
||||||
|
}, $true)
|
||||||
|
$script:provisionedRemovalScriptBlock = $ps5CommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
|
||||||
|
|
||||||
function Write-WinUtilLog {
|
function Write-WinUtilLog {
|
||||||
param($Message, $Level, $Component)
|
param($Message, $Level, $Component)
|
||||||
}
|
}
|
||||||
@@ -23,18 +36,25 @@ BeforeAll {
|
|||||||
param(
|
param(
|
||||||
[Parameter(ValueFromPipeline = $true)]
|
[Parameter(ValueFromPipeline = $true)]
|
||||||
$InputObject,
|
$InputObject,
|
||||||
[switch]$AllUsers
|
$Package,
|
||||||
|
[switch]$AllUsers,
|
||||||
|
$ErrorAction
|
||||||
)
|
)
|
||||||
process { }
|
process { }
|
||||||
}
|
}
|
||||||
|
function Remove-WinUtilProvisionedAPPX {
|
||||||
|
param($PackageList)
|
||||||
|
}
|
||||||
function Get-AppxProvisionedPackage {
|
function Get-AppxProvisionedPackage {
|
||||||
param([switch]$Online)
|
param([switch]$Online, $ErrorAction)
|
||||||
}
|
}
|
||||||
function Remove-AppxProvisionedPackage {
|
function Remove-AppxProvisionedPackage {
|
||||||
param(
|
param(
|
||||||
[Parameter(ValueFromPipeline = $true)]
|
[Parameter(ValueFromPipeline = $true)]
|
||||||
$InputObject,
|
$InputObject,
|
||||||
[switch]$Online
|
[switch]$Online,
|
||||||
|
$PackageName,
|
||||||
|
$ErrorAction
|
||||||
)
|
)
|
||||||
process { }
|
process { }
|
||||||
}
|
}
|
||||||
@@ -58,35 +78,90 @@ Describe "Remove-WinUtilAPPX" {
|
|||||||
Mock Write-Host { }
|
Mock Write-Host { }
|
||||||
Mock Write-WinUtilLog { }
|
Mock Write-WinUtilLog { }
|
||||||
Mock Get-AppxPackage {
|
Mock Get-AppxPackage {
|
||||||
[pscustomobject]@{
|
@(
|
||||||
Name = $Name
|
[pscustomobject]@{
|
||||||
}
|
Name = $Name
|
||||||
|
PackageFullName = "$Name.FullName"
|
||||||
|
}
|
||||||
|
[pscustomobject]@{
|
||||||
|
Name = $Name
|
||||||
|
PackageFullName = "$Name.FullName"
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Mock Remove-AppxPackage { }
|
Mock Remove-AppxPackage { }
|
||||||
|
}
|
||||||
|
|
||||||
|
It "removes matching installed AppX packages" {
|
||||||
|
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
|
||||||
|
|
||||||
|
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$Name -eq "*Microsoft.Xbox**" -and $AllUsers -eq $true
|
||||||
|
}
|
||||||
|
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$Package -eq "*Microsoft.Xbox**.FullName" -and
|
||||||
|
$AllUsers -eq $true -and
|
||||||
|
$ErrorAction -eq "Stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "logs installed AppX removal failures" {
|
||||||
|
Mock Remove-AppxPackage { throw "Removal failed" }
|
||||||
|
|
||||||
|
{ Remove-WinUtilAPPX -Name "Example.Package" } | Should -Not -Throw
|
||||||
|
|
||||||
|
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$Level -eq "ERROR" -and
|
||||||
|
$Component -eq "AppX" -and
|
||||||
|
$Message -eq "Failed to remove AppX package *Example.Package*.FullName: Removal failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Remove-WinUtilProvisionedAPPX" {
|
||||||
|
BeforeEach {
|
||||||
Mock Get-AppxProvisionedPackage {
|
Mock Get-AppxProvisionedPackage {
|
||||||
@(
|
@(
|
||||||
[pscustomobject]@{ DisplayName = "Microsoft.XboxGamingOverlay" }
|
[pscustomobject]@{ DisplayName = "Example.One"; PackageName = "Example.One_1.0" }
|
||||||
[pscustomobject]@{ DisplayName = "Microsoft.WindowsCalculator" }
|
[pscustomobject]@{ DisplayName = "Example.Two"; PackageName = "Example.Two_1.0" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Mock Remove-AppxProvisionedPackage { }
|
Mock Remove-AppxProvisionedPackage { }
|
||||||
}
|
}
|
||||||
|
|
||||||
It "removes matching installed and provisioned AppX packages" {
|
It "queries provisioned packages once for all selected package names" {
|
||||||
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
|
& $script:provisionedRemovalScriptBlock "Example.One" "Example.Two"
|
||||||
|
|
||||||
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
|
||||||
$Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
|
|
||||||
}
|
|
||||||
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
|
||||||
$InputObject.Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
|
|
||||||
}
|
|
||||||
Should -Invoke -CommandName Get-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Get-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
$Online -eq $true
|
$Online -eq $true
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
$InputObject.DisplayName -eq "Microsoft.XboxGamingOverlay" -and $Online -eq $true
|
$PackageName -eq "Example.One_1.0" -and $Online -eq $true
|
||||||
}
|
}
|
||||||
|
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$PackageName -eq "Example.Two_1.0" -and $Online -eq $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "surfaces provisioned package removal failures from the child process" {
|
||||||
|
Mock Remove-AppxProvisionedPackage { throw "DISM failed" }
|
||||||
|
|
||||||
|
{ & $script:provisionedRemovalScriptBlock "Example.One" } |
|
||||||
|
Should -Throw "*Failed to remove provisioned AppX package Example.One_1.0: DISM failed*"
|
||||||
|
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$PackageName -eq "Example.One_1.0" -and
|
||||||
|
$Online -eq $true -and
|
||||||
|
$ErrorAction -eq "Stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "handles child process failures before logging completion" {
|
||||||
|
$source = Get-Content -Path $provisionedSourcePath -Raw
|
||||||
|
|
||||||
|
$source | Should -Match '\$removalOutput = powershell\.exe .* 2>&1'
|
||||||
|
$source | Should -Match 'if \(\$LASTEXITCODE -ne 0 -or \$null -ne \$removalOutput\)'
|
||||||
|
$source | Should -Match 'Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX provisioned package removal failed:'
|
||||||
|
$source | Should -Match '(?s)AppX provisioned package removal failed:.*return.*AppX provisioned package removal completed\.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +265,11 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
|||||||
Mock Get-AppxPackage {
|
Mock Get-AppxPackage {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
Name = $Name
|
Name = $Name
|
||||||
|
PackageFullName = "$Name.FullName"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mock Remove-AppxPackage { }
|
Mock Remove-AppxPackage { }
|
||||||
|
Mock Remove-WinUtilProvisionedAPPX { }
|
||||||
Mock Get-Package {
|
Mock Get-Package {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
Name = "Microsoft Teams Meeting Add-in"
|
Name = "Microsoft Teams Meeting Add-in"
|
||||||
@@ -216,10 +293,13 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
|||||||
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
||||||
|
|
||||||
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
$Name -eq "Example.Package" -and $AllUsers -eq $true
|
$Name -eq "*Example.Package*" -and $AllUsers -eq $true
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
|
||||||
$InputObject.Name -eq "Example.Package" -and $AllUsers -eq $true
|
$Package -eq "*Example.Package*.FullName"
|
||||||
|
}
|
||||||
|
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Example.Package"
|
||||||
}
|
}
|
||||||
$script:sync.ProcessRunning | Should -BeFalse
|
$script:sync.ProcessRunning | Should -BeFalse
|
||||||
}
|
}
|
||||||
@@ -239,7 +319,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
|||||||
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
|
||||||
|
|
||||||
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
|
||||||
$Name -eq "GameBarFTServer"
|
$Name -eq "GameBarFTServer" -and
|
||||||
|
$Force -eq $true -and
|
||||||
|
$Confirm -eq $false -and
|
||||||
|
$ErrorAction -eq "SilentlyContinue"
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
|
||||||
$Path -eq "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR" -and
|
$Path -eq "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR" -and
|
||||||
@@ -247,7 +330,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
|||||||
$Value -eq 0
|
$Value -eq 0
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
|
||||||
$Name -eq "dllhost"
|
$Name -eq "dllhost" -and
|
||||||
|
$Force -eq $true -and
|
||||||
|
$Confirm -eq $false -and
|
||||||
|
$ErrorAction -eq "SilentlyContinue"
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Get-Package -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Get-Package -Times 1 -Exactly -ParameterFilter {
|
||||||
$Name -eq "Microsoft Teams*"
|
$Name -eq "Microsoft Teams*"
|
||||||
@@ -256,6 +342,12 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
|
|||||||
$InputObject.Name -eq "Microsoft Teams Meeting Add-in" -and $Force -eq $true
|
$InputObject.Name -eq "Microsoft Teams Meeting Add-in" -and $Force -eq $true
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Remove-AppxPackage -Times 3 -Exactly
|
Should -Invoke -CommandName Remove-AppxPackage -Times 3 -Exactly
|
||||||
|
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$PackageList.Count -eq 3 -and
|
||||||
|
$PackageList[0] -eq "Microsoft.XboxGamingOverlay" -and
|
||||||
|
$PackageList[1] -eq "Microsoft.WindowsNotepad" -and
|
||||||
|
$PackageList[2] -eq "MSTeams"
|
||||||
|
}
|
||||||
$script:sync.ProcessRunning | Should -BeFalse
|
$script:sync.ProcessRunning | Should -BeFalse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ BeforeAll {
|
|||||||
function Remove-WinUtilAPPX {
|
function Remove-WinUtilAPPX {
|
||||||
param($Name)
|
param($Name)
|
||||||
}
|
}
|
||||||
|
function Remove-WinUtilProvisionedAPPX {
|
||||||
|
param($PackageList)
|
||||||
|
}
|
||||||
function Set-WinUtilDNS {
|
function Set-WinUtilDNS {
|
||||||
param($DNSProvider)
|
param($DNSProvider)
|
||||||
}
|
}
|
||||||
@@ -89,6 +92,7 @@ Describe "Invoke-WinUtilTweaks" {
|
|||||||
Mock Set-WinUtilRegistry { }
|
Mock Set-WinUtilRegistry { }
|
||||||
Mock Invoke-WinUtilScript { }
|
Mock Invoke-WinUtilScript { }
|
||||||
Mock Remove-WinUtilAPPX { }
|
Mock Remove-WinUtilAPPX { }
|
||||||
|
Mock Remove-WinUtilProvisionedAPPX { }
|
||||||
Mock Write-WinUtilLog { }
|
Mock Write-WinUtilLog { }
|
||||||
Mock Write-Warning { }
|
Mock Write-Warning { }
|
||||||
}
|
}
|
||||||
@@ -118,6 +122,9 @@ Describe "Invoke-WinUtilTweaks" {
|
|||||||
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
|
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
|
||||||
$Name -eq "Microsoft.ExampleApp"
|
$Name -eq "Microsoft.ExampleApp"
|
||||||
}
|
}
|
||||||
|
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
|
||||||
|
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Microsoft.ExampleApp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
It "uses original registry values and service startup types in undo mode" {
|
It "uses original registry values and service startup types in undo mode" {
|
||||||
@@ -137,6 +144,7 @@ Describe "Invoke-WinUtilTweaks" {
|
|||||||
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'undo tweak'"
|
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'undo tweak'"
|
||||||
}
|
}
|
||||||
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 0 -Exactly
|
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 0 -Exactly
|
||||||
|
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 0 -Exactly
|
||||||
}
|
}
|
||||||
|
|
||||||
It "keeps a user-changed service startup type by default" {
|
It "keeps a user-changed service startup type by default" {
|
||||||
|
|||||||
+23
-1
@@ -134,7 +134,6 @@ Describe "XAML document" {
|
|||||||
"WPFTab3BT",
|
"WPFTab3BT",
|
||||||
"WPFTab4BT",
|
"WPFTab4BT",
|
||||||
"WPFTab5BT",
|
"WPFTab5BT",
|
||||||
"WPFTab6BT",
|
|
||||||
"SearchBar",
|
"SearchBar",
|
||||||
"SearchBarClearButton",
|
"SearchBarClearButton",
|
||||||
"appscategory",
|
"appscategory",
|
||||||
@@ -148,6 +147,7 @@ Describe "XAML document" {
|
|||||||
"WPFAdvanced",
|
"WPFAdvanced",
|
||||||
"WPFClearTweaksSelection",
|
"WPFClearTweaksSelection",
|
||||||
"WPFGetInstalledTweaks",
|
"WPFGetInstalledTweaks",
|
||||||
|
"WPFAppxRemoval",
|
||||||
"WPFTweaksbutton",
|
"WPFTweaksbutton",
|
||||||
"WPFUndoall",
|
"WPFUndoall",
|
||||||
"WPFUpdatesdefault",
|
"WPFUpdatesdefault",
|
||||||
@@ -157,6 +157,7 @@ Describe "XAML document" {
|
|||||||
"WPFGetInstalledAppx",
|
"WPFGetInstalledAppx",
|
||||||
"WPFSelectAllAppx",
|
"WPFSelectAllAppx",
|
||||||
"WPFClearAppxSelection",
|
"WPFClearAppxSelection",
|
||||||
|
"WPFBackToTweaks",
|
||||||
"WPFRemoveSelectedAppx"
|
"WPFRemoveSelectedAppx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,6 +224,27 @@ Describe "XAML document" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
It "opens AppX removal from Tweaks and provides a return path" {
|
||||||
|
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
|
||||||
|
$tweaksTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab2"]')
|
||||||
|
$appxTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab6"]')
|
||||||
|
$openButton = $tweaksTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFAppxRemoval"]')
|
||||||
|
$buttonNames = @($openButton.ParentNode.SelectNodes('./*[local-name()="Button"]') | ForEach-Object { $_.GetAttribute("Name") })
|
||||||
|
$getInstalledIndex = [array]::IndexOf($buttonNames, "WPFGetInstalledTweaks")
|
||||||
|
$openAppxIndex = [array]::IndexOf($buttonNames, "WPFAppxRemoval")
|
||||||
|
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
|
||||||
|
$tabSource = Get-Content -Path (Join-Path $script:functionRoot "public\Invoke-WPFTab.ps1") -Raw
|
||||||
|
|
||||||
|
$navPanel.SelectSingleNode('./*[local-name()="ToggleButton"][@Name="WPFTab6BT"]') | Should -BeNullOrEmpty
|
||||||
|
$openButton.GetAttribute("Content").Trim() | Should -Be "AppX Removal"
|
||||||
|
$openAppxIndex | Should -Be ($getInstalledIndex + 1)
|
||||||
|
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFBackToTweaks"]') | Should -Not -BeNullOrEmpty
|
||||||
|
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFRemoveSelectedAppx"]') | Should -Not -BeNullOrEmpty
|
||||||
|
$buttonSource | Should -Match '"WPFAppxRemoval"\s*\{Invoke-WPFTab "WPFTab6BT"\}'
|
||||||
|
$buttonSource | Should -Match '"WPFBackToTweaks"\s*\{Invoke-WPFTab "WPFTab2BT"\}'
|
||||||
|
$tabSource | Should -Match '\$sync\.\$tabNav\.Items\[\$tabNumber\]\.IsSelected = \$true'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Describe "XAML and sync wiring" {
|
Describe "XAML and sync wiring" {
|
||||||
|
|||||||
+2
-8
@@ -983,14 +983,6 @@
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
</ToggleButton.Content>
|
</ToggleButton.Content>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="Auto" MinWidth="{DynamicResource TabButtonWidth}"
|
|
||||||
Background="{DynamicResource ButtonAppxBackgroundColor}" Foreground="{DynamicResource ButtonAppxForegroundColor}" FontWeight="Bold" Name="WPFTab6BT">
|
|
||||||
<ToggleButton.Content>
|
|
||||||
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonAppxForegroundColor}">
|
|
||||||
<Underline>A</Underline>ppX Removal
|
|
||||||
</TextBlock>
|
|
||||||
</ToggleButton.Content>
|
|
||||||
</ToggleButton>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Search Bar and Action Buttons -->
|
<!-- Search Bar and Action Buttons -->
|
||||||
@@ -1237,6 +1229,7 @@
|
|||||||
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
|
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
@@ -1717,6 +1710,7 @@
|
|||||||
|
|
||||||
<Border Grid.Row="1" Background="{DynamicResource MainBackgroundColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="5" HorizontalAlignment="Stretch" Padding="10">
|
<Border Grid.Row="1" Background="{DynamicResource MainBackgroundColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="5" HorizontalAlignment="Stretch" Padding="10">
|
||||||
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
|
<Button Name="WPFBackToTweaks" Content="Back to Tweaks" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
<Button Name="WPFRemoveSelectedAppx" Content="Remove Selected" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
<Button Name="WPFRemoveSelectedAppx" Content="Remove Selected" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
Reference in New Issue
Block a user