Private/Invoke-WUSetupDiag.ps1
function Invoke-WUSetupDiag { <# .SYNOPSIS Downloads, executes, and parses Microsoft SetupDiag tool for Windows upgrade analysis. .DESCRIPTION Comprehensive wrapper for Microsoft SetupDiag tool that handles download, execution, and parsing of results. SetupDiag analyzes Windows Setup log files to diagnose upgrade failures and provides detailed failure analysis. .PARAMETER OutputPath Directory path where SetupDiag will be downloaded and results saved. .PARAMETER LogPath Path to the log file for detailed logging. .PARAMETER SetupDiagPath Custom path to existing SetupDiag.exe. If not provided, will download latest version. .PARAMETER OutputFormat Output format for SetupDiag results. Default is JSON for better parsing. .EXAMPLE $setupDiagResults = Invoke-WUSetupDiag -OutputPath "C:\Temp" -LogPath "C:\Logs\wu.log" .NOTES This is a private function used internally by the WindowsUpdateTools module. Requires .NET Framework 4.7.2+ for SetupDiag functionality. Returns detailed analysis results from SetupDiag execution. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$OutputPath, [string]$LogPath, [string]$SetupDiagPath, [ValidateSet('JSON', 'XML', 'Text')] [string]$OutputFormat = 'JSON' ) Write-WULog -Message "Starting SetupDiag analysis" -LogPath $LogPath # Initialize results object $results = [PSCustomObject]@{ ExecutionSuccessful = $false FailuresDetected = 0 FailureReasons = @() SystemInfo = $null RawOutput = $null OutputFile = $null UpgradeAttemptDetected = $false Issues = @() ExecutionTime = $null SetupDiagVersion = $null LogAnalysis = @() ErrorMessage = $null } try { # Check .NET Framework version first $dotNetVersion = Get-WUDotNetVersion Write-WULog -Message ".NET Framework Version: $dotNetVersion" -LogPath $LogPath if ($dotNetVersion -lt [Version]"4.7.2") { $errorMsg = ".NET Framework 4.7.2 or newer required for SetupDiag (found: $dotNetVersion)" Write-WULog -Message $errorMsg -Level Warning -LogPath $LogPath $results.Issues += $errorMsg $results.ErrorMessage = $errorMsg return $results } # Check for Windows Setup logs availability $logsFound = Test-WUSetupLogAvailability -LogPath $LogPath if (-not $logsFound) { $errorMsg = "No Windows Setup logs found - SetupDiag requires upgrade attempt logs" Write-WULog -Message $errorMsg -Level Warning -LogPath $LogPath $results.Issues += $errorMsg $results.ErrorMessage = $errorMsg return $results } # Get or download SetupDiag executable if (-not $SetupDiagPath) { $SetupDiagPath = Get-WUSetupDiagExecutable -OutputPath $OutputPath -LogPath $LogPath } if (-not $SetupDiagPath -or -not (Test-Path $SetupDiagPath)) { $errorMsg = "SetupDiag executable not available" Write-WULog -Message $errorMsg -Level Error -LogPath $LogPath $results.ErrorMessage = $errorMsg return $results } Write-WULog -Message "SetupDiag path validated: $SetupDiagPath" -LogPath $LogPath # Prepare output file $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $outputExtension = switch ($OutputFormat) { 'JSON' { 'json' } 'XML' { 'xml' } 'Text' { 'txt' } } $outputFile = Join-Path $OutputPath "SetupDiagResults-$timestamp.$outputExtension" $results.OutputFile = $outputFile # Prepare SetupDiag arguments $setupDiagArgs = @( "/Output:$outputFile", "/Format:$($OutputFormat.ToLower())", "/ZipLogs:False" ) Write-WULog -Message "Running SetupDiag with output: $outputFile" -LogPath $LogPath Write-WULog -Message "SetupDiag arguments: $($setupDiagArgs -join ' ')" -LogPath $LogPath # Execute SetupDiag $startTime = Get-Date Write-WULog -Message "Starting SetupDiag execution at $($startTime.ToString('HH:mm:ss')) - this may take several minutes" -LogPath $LogPath try { $process = Start-Process -FilePath $SetupDiagPath -ArgumentList $setupDiagArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput (Join-Path $OutputPath "setupdiag-stdout.txt") -RedirectStandardError (Join-Path $OutputPath "setupdiag-stderr.txt") $exitCode = $process.ExitCode $endTime = Get-Date $executionTime = $endTime - $startTime $results.ExecutionTime = $executionTime Write-WULog -Message "SetupDiag completed in $($executionTime.ToString('mm\:ss')) with exit code: $exitCode" -LogPath $LogPath # Handle different exit codes switch ($exitCode) { 0 { if (Test-Path $outputFile) { Write-WULog -Message "SetupDiag completed successfully" -LogPath $LogPath $results.ExecutionSuccessful = $true } else { Write-WULog -Message "SetupDiag exit code 0 but no output file created" -Level Warning -LogPath $LogPath $results.Issues += "SetupDiag completed but no output file was created" } } -2146233079 { Write-WULog -Message "SetupDiag .NET runtime exception - possible missing .NET 4.7.2+ or corrupted installation" -Level Error -LogPath $LogPath $results.Issues += "SetupDiag .NET runtime error" $results.ErrorMessage = "SetupDiag .NET runtime exception" } -2147467259 { Write-WULog -Message "SetupDiag access denied - insufficient permissions" -Level Error -LogPath $LogPath $results.Issues += "SetupDiag access denied" $results.ErrorMessage = "SetupDiag access denied" } default { if (Test-Path $outputFile) { Write-WULog -Message "SetupDiag exited with code: $exitCode - but created output file" -Level Warning -LogPath $LogPath $results.ExecutionSuccessful = $true $results.Issues += "SetupDiag completed with warnings (exit code: $exitCode)" } else { Write-WULog -Message "SetupDiag exited with code: $exitCode - no output file created" -Level Error -LogPath $LogPath $results.Issues += "SetupDiag failed with exit code: $exitCode" $results.ErrorMessage = "SetupDiag failed with exit code: $exitCode" } } } # Check for stdout/stderr output $stderrFile = Join-Path $OutputPath "setupdiag-stderr.txt" if (Test-Path $stderrFile) { $stderrContent = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue if ($stderrContent -and $stderrContent.Trim()) { Write-WULog -Message "SetupDiag stderr output: $stderrContent" -Level Warning -LogPath $LogPath } } } catch { Write-WULog -Message "Failed to execute SetupDiag: $($_.Exception.Message)" -Level Error -LogPath $LogPath $results.ErrorMessage = "Failed to execute SetupDiag: $($_.Exception.Message)" return $results } # Parse results if output file was created if ($results.ExecutionSuccessful -and (Test-Path $outputFile)) { Write-WULog -Message "Parsing SetupDiag results from: $outputFile" -LogPath $LogPath try { if ($OutputFormat -eq 'JSON') { $parseResults = ConvertFrom-WUSetupDiagJSON -FilePath $outputFile -LogPath $LogPath } else { $parseResults = ConvertFrom-WUSetupDiagText -FilePath $outputFile -LogPath $LogPath } # Merge parse results $results.FailuresDetected = $parseResults.FailuresDetected $results.FailureReasons = $parseResults.FailureReasons $results.SystemInfo = $parseResults.SystemInfo $results.RawOutput = $parseResults.RawOutput $results.UpgradeAttemptDetected = $parseResults.UpgradeAttemptDetected $results.LogAnalysis = $parseResults.LogAnalysis $results.SetupDiagVersion = $parseResults.SetupDiagVersion Write-WULog -Message "SetupDiag analysis completed - found $($results.FailuresDetected) failure(s)" -LogPath $LogPath if ($results.FailureReasons.Count -gt 0) { Write-WULog -Message "Failure reasons identified:" -LogPath $LogPath foreach ($reason in $results.FailureReasons) { Write-WULog -Message " - $reason" -Level Warning -LogPath $LogPath } } } catch { Write-WULog -Message "Error parsing SetupDiag results: $($_.Exception.Message)" -Level Error -LogPath $LogPath $results.Issues += "Error parsing SetupDiag results" $results.ErrorMessage = "Error parsing SetupDiag results: $($_.Exception.Message)" } } } catch { Write-WULog -Message "Critical error during SetupDiag analysis: $($_.Exception.Message)" -Level Error -LogPath $LogPath $results.ErrorMessage = "Critical error during SetupDiag analysis: $($_.Exception.Message)" } # Summary if ($results.ExecutionSuccessful) { Write-WULog -Message "SetupDiag analysis completed successfully" -LogPath $LogPath if ($results.FailuresDetected -gt 0) { Write-WULog -Message "SetupDiag detected $($results.FailuresDetected) upgrade failure(s)" -LogPath $LogPath } else { Write-WULog -Message "SetupDiag did not detect any upgrade failures" -LogPath $LogPath } } else { Write-WULog -Message "SetupDiag analysis failed: $($results.ErrorMessage)" -Level Error -LogPath $LogPath } return $results } function Test-WUSetupLogAvailability { <# .SYNOPSIS Tests availability of Windows Setup log files for SetupDiag analysis. #> param([string]$LogPath) Write-WULog -Message "Checking Windows Setup log availability for SetupDiag analysis" -LogPath $LogPath $foundAnyLogs = $false foreach ($path in $script:SetupLogPaths) { if (Test-Path $path) { $logFiles = Get-ChildItem -Path $path -Filter "*.log" -ErrorAction SilentlyContinue if ($logFiles) { Write-WULog -Message "Found $($logFiles.Count) log files in: $path" -LogPath $LogPath $foundAnyLogs = $true # Check for recent logs (less than 90 days old) $recentLogs = $logFiles | Where-Object { (Get-Date) - $_.LastWriteTime -lt [TimeSpan]::FromDays(90) } if ($recentLogs) { Write-WULog -Message " $($recentLogs.Count) recent logs (< 90 days old)" -LogPath $LogPath } } } } if (-not $foundAnyLogs) { Write-WULog -Message "No Windows Setup logs found in standard locations" -LogPath $LogPath Write-WULog -Message "This typically indicates no recent Windows upgrade attempts" -LogPath $LogPath } return $foundAnyLogs } function Get-WUSetupDiagExecutable { <# .SYNOPSIS Downloads the latest SetupDiag executable from Microsoft. #> param( [string]$OutputPath, [string]$LogPath ) $downloadUrl = $script:SetupDiagUrl $destination = Join-Path $OutputPath "SetupDiag.exe" Write-WULog -Message "Attempting to download SetupDiag from: $downloadUrl" -LogPath $LogPath try { # Check if we already have a recent version if (Test-Path $destination) { $fileInfo = Get-Item $destination $fileAge = (Get-Date) - $fileInfo.LastWriteTime if ($fileAge.TotalDays -lt 7) { Write-WULog -Message "Using existing SetupDiag (less than 7 days old)" -LogPath $LogPath return $destination } else { Write-WULog -Message "Existing SetupDiag is $([math]::Round($fileAge.TotalDays)) days old - downloading newer version" -LogPath $LogPath } } # Ensure output directory exists if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } # Download with progress suppression for cleaner logs $progressPreference = 'SilentlyContinue' Invoke-WebRequest -Uri $downloadUrl -OutFile $destination -UseBasicParsing -ErrorAction Stop # Verify download if (Test-Path $destination) { $fileSize = (Get-Item $destination).Length Write-WULog -Message "SetupDiag downloaded successfully ($([math]::Round($fileSize / 1KB)) KB)" -LogPath $LogPath return $destination } else { throw "Download completed but file not found at destination" } } catch { Write-WULog -Message "Failed to download SetupDiag: $($_.Exception.Message)" -Level Error -LogPath $LogPath return $null } } function ConvertFrom-WUSetupDiagJSON { <# .SYNOPSIS Parses SetupDiag JSON output for failure analysis. #> param( [string]$FilePath, [string]$LogPath ) $parseResults = [PSCustomObject]@{ FailuresDetected = 0 FailureReasons = @() SystemInfo = $null RawOutput = $null UpgradeAttemptDetected = $false LogAnalysis = @() SetupDiagVersion = $null } try { $jsonContent = Get-Content $FilePath -Raw | ConvertFrom-Json $parseResults.RawOutput = $jsonContent # Extract system information if ($jsonContent.SystemInfo) { $parseResults.SystemInfo = $jsonContent.SystemInfo $parseResults.UpgradeAttemptDetected = $true Write-WULog -Message "SetupDiag System Info - Host: $($jsonContent.SystemInfo.HostOSVersion), Target: $($jsonContent.SystemInfo.TargetOSBuildString)" -LogPath $LogPath } # Check for failure data if ($jsonContent.FailureData -and $jsonContent.FailureData.Count -gt 0) { $parseResults.FailuresDetected = $jsonContent.FailureData.Count $parseResults.FailureReasons = $jsonContent.FailureData Write-WULog -Message "SetupDiag detected $($parseResults.FailuresDetected) failure(s)" -LogPath $LogPath } # Check rules for additional context if ($jsonContent.Rules -and $jsonContent.Rules.Count -gt 0) { $highSeverityRules = $jsonContent.Rules | Where-Object { $_.Severity -gt 2 } if ($highSeverityRules -and -not $parseResults.FailureReasons) { $parseResults.FailureReasons = $highSeverityRules | ForEach-Object { $_.Description } $parseResults.FailuresDetected = $highSeverityRules.Count } } # Check for remediation suggestions if ($jsonContent.Remediation) { $parseResults.FailureReasons += $jsonContent.Remediation if ($parseResults.FailuresDetected -eq 0) { $parseResults.FailuresDetected = 1 } } # Check version information if ($jsonContent.Version) { $parseResults.SetupDiagVersion = $jsonContent.Version } } catch { Write-WULog -Message "Error parsing SetupDiag JSON: $($_.Exception.Message)" -Level Error -LogPath $LogPath throw } return $parseResults } function ConvertFrom-WUSetupDiagText { <# .SYNOPSIS Parses SetupDiag text output for failure analysis. #> param( [string]$FilePath, [string]$LogPath ) $parseResults = [PSCustomObject]@{ FailuresDetected = 0 FailureReasons = @() SystemInfo = $null RawOutput = $null UpgradeAttemptDetected = $false LogAnalysis = @() SetupDiagVersion = $null } try { $textContent = Get-Content $FilePath -Raw $parseResults.RawOutput = $textContent # Look for failure indicators in text $failureLines = $textContent -split "`n" | Where-Object { $_ -like "*failure*" -or $_ -like "*error*" -or $_ -like "*failed*" } if ($failureLines) { $parseResults.FailuresDetected = $failureLines.Count $parseResults.FailureReasons = $failureLines | ForEach-Object { $_.Trim() } } # Check for upgrade attempt indicators if ($textContent -like "*upgrade*" -or $textContent -like "*setup*") { $parseResults.UpgradeAttemptDetected = $true } } catch { Write-WULog -Message "Error parsing SetupDiag text: $($_.Exception.Message)" -Level Error -LogPath $LogPath throw } return $parseResults } function Get-WUDotNetVersion { <# .SYNOPSIS Gets the installed .NET Framework version. .DESCRIPTION Determines the highest .NET Framework version installed on the system. #> try { $netVersions = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP" -Recurse -ErrorAction SilentlyContinue | Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match '^(?!S)\d+' } | Sort-Object Version -Descending | Select-Object -First 1 if ($netVersions.Release -ge 528040) { return [Version]"4.8" } elseif ($netVersions.Release -ge 461808) { return [Version]"4.7.2" } elseif ($netVersions.Release -ge 460798) { return [Version]"4.7" } elseif ($netVersions.Release -ge 394802) { return [Version]"4.6.2" } elseif ($netVersions.Release -ge 394254) { return [Version]"4.6.1" } elseif ($netVersions.Release -ge 393295) { return [Version]"4.6" } else { return [Version]$netVersions.Version } } catch { return [Version]"0.0" } } |