Skip to content

Enhance MSI registration scripts with idempotency and timeout handling; add tests for registration scripts#26740

Open
TravisEz13 wants to merge 14 commits intoPowerShell:masterfrom
TravisEz13:wix-fixes
Open

Enhance MSI registration scripts with idempotency and timeout handling; add tests for registration scripts#26740
TravisEz13 wants to merge 14 commits intoPowerShell:masterfrom
TravisEz13:wix-fixes

Conversation

@TravisEz13
Copy link
Member

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 for wevtutil operations, 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:

Testing Improvements:

Documentation and Parameter Consistency:

Summary of Most Important Changes

Registration script reliability and idempotency

  • Refactored RegisterMicrosoftUpdate.ps1 for idempotency, external process timeout, and non-fatal exit; handles constrained environments gracefully.
  • Manifest registration script now checks registry for idempotency, handles missing files gracefully, and always exits 0. [1] [2]

Timeout and error handling

  • Added Invoke-WevtutilWithTimeout to run event manifest registration/unregistration with a timeout, ensuring installer does not hang.
  • Microsoft Update registration now runs in a job with timeout, cleans up resources, and exits 0 regardless of outcome.

Testing

  • Added comprehensive Pester tests to validate script behavior, including idempotency, error and timeout handling, and correct exit codes.

Documentation

  • Improved parameter documentation in manifest registration script for clarity.

PR Context

PR Checklist

…eout handling; add tests for registration scripts
@TravisEz13 TravisEz13 requested a review from a team as a code owner February 2, 2026 22:02
Copilot AI review requested due to automatic review settings February 2, 2026 22:02
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-ThreadJob with 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

@TravisEz13 TravisEz13 added the CL-BuildPackaging Indicates that a PR should be marked as a build or packaging change in the Change Log label Feb 3, 2026
@TravisEz13 TravisEz13 requested a review from Copilot February 3, 2026 18:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

…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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.


try {
Write-Verbose "EventManifest: running wevtutil.exe $Arguments (timeout: ${TimeoutSeconds}s)" -Verbose

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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." }

Suggested change
# 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
}

Copilot uses AI. Check for mistakes.
Comment on lines 76 to 83
$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
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
$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
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +114
Write-Verbose "EventManifest: wevtutil.exe not found at $wevtutilPath" -Verbose
return $false
}

try {
Write-Verbose "EventManifest: running wevtutil.exe $Arguments (timeout: ${TimeoutSeconds}s)" -Verbose
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:".

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +14
.PARAMETER Unregister
Specify to unregister the manifest.
.Notes

.NOTES
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
}

return $exitCode
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
}
Set-Item -Path Function:Invoke-NativeProcess -Option Private

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +132
if ($TestHook) {
$job = Start-ThreadJob -ScriptBlock $jobScript
}
else {
$job = Start-ThreadJob -ScriptBlock $jobScript -ArgumentList $MicrosoftUpdateServiceId, $MicrosoftUpdateServiceRegistrationFlags
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
if ($isRegistered -eq $true) {
Write-Verbose "EventManifest: already registered, skipping" -Verbose
exit 0
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

Suggested change
}
}
elseif ($null -eq $isRegistered) {
Write-Verbose "EventManifest: unable to determine registration status, proceeding with registration" -Verbose
}

Copilot uses AI. Check for mistakes.
Comment on lines 195 to 197
if (Test-Path -Path $Path -PathType Container) {
$resolvedPath = (Resolve-Path -Path $Path).Path
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Stop-Job -Job $job
# at the time this was written, the MSI is ignoring the exit code
exit 258
catch {
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

Suggested change
catch {
catch {
if ($job) {
Stop-Job -Job $job -ErrorAction SilentlyContinue
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}

Copilot uses AI. Check for mistakes.
TravisEz13 and others added 6 commits February 10, 2026 10:35
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-BuildPackaging Indicates that a PR should be marked as a build or packaging change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant