diff --git a/Config/Apps.json b/Config/Apps.json index 9730a97..6a034f6 100644 --- a/Config/Apps.json +++ b/Config/Apps.json @@ -781,7 +781,7 @@ { "FriendlyName": "Windows Terminal", "AppId": "Microsoft.WindowsTerminal", - "Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL), WARNING: Win11Debloat if it is launched via Windows Terminal.", + "Description": "Default terminal app in windows 11 (Command Prompt, PowerShell, WSL), WARNING: Do not remove if you launched Win11Debloat from Windows Terminal, as this will cause the script to fail.", "SelectedByDefault": false, "Recommendation": "unsafe" }, diff --git a/Scripts/Features/BackupRegistrySnapshotCapture.ps1 b/Scripts/Features/BackupRegistrySnapshotCapture.ps1 index cbdf4ac..645691b 100644 --- a/Scripts/Features/BackupRegistrySnapshotCapture.ps1 +++ b/Scripts/Features/BackupRegistrySnapshotCapture.ps1 @@ -147,31 +147,14 @@ function Invoke-WithLoadedBackupHive { $ArgumentObject = $null ) - $hiveDatPath = if ($script:Params.ContainsKey('Sysprep')) { - GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT' + $targetUserName = if ($script:Params.ContainsKey('Sysprep')) { + 'Default' } else { - GetUserDirectory -userName $script:Params.Item('User') -fileName 'NTUSER.DAT' + $script:Params.Item('User') } - $global:LASTEXITCODE = 0 - reg load 'HKU\Default' "$hiveDatPath" | Out-Null - $loadExitCode = $LASTEXITCODE - if ($loadExitCode -ne 0) { - throw "Failed to load user hive for registry backup at '$hiveDatPath' (exit code: $loadExitCode)" - } - - try { - return & $ScriptBlock $ArgumentObject - } - finally { - $global:LASTEXITCODE = 0 - reg unload 'HKU\Default' | Out-Null - $unloadExitCode = $LASTEXITCODE - if ($unloadExitCode -ne 0) { - throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)" - } - } + return Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject } function Get-RegistryKeySnapshot { diff --git a/Scripts/Features/ExecuteChanges.ps1 b/Scripts/Features/ExecuteChanges.ps1 index 2152fb9..e1714b0 100644 --- a/Scripts/Features/ExecuteChanges.ps1 +++ b/Scripts/Features/ExecuteChanges.ps1 @@ -128,7 +128,14 @@ function ExecuteParameter { # Executes all selected parameters/features -function ExecuteAllChanges { +function ExecuteAllChanges { + # When running as SYSTEM, require -User or -Sysprep to prevent applying + # changes to the SYSTEM profile instead of a real user. + $isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18') + if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) { + throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user." + } + $script:RegistryImportFailures = 0 # Build list of actionable parameters (skip control params and data-only params) diff --git a/Scripts/Features/ImportRegistryFile.ps1 b/Scripts/Features/ImportRegistryFile.ps1 index 626e1be..1bfb73c 100644 --- a/Scripts/Features/ImportRegistryFile.ps1 +++ b/Scripts/Features/ImportRegistryFile.ps1 @@ -18,24 +18,19 @@ function ImportRegistryFile { throw $errorMessage } - $regResult = $null - $offlineHiveLoaded = $false + $importScript = { + param($targetRegFilePath, $hiveContext) - try { - if ($usesOfflineHive) { - # Sysprep targets Default user, User targets the specified user - $targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") } - $hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT" + # When the target user's hive is already loaded under their SID, the .reg file's + # HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead, + # which remaps Default → SID via Split-RegistryPath. + $usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded - $global:LASTEXITCODE = 0 - reg load "HKU\Default" $hiveDatPath | Out-Null - $loadExitCode = $LASTEXITCODE - - if ($loadExitCode -ne 0) { - throw "Failed importing registry file '$path'. Offline hive load failed: Failed to load user hive at '$hiveDatPath' (exit code: $loadExitCode)" - } - - $offlineHiveLoaded = $true + if ($usePowerShellFallbackOnly) { + Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath + Write-Host "The operation completed successfully via PowerShell registry writer." + Write-Host "" + return } $regResult = Invoke-NonBlocking -ScriptBlock { @@ -66,7 +61,7 @@ function ImportRegistryFile { } return $result - } -ArgumentList $regFilePath + } -ArgumentList $targetRegFilePath $regOutput = @($regResult.Output) $hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error @@ -88,26 +83,26 @@ function ImportRegistryFile { if (-not $hasSuccess) { $details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" } Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details" - Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath - Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow + Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath + Write-Host "The operation completed successfully via PowerShell registry writer." } Write-Host "" } + + try { + if ($usesOfflineHive) { + # Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\. + $targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") } + Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext + } + else { + & $importScript $regFilePath $null + } + } catch { $script:RegistryImportFailures++ Write-Host $_.Exception.Message -ForegroundColor Red Write-Host "" } - finally { - if ($offlineHiveLoaded) { - $global:LASTEXITCODE = 0 - reg unload "HKU\Default" | Out-Null - $unloadExitCode = $LASTEXITCODE - - if ($unloadExitCode -ne 0) { - Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)" - } - } - } -} \ No newline at end of file +} diff --git a/Scripts/Features/RestartExplorer.ps1 b/Scripts/Features/RestartExplorer.ps1 index e6b5e5e..eff2bc2 100644 --- a/Scripts/Features/RestartExplorer.ps1 +++ b/Scripts/Features/RestartExplorer.ps1 @@ -1,8 +1,13 @@ # Restart the Windows Explorer process function RestartExplorer { + # Restarting Explorer while running in Sysprep or User context is not necessary + if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")) { + return + } + Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..." - if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("NoRestartExplorer")) { + if ($script:Params.ContainsKey("NoRestartExplorer")) { Write-Host "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow return } diff --git a/Scripts/Features/RestoreRegistryApplyState.ps1 b/Scripts/Features/RestoreRegistryApplyState.ps1 index 705dbb7..54b4011 100644 --- a/Scripts/Features/RestoreRegistryApplyState.ps1 +++ b/Scripts/Features/RestoreRegistryApplyState.ps1 @@ -7,38 +7,21 @@ function Invoke-WithLoadedRestoreHive { $ArgumentObject = $null ) - $hiveDatPath = if ($Target -eq 'DefaultUserProfile') { - GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT' + $targetUserName = if ($Target -eq 'DefaultUserProfile') { + 'Default' } elseif ($Target -like 'User:*') { $userName = $Target.Substring(5) if ([string]::IsNullOrWhiteSpace($userName)) { throw 'Invalid backup target format for user restore.' } - GetUserDirectory -userName $userName -fileName 'NTUSER.DAT' + $userName } else { throw "Unsupported backup target '$Target'." } - $global:LASTEXITCODE = 0 - reg load 'HKU\Default' "$hiveDatPath" | Out-Null - $loadExitCode = $LASTEXITCODE - if ($loadExitCode -ne 0) { - throw "Failed to load target user hive '$hiveDatPath' (exit code: $loadExitCode)." - } - - try { - & $ScriptBlock $ArgumentObject - } - finally { - $global:LASTEXITCODE = 0 - reg unload 'HKU\Default' | Out-Null - $unloadExitCode = $LASTEXITCODE - if ($unloadExitCode -ne 0) { - throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)" - } - } + Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject } function Restore-RegistryKeySnapshot { diff --git a/Scripts/GUI/GetSystemUsesDarkMode.ps1 b/Scripts/GUI/GetSystemUsesDarkMode.ps1 index 5f4263b..05c1790 100644 --- a/Scripts/GUI/GetSystemUsesDarkMode.ps1 +++ b/Scripts/GUI/GetSystemUsesDarkMode.ps1 @@ -1,7 +1,14 @@ # Checks if the system is set to use dark mode for apps function GetSystemUsesDarkMode { try { - return (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme').AppsUseLightTheme -eq 0 + $personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' + + if ($null -eq $personalizeKey) { + Write-Host "WARNING: Unable to retrieve personalization settings." -ForegroundColor Yellow + return $false + } + + return $personalizeKey.AppsUseLightTheme -eq 0 } catch { return $false diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 index e18a664..5d48aef 100644 --- a/Scripts/GUI/Show-MainWindow.ps1 +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -831,6 +831,18 @@ } } + # When running as SYSTEM, the "Current User" option is not meaningful. + # Hide it from the dropdown and default to "Other User". + $isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18') + if ($isSystem -and $userSelectionCombo.Items.Count -gt 0) { + $currentUserItem = $userSelectionCombo.Items[0] + if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) { + $currentUserItem.Visibility = 'Collapsed' + $currentUserItem.IsEnabled = $false + } + $userSelectionCombo.SelectedIndex = 1 + } + $restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox') if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) { $restartExplorerCheckBox.IsChecked = $false diff --git a/Scripts/Helpers/RegistryPathHelpers.ps1 b/Scripts/Helpers/RegistryPathHelpers.ps1 index be22fa3..683fd49 100644 --- a/Scripts/Helpers/RegistryPathHelpers.ps1 +++ b/Scripts/Helpers/RegistryPathHelpers.ps1 @@ -27,16 +27,19 @@ function Split-RegistryPath { $null } - if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::IsNullOrWhiteSpace($normalizedSubKey)) { + if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and + -not [string]::IsNullOrWhiteSpace($normalizedSubKey) -and + -not [string]::IsNullOrWhiteSpace([string]$script:RegistryTargetHiveMountName)) { if ($normalizedSubKey -match '^(?[^\\]+)(?:\\(?.*))?$') { $mountName = [string]$matches.mount - if ($mountName.Equals('.DEFAULT', [System.StringComparison]::OrdinalIgnoreCase)) { + if ($mountName.Equals('Default', [System.StringComparison]::OrdinalIgnoreCase)) { $remainingSubKey = if ($matches.rest) { [string]$matches.rest } else { '' } + $targetMountName = [string]$script:RegistryTargetHiveMountName if ([string]::IsNullOrWhiteSpace($remainingSubKey)) { - $normalizedSubKey = 'Default' + $normalizedSubKey = $targetMountName } else { - $normalizedSubKey = "Default\$remainingSubKey" + $normalizedSubKey = "$targetMountName\$remainingSubKey" } } } diff --git a/Scripts/Helpers/Test-TargetUserName.ps1 b/Scripts/Helpers/Test-TargetUserName.ps1 index e9369a9..929eb18 100644 --- a/Scripts/Helpers/Test-TargetUserName.ps1 +++ b/Scripts/Helpers/Test-TargetUserName.ps1 @@ -31,14 +31,6 @@ function Test-TargetUserName { } } - if (TestIfUserIsLoggedIn -Username $normalizedUserName) { - return [PSCustomObject]@{ - IsValid = $false - UserName = $normalizedUserName - Message = "User '$normalizedUserName' is currently logged in. Please sign out that user first." - } - } - return [PSCustomObject]@{ IsValid = $true UserName = $normalizedUserName diff --git a/Scripts/Helpers/TestIfUserIsLoggedIn.ps1 b/Scripts/Helpers/TestIfUserIsLoggedIn.ps1 deleted file mode 100644 index 9c0d759..0000000 --- a/Scripts/Helpers/TestIfUserIsLoggedIn.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -function TestIfUserIsLoggedIn { - param( - [Parameter(Mandatory)] - [string]$Username - ) - - try { - $quserOutput = @(& quser 2>$null) - if ($LASTEXITCODE -ne 0 -or -not $quserOutput) { - return $false - } - - foreach ($line in ($quserOutput | Select-Object -Skip 1)) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - - # Remove current-session marker and split columns. - $normalizedLine = $line.TrimStart('>', ' ') - $parts = $normalizedLine -split '\s+' - if ($parts.Count -eq 0) { continue } - - $sessionUser = $parts[0] - if ([string]::IsNullOrWhiteSpace($sessionUser)) { continue } - - # Normalize possible DOMAIN\user or user@domain formats. - if ($sessionUser.Contains('\')) { - $sessionUser = ($sessionUser -split '\\')[-1] - } - if ($sessionUser.Contains('@')) { - $sessionUser = ($sessionUser -split '@')[0] - } - - if ($sessionUser.Equals($Username, [System.StringComparison]::OrdinalIgnoreCase)) { - return $true - } - } - } - catch { - return $false - } - - return $false -} diff --git a/Scripts/Helpers/UserHiveHelpers.ps1 b/Scripts/Helpers/UserHiveHelpers.ps1 new file mode 100644 index 0000000..e372d7d --- /dev/null +++ b/Scripts/Helpers/UserHiveHelpers.ps1 @@ -0,0 +1,152 @@ +function New-TargetUserHiveContext { + param( + [Parameter(Mandatory)] + [string]$TargetUserName, + [AllowNull()] + [object]$UserContext, + [Parameter(Mandatory)] + [string]$HiveDatPath, + [AllowNull()] + [string]$MountName, + [bool]$WasAlreadyLoaded = $false, + [bool]$WasLoadedByScript = $false + ) + + $effectiveMountName = if ([string]::IsNullOrWhiteSpace($MountName)) { 'Default' } else { $MountName } + + return [PSCustomObject]@{ + TargetUserName = $TargetUserName + UserSid = if ($UserContext) { $UserContext.UserSid } else { $null } + ProfilePath = if ($UserContext) { $UserContext.ProfilePath } else { $null } + HiveDatPath = $HiveDatPath + MountName = $effectiveMountName + WasAlreadyLoaded = $WasAlreadyLoaded + WasLoadedByScript = $WasLoadedByScript + } +} + +function Resolve-TargetUserHiveContext { + param( + [Parameter(Mandatory)] + [string]$TargetUserName + ) + + $normalizedTargetUserName = NormalizeUserLookupValue -Value $TargetUserName + if ([string]::IsNullOrWhiteSpace($normalizedTargetUserName)) { + throw 'Target user name for registry hive resolution is empty.' + } + + $userContext = ResolveUserProfileContext -UserName $normalizedTargetUserName + if (-not $userContext -or [string]::IsNullOrWhiteSpace([string]$userContext.ProfilePath)) { + throw "Unable to resolve profile path for target user '$normalizedTargetUserName'." + } + + $hiveDatPath = Join-Path $userContext.ProfilePath 'NTUSER.DAT' + if (-not (Test-Path -LiteralPath $hiveDatPath)) { + throw "Unable to find target user hive at '$hiveDatPath'." + } + + $isDefaultProfile = $normalizedTargetUserName.Equals('Default', [System.StringComparison]::OrdinalIgnoreCase) + $userSid = if ($userContext) { [string]$userContext.UserSid } else { '' } + + if ((-not $isDefaultProfile) -and (-not [string]::IsNullOrWhiteSpace($userSid))) { + $loadedHivePath = "Registry::HKEY_USERS\$userSid" + if (Test-Path -LiteralPath $loadedHivePath) { + return (New-TargetUserHiveContext ` + -TargetUserName $normalizedTargetUserName ` + -UserContext $userContext ` + -HiveDatPath $hiveDatPath ` + -MountName $userSid ` + -WasAlreadyLoaded $true ` + -WasLoadedByScript $false) + } + } + + return (New-TargetUserHiveContext ` + -TargetUserName $normalizedTargetUserName ` + -UserContext $userContext ` + -HiveDatPath $hiveDatPath ` + -MountName 'Default' ` + -WasAlreadyLoaded $false ` + -WasLoadedByScript $false) +} + +function Resolve-LoadedTargetUserHiveContext { + param( + [Parameter(Mandatory)] + $HiveContext + ) + + $userSid = [string]$HiveContext.UserSid + if ([string]::IsNullOrWhiteSpace($userSid)) { + return $null + } + + $loadedHivePath = "Registry::HKEY_USERS\$userSid" + if (-not (Test-Path -LiteralPath $loadedHivePath)) { + return $null + } + + return (New-TargetUserHiveContext ` + -TargetUserName $HiveContext.TargetUserName ` + -UserContext ([PSCustomObject]@{ UserSid = $HiveContext.UserSid; ProfilePath = $HiveContext.ProfilePath }) ` + -HiveDatPath $HiveContext.HiveDatPath ` + -MountName $userSid ` + -WasAlreadyLoaded $true ` + -WasLoadedByScript $false) +} + +function Invoke-WithTargetUserHive { + param( + [Parameter(Mandatory)] + [string]$TargetUserName, + [Parameter(Mandatory)] + [scriptblock]$ScriptBlock, + $ArgumentObject = $null, + [switch]$PassHiveContext + ) + + $hiveContext = Resolve-TargetUserHiveContext -TargetUserName $TargetUserName + $previousHiveMountName = $script:RegistryTargetHiveMountName + + try { + if (-not $hiveContext.WasAlreadyLoaded) { + $global:LASTEXITCODE = 0 + reg load "HKU\$($hiveContext.MountName)" "$($hiveContext.HiveDatPath)" | Out-Null + $loadExitCode = $LASTEXITCODE + + if ($loadExitCode -ne 0) { + $loadedSidContext = Resolve-LoadedTargetUserHiveContext -HiveContext $hiveContext + if ($loadedSidContext) { + $hiveContext = $loadedSidContext + } + else { + throw "Failed to load target user hive '$($hiveContext.HiveDatPath)' (exit code: $loadExitCode)." + } + } + else { + $hiveContext.WasLoadedByScript = $true + } + } + + $script:RegistryTargetHiveMountName = [string]$hiveContext.MountName + + if ($PassHiveContext) { + return & $ScriptBlock $ArgumentObject $hiveContext + } + + return & $ScriptBlock $ArgumentObject + } + finally { + $script:RegistryTargetHiveMountName = $previousHiveMountName + + if ($hiveContext -and $hiveContext.WasLoadedByScript) { + $global:LASTEXITCODE = 0 + reg unload "HKU\$($hiveContext.MountName)" | Out-Null + $unloadExitCode = $LASTEXITCODE + if ($unloadExitCode -ne 0) { + Write-Warning "Failed to unload registry hive 'HKU\$($hiveContext.MountName)' (exit code: $unloadExitCode)" + } + } + } +} diff --git a/Win11Debloat.ps1 b/Win11Debloat.ps1 index 64bdd77..fe205f0 100644 --- a/Win11Debloat.ps1 +++ b/Win11Debloat.ps1 @@ -341,6 +341,7 @@ if (-not $script:WingetInstalled -and -not $Silent) { # Helper functions . "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1" . "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1" +. "$PSScriptRoot/Scripts/Helpers/UserHiveHelpers.ps1" . "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1" . "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1" . "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1" @@ -354,7 +355,6 @@ if (-not $script:WingetInstalled -and -not $Silent) { . "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1" . "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1" -. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1" . "$PSScriptRoot/Scripts/Helpers/ConfirmUnsafeAppRemoval.ps1" # Threading functions