Files
microsoft-ui-xaml/test/CreateTestPayload.ps1
George Gao d0cf13fc06 Persist WinUI metadata across WinUI shutdown to allow WinUI restart (#11982)
## Fixes

WinUI cannot be shut down and restarted in a process. This change fixes
that problem, allowing for shut down and restart.

Addresses #11741

## PR Type

- [x] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):

## Description

Second attempt to solve this problem. This one is simpler and does not
involve new events on `WindowsXamlManager`. Thanks to Evelyn and Morten
for the suggestion.

Shutting down WinUI and restarting results in a crash in
`MicaBackdrop`'s `Kind` property. The problem here is stale data. MUXC
registers custom `DependencyProperty` objects (including
`MicaBackdrop.Kind`) with MUX, and also has references to those DP
objects. When MUX shuts down, it clears out all fields in its registered
`CCustomDependencyProperty` objects, including those from MUXC. MUXC
continues to hold on to its old and cleared out DP objects. When MUXC
restarts, it sees that its caches are still filled, so it doesn't bother
registering new custom DPs. It then tries to reuse its old `Kind`
property, finds that the default value is now null, and crashes.

The fix is for MUX to not clear out its registered
`CCustomDependencyProperty` objects when shutting down. These types are
agile and are valid on any UI thread. They are also potentially stored
in `static readonly` fields, which means they can't be discarded and
re-registered. In practice, both MUXC and the Windows Community Toolkit
have custom DPs that can be reused across WinUI shutdown and restart.

Two other changes as part of this PR:
* Breaks a reference cycle that leaks the `WindowsXamlManager`.
Previously it referenced a `XamlCore` object that referenced it back.
* Updates WinUICppIslandsSampleApp to show a count of WinUI objects
(Application, DispatcherQueueController, WindowsXamlManager, and
DesktopWindowXamlSource).

### Current Behavior

When WinUI shuts down we clear out all metadata and custom DPs. When
WinUI restarts, external components (like MUXC) will find their
previously registered custom DPs invalid and hit errors.

After WinUI shuts down the `WindowsXamlManager` object will still be
alive due to a leak.

### New Behavior

When WinUI shuts down we leave all existing metadata and custom DPs
alone. When WinUI restarts, external components (like MUXC) can continue
using their custom DPs.

Note that this has caveats for cleanup. The `Application` object now
survives WinUI shutdown. Metadata objects also survive until process
shutdown and technically leak, were it not for the entire process being
torn down. Previously WinUI shutting down was where we cleared metadata.
Now that metadata persists past shutdown, the next place that we were
attempting to delete metadata is unloading the MUX binary, which only
happens during process shutdown. But binary unload is not a safe place
to do cleanup, because metadata objects may point to objects in other
binaries which have already been unloaded. We choose to leak these
objects and let the process take them down instead.

When WinUI shuts down the `WindowsXamlManager` object will be released
afterwards.

## Customer Impact

Non-WASDK (e.g. Win32, WinForms) apps using Xaml islands can now release
every single island, then re-create an island later.

## Regression Potential

- [x] Low risk — isolated change, limited scope
- [ ] Medium risk — touches shared components or public APIs
- [ ] High risk — architectural or breaking API change

## How Has This Been Tested?

- [x] I have performed a self-review of my own code
- [x] I have added tests to cover my changes
- [x] Existing tests pass locally

## Screenshots (if appropriate)

<!-- If you are making visual changes, include before/after screenshots.
-->
<img width="1631" height="1346" alt="WinUICppIslandsSampleApp"
src="https://github.com/user-attachments/assets/2f2ed871-1a1f-4ca1-8668-a2b18a5bb0d2"
/>
<img width="1368" height="1069" alt="WinUICsIslandsSampleApp"
src="https://github.com/user-attachments/assets/459f0a12-5833-4282-9694-4e65952c97c5"
/>
2026-10-13 12:23:27 -07:00

337 lines
14 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
[CmdletBinding()]
param(
[String]$BinSourceRoot = "",
[ValidateSet("x86", "x64", "arm64", "arm64ec")]
[string]$Platform = "$env:BUILDPLATFORM",
[ValidateSet("chk", "fre")]
[string]$Configuration = "$env:_BuildType",
[string][ValidateSet("DevTestSuite", "ScenarioTestSuite", "PGO", IgnoreCase = $false)]
$Mode = "DevTestSuite",
[switch]$ShowPayload,
[switch]$SkipSymbols,
[switch]$SkipWinUIGallery,
[switch]$IncludeWebView2RuntimeInstaller,
[switch]$Clean,
[switch]$Quiet
)
if (-not $Platform)
{
$Platform = "x86"
}
if (-not $Configuration)
{
$Configuration = "chk"
}
$Arch = $Platform
if($Platform -eq "x64")
{
$Arch = "amd64"
}
$repoRoot = Split-Path -Parent $PSScriptRoot
if(!$BinSourceRoot)
{
$BinSourceRoot = "$repoRoot\BuildOutput\bin"
}
$outpath = "$repoRoot\TestPayload\$Platform$Configuration"
$binpath = "$BinSourceRoot\$Arch$Configuration"
$genxbfPath = "$BinSourceRoot\GenXBF\$Platform"
function Print-Config
{
if ($Quiet) { return }
$modes = [System.Collections.ArrayList]::new()
[void]$modes.Add('DevTestSuite')
[void]$modes.Add('ScenarioTestSuite')
[void]$modes.Add('PGO')
$modes.Remove($Mode)
Write-Host "==================================================="
Write-Host "outpath: $outpath"
Write-Host "binpath: $binpath"
Write-Host "genxbfPath: $genxbfPath"
Write-Host "BinSourceRoot: $BinSourceRoot"
Write-Host "Platform: $Platform"
Write-Host "Configuration: $Configuration"
Write-Host "Skip Symbols: $SkipSymbols"
Write-Host "Skip WinUIGallery: $SkipWinUIGallery"
Write-Host "Show Payload: $ShowPayload"
Write-Host "Mode set (manually set by -mode): $Mode"
Write-Host "Modes not set [X] : $modes"
Write-Host "==================================================="
}
Print-Config
if ($Clean -and (Test-Path $outpath))
{
Get-ChildItem "$outpath\*" -Recurse -File | ForEach-Object { Remove-Item $_.FullName -Force }
Get-ChildItem "$outpath\*" -Directory | ForEach-Object { Remove-Item $_.FullName -Force -Recurse }
}
$buildconfig = @{
Platform = $Platform
Configuration = $Configuration
}
if (-not (Test-Path $outpath))
{
$ignore = New-Item -ItemType Directory $outpath
}
$buildconfig | ConvertTo-Json | Out-File (Join-Path $outpath "buildconfig.json")
$logFile = (Join-Path $outpath "CreateTestPayload.log")
if (Test-Path $logFile)
{
Remove-Item $logFile
}
function Publish-Item {
Param($source, $destinationDir, [switch]$IfExists = $false)
if ((-not $IfExists) -or (Test-Path $source))
{
if (-not $Quiet) { Write-Host "Copy from '$source' to '$destinationDir'" }
$sourceDirectory = [System.IO.Path]::GetDirectoryName($source).Trim()
$sourceFileName = [System.IO.Path]::GetFileName($source).Trim()
# If there was a file name specified, we don't want to copy recursively.
# Otherwise, we do.
if ($sourceFileName)
{
& robocopy "$sourceDirectory" "$destinationDir" "$sourceFileName" /XX /LOG+:$logFile | Out-Null
}
else
{
& robocopy "$sourceDirectory" "$destinationDir" "*" /E /XX /LOG+:$logFile | Out-Null
}
"$([Environment]::NewLine)Exit code: $global:LASTEXITCODE" | Out-File $logFile -Encoding utf8 -Append
# robocopy returns a bunch of different exit codes to indicate exactly what happened during copy,
# many of which indicate success. Only exit codes 8 or above indicate failure.
# This is kind of annoying, since most other things interpret a non-zero exit code to mean failure.
# We'll set $LASTEXITCODE to 0 to avoid this if the exit code indicates success.
#
# Inquiring minds can find the full list of exit codes here:
#
# https://learn.microsoft.com/en-us/troubleshoot/windows-server/backup-and-storage/return-codes-used-robocopy-utility
#
if ($global:LASTEXITCODE -lt 8)
{
$global:LASTEXITCODE = 0
}
else
{
exit $global:LASTEXITCODE
}
}
else
{
if (-not $Quiet) { Write-Host "Not copying '$source' to '$destinationDir' because it did not exist." }
}
}
$redistPlatform = $Platform
if($Platform -eq "arm64ec")
{
$redistPlatform = "x64"
}
if ($Mode -eq "DevTestSuite" -or $Mode -eq "ScenarioTestSuite")
{
Publish-Item "$binpath\TAEF\*" "$outpath"
Publish-Item "$binpath\TAEF\NetFx4.5\*" "$outpath"
Publish-Item "$binpath\TestDependencies\localDotNet\*" "$outpath\localDotNet"
Publish-Item "$binpath\TestDependencies\localDotNet\WinRT.Runtime.dll" "$outpath\"
Publish-Item "$binpath\TestDependencies\WinUITest.cer" "$outpath"
Publish-Item "$binpath\TestDependencies\*.cer" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\crt\vc_redist.$redistPlatform.exe" "$outpath"
# Publish items from repo:
Publish-Item "$repoRoot\Test\scripts\*" "$outpath"
Publish-Item "$repoRoot\Helix\scripts\*" "$outpath"
Publish-Item "$repoRoot\Helix\common\test\*" "$outpath"
Publish-Item "$repoRoot\controls\tools\EnableMUXControlsTestAppManagedDebugging.*" "$outpath"
Publish-Item "$binpath\TestDependencies\dotnet-windowsdesktop-runtime-installer.exe" "$outpath"
if ($IncludeWebView2RuntimeInstaller)
{
$webView2RuntimeInstaller = "$binpath\TestDependencies\tools\x64\MicrosoftEdgeWebView2RuntimeInstallerX64.exe"
if (-not (Test-Path -LiteralPath $webView2RuntimeInstaller -PathType Leaf))
{
throw "The x64 Evergreen WebView2 Standalone Installer was not found at '$webView2RuntimeInstaller'."
}
$signature = Get-AuthenticodeSignature -FilePath $webView2RuntimeInstaller
$signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "<none>" }
if ($signature.Status -ne "Valid" -or $signerSubject -notmatch "(^|,\s*)O=Microsoft Corporation(,|$)")
{
throw "Evergreen WebView2 Standalone Installer signature validation failed. Status: $($signature.Status); signer: $signerSubject"
}
Publish-Item $webView2RuntimeInstaller "$outpath\tools\x64"
}
}
if ($Mode -eq "DevTestSuite")
{
Publish-Item "$binpath\Product\*.dll" "$outpath"
Publish-Item "$binpath\Product\*.dll" "$outpath\Test"
Publish-Item "$binpath\Product\*.winmd" "$outpath\Test"
Publish-Item "$binpath\Product\en-US\*.dll.mui" "$outpath\Test\en-US"
Publish-Item "$binpath\Product\Microsoft.Ui.Xaml\*" "$outpath\Test\Microsoft.UI.Xaml"
Publish-Item "$binpath\Test\" "$outpath\Test"
Publish-Item "$binpath\TAEF\EtwProcessor.dll" "$outpath\Test"
Publish-Item "$binpath\TAEF\TE.AppxUnitTestClient.dll" "$outpath\Test"
Publish-Item "$binpath\TAEF\Microsoft.VisualStudio.TestPlatform.TestExecutor.WinRTCore.winmd" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\*.cmd" "$outpath"
Publish-Item "$binpath\TestDependencies\*.dll" "$outpath"
Publish-Item "$binpath\TestDependencies\*.exe" "$outpath"
Publish-Item "$binpath\TestDependencies\*.winmd" "$outpath"
Publish-Item "$binpath\TestDependencies\localDotNet\D3DCompiler_47_cor3.dll" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\localDotNet\Microsoft.Windows.SDK.NET.dll" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\crtforwarders\*.dll" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\AppX\*" "$outpath\Test"
Publish-Item "$binpath\Test\XamlDiagnosticsTap.dll" "$outpath"
Publish-Item "$binpath\Test\GenericXaml\generic.Xaml" "$outpath\Test\GenXbfValidation\"
Publish-Item "$binpath\Test\GenericXaml\split\*" "$outpath\Test\GenXbfValidation\"
Publish-Item "$genxbfPath\genxbf.dll" "$outpath\Test\GenXbfValidation\"
Publish-Item "$binpath\product\Microsoft.WinUI.dll" "$outpath\Test\PrivateAPITests\"
Publish-Item "$binpath\test\private\Microsoft.WinUI.dll" "$outpath\Test\"
Publish-Item "$binpath\test\private\Microsoft.WinUI.dll" "$outpath\"
# Since API tests run within the context of TE.ProcessHost.exe, we need to make sure we use the one modified to have WinUI 3 WinRT types in its manifest.
Publish-Item "$binpath\Test\UnpackagedApps\MUXControlsTestApp\TE.ProcessHost.exe" "$outpath\"
# MUXC WebView2 test infra gets CoreWebView2 SDK Version from this DLL
Publish-Item "$binpath\Test\UnpackagedApps\MUXControlsTestApp\Microsoft.Web.WebView2.Core.dll" "$outpath\Test"
if (!$SkipSymbols)
{
Publish-Item "$binpath\Symbols\Product\Microsoft.ui.xaml.pdb" "$outpath\Test"
Publish-Item "$binpath\Symbols\Product\Microsoft.ui.xaml.controls.pdb" "$outpath\Test"
}
else
{
# The recursive copy of Test\ picks up PDBs from UnpackagedApps/. Remove them.
Get-ChildItem "$outpath\Test" -Recurse -Filter *.pdb -ErrorAction SilentlyContinue | Remove-Item -Force
}
}
if ($Mode -eq "ScenarioTestSuite")
{
# This check for ValidateReunion/ WindowsAppSDK is temporary to limit test run only for Gallery against WAP. This shall no longer be needed
# TODO: once pipeline is ready to run all sample apps, remove this check
if (-not ($env:BUILD_DEFINITIONNAME -and ($env:BUILD_DEFINITIONNAME.Contains("ValidateReunion") -or $env:BUILD_DEFINITIONNAME.Contains("WindowsAppSDK"))))
{
Publish-Item "$binpath\Samples\WinUICsDesktopSampleApp_Test\*.msix*" "$outpath\Test\"
Publish-Item "$binpath\Samples\WinUICppDesktopSampleApp_Test\*.msix*" "$outpath\Test\"
Publish-Item "$binpath\Samples\WinUICppIslandsSampleApp_Test\*.msix*" "$outpath\Test\"
Publish-Item "$binpath\Samples\DisableXamlGeneratedMainCs_Test\*.msix" "$outpath\Test\"
Publish-Item "$binpath\Samples\DisableXamlGeneratedMainNoCtorCs_Test\*.msix" "$outpath\Test\"
Publish-Item "$binpath\Samples\DisableXamlGeneratedMainCpp_Test\*.msix" "$outpath\Test\"
Publish-Item "$binpath\Samples\DisableXamlGeneratedMainNoCtorCpp_Test\*.msix" "$outpath\Test\"
}
if (-not $SkipWinUIGallery)
{
# The Windows App SDK MSIX tooling stamps platform/version/config into the test-package directory name,
# e.g. WinUIGallery_x86_1.0.0.0_Debug_Test. Discover it dynamically rather than hard-coding the suffix.
$galleryTestDir = Get-ChildItem -Path "$binpath\Samples" -Directory -Filter "WinUIGallery*Test" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $galleryTestDir)
{
throw "WinUIGallery test package directory not found under '$binpath\Samples' (expected a folder matching 'WinUIGallery*Test')."
}
Publish-Item "$($galleryTestDir.FullName)\WinUIGallery*.msix*" "$outpath\Test\"
Get-ChildItem -Path "$outpath\Test" -Filter "WinUIGallery*.msix*" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in @(".msix", ".msixbundle") } |
ForEach-Object {
$target = Join-Path $_.DirectoryName ("WinUIGallery$($_.Extension)")
if ($_.FullName -ne $target) { Move-Item -Path $_.FullName -Destination $target -Force }
}
Publish-Item "$binpath\Test\WinUIGalleryTestData.xml" "$outpath\Test\"
}
Publish-Item "$binpath\Test\MUXControls.Test.dll" "$outpath\Test\"
Publish-Item "$binpath\Test\MUXTestInfra.dll" "$outpath\Test\"
Publish-Item "$binpath\Test\Microsoft.Windows.Apps.Test.*.dll" "$outpath\Test\"
# TODO: Remove below check
if (-not ($env:BUILD_DEFINITIONNAME -and ($env:BUILD_DEFINITIONNAME.Contains("ValidateReunion") -or $env:BUILD_DEFINITIONNAME.Contains("WindowsAppSDK"))))
{
Publish-Item "$binpath\Samples\WinUICppDesktopSampleApp_Test\Dependencies\$redistPlatform\*.appx" "$outpath\Test\"
Publish-Item "$binpath\Samples\WinUICppIslandsSampleApp_Test\Dependencies\$redistPlatform\*.appx" "$outpath\Test\"
}
# TODO: Remove below check
if ((-not $SkipWinUIGallery) -and $env:BUILD_DEFINITIONNAME -and ($env:BUILD_DEFINITIONNAME.Contains("ValidateReunion") -or $env:BUILD_DEFINITIONNAME.Contains("WindowsAppSDK")))
{
Publish-Item "$($galleryTestDir.FullName)\Dependencies\$redistPlatform\*.msix" "$outpath\Test\"
}
Publish-Item "$repoRoot\Samples\TestAutomation\scripts\*" "$outpath"
}
if ($Mode -eq "PGO")
{
# Publish items from build:
Publish-Item "$binpath\Product\*.dll" "$outpath\Test"
Publish-Item "$binpath\Product\*.winmd" "$outpath\Test"
Publish-Item "$binpath\Product\en-US\*.dll.mui" "$outpath\Test\en-US"
Publish-Item "$binpath\Product\Microsoft.Ui.Xaml\*" "$outpath\Test\Microsoft.UI.Xaml"
Publish-Item "$binpath\TAEF\*" "$outpath"
Publish-Item "$binpath\TAEF\NetFx4.5\*" "$outpath"
Publish-Item "$binpath\Test\" "$outpath\Test"
Publish-Item "$binpath\TestDependencies\WinUITest.cer" "$outpath"
Publish-Item "$binpath\TestDependencies\crt\vc_redist.$redistPlatform.exe" "$outpath"
Publish-Item "$binpath\TestDependencies\pgosweep.exe" "$outpath"
# Publish items from repo:
Publish-Item "$repoRoot\Helix\scripts\*" "$outpath"
Publish-Item "$repoRoot\Helix\common\test\*" "$outpath"
}
if ($ShowPayload)
{
Get-ChildItem *.* -Recurse
}
if (-not $Quiet) {
Write-Host
Write-Host "Log file: $logFile"
Write-Host
}
Print-Config
if (-not $Quiet) {
Write-Host
Write-Host "Exit code: $global:LASTEXITCODE"
Write-Host
}
exit $global:LASTEXITCODE