MSIX.AppData.ps1
|
# ============================================================================= # MSIX AppData / out-of-package helpers # ----------------------------------------------------------------------------- # Bridge between the host filesystem and packaged-app virtualised storage. # Useful when: # - A legacy installer dropped data in %AppData%\Roaming during conversion # (and the packaged app can't see it). # - You need to inspect / copy data into the package container's redirected # AppData (LocalCache\Roaming). # - A package was uninstalled but left orphaned data behind. # # References: # - manage/troubleshoot-msix-container (Invoke-CommandInDesktopPackage) # - desktop/desktop-to-uwp-known-issues # - PSF FileRedirectionFixup behaviour # ============================================================================= function Get-MsixContainerAppData { <# .SYNOPSIS Returns the per-package redirected AppData paths for an installed MSIX. .DESCRIPTION Packaged Win32 apps don't see the real %AppData%\Roaming. Their writes get redirected to %LocalAppData%\Packages\<PackageFamilyName>\LocalCache\ which is laid out as: LocalCache\Local <- maps to %LocalAppData% LocalCache\Roaming <- maps to %AppData% (Roaming) LocalCache\Temp <- maps to %Temp% This function returns those four paths for the named package. .PARAMETER PackageName Full or partial package name (wildcards accepted). .EXAMPLE Get-MsixContainerAppData -PackageName 'Contoso.App' #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [string]$PackageName ) PROCESS { $appx = Get-AppxPackage -Name $PackageName -ErrorAction SilentlyContinue if (-not $appx) { $appx = Get-AppxPackage | Where-Object { $_.Name -like "*$PackageName*" } } if (-not $appx) { throw "No installed package matches '$PackageName'." } if (@($appx).Count -gt 1) { throw "Multiple matches for '$PackageName'. Be more specific. Found: $(($appx.Name) -join ', ')" } $base = Join-Path $env:LOCALAPPDATA "Packages\$($appx.PackageFamilyName)" $cache = Join-Path $base 'LocalCache' return [pscustomobject]@{ Name = $appx.Name PackageFamilyName = $appx.PackageFamilyName PackageRoot = $base VirtualLocal = Join-Path $cache 'Local' VirtualRoaming = Join-Path $cache 'Roaming' VirtualTemp = Join-Path $cache 'Temp' AcExists = Test-Path (Join-Path $base 'AC') CacheExists = Test-Path $cache } } } function Get-MsixOrphanedAppData { <# .SYNOPSIS Lists %AppData%\Roaming subfolders that belong to no installed MSIX package family — i.e. probable leftovers from legacy installers run before/during MSIX conversion. .DESCRIPTION For every subfolder of %AppData%\Roaming this checks whether the folder name appears in any installed AppxPackage display/full name. Anything with no match is returned as a candidate orphan. False positives are expected — this is a triage list, not a delete list. Background: legacy installers run during MSIX conversion (Capture phase) sometimes write to %AppData%\Roaming. Those writes land on the host AppData, not inside the package. The packaged app can't see them on first launch and either re-creates state or fails. See https://learn.microsoft.com/windows/msix/desktop/desktop-to-uwp-known-issues .PARAMETER PackageHints Optional list of additional substrings that should be considered as "owned" (e.g. vendor names that don't appear in the package identity). .OUTPUTS Folder objects with .Path, .SizeMB, .LastWriteTime #> [CmdletBinding()] param( [string[]]$PackageHints ) $roaming = $env:APPDATA if (-not (Test-Path $roaming)) { throw "Roaming AppData not found: $roaming" } $installed = Get-AppxPackage | ForEach-Object { @($_.Name, $_.PackageFamilyName, $_.PublisherId, $_.Publisher) } | Where-Object { $_ } | Sort-Object -Unique $hints = @($PackageHints) + $installed | Where-Object { $_ } Get-ChildItem $roaming -Directory -ErrorAction SilentlyContinue | ForEach-Object { $folder = $_ $matched = $false foreach ($h in $hints) { if ($folder.Name -like "*$h*" -or $h -like "*$($folder.Name)*") { $matched = $true; break } } if (-not $matched) { $size = 0 try { $size = (Get-ChildItem $folder.FullName -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum } catch { Write-MsixLog Debug "Could not measure orphaned AppData folder '$($folder.FullName)': $_" } [pscustomobject]@{ Path = $folder.FullName Name = $folder.Name SizeMB = if ($size) { [math]::Round($size / 1MB, 2) } else { 0 } LastWriteTime = $folder.LastWriteTime } } } | Sort-Object SizeMB -Descending } function Copy-MsixHostAppDataIntoPackage { <# .SYNOPSIS Copies a host filesystem folder into the redirected Roaming directory of an installed MSIX package, so the packaged app sees the data on next launch. .DESCRIPTION Solves the common scenario where a legacy installer wrote per-user data to %AppData%\Roaming\<Vendor> before the package was converted, and the packaged app now starts with empty state. After running this, the data appears under LocalCache\Roaming (which the app sees as %AppData%). .PARAMETER SourcePath Folder on the host (typically under %AppData%\Roaming) to copy from. .PARAMETER PackageName Installed MSIX package name (or partial; wildcards allowed). .PARAMETER DestinationSubfolder Subfolder name inside LocalCache\Roaming. Defaults to source folder name. .PARAMETER WhatIf Show what would happen without copying. .EXAMPLE Copy-MsixHostAppDataIntoPackage -SourcePath "$env:APPDATA\ContosoLegacy" -PackageName 'Contoso.App' #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory)] [string]$PackageName, [string]$DestinationSubfolder ) if (-not (Test-Path $SourcePath)) { throw "Source not found: $SourcePath" } $info = Get-MsixContainerAppData -PackageName $PackageName if (-not $DestinationSubfolder) { $DestinationSubfolder = (Get-Item $SourcePath).Name } $dest = Join-Path $info.VirtualRoaming $DestinationSubfolder if ($PSCmdlet.ShouldProcess($dest, "Copy from $SourcePath")) { New-Item -ItemType Directory -Path $dest -Force | Out-Null Copy-Item "$SourcePath\*" $dest -Recurse -Force Write-MsixLog Info "Copied $SourcePath -> $dest" } return $dest } function Invoke-MsixContainerCommand { <# .SYNOPSIS Convenience wrapper around Invoke-CommandInDesktopPackage. .DESCRIPTION Launches a command inside the package container so you can inspect the merged file system and registry as the app sees them. Common uses: cmd.exe, regedit.exe, powershell.exe. See https://learn.microsoft.com/windows/msix/manage/troubleshoot-msix-container .PARAMETER PackageName Package name (wildcards allowed). .PARAMETER Command Command to run. Default: cmd.exe. .PARAMETER AppId Override Application Id. Defaults to the first one in the manifest. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [string]$PackageName, [string]$Command = 'cmd.exe', [string]$AppId ) PROCESS { $appx = Get-AppxPackage -Name $PackageName -ErrorAction SilentlyContinue if (-not $appx) { $appx = Get-AppxPackage | Where-Object { $_.Name -like "*$PackageName*" } } if (-not $appx) { throw "No installed package matches '$PackageName'." } if (@($appx).Count -gt 1) { throw "Multiple packages match '$PackageName'. Be specific." } if (-not $AppId) { $manifest = Get-AppPackageManifest -Package $appx.PackageFullName $AppId = (@($manifest.Package.Applications.Application))[0].Id } Write-MsixLog Info "Container exec: $($appx.PackageFamilyName)!$AppId -> $Command" Invoke-CommandInDesktopPackage -PackageFamilyName $appx.PackageFamilyName ` -AppId $AppId ` -Command $Command ` -PreventBreakaway } } function Get-MsixPackageStorageSummary { <# .SYNOPSIS Summarises disk usage of an installed package: install root, virtualised AppData (Local/Roaming/Temp), and AppContainer state. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [string]$PackageName ) PROCESS { $info = Get-MsixContainerAppData -PackageName $PackageName $appx = Get-AppxPackage -Name $info.Name | Select-Object -First 1 function _size($p) { if (-not (Test-Path $p)) { return 0 } $s = (Get-ChildItem $p -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum [math]::Round(([double]$s) / 1MB, 2) } [pscustomobject]@{ Name = $info.Name PackageFamilyName = $info.PackageFamilyName InstallLocation = $appx.InstallLocation InstallSizeMB = _size $appx.InstallLocation RoamingMB = _size $info.VirtualRoaming LocalMB = _size $info.VirtualLocal TempMB = _size $info.VirtualTemp PackageRoot = $info.PackageRoot } } } |