From b2568921972208a60cc7f0669577ea9e49265812 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 13 Jul 2026 13:47:20 -0400 Subject: [PATCH] Fix Compare-InstallerSnapshots.ps1 StrictMode crash on empty UninstallEntries Compare-InstallerSnapshots.ps1 runs under Set-StrictMode -Version Latest and read $p.DisplayName directly for every item in a snapshot's UninstallEntries. When a snapshot has no uninstall entries, Collect-InstallerSnapshot serializes the empty list as {} (a Windows PowerShell ConvertTo-Json quirk), so ConvertFrom-Json yields a single property-less object. Reading .DisplayName on it threw "The property 'DisplayName' cannot be found on this object", aborting the diff (observed after a silent MSI install whose before-snapshot had 0 entries). Extract DisplayName via a helper that tolerates $null, a single unwrapped object, and the empty {} object, skipping entries without a usable DisplayName. Verified: re-running against the real before/after snapshots that triggered the crash now completes (exit 0) and writes diff-before-vs-after-install.txt. Co-Authored-By: Claude Opus 4.8 --- scripts/Agent/Compare-InstallerSnapshots.ps1 | 32 +++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/scripts/Agent/Compare-InstallerSnapshots.ps1 b/scripts/Agent/Compare-InstallerSnapshots.ps1 index b6ab9e96d4..f896f794a8 100644 --- a/scripts/Agent/Compare-InstallerSnapshots.ps1 +++ b/scripts/Agent/Compare-InstallerSnapshots.ps1 @@ -54,6 +54,28 @@ function Diff-Sets { return [pscustomobject]@{ Added = $added; Removed = $removed } } +function Get-DisplayNameList { + # Safely extract DisplayName values from a snapshot's UninstallEntries. + # Robust to $null, to a single unwrapped object, and to the empty object ({}) + # that ConvertTo-Json can emit for an empty list (Collect-InstallerSnapshot serializes + # an empty List as {} in Windows PowerShell). Under Set-StrictMode, blindly reading + # $entry.DisplayName on such an object throws PropertyNotFoundException. + param([Parameter(Mandatory = $false)]$Entries) + + $names = New-Object System.Collections.Generic.List[string] + if ($null -eq $Entries) { return ,$names } + + foreach ($entry in @($Entries)) { + if ($null -eq $entry) { continue } + $prop = $entry.PSObject.Properties['DisplayName'] + if ($null -eq $prop) { continue } + $value = [string]$prop.Value + if (-not [string]::IsNullOrWhiteSpace($value)) { $names.Add($value) } + } + + return ,$names +} + function ConvertTo-PropertyMap { param([Parameter(Mandatory = $false)]$Value) @@ -86,14 +108,8 @@ $reportLines.Add("After: $AfterSnapshotPath") $reportLines.Add("") # Uninstall entries -$beforeProducts = @() -foreach ($p in ($before.UninstallEntries | ForEach-Object { $_ })) { - $beforeProducts += ([string]$p.DisplayName) -} -$afterProducts = @() -foreach ($p in ($after.UninstallEntries | ForEach-Object { $_ })) { - $afterProducts += ([string]$p.DisplayName) -} +$beforeProducts = Get-DisplayNameList -Entries $before.UninstallEntries +$afterProducts = Get-DisplayNameList -Entries $after.UninstallEntries $diffProducts = Diff-Sets -Before (To-StringSet -Items $beforeProducts) -After (To-StringSet -Items $afterProducts) $reportLines.Add("Uninstall entries")