Runtime/Invoke-OSDAppWindowsInboxCleanup.ps1
|
# Built-in Windows Inbox Cleanup - run once during SetupComplete as SYSTEM. # Deliberately self-contained; the normal OSDApps runtime only supplies this file and config.json. [CmdletBinding()] param( [string]$ConfigurationPath, [switch]$DiagnoseOnly ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $script:RunId = [guid]::NewGuid().ToString('D') $script:StartedUtc = (Get-Date).ToUniversalTime().ToString('o') $script:LogPaths = @( (Join-Path $env:SystemRoot 'Temp\OSDApps-InboxCleanup-Diagnostics.log'), (Join-Path $env:ProgramData 'OSDApps\Logs\InboxCleanup.log') ) $script:ResultPath = Join-Path $env:ProgramData 'OSDApps\Results\InboxCleanup-Result.json' $script:Outcomes = [System.Collections.Generic.List[object]]::new() $script:Counts = @{ Removed=0; Absent=0; Skipped=0; Failed=0; WouldRemove=0 } function Write-InboxLog { param([string]$Message, [ValidateSet('INFO','WARN','ERROR')][string]$Level='INFO') $line = '{0} [{1}] [RunId={2}] {3}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'), $Level, $script:RunId, ($Message -replace '[\r\n]+',' | ') foreach ($path in $script:LogPaths) { try { [void][IO.Directory]::CreateDirectory((Split-Path -Path $path -Parent)) [IO.File]::AppendAllText($path, ($line + [Environment]::NewLine), [Text.Encoding]::UTF8) } catch { Write-Host ('[LOGGING ERROR] {0}: {1}' -f $path, $_.Exception.Message) } } Write-Host $line } function Write-InboxError { param([string]$Operation,[System.Management.Automation.ErrorRecord]$Record) Write-InboxLog -Level ERROR -Message ('{0}: {1}; ErrorId={2}; At={3}; Stack={4}' -f $Operation,$Record.Exception.Message,$Record.FullyQualifiedErrorId,$Record.InvocationInfo.PositionMessage,$Record.ScriptStackTrace) } function Add-InboxOutcome { param([string]$Id,[string]$Status,[int]$ProvisionedBefore=0,[int]$RegisteredBefore=0,[int]$ProvisionedAfter=0,[int]$RegisteredAfter=0,[string[]]$Errors=@()) [void]$script:Outcomes.Add([pscustomobject]@{ Id=$Id; Status=$Status; ProvisionedBefore=$ProvisionedBefore; RegisteredBefore=$RegisteredBefore ProvisionedAfter=$ProvisionedAfter; RegisteredAfter=$RegisteredAfter; Errors=@($Errors) }) } function Save-InboxResult { param([int]$ExitCode,[string]$Fatal) $report = [ordered]@{ SchemaVersion=1; ScriptVersion='BuiltIn-1.0'; RunId=$script:RunId StartUtc=$script:StartedUtc; CompletedUtc=(Get-Date).ToUniversalTime().ToString('o') Mode=$(if($DiagnoseOnly){'DiagnoseOnly'}else{'Apply'}) ExitCode=$ExitCode Identity=[Security.Principal.WindowsIdentity]::GetCurrent().Name Configuration=$ConfigurationPath Removed=$script:Counts.Removed; Absent=$script:Counts.Absent Skipped=$script:Counts.Skipped; Failed=$script:Counts.Failed WouldRemove=$script:Counts.WouldRemove; Fatal=$Fatal Items=@($script:Outcomes.ToArray()) } try { [void][IO.Directory]::CreateDirectory((Split-Path -Path $script:ResultPath -Parent)) $temporary = $script:ResultPath + '.' + $script:RunId + '.tmp' $report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $temporary -Encoding UTF8 -ErrorAction Stop Move-Item -LiteralPath $temporary -Destination $script:ResultPath -Force -ErrorAction Stop Write-InboxLog ('Result saved: {0}' -f $script:ResultPath) } catch { Write-InboxLog -Level WARN -Message ('Could not save result: {0}' -f $_.Exception.Message) } } # This log is written BEFORE reading configuration or enumerating Appx. Write-InboxLog ('ENTRY BuiltIn-1.0; PID={0}; Mode={1}; Script={2}' -f $PID,$(if($DiagnoseOnly){'DiagnoseOnly'}else{'Apply'}),$PSCommandPath) $exitCode = 0 $fatal = $null try { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $sid = if ($identity.User) { $identity.User.Value } else { '' } Write-InboxLog ('CONTEXT Identity={0}; SID={1}; IsSystem={2}; PSVersion={3}; Is64Bit={4}' -f $identity.Name,$sid,($sid -eq 'S-1-5-18'),$PSVersionTable.PSVersion,[Environment]::Is64BitProcess) foreach ($command in @('Get-AppxProvisionedPackage','Remove-AppxProvisionedPackage','Get-AppxPackage','Remove-AppxPackage')) { if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) { throw ('Required Appx command unavailable: {0}' -f $command) } } # Resolve the default inside the script body, not during parameter binding. # powershell.exe -File may see an empty $PSScriptRoot in parameter defaults. if ([string]::IsNullOrWhiteSpace($ConfigurationPath)) { $ConfigurationPath = Join-Path $PSScriptRoot 'config.json' } Write-InboxLog ('CONFIG path={0}' -f $ConfigurationPath) if (-not (Test-Path -LiteralPath $ConfigurationPath -PathType Leaf)) { throw ('Configuration not found: {0}' -f $ConfigurationPath) } $config = Get-Content -LiteralPath $ConfigurationPath -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop if ($null -eq $config -or $null -eq $config.PSObject.Properties['Remove'] -or $config.Remove -isnot [array]) { throw 'Configuration must contain a Remove array.' } $names = @($config.Remove | ForEach-Object { if ($_ -isnot [string] -or [string]::IsNullOrWhiteSpace($_) -or $_ -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$' -or $_.Contains('..')) { throw ('Invalid Windows package name: {0}' -f $_) } [string]$_ } | Select-Object -Unique) if ($names.Count -eq 0) { throw 'Remove array cannot be empty.' } Write-InboxLog ('CONFIG selected={0}; Names={1}' -f $names.Count,($names -join ', ')) $protectedPackages = @( 'Microsoft.Windows.ShellExperienceHost','Microsoft.Windows.StartMenuExperienceHost', 'MicrosoftWindows.Client.CBS','MicrosoftWindows.Client.Core', 'Microsoft.AAD.BrokerPlugin','Microsoft.AccountsControl', 'Microsoft.Windows.CloudExperienceHost','Microsoft.Windows.OOBENetworkConnectionFlow', 'Microsoft.Windows.OOBENetworkCaptivePortal','Microsoft.Windows.SecHealthUI', 'Microsoft.DesktopAppInstaller','Microsoft.WindowsStore','Microsoft.StorePurchaseApp', 'Microsoft.VCLibs.140.00','Microsoft.NET.Native.Framework.2.2', 'Microsoft.NET.Native.Runtime.2.2','Microsoft.UI.Xaml.2.8' ) foreach ($name in $names) { Write-InboxLog ('INSPECT {0}' -f $name) $operationErrors = @() $beforeProvisioned = 0 $beforeRegistered = 0 try { $provisioned = @(Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object { $_.DisplayName -eq $name }) $registered = @(Get-AppxPackage -AllUsers -Name $name -ErrorAction Stop) $beforeProvisioned = $provisioned.Count $beforeRegistered = $registered.Count Write-InboxLog ('FOUND {0}: provisioned={1}; registered={2}' -f $name,$beforeProvisioned,$beforeRegistered) if ($name -in $protectedPackages) { $script:Counts.Skipped++ Add-InboxOutcome -Id $name -Status SkippedProtected -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -ProvisionedAfter $beforeProvisioned -RegisteredAfter $beforeRegistered Write-InboxLog -Level WARN -Message ('SKIPPED {0}: protected' -f $name) continue } if (@($registered | Where-Object { $_.NonRemovable -eq $true }).Count -gt 0) { $script:Counts.Skipped++ Add-InboxOutcome -Id $name -Status SkippedNonRemovable -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -ProvisionedAfter $beforeProvisioned -RegisteredAfter $beforeRegistered Write-InboxLog -Level WARN -Message ('SKIPPED {0}: NonRemovable' -f $name) continue } if ($beforeProvisioned -eq 0 -and $beforeRegistered -eq 0) { $script:Counts.Absent++ Add-InboxOutcome -Id $name -Status Absent Write-InboxLog ('ABSENT {0}' -f $name) continue } if ($DiagnoseOnly) { $script:Counts.WouldRemove++ Add-InboxOutcome -Id $name -Status WouldRemove -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -ProvisionedAfter $beforeProvisioned -RegisteredAfter $beforeRegistered Write-InboxLog ('WOULD_REMOVE {0}' -f $name) continue } foreach ($package in $provisioned) { try { Remove-AppxProvisionedPackage -Online -PackageName $package.PackageName -ErrorAction Stop | Out-Null Write-InboxLog ('DEPROVISIONED {0} ({1})' -f $name,$package.PackageName) } catch { $operationErrors += ('Provisioning: {0}' -f $_.Exception.Message) Write-InboxError -Operation ('Deprovision {0}' -f $name) -Record $_ } } foreach ($package in $registered) { try { Remove-AppxPackage -Package $package.PackageFullName -AllUsers -ErrorAction Stop Write-InboxLog ('UNREGISTERED {0} ({1})' -f $name,$package.PackageFullName) } catch { $operationErrors += ('Registration: {0}' -f $_.Exception.Message) Write-InboxError -Operation ('Unregister {0}' -f $name) -Record $_ } } $remainingProvisioned = @(Get-AppxProvisionedPackage -Online -ErrorAction Stop | Where-Object { $_.DisplayName -eq $name }) $remainingRegistered = @(Get-AppxPackage -AllUsers -Name $name -ErrorAction Stop) if ($remainingProvisioned.Count -eq 0 -and $remainingRegistered.Count -eq 0) { $script:Counts.Removed++ Add-InboxOutcome -Id $name -Status Removed -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -Errors $operationErrors Write-InboxLog ('REMOVED {0}' -f $name) } else { $script:Counts.Failed++ Add-InboxOutcome -Id $name -Status Failed -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -ProvisionedAfter $remainingProvisioned.Count -RegisteredAfter $remainingRegistered.Count -Errors $operationErrors Write-InboxLog -Level ERROR -Message ('FAILED {0}: remaining provisioned={1}, registered={2}; errors={3}' -f $name,$remainingProvisioned.Count,$remainingRegistered.Count,($operationErrors -join ' | ')) } } catch { $script:Counts.Failed++ $operationErrors += $_.Exception.Message Write-InboxError -Operation ('Inspect/process {0}' -f $name) -Record $_ Add-InboxOutcome -Id $name -Status Failed -ProvisionedBefore $beforeProvisioned -RegisteredBefore $beforeRegistered -Errors $operationErrors } } Write-InboxLog ('COMPLETE Removed={0}; Absent={1}; Skipped={2}; Failed={3}; WouldRemove={4}; Selected={5}' -f $script:Counts.Removed,$script:Counts.Absent,$script:Counts.Skipped,$script:Counts.Failed,$script:Counts.WouldRemove,$names.Count) if ($script:Counts.Failed -gt 0) { $exitCode = 1 } } catch { $exitCode = 2 $fatal = $_.Exception.Message Write-InboxError -Operation FATAL -Record $_ } finally { Save-InboxResult -ExitCode $exitCode -Fatal $fatal Write-InboxLog ('EXIT code={0}' -f $exitCode) } exit $exitCode |