Public/Remove-MSIXServices.ps1
|
function Remove-MSIXServices { <# .SYNOPSIS Removes windows.service declarations (+ matching COM ServiceServer and empty service-host apps) from an MSIX manifest. Once no service remains, the localSystemServices/packagedServices capabilities are dropped too (keeps the install dialog from demanding admin) - unless -KeepServiceCapabilities. .PARAMETER MSIXFolderPath Expanded MSIX package folder. Pipeline by property name. .PARAMETER ServiceName Name of a specific service. Omit to remove all. .PARAMETER KeepHostApplication Keep the hosting Application even if it becomes an empty service-host. .EXAMPLE Get-MSIXServices -MSIXFolder $pkg | Remove-MSIXServices .NOTES Andreas Nick, 2026 #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [System.IO.DirectoryInfo] $MSIXFolderPath, [Parameter(ValueFromPipelineByPropertyName = $true, Position = 1)] [string] $ServiceName, [switch] $KeepHostApplication, # Keep the rescap service capabilities even when no service remains. # By default they are removed once the last service is gone - otherwise # the install dialog keeps showing "installs a service" + admin prompt. [switch] $KeepServiceCapabilities ) begin { # Keyed by resolved folder path -> list of service names to remove (empty = all) $pending = @{} } process { $key = $MSIXFolderPath.FullName if (-not $pending.ContainsKey($key)) { $pending[$key] = [System.Collections.Generic.List[string]]::new() } if (-not [string]::IsNullOrEmpty($ServiceName)) { $pending[$key].Add($ServiceName) } } end { foreach ($folder in $pending.Keys) { $manifestPath = Join-Path $folder 'AppxManifest.xml' if (-not (Test-Path $manifestPath)) { Write-Warning "AppxManifest.xml not found in '$folder' - skipping." continue } $manifest = New-Object System.Xml.XmlDocument $manifest.PreserveWhitespace = $false $manifest.Load($manifestPath) $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable) $AppXNamespaces.GetEnumerator() | ForEach-Object { $null = $nsmgr.AddNamespace($_.Key, $_.Value) } $targetNames = $pending[$folder] # empty list = remove all $removedAny = $false $applications = @($manifest.SelectNodes('//ns:Package/ns:Applications/ns:Application', $nsmgr)) foreach ($app in $applications) { $appId = $app.GetAttribute('Id') $extensionsNode = $app.SelectSingleNode('ns:Extensions', $nsmgr) if ($null -eq $extensionsNode) { continue } $svcExtensions = @($extensionsNode.SelectNodes("desktop6:Extension[@Category='windows.service']", $nsmgr)) foreach ($ext in $svcExtensions) { $svc = $ext.SelectSingleNode('desktop6:Service', $nsmgr) $svcName = if ($null -ne $svc) { $svc.GetAttribute('Name') } else { '' } if ($targetNames.Count -gt 0 -and $targetNames -notcontains $svcName) { continue } if (-not $PSCmdlet.ShouldProcess($folder, "Remove service '$svcName' (Application '$appId')")) { continue } $null = $extensionsNode.RemoveChild($ext) $removedAny = $true Write-Verbose "Removed windows.service '$svcName' from Application '$appId'." # Strip a matching COM ServiceServer extension (same ServiceName) foreach ($comExt in @($extensionsNode.SelectNodes('com2:Extension', $nsmgr))) { $serviceServer = $comExt.SelectSingleNode(".//*[local-name()='ServiceServer']", $nsmgr) if ($null -ne $serviceServer -and $serviceServer.GetAttribute('ServiceName') -eq $svcName) { $null = $extensionsNode.RemoveChild($comExt) Write-Verbose "Removed COM ServiceServer for '$svcName' from Application '$appId'." } } } # Drop the hosting Application if it is now a pure, empty service host if (-not $KeepHostApplication) { $remaining = $extensionsNode.SelectNodes('*') $visualElems = $app.SelectSingleNode("*[local-name()='VisualElements']") $appListEntry = if ($null -ne $visualElems) { $visualElems.GetAttribute('AppListEntry') } else { '' } if ($remaining.Count -eq 0) { # Remove the now-empty <Extensions> node either way $null = $app.RemoveChild($extensionsNode) if ($appListEntry -eq 'none') { $null = $app.ParentNode.RemoveChild($app) Write-Verbose "Removed empty service-host Application '$appId' (AppListEntry=none)." } } } } # Once no windows.service remains, drop the now-pointless service # capabilities - otherwise the install dialog still shows "installs a # service" and demands admin rights. if ($removedAny -and -not $KeepServiceCapabilities) { $stillHasService = $manifest.SelectSingleNode("//desktop6:Extension[@Category='windows.service']", $nsmgr) if ($null -eq $stillHasService) { foreach ($capName in 'localSystemServices', 'packagedServices') { $cap = $manifest.SelectSingleNode("//*[local-name()='Capability'][@Name='$capName']", $nsmgr) if ($null -ne $cap) { $null = $cap.ParentNode.RemoveChild($cap) Write-Verbose "Removed service capability '$capName'." } } } } if ($removedAny) { $manifest.Save($manifestPath) Write-Verbose "Saved $manifestPath" } else { Write-Warning "No matching services found in '$folder'." } } } } |