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
127 changes: 104 additions & 23 deletions Scripts/AppRemoval/Invoke-ForceRemoveEdge.ps1
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
<#
.SYNOPSIS
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.

.OUTPUTS
System.Boolean. $true when Edge is uninstalled and cleanup succeeds; otherwise $false.
#>
function Invoke-ForceRemoveEdge {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
Write-Host ""
return
return $true
}

Write-Host "> Forcefully uninstalling Microsoft Edge..."
try {
Write-Host "> Forcefully uninstalling Microsoft Edge..."

$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev').SetValue('AllowUninstall', '')
$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$edgeUpdateKey = $hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev')
$edgeUpdateKey.SetValue('AllowUninstall', '')

# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" | Out-Null
# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory -Force -ErrorAction Stop | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" -ItemType File -Force -ErrorAction Stop | Out-Null

# Remove edge
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -ne $uninstallRegKey) {
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -eq $uninstallRegKey) {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
return $false
}

Write-Host "Running uninstaller..."
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
Invoke-NonBlocking -ScriptBlock {
$exitCode = Invoke-NonBlocking -ScriptBlock {
param($cmd)
Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait
$process = Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait -PassThru
return $process.ExitCode
} -ArgumentList $uninstallString
if ($exitCode -ne 0) {
Write-Warning "Microsoft Edge uninstaller failed with exit code $exitCode."
return $false
}

Write-Host "Removing leftover files..."
$cleanupSucceeded = $true

$edgePaths = @(
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
Expand All @@ -44,22 +58,89 @@ function Invoke-ForceRemoveEdge {

foreach ($path in $edgePaths) {
if (Test-Path -Path $path) {
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue
Write-Host " Removed $path" -ForegroundColor DarkGray
try {
Remove-Item -Path $path -Force -Recurse -ErrorAction Stop
Write-Host " Removed $path" -ForegroundColor DarkGray
}
catch {
Write-Warning "Failed to remove Edge leftover '$path': $($_.Exception.Message)"
$cleanupSucceeded = $false
}
}
}

Write-Host "Cleaning up registry..."
$registryCleanupSucceeded = $true

# Remove MS Edge from autostart
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Update" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "Microsoft Edge Update" /f *>$null
# Remove MS Edge from autostart. Missing values are already-clean state,
# while failures to inspect or remove an existing value are reported.
$autostartValues = @(
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'Microsoft Edge Update' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'Microsoft Edge Update' }
)
foreach ($autostartValue in $autostartValues) {
if (-not (Remove-EdgeAutostartValue -Path $autostartValue.Path -Name $autostartValue.Name)) {
$registryCleanupSucceeded = $false
}
}

if (-not $cleanupSucceeded -or -not $registryCleanupSucceeded) {
Write-Warning "Microsoft Edge was uninstalled, but some leftover files or autostart entries could not be removed."
return $false
}

Write-Host "Microsoft Edge was uninstalled"
return $true
}
catch {
Write-Warning "Failed to forcefully uninstall Microsoft Edge: $($_.Exception.Message)"
return $false
}
finally {
if ($edgeUpdateKey) { $edgeUpdateKey.Dispose() }
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
if ($hklm) { $hklm.Dispose() }
}
}

<#
.SYNOPSIS
Removes an Edge autostart registry value when it exists.

.OUTPUTS
System.Boolean. $true when the value is absent or removed; $false when inspection or removal fails.
#>
function Remove-EdgeAutostartValue {
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Name
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try {
$properties = Get-ItemProperty -Path $Path -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
return $true
}
catch {
Write-Warning "Failed to inspect Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
}

if (-not $properties.PSObject.Properties[$Name]) {
return $true
}

try {
Remove-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
return $true
}
else {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
catch {
Write-Warning "Failed to remove Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
}
}
95 changes: 73 additions & 22 deletions Scripts/AppRemoval/Remove-SelectedApps.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

.EXAMPLE
Remove-SelectedApps -appsList (Generate-AppsList)

.OUTPUTS
System.Boolean. $true when all removals can be confirmed; otherwise $false.
#>
function Remove-SelectedApps {
param (
Expand All @@ -29,10 +32,10 @@ function Remove-SelectedApps {
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
}

Write-Host ""
return
return $true
}

$failuresBefore = $script:AppRemovalFailures
$targetUser = Get-TargetUserForAppRemoval
$appCount = @($appsList).Count
$appIndex = 0
Expand All @@ -42,7 +45,7 @@ function Remove-SelectedApps {
$wingetRemovalFailures = @{}

Foreach ($app in $appsList) {
if ($script:CancelRequested) { return }
if ($script:CancelRequested) { return $false }

$appIndex++

Expand All @@ -53,10 +56,11 @@ function Remove-SelectedApps {
Write-Host "Removing $app"

if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
if (-not (Remove-WinGetApp -app $app)) {
$removalSucceeded = Remove-WinGetApp -app $app
$wingetRemovedApps += $app
if (($script:Params.ContainsKey('User') -or $script:Params.ContainsKey('Sysprep')) -and -not $removalSucceeded) {
$wingetRemovalFailures[$app] = $true
}
$wingetRemovedApps += $app
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else {
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
Expand All @@ -66,17 +70,20 @@ function Remove-SelectedApps {
}

if ($script:CancelRequested) {
Write-Host ""
return
return $false
}

# Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
$edgeForceRemoveSucceeded = $false

if ($null -eq $postRemovalList) {
$script:AppRemovalVerificationUnavailable = $true
foreach ($app in $wingetRemovedApps) {
$wingetRemovalFailures[$app] = $true
}
}
else {
foreach ($app in $wingetRemovedApps) {
Expand All @@ -87,8 +94,11 @@ function Remove-SelectedApps {
if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) {
Request-EdgeForceRemove
$edgeForceRemoveRequested = $true
$edgeForceRemoveSucceeded = Request-EdgeForceRemove
}
if ($edgeForceRemoveSucceeded) {
continue
}
}
else {
Expand All @@ -101,7 +111,7 @@ function Remove-SelectedApps {

$script:AppRemovalFailures += $wingetRemovalFailures.Count

Write-Host ""
return ($script:AppRemovalFailures -eq $failuresBefore)
}

<#
Expand All @@ -110,15 +120,23 @@ function Remove-SelectedApps {

.DESCRIPTION
Runs winget uninstall for a single app, with a bounded execution time.
If the User or Sysprep parameter was passed, also schedules removal for
future logins.
WinGet's own exit code/success reporting is unreliable and is only logged
for diagnostics; it never causes this function to report failure. Callers
verify removal with a post-removal inventory check instead. This function
only reports failure when the winget invocation itself throws a terminating
error (e.g. it times out or cannot be started). If the User or Sysprep
parameter was passed, also schedules removal for future logins.

.PARAMETER app
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').

.PARAMETER TimeoutSeconds
Maximum time to allow the foreground WinGet uninstall to run. Defaults
to 120 seconds.

.OUTPUTS
System.Boolean. $true unless the winget invocation threw a terminating error
or any required RunOnce scheduling failed; otherwise $false.
#>
function Remove-WinGetApp {
param(
Expand All @@ -131,22 +149,28 @@ function Remove-WinGetApp {
return $false
}

$uninstallSucceeded = $true
$uninstallCommandSucceeded = $true
$exitCode = $null
try {
$uninstallSucceeded = Invoke-NonBlocking -ScriptBlock {
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
param($appId)
$null = & winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1
return $true
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
return [PSCustomObject]@{
ExitCode = $LASTEXITCODE
Output = $output
}
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
$uninstallSucceeded = [bool]$uninstallSucceeded
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode."
}
catch {
$uninstallSucceeded = $false
$uninstallCommandSucceeded = $false
if ($_.Exception.Message -like 'Operation timed out after *') {
Write-Error "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
}
else {
Write-Error "WinGet uninstall for $app failed: $_"
Write-Verbose "WinGet uninstall for $app failed: $_"
}
}

Expand All @@ -160,7 +184,29 @@ function Remove-WinGetApp {
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}

return ($uninstallSucceeded -and $scheduleSucceeded)
return ($uninstallCommandSucceeded -and $scheduleSucceeded)
}

<#
.SYNOPSIS
Writes captured WinGet uninstall output to the verbose stream.

.OUTPUTS
None.
#>
function Write-WinGetUninstallOutput {
param(
[object[]]$Output
)

foreach ($line in @($Output)) {
if ($null -eq $line) { continue }

$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ([string]::IsNullOrWhiteSpace($lineText)) { continue }

Write-Verbose $lineText
}
}

<#
Expand Down Expand Up @@ -277,19 +323,24 @@ function Get-AppRemovalMethod {
following all winget uninstall attempts. In GUI mode, displays a
warning message box; in CLI mode, prompts via Read-Host. On
confirmation, performs a force-remove of the Edge package.

.OUTPUTS
System.Boolean. $true when Edge is forcefully removed; otherwise $false.
#>
function Request-EdgeForceRemove {
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
Invoke-ForceRemoveEdge
return (Invoke-ForceRemoveEdge)
}
}
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
Write-Host ""
Invoke-ForceRemoveEdge
return (Invoke-ForceRemoveEdge)
}

return $false
}

<#
Expand Down
Loading
Loading