Public/Fixes/Add-MSIXFixSSMS22.ps1

function Add-MSIXFixSSMS22 {
<#
.SYNOPSIS
    Fixes "Error opening the connection dialog - Element not found (0x80070490)" in an SSMS 21/22 MSIX package.

.DESCRIPTION
    SSMS 21/22 is based on the Visual Studio shell. The connection dialog calls
    ISetupConfiguration2.GetInstanceForCurrentProcess() (VS Setup API), which reads
    %ProgramData%\Microsoft\VisualStudio\Packages\_Instances\<id>\state.json and checks that
    SSMS.exe lies below its installationPath. In a captured package this file is missing or points to
    the installation folder of the packaging machine.

    The fix:
      - creates VFS\Common AppData\Microsoft\VisualStudio\Packages\_Instances\<id>\state.json with
        installationPath = -InstallationRoot (default 'C:\Program Files\WindowsApps', matches every
        version/publisher)
      - removes the NGEN native images of the packaging machine (VFS\Windows\assembly\NativeImages_*)
      - removes the Microsoft.WindowsAppRuntime package dependency (not used by SSMS) unless
        -KeepWindowsAppRuntimeDependency is set
      - removes the captured services (VSInstallerElevationService, VSStandardCollectorService150)
        incl. their COM server and service capabilities unless -NotRemoveServices is set
      - removes the Visual Studio Installer application and its Start menu shortcut
      - removes the captured account stores (VFS\Local AppData\.IdentityService)
      - removes the VS Installer payload (VFS\Common AppData\Package Cache and ...\VisualStudio\Packages)
      - removes the .NETFramework machine keys (incl. Wow6432Node) from Registry.dat

    For SSMS 20 and earlier use Add-MSIXFixSSMS.

.EXAMPLE
    Add-MSIXFixSSMS22 -MsixFile .\SSMS22-x64.msix -OutputFilePath .\SSMS22-x64_fixed.msix -Force -Verbose

.NOTES
    Writing to Registry.dat usually requires an elevated PowerShell.
    https://www.nick-it.de
    Andreas Nick, 2026
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.IO.FileInfo] $MsixFile,

        [System.IO.FileInfo] $OutputFilePath = $null,

        [System.IO.DirectoryInfo] $MSIXFolder = ($env:Temp + '\MSIX_TEMP_' + [System.Guid]::NewGuid().ToString()),

        [string] $Subject = '',
        [string] $Version = '',

        [string] $InstallationRoot = 'C:\Program Files\WindowsApps',

        [switch] $KeepWindowsAppRuntimeDependency,
        [switch] $NotRemoveServices,
        [switch] $Force,
        [switch] $KeepMSIXFolder
    )

    process {
        if ($null -eq $OutputFilePath) {
            $OutputFilePath = $MsixFile
        }

        try {
            $package = Open-MSIXPackage -MsixFile $MsixFile -Force:$Force -MSIXFolder $MSIXFolder -ErrorAction Stop
            if (-not $package) {
                throw "Failed to open MSIX package: $($MsixFile.FullName)"
            }
            $root = $MSIXFolder.FullName

            if ($Subject -ne '') {
                Set-MSIXPublisher -MSIXFolder $MSIXFolder -PublisherSubject $Subject
            }
            if ($Version -ne '') {
                Set-MSIXPackageVersion -MSIXFolder $MSIXFolder -MSVersion $Version
            }

            # NGEN native images of the packaging machine (~1 GB, never used in the container)
            foreach ($ni in @(Get-ChildItem -LiteralPath (Join-Path $root 'VFS\Windows\assembly') -Directory -Filter 'NativeImages_*' -ErrorAction SilentlyContinue)) {
                Remove-Item -LiteralPath $ni.FullName -Recurse -Force
                Write-Verbose "Removed native images: $($ni.Name)"
            }

            # WindowsAppRuntime dependency (added by the VS Installer during capture)
            if (-not $KeepWindowsAppRuntimeDependency -and
                (Get-MSIXDependencies -MSIXFolder $MSIXFolder | Where-Object Name -like 'Microsoft.WindowsAppRuntime*')) {
                Remove-MSIXDependencies -MSIXFolderPath $MSIXFolder -Name 'Microsoft.WindowsAppRuntime*'
            }

            # Services captured with the VS Installer (installer elevation, diagnostics collector) -
            # not needed by SSMS in the container.
            if (-not $NotRemoveServices) {
                $services = @(Get-MSIXServices -MSIXFolder $MSIXFolder)
                if ($services.Count -gt 0) {
                    $services | Remove-MSIXServices
                }
            }

            # The captured VS Installer (setup.exe) must not show up in the Start menu: drop its
            # desktop7 shortcut (incl. the .lnk) and the application entry itself.
            $installerApps = @(Get-MSIXApplications -MSIXFolder $MSIXFolder |
                Where-Object { $_.Executable -like '*\Microsoft Visual Studio\Installer\setup.exe' })
            foreach ($installerApp in $installerApps) {
                Get-MSIXDesktop7Shortcut -MSIXFolder $MSIXFolder |
                    Where-Object ApplicationId -eq $installerApp.Id |
                    Remove-MSIXDesktop7Shortcut
                Remove-MSIXApplications -MSIXFolder $MSIXFolder -MSIXAppID $installerApp.Id
                Write-Verbose "Removed the Visual Studio Installer application '$($installerApp.Id)'."
            }

            # Account stores of the user who captured the package
            $identityService = Join-Path $root 'VFS\Local AppData\.IdentityService'
            if (Test-Path -LiteralPath $identityService) {
                Remove-Item -LiteralPath $identityService -Recurse -Force
                Write-Verbose 'Removed VFS\Local AppData\.IdentityService'
            }

            # Installer payload of the VS Installer (several GB, not needed in the container).
            # The VS Setup instance is created again below.
            foreach ($rel in @('VFS\Common AppData\Package Cache',
                               'VFS\Common AppData\Microsoft\VisualStudio\Packages')) {
                $p = Join-Path $root $rel
                if (Test-Path -LiteralPath $p) {
                    $sizeMB = [Math]::Round(((Get-ChildItem -LiteralPath $p -Recurse -File -ErrorAction SilentlyContinue |
                                              Measure-Object -Property Length -Sum).Sum / 1MB), 1)
                    Remove-Item -LiteralPath $p -Recurse -Force
                    Write-Verbose ("Removed {0} ({1} MB)" -f $rel, $sizeMB)
                }
            }

            # .NETFramework machine keys: NGEN data of the packaging machine would hide the real
            # values of the target system.
            $registryDat = Join-Path $root 'Registry.dat'
            foreach ($key in @('REGISTRY\MACHINE\SOFTWARE\Microsoft\.NETFramework',
                               'REGISTRY\MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework')) {
                Remove-MSIXVirtualRegistryKey -HiveFilePath $registryDat -KeyPath $key -Recurse
            }

            # VS Setup instance (state.json)
            $vsPackages  = Join-Path $root 'VFS\Local AppData\Microsoft\VisualStudio\Packages'
            # @(...)[0] instead of Select-Object -First 1: on Windows PowerShell 5.1 that reports a
            # StopUpstreamCommandsException to a caller's -ErrorVariable.
            $instanceDir = @(Get-ChildItem -LiteralPath (Join-Path $vsPackages '_Instances') -Directory -ErrorAction SilentlyContinue)[0]
            $channelDir  = @(Get-ChildItem -LiteralPath (Join-Path $vsPackages '_Channels') -Directory -ErrorAction SilentlyContinue)[0]
            if ($null -eq $instanceDir -or $null -eq $channelDir) {
                throw "No captured VS setup instance/channel found under $vsPackages"
            }

            $channelManifest = Get-Content -LiteralPath (Join-Path $channelDir.FullName 'channelManifest.json') -Raw | ConvertFrom-Json
            $product         = @($channelManifest.channelItems | Where-Object { $_.type -eq 'ChannelProduct' })[0]

            # Only catalog.info is needed: a flat object at the start of the ~18 MB catalog.json.
            # Read the first 64 KB instead of parsing the whole file; fall back to a full parse.
            $catalogPath = Join-Path $channelDir.FullName 'catalog.json'
            $catalogInfo = $null
            $reader = New-Object System.IO.StreamReader $catalogPath
            try {
                $buffer = New-Object char[] 65536
                $head   = [string]::new($buffer, 0, $reader.Read($buffer, 0, $buffer.Length))
            }
            finally {
                $reader.Dispose()
            }
            if ($head -match '"info"\s*:\s*(\{[^{}]*\})') {
                try { $catalogInfo = $Matches[1] | ConvertFrom-Json } catch { $catalogInfo = $null }
            }
            if ($null -eq $catalogInfo) {
                $catalogInfo = (Get-Content -LiteralPath $catalogPath -Raw | ConvertFrom-Json).info
            }
            if ($null -eq $product) {
                throw "No ChannelProduct entry in $(Join-Path $channelDir.FullName 'channelManifest.json')"
            }
            $now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", [System.Globalization.CultureInfo]::InvariantCulture)

            $state = [ordered]@{
                installationName    = $catalogInfo.id
                installationPath    = $InstallationRoot
                launchParams        = [ordered]@{ fileName = 'Common7\IDE\SSMS.exe'; arguments = '' }
                installationVersion = $product.version
                installDate         = $now
                updateDate          = $now
                layoutPath          = $null
                channelId           = $channelManifest.info.manifestName
                channelPath         = "$env:ProgramData\Microsoft\VisualStudio\Packages\_Channels\$($channelDir.Name)\catalog.json"
                channelUri          = $channelManifest.updateUri
                installChannelUri   = $channelManifest.updateUri
                localizedResources  = @([ordered]@{ language = 'en-us'; title = "$($catalogInfo.productName) $($catalogInfo.productLineVersion)"; description = ''; license = '' })
                product             = [ordered]@{ id = $product.id; version = $product.version; chip = 'x64'; type = 'Product'; isExtension = $false }
                selectedPackages    = @([ordered]@{ id = $product.id; version = $product.version; chip = 'x64'; type = 'Product'; selectedState = 'IndividuallySelected'; userSelected = $true })
                catalogInfo         = $catalogInfo
                properties          = [ordered]@{
                    channelManifestId   = $channelManifest.info.id
                    nickname            = ''
                    setupEngineFilePath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\setup.exe"
                }
            }

            $stateFile = Join-Path $root "VFS\Common AppData\Microsoft\VisualStudio\Packages\_Instances\$($instanceDir.Name)\state.json"
            $null = New-Item -ItemType Directory -Force -Path (Split-Path $stateFile)
            [System.IO.File]::WriteAllText($stateFile, ($state | ConvertTo-Json -Depth 6), (New-Object System.Text.UTF8Encoding $false))
            Write-Verbose "Created state.json for instance $($instanceDir.Name), installationPath: $InstallationRoot"

            Close-MSIXPackage -MSIXFolder $MSIXFolder -MSIXFile $OutputFilePath -Force:$Force -KeepMSIXFolder:$KeepMSIXFolder -ErrorAction Stop
            "SSMS 21/22 fix applied: $OutputFilePath"
        }
        catch {
            $failure = $_
            # Do not leave the extracted package (several GB) behind in %TEMP%: remove the default
            # temporary folder unless it should be kept. A folder passed via -MSIXFolder stays.
            if ($PSBoundParameters.ContainsKey('KeepMSIXFolder')) { $keepFolder = $KeepMSIXFolder.IsPresent }
            else { $keepFolder = [bool]$Script:MSIXForceletsConfig.KeepTempFolder }
            if (-not $keepFolder -and -not $PSBoundParameters.ContainsKey('MSIXFolder') -and (Test-Path -LiteralPath $MSIXFolder.FullName)) {
                Remove-Item -LiteralPath $MSIXFolder.FullName -Recurse -Force -ErrorAction SilentlyContinue
                if (Test-Path -LiteralPath $MSIXFolder.FullName) { & cmd.exe /C rmdir /S /Q "$($MSIXFolder.FullName)" 2>$null }
                Write-Verbose "Removed extraction folder after the error: $($MSIXFolder.FullName)"
            }
            throw (New-Object System.Exception ("Error applying SSMS 21/22 fix: $($failure.Exception.Message)", $failure.Exception))
        }
    }
}