Enhance MSI registration scripts with idempotency and timeout handling; add tests for registration scripts#26740
Enhance MSI registration scripts with idempotency and timeout handling; add tests for registration scripts#26740TravisEz13 wants to merge 14 commits intoPowerShell:masterfrom
Conversation
…eout handling; add tests for registration scripts
There was a problem hiding this comment.
Pull request overview
This pull request enhances the Windows MSI registration scripts for Microsoft Update and PowerShell event manifest registration by adding idempotency checks, timeout handling, and comprehensive error recovery. The changes ensure that registration operations never block MSI installation or uninstallation, even when they fail or timeout.
Changes:
- Added idempotency checks to both registration scripts to avoid redundant operations when services are already registered
- Implemented timeout mechanisms using
Start-ThreadJobwith configurable timeouts (120s for Microsoft Update, 60s for event manifest operations) to prevent installer hangs - Ensured all scripts exit with code 0 regardless of registration success, making registration best-effort rather than blocking
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
test/packaging/windows/registration-scripts.tests.ps1 |
New Pester test file validating registration script behavior including idempotency, timeout handling, and graceful failure scenarios |
src/PowerShell.Core.Instrumentation/RegisterManifest.ps1 |
Added Invoke-WevtutilWithTimeout function, idempotency registry checks via Test-ManifestRegistered, timeout handling for wevtutil operations, and non-blocking error handling |
assets/MicrosoftUpdate/RegisterMicrosoftUpdate.ps1 |
Refactored with Test-MicrosoftUpdateRegistered for idempotency, external process execution with timeout, proper COM object cleanup via Release-ComObject, and graceful handling of constrained language mode |
… variable names and improve COM object handling in tests
…into wix-fixes
…rror suppression and registration checks. Add Invoke-NativeProcess function to streamline native process execution in RegisterManifest.ps1.
…resolved to absolute and handle non-existent paths gracefully.
|
|
||
| try { | ||
| Write-Verbose "EventManifest: running wevtutil.exe $Arguments (timeout: ${TimeoutSeconds}s)" -Verbose | ||
|
|
There was a problem hiding this comment.
Potential compatibility issue: Start-ThreadJob is used without checking if the module is available. While ThreadJob ships with PowerShell 7+, if this script runs in Windows PowerShell 5.1 or older PowerShell Core versions, it could fail. Consider adding a check for the cmdlet availability or documenting the minimum PowerShell version requirement. For example: if (-not (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)) { throw "Start-ThreadJob cmdlet not found. This script requires PowerShell 6.1 or later." }
| # Check for Start-ThreadJob availability to ensure compatibility with older PowerShell versions | |
| if (-not (Get-Command -Name Start-ThreadJob -ErrorAction SilentlyContinue)) { | |
| Write-Verbose "EventManifest: Start-ThreadJob not available; running wevtutil without timeout" -Verbose | |
| $exitCode = Invoke-NativeProcess $wevtutilPath $Arguments | |
| if ($exitCode -ne 0 -and -not $IgnoreExitCode) { | |
| Write-Verbose "EventManifest: wevtutil failed with exit code $exitCode" -Verbose | |
| return $false | |
| } | |
| return $true | |
| } |
| $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile | ||
| $process.WaitForExit() | ||
| $exitCode = $process.ExitCode | ||
|
|
||
| # Only clean up temp files on success; keep them for debugging on failure | ||
| if ($exitCode -eq 0) { | ||
| Remove-Item $outFile, $errFile -Force -ErrorAction SilentlyContinue | ||
| } |
There was a problem hiding this comment.
Potential temp file leak on timeout or job termination: When the wevtutil job times out or is stopped (lines 132-134), the job is terminated while Invoke-NativeProcess may still be running. The temp files created at lines 74-75 won't be cleaned up because the process exit code check at line 81 won't execute. This could lead to temp file accumulation. Consider using a try/finally block within Invoke-NativeProcess to ensure cleanup, or using a different approach that doesn't leave temp files behind on abnormal termination.
| $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile | |
| $process.WaitForExit() | |
| $exitCode = $process.ExitCode | |
| # Only clean up temp files on success; keep them for debugging on failure | |
| if ($exitCode -eq 0) { | |
| Remove-Item $outFile, $errFile -Force -ErrorAction SilentlyContinue | |
| } | |
| $exitCode = $null | |
| try { | |
| $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile | |
| $process.WaitForExit() | |
| $exitCode = $process.ExitCode | |
| # Only clean up temp files on success; keep them for debugging on failure | |
| if ($exitCode -eq 0) { | |
| Remove-Item $outFile, $errFile -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| finally { | |
| # Ensure temp files are not leaked when the process is interrupted | |
| # (e.g. job timeout or terminating error before $exitCode is set). | |
| if ($null -eq $exitCode) { | |
| Remove-Item $outFile, $errFile -Force -ErrorAction SilentlyContinue | |
| } | |
| } |
| Write-Verbose "EventManifest: wevtutil.exe not found at $wevtutilPath" -Verbose | ||
| return $false | ||
| } | ||
|
|
||
| try { | ||
| Write-Verbose "EventManifest: running wevtutil.exe $Arguments (timeout: ${TimeoutSeconds}s)" -Verbose |
There was a problem hiding this comment.
Inconsistent verbose message prefixes: The RegisterManifest script uses "EventManifest:" as a prefix for verbose messages (e.g., line 109), while RegisterMicrosoftUpdate uses "RegisterMicrosoftUpdate:" (e.g., line 84). For consistency, consider using the script name as the prefix in both cases, or using a consistent naming pattern like "MicrosoftUpdate:" to match "EventManifest:".
| .PARAMETER Unregister | ||
| Specify to unregister the manifest. | ||
| .Notes | ||
|
|
||
| .NOTES |
There was a problem hiding this comment.
The .NOTES keyword has been uppercased for consistency, which is good. However, the .Parameter Path on line 7 (not shown in the diff but part of the file) remains lowercase while the updated .PARAMETER Unregister on line 11 is uppercase. For consistency throughout the help documentation, all comment-based help keywords should use the same case convention (preferably all uppercase as per PowerShell documentation standards).
| } | ||
|
|
||
| return $exitCode | ||
| } |
There was a problem hiding this comment.
The Invoke-NativeProcess function doesn't enforce any limits on the file path or arguments, which could be a concern if called from other contexts. However, since this script requires elevation and is only called during MSI installation with controlled inputs, the security risk is minimal. Consider adding path validation or making this function private (e.g., using New-Variable -Option Private) to prevent misuse if this script is ever dot-sourced.
| } | |
| } | |
| Set-Item -Path Function:Invoke-NativeProcess -Option Private |
| if ($TestHook) { | ||
| $job = Start-ThreadJob -ScriptBlock $jobScript | ||
| } | ||
| else { | ||
| $job = Start-ThreadJob -ScriptBlock $jobScript -ArgumentList $MicrosoftUpdateServiceId, $MicrosoftUpdateServiceRegistrationFlags |
There was a problem hiding this comment.
Potential compatibility issue: Start-ThreadJob is used without checking if the module is available. While ThreadJob ships with PowerShell 7+, if this script runs in Windows PowerShell 5.1 or in older PowerShell Core versions during MSI installation, it could fail. Consider adding a check for cmdlet availability or documenting the minimum PowerShell version requirement.
| if ($isRegistered -eq $true) { | ||
| Write-Verbose "EventManifest: already registered, skipping" -Verbose | ||
| exit 0 | ||
| } |
There was a problem hiding this comment.
Incomplete null handling: Test-ManifestRegistered can return $null when unable to check the registry (line 164), but the calling code only checks for $true (line 230). If the function returns $null, the condition $isRegistered -eq $true will be false, and the script will proceed with registration. While this "fail open" behavior might be intentional for robustness, it could lead to unnecessary registration attempts or warnings. Consider explicitly handling the $null case or documenting this behavior, e.g., if ($isRegistered -eq $true) { ... } elseif ($null -eq $isRegistered) { Write-Verbose "EventManifest: unable to determine registration status, proceeding with registration" -Verbose }
| } | |
| } | |
| elseif ($null -eq $isRegistered) { | |
| Write-Verbose "EventManifest: unable to determine registration status, proceeding with registration" -Verbose | |
| } |
| if (Test-Path -Path $Path -PathType Container) { | ||
| $resolvedPath = (Resolve-Path -Path $Path).Path | ||
| } |
There was a problem hiding this comment.
Path resolution logic could be more robust: If the path exists but is not a directory (e.g., a file), the code sets $resolvedPath = $Path without resolving it, then proceeds to construct file paths. This could lead to incorrect file paths if someone passes a file path instead of a directory path. Consider adding validation: if (Test-Path -Path $Path -PathType Leaf) { throw "Path parameter must be a directory, not a file: $Path" } or use -PathType Container validation in the parameter definition.
| if (Test-Path -Path $Path -PathType Container) { | |
| $resolvedPath = (Resolve-Path -Path $Path).Path | |
| } | |
| if (Test-Path -Path $Path -PathType Container) { | |
| # Path is a directory; resolve to its canonical form | |
| $resolvedPath = (Resolve-Path -Path $Path).Path | |
| } | |
| elseif (Test-Path -Path $Path -PathType Leaf) { | |
| # Path is a file; resolve and use its containing directory | |
| $resolvedFilePath = (Resolve-Path -Path $Path).Path | |
| $resolvedPath = Split-Path -Path $resolvedFilePath -Parent | |
| } |
| Stop-Job -Job $job | ||
| # at the time this was written, the MSI is ignoring the exit code | ||
| exit 258 | ||
| catch { |
There was a problem hiding this comment.
Missing job cleanup in exception handler: If an exception occurs after the job is created (line 128-133) but before job cleanup completes (lines 140, 153), the job may be left running. The catch block at line 156 should include job cleanup to prevent resource leaks. Consider adding: if ($job) { Stop-Job -Job $job -ErrorAction SilentlyContinue; Remove-Job -Job $job -Force -ErrorAction SilentlyContinue }
| catch { | |
| catch { | |
| if ($job) { | |
| Stop-Job -Job $job -ErrorAction SilentlyContinue | |
| Remove-Job -Job $job -Force -ErrorAction SilentlyContinue | |
| } |
…anup to prevent resource leaks during unexpected errors.
…ssages to use consistent prefix for clarity.
…rary files are cleaned up on process interruption and improve registration status logging.
…meter is a directory and throw an error for file inputs.
…ling of paths with and without spaces, and ensure proper quoting for Start-Process.
PR Summary
This pull request introduces robust improvements to the Windows registration scripts for Microsoft Update and PowerShell instrumentation event manifest, focusing on reliability, idempotency, and graceful handling of errors and timeouts. Additionally, comprehensive Pester tests are added to ensure correct script behavior in various scenarios.
Reliability and Idempotency Improvements:
assets/MicrosoftUpdate/RegisterMicrosoftUpdate.ps1: Refactored to be idempotent (exits early if already registered), time-bounded (registration runs in an external process with a 120s timeout), and non-fatal (always exits 0 so MSI can complete). Handles constrained language mode and COM failures gracefully.src/PowerShell.Core.Instrumentation/RegisterManifest.ps1: Added idempotent registry checks for manifest registration, timeout handling forwevtutiloperations, and ensured non-blocking behavior on errors or missing files. Unregistration is now best-effort and does not block uninstall. [1] [2] [3]Timeout and Error Handling Enhancements:
src/PowerShell.Core.Instrumentation/RegisterManifest.ps1: IntroducedInvoke-WevtutilWithTimeoutto runwevtutilcommands with a configurable timeout, preventing installer hangs. All registration and unregistration steps now exit 0 regardless of failures or timeouts. [1] [2]assets/MicrosoftUpdate/RegisterMicrosoftUpdate.ps1: Registration job is run with a timeout and always cleans up resources; script exits 0 on timeout, failure, or success.Testing Improvements:
test/packaging/windows/registration-scripts.tests.ps1: Added Pester tests to validate registration scripts, including checks for idempotency, graceful handling of timeouts and failures, and correct exit codes in all scenarios.Documentation and Parameter Consistency:
src/PowerShell.Core.Instrumentation/RegisterManifest.ps1: Improved parameter documentation for clarity and consistency.Summary of Most Important Changes
Registration script reliability and idempotency
RegisterMicrosoftUpdate.ps1for idempotency, external process timeout, and non-fatal exit; handles constrained environments gracefully.Timeout and error handling
Invoke-WevtutilWithTimeoutto run event manifest registration/unregistration with a timeout, ensuring installer does not hang.Testing
Documentation
PR Context
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header