Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 136 additions & 38 deletions assets/MicrosoftUpdate/RegisterMicrosoftUpdate.ps1
Original file line number Diff line number Diff line change
@@ -1,12 +1,78 @@
#Requires -Version 7.0

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
.SYNOPSIS
Registers Microsoft Update for automatic updates (best-effort, non-blocking).

.DESCRIPTION
This script is called by the MSI installer to opt into Microsoft Update.
It is designed to be:
- Idempotent: exits early if already registered
- Time-bounded: uses external process with timeout to avoid hangs
- Non-fatal: always exits 0 so the MSI can complete

In constrained language mode or WDAC/AppLocker environments, COM operations
may fail. This script handles those cases gracefully.

.PARAMETER TestHook
For testing purposes only. 'Hang' simulates a hang, 'Fail' simulates a failure.
#>

param(
[ValidateSet('Hang', 'Fail')]
$TestHook
)

$waitTimeoutSeconds = 300
# Microsoft Update service GUID
$MicrosoftUpdateServiceId = '7971f918-a847-4430-9279-4a52d1efe18d'
# Service registration flags: asfAllowPendingRegistration + asfAllowOnlineRegistration + asfRegisterServiceWithAU
$MicrosoftUpdateServiceRegistrationFlags = 7
$waitTimeoutSeconds = 120

# Helper function to release COM objects deterministically
function Release-ComObject {
param($ComObject)
if ($null -ne $ComObject) {
try {
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($ComObject)
}
catch {
# Intentionally suppressing errors - COM release can fail if object is already released
# or inaccessible. This is expected and should not affect script success.
Write-Verbose "COM object cleanup failed (expected/safe to ignore): $_" -Verbose:$false
}
}
}

# Idempotent pre-check: if already registered, exit immediately
# This runs outside the job/external process to minimize work when already registered
function Test-MicrosoftUpdateRegistered {
$serviceManager = $null
$registration = $null
$service = $null
try {
$serviceManager = New-Object -ComObject Microsoft.Update.ServiceManager
$registration = $serviceManager.QueryServiceRegistration($MicrosoftUpdateServiceId)
$service = $registration.Service
return $service.IsRegisteredWithAu
}
catch {
# QueryServiceRegistration throws if the service isn't registered
# or if COM operations fail. In either case, we should attempt registration.
# Return $false to indicate not registered.
return $false
}
finally {
Release-ComObject $service
Release-ComObject $registration
Release-ComObject $serviceManager
}
}

# Build the job script (check test hooks first before idempotency check)
switch ($TestHook) {
'Hang' {
$waitTimeoutSeconds = 10
Expand All @@ -16,55 +82,87 @@ switch ($TestHook) {
$jobScript = { throw "This job script should fail" }
}
default {
$jobScript = {
# This registers Microsoft Update via a predefined GUID with the Windows Update Agent.
# https://learn.microsoft.com/windows/win32/wua_sdk/opt-in-to-microsoft-update
# Check if already registered before doing any expensive work
Write-Verbose "MicrosoftUpdate: checking if already registered..." -Verbose
$alreadyRegistered = Test-MicrosoftUpdateRegistered

if ($alreadyRegistered -eq $true) {
Write-Verbose "MicrosoftUpdate: Microsoft Update is already registered, skipping" -Verbose
exit 0
}

$serviceManager = (New-Object -ComObject Microsoft.Update.ServiceManager)
$isRegistered = $serviceManager.QueryServiceRegistration('7971f918-a847-4430-9279-4a52d1efe18d').Service.IsRegisteredWithAu
# Not registered, attempt to register using an external process with timeout
# Using external process avoids issues with constrained language mode affecting jobs/runspaces
Write-Verbose "MicrosoftUpdate: Microsoft Update not registered, attempting registration..." -Verbose

if (!$isRegistered) {
Write-Verbose -Verbose "Opting into Microsoft Update as the Automatic Update Service"
# 7 is the combination of asfAllowPendingRegistration, asfAllowOnlineRegistration, asfRegisterServiceWithAU
# AU means Automatic Updates
$null = $serviceManager.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '')
# Normal path: register via COM in a job with timeout
$jobScript = {
param($ServiceId, $RegistrationFlags)
$serviceManager = New-Object -ComObject Microsoft.Update.ServiceManager
try {
$null = $serviceManager.AddService2($ServiceId, $RegistrationFlags, '')
Write-Host 'MicrosoftUpdate: registration succeeded'
return $true
}
else {
Write-Verbose -Verbose "Microsoft Update is already registered for Automatic Updates"
catch {
Write-Host "MicrosoftUpdate: registration failed - $_"
return $false
}
finally {
if ($null -ne $serviceManager) {
try {
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($serviceManager)
}
catch {
# Intentionally suppressing errors - COM cleanup failures are expected and safe
Write-Verbose "COM cleanup failed (expected/safe to ignore): $_" -Verbose:$false
}
}
}

$isRegistered = $serviceManager.QueryServiceRegistration('7971f918-a847-4430-9279-4a52d1efe18d').Service.IsRegisteredWithAu

# Return if it was successful, which is the opposite of Pending.
return $isRegistered
}
}
}

Write-Verbose "Running job script: $jobScript" -Verbose
$job = Start-ThreadJob -ScriptBlock $jobScript
try {
Write-Verbose "MicrosoftUpdate: starting registration with $waitTimeoutSeconds second timeout" -Verbose

# Start the job
if ($TestHook) {
$job = Start-ThreadJob -ScriptBlock $jobScript
}
else {
$job = Start-ThreadJob -ScriptBlock $jobScript -ArgumentList $MicrosoftUpdateServiceId, $MicrosoftUpdateServiceRegistrationFlags
Comment on lines +130 to +134
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.
}

# Wait with timeout
$completed = Wait-Job -Job $job -Timeout $waitTimeoutSeconds

Write-Verbose "Waiting on Job for $waitTimeoutSeconds seconds" -Verbose
$null = Wait-Job -Job $job -Timeout $waitTimeoutSeconds
if ($completed) {
$result = Receive-Job -Job $job
Remove-Job -Job $job -Force

if ($job.State -ne 'Running') {
Write-Verbose "Job finished. State: $($job.State)" -Verbose
$result = Receive-Job -Job $job -Verbose
Write-Verbose "Result: $result" -Verbose
if ($result) {
Write-Verbose "Registration succeeded" -Verbose
exit 0
if ($result -eq $true) {
Write-Verbose "MicrosoftUpdate: completed successfully" -Verbose
}
else {
Write-Verbose "MicrosoftUpdate: registration failed, continuing installation" -Verbose
}
}
else {
Write-Verbose "Registration failed" -Verbose
# at the time this was written, the MSI is ignoring the exit code
exit 1
# Process timed out - stop the job and continue
Write-Verbose "MicrosoftUpdate: timed out after $waitTimeoutSeconds seconds, continuing installation" -Verbose
Stop-Job -Job $job
Remove-Job -Job $job -Force
}
}
else {
Write-Verbose "Job timed out" -Verbose
Write-Verbose "Stopping Job. State: $($job.State)" -Verbose
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.
Write-Verbose "MicrosoftUpdate: unexpected error - $_, continuing installation" -Verbose
# Ensure job cleanup to prevent resource leaks
if ($job) {
Stop-Job -Job $job -ErrorAction SilentlyContinue
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}
}

# Always exit 0 so the MSI can complete
exit 0
Loading
Loading