@ -7,13 +7,6 @@
# Requires -Module Ansible.ModuleUtils.Legacy
# Requires -Module Ansible.ModuleUtils.Legacy
<# Most of the Windows Update API will not run under a remote token, which a
remote WinRM session always has . We set the below AnsibleRequires flag to
require become being used when executing the module to bypass this restriction .
This means we don ' t have to mess around with scheduled tasks . #>
#AnsibleRequires -Become
$ErrorActionPreference = " Stop "
$ErrorActionPreference = " Stop "
$params = Parse-Args -arguments $args -supports_check_mode $true
$params = Parse-Args -arguments $args -supports_check_mode $true
@ -25,22 +18,6 @@ $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "insta
$blacklist = Get-AnsibleParam -obj $params -name " blacklist " -type " list "
$blacklist = Get-AnsibleParam -obj $params -name " blacklist " -type " list "
$whitelist = Get-AnsibleParam -obj $params -name " whitelist " -type " list "
$whitelist = Get-AnsibleParam -obj $params -name " whitelist " -type " list "
$result = @ {
changed = $false
updates = @ { }
filter ed_updates = @ { }
}
Function Write-DebugLog($msg ) {
$date_str = Get-Date -Format u
$msg = " $date_str $msg "
Write-Debug -Message $msg
if ( $log_path -ne $null -and ( -not $check_mode ) ) {
Add-Content -Path $log_path -Value $msg
}
}
Function Get-CategoryGuid($category_name ) {
Function Get-CategoryGuid($category_name ) {
$guid = switch -exact ( $category_name ) {
$guid = switch -exact ( $category_name ) {
" Application " { " 5C9376AB-8CE6-464A-B136-22113DD69801 " }
" Application " { " 5C9376AB-8CE6-464A-B136-22113DD69801 " }
@ -55,270 +32,523 @@ Function Get-CategoryGuid($category_name) {
" Tools " { " B4832BD8-E735-4761-8DAF-37F882276DAB " }
" Tools " { " B4832BD8-E735-4761-8DAF-37F882276DAB " }
" UpdateRollups " { " 28BC880E-0592-4CBF-8F95-C79B17911D5F " }
" UpdateRollups " { " 28BC880E-0592-4CBF-8F95-C79B17911D5F " }
" Updates " { " CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83 " }
" Updates " { " CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83 " }
default { Fail-Json - obj $result - message " Unknown category_name $category_name , must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates) " }
default { Fail-Json - message " Unknown category_name $category_name , must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates) " }
}
}
return $guid
return $guid
}
}
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
Function Get-RebootStatus ( ) {
$common_functions = {
try {
Function Write-DebugLog($msg ) {
$system_info = New-Object -ComObject Microsoft . Update . SystemInfo
$date_str = Get-Date -Format u
} catch {
$msg = " $date_str $msg "
Fail-Json -obj $result -message " Failed to create Microsoft.Update.SystemInfo COM object for reboot status: $( $_ . Exception . Message ) "
}
return $system_info . RebootRequired
Write-Debug -Message $msg
if ( $log_path -ne $null -and ( -not $check_mode ) ) {
Add-Content -Path $log_path -Value $msg
}
}
}
}
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
$update_script_block = {
Param (
[ hashtable ] $arguments
)
$ErrorActionPreference = " Stop "
$DebugPreference = " Continue "
Function Start-Updates {
Param (
$category_guids ,
$log_path ,
$state ,
$blacklist ,
$whitelist
)
$result = @ {
changed = $false
updates = @ { }
filter ed_updates = @ { }
}
Write-DebugLog -msg " Creating Windows Update session... "
Write-DebugLog -msg " Creating Windows Update session... "
try {
try {
$session = New-Object -ComObject Microsoft . Update . Session
$session = New-Object -ComObject Microsoft . Update . Session
} catch {
} catch {
Fail-Json -obj $result -message " Failed to create Microsoft.Update.Session COM object: $( $_ . Exception . Message ) "
$result . failed = $true
}
$result . msg = " Failed to create Microsoft.Update.Session COM object: $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Create Windows Update searcher... "
Write-DebugLog -msg " Create Windows Update searcher... "
try {
try {
$searcher = $session . CreateUpdateSearcher ( )
$searcher = $session . CreateUpdateSearcher ( )
} catch {
} catch {
Fail-Json -obj $result -message " Failed to create Windows Update search from session: $( $_ . Exception . Message ) "
$result . failed = $true
}
$result . msg = " Failed to create Windows Update search from session: $( $_ . Exception . Message ) "
return $result
}
# OR is only allowed at the top-level, so we have to repeat base criteria inside
# OR is only allowed at the top-level, so we have to repeat base criteria inside
# FUTURE: change this to client-side filtered?
# FUTURE: change this to client-side filtered?
$criteria_base = " IsInstalled = 0 "
$criteria_base = " IsInstalled = 0 "
$criteria_list = $category_guids | ForEach-Object { " ( $criteria_base AND CategoryIds contains ' $_ ') " }
$criteria_list = $category_guids | ForEach-Object { " ( $criteria_base AND CategoryIds contains ' $_ ') " }
$criteria = [ string ] :: Join ( " OR " , $criteria_list )
$criteria = [ string ] :: Join ( " OR " , $criteria_list )
Write-DebugLog -msg " Search criteria: $criteria "
Write-DebugLog -msg " Search criteria: $criteria "
Write-DebugLog -msg " Searching for updates to install in category Ids $category_guids ... "
Write-DebugLog -msg " Searching for updates to install in category Ids $category_guids ... "
try {
try {
$search_result = $searcher . Search ( $criteria )
$search_result = $searcher . Search ( $criteria )
} catch {
} catch {
Fail-Json -obj $result -message " Failed to search for updates with criteria ' $criteria ': $( $_ . Exception . Message ) "
$result . failed = $true
}
$result . msg = " Failed to search for updates with criteria ' $criteria ': $( $_ . Exception . Message ) "
Write-DebugLog -msg " Found $( $search_result . Updates . Count ) updates "
return $result
}
Write-DebugLog -msg " Found $( $search_result . Updates . Count ) updates "
Write-DebugLog -msg " Creating update collection... "
Write-DebugLog -msg " Creating update collection... "
try {
try {
$updates_to_install = New-Object -ComObject Microsoft . Update . UpdateColl
$updates_to_install = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
} catch {
Fail-Json -obj $result -message " Failed to create update collection object: $( $_ . Exception . Message ) "
$result . failed = $true
}
$result . msg = " Failed to create update collection object: $( $_ . Exception . Message ) "
return $result
}
foreach ( $update in $search_result . Updates ) {
foreach ( $update in $search_result . Updates ) {
$update_info = @ {
$update_info = @ {
title = $update . Title
title = $update . Title
# TODO: pluck the first KB out (since most have just one)?
# TODO: pluck the first KB out (since most have just one)?
kb = $update . KBArticleIDs
kb = $update . KBArticleIDs
id = $update . Identity . UpdateId
id = $update . Identity . UpdateId
installed = $false
installed = $false
}
}
# validate update again blacklist/whitelist
$skipped = $false
$whitelist_match = $false
foreach ( $whitelist_entry in $whitelist ) {
if ( $update_info . title -imatch $whitelist_entry ) {
$whitelist_match = $true
break
}
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $whitelist_entry ) {
$whitelist_match = $true
break
}
}
}
if ( $whitelist . Length -gt 0 -and -not $whitelist_match ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was not found in the whitelist "
$skipped = $true
}
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $blacklist_entry ) {
$kb_match = $true
}
foreach ( $blacklist_entry in $blacklist ) {
$kb_match = $false
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $blacklist_entry ) {
$kb_match = $true
}
}
if ( $kb_match -or $update_info . title -imatch $blacklist_entry ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was found in the blacklist "
$skipped = $true
break
}
}
}
if ( $skipped ) {
$result . filtered_updates [ $update_info . id ] = $update_info
continue
}
if ( -not $update . EulaAccepted ) {
Write-DebugLog -msg " Accepting EULA for $( $update_info . id ) "
try {
$update . AcceptEula ( )
} catch {
$result . failed = $true
$result . msg = " Failed to accept EULA for update $( $update_info . id ) - $( $update_info . title ) "
return $result
}
}
if ( $update . IsHidden ) {
Write-DebugLog -msg " Skipping hidden update $( $update_info . title ) "
continue
}
# validate update again blacklist/whitelist
Write-DebugLog -msg " Adding update $( $update_info . id ) - $( $update_info . title ) "
$skipped = $false
$updates_to_install . Add ( $update ) > $null
$whitelist_match = $false
foreach ( $whitelist_entry in $whitelist ) {
$result . updates [ $update_info . id ] = $update_info
if ( $update_info . title -imatch $whitelist_entry ) {
$whitelist_match = $true
break
}
}
foreach ( $kb in $update_info . kb ) {
if ( " KB $kb " -imatch $whitelist_entry ) {
Write-DebugLog -msg " Calculating pre-install reboot requirement... "
$whitelist_match = $true
break
# calculate this early for check mode, and to see if we should allow updates to continue
$result . reboot_required = ( New-Object -ComObject Microsoft . Update . SystemInfo ) . RebootRequired
$result . found_update_count = $updates_to_install . Count
$result . installed_update_count = 0
# Early exit of check mode/state=searched as it cannot do more after this
if ( $check_mode -or $state -eq " searched " ) {
Write-DebugLog -msg " Check mode: exiting... "
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
if ( $updates_to_install . Count -gt 0 -and ( $state -ne " searched " ) ) {
$result . changed = $true
}
}
return $result
}
}
}
if ( $whitelist . Length -gt 0 -and -not $whitelist_match ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was not found in the whitelist "
$skipped = $true
}
foreach ( $blacklist_entry in $blacklist ) {
if ( $updates_to_install . Count -gt 0 ) {
$kb_match = $false
if ( $result . reboot_required ) {
foreach ( $kb in $update_info . kb ) {
Write-DebugLog -msg " FATAL: A reboot is required before more updates can be installed "
if ( " KB $kb " -imatch $blacklist_entry ) {
$result . failed = $true
$kb_match = $true
$result . msg = " A reboot is required before more updates can be installed "
return $result
}
}
Write-DebugLog -msg " No reboot is pending... "
} else {
# no updates to install exit here
return $result
}
}
if ( $kb_match -or $update_info . title -imatch $blacklist_entry ) {
Write-DebugLog -msg " Skipping update $( $update_info . id ) - $( $update_info . title ) as it was found in the blacklist "
Write-DebugLog -msg " Downloading updates... "
$skipped = $true
$update_index = 1
break
foreach ( $update in $updates_to_install ) {
$update_number = " ( $update_index of $( $updates_to_install . Count ) ) "
if ( $update . IsDownloaded ) {
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateId ) already downloaded, skipping... "
$update_index + +
continue
}
Write-DebugLog -msg " Creating downloader object... "
try {
$dl = $session . CreateUpdateDownloader ( )
} catch {
$result . failed = $true
$result . msg = " Failed to create downloader object: $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Creating download collection... "
try {
$dl . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
$result . failed = $true
$result . msg = " Failed to create download collection object: $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Adding update $update_number $( $update . Identity . UpdateId ) "
$dl . Updates . Add ( $update ) > $null
Write-DebugLog -msg " Downloading $update_number $( $update . Identity . UpdateId ) "
try {
$download_result = $dl . Download ( )
} catch {
$result . failed = $true
$result . msg = " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Download result code for $update_number $( $update . Identity . UpdateId ) = $( $download_result . ResultCode ) "
# FUTURE: configurable download retry
if ( $download_result . ResultCode -ne 2 ) { # OperationResultCode orcSucceeded
$result . failed = $true
$result . msg = " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : Download Result $( $download_result . ResultCode ) "
return $result
}
$result . changed = $true
$update_index + +
}
}
}
if ( $skipped ) {
$result . filtered_updates [ $update_info . id ] = $update_info
continue
}
Write-DebugLog -msg " Installing updates... "
if ( -not $update . EulaAccepted ) {
# install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog -msg " Accepting EULA for $( $update_info . id ) "
Write-DebugLog -msg " Creating installer object... "
try {
try {
$update . AcceptEula ( )
$ installer = $session . CreateUpdateInstaller ( )
} catch {
} catch {
Fail-Json -obj $result -message " Failed to accept EULA for update $( $update_info . id ) - $( $update_info . title ) "
$result . failed = $true
$result . msg = " Failed to create Update Installer object: $( $_ . Exception . Message ) "
return $result
}
}
}
if ( $update . IsHidden ) {
Write-DebugLog -msg " Creating install collection... "
Write-DebugLog -msg " Skipping hidden update $( $update_info . title ) "
try {
continue
$installer . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
}
} catch {
$result . failed = $true
$result . msg = " Failed to create Update Collection object: $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Adding update $( $update_info . id ) - $( $update_info . title ) "
foreach ( $update in $updates_to_install ) {
$updates_to_install . Add ( $update ) > $null
Write-DebugLog -msg " Adding update $( $update . Identity . UpdateID ) "
$installer . Updates . Add ( $update ) > $null
}
$result . updates [ $update_info . id ] = $update_info
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
}
try {
$install_result = $installer . Install ( )
} catch {
$result . failed = $true
$result . msg = " Failed to install update from Update Collection: $( $_ . Exception . Message ) "
return $result
}
Write-DebugLog -msg " Calculating pre-install reboot requirement... "
$update_success_count = 0
$update_fail_count = 0
# WU result API requires us to index in to get the install results
$update_index = 0
foreach ( $update in $updates_to_install ) {
$update_number = " ( $( $update_index + 1 ) of $( $updates_to_install . Count ) ) "
try {
$update_result = $install_result . GetUpdateResult ( $update_index )
} catch {
$result . failed = $true
$result . msg = " Failed to get update result for update $update_number $( $update . Identity . UpdateID ) - $( $update . Title ) : $( $_ . Exception . Message ) "
return $result
}
$update_resultcode = $update_result . ResultCode
$update_hresult = $update_result . HResult
$update_index + +
$update_dict = $result . updates [ $update . Identity . UpdateID ]
if ( $update_resultcode -eq 2 ) { # OperationResultCode orcSucceeded
$update_success_count + +
$update_dict . installed = $true
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) succeeded "
} else {
$update_fail_count + +
$update_dict . installed = $false
$update_dict . failed = $true
$update_dict . failure_hresult_code = $update_hresult
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) failed, resultcode: $update_resultcode , hresult: $update_hresult "
}
}
# calculate this early for check mode, and to see if we should allow updates to continue
Write-DebugLog -msg " Performing post-install reboot requirement check... "
$result . reboot_required = Get-RebootStatus
$result . reboot_required = ( New-Object -ComObject Microsoft . Update . SystemInfo ) . RebootRequired
$result . found_update_count = $updates_to_install . Count
$result . installed_update_count = $update_success_c ount
$result . installed_update_count = 0
$result . failed_update_count = $update_fail_count
# Early exit of check mode/state=searched as it cannot do more after this
if ( $update_fail_count -gt 0 ) {
if ( $check_mode -or $state -eq " searched " ) {
$result . failed = $true
Write-DebugLog -msg " Check mode: exiting... "
$result . msg = " Failed to install one or more updates "
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
return $result
}
if ( $updates_to_install . Count -gt 0 -and ( $state -ne " searched " ) ) {
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
$result . changed = $true
}
}
Exit-Json -obj $result
}
if ( $updates_to_install . Count -gt 0 ) {
$check_mode = $arguments . check_mode
if ( $result . reboot_required ) {
try {
Write-DebugLog -msg " FATAL: A reboot is required before more updates can be installed "
return @ {
Fail-Json -obj $result -message " A reboot is required before more updates can be installed "
job_output = Start-Updates @arguments
}
} catch {
Write-DebugLog -msg " Fatal exception: $( $_ . Exception . Message ) at $( $_ . ScriptStackTrace ) "
return @ {
job_output = @ {
failed = $true
msg = $_ . Exception . Message
location = $_ . ScriptStackTrace
}
}
}
}
Write-DebugLog -msg " No reboot is pending... "
} else {
# no updates to install exit here
Exit-Json -obj $result
}
}
Write-DebugLog -msg " Downloading updates... "
Function Start-Natively($common_functions , $script ) {
$update_index = 1
$runspace_pool = [ RunspaceFactory ] :: CreateRunspacePool ( )
foreach ( $update in $updates_to_install ) {
$runspace_pool . Open ( )
$update_number = " ( $update_index of $( $updates_to_install . Count ) ) "
if ( $update . IsDownloaded ) {
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateId ) already downloaded, skipping... "
$update_index + +
continue
}
Write-DebugLog -msg " Creating downloader object... "
try {
try {
$dl = $session . CreateUpdateDownloader ( )
$ps_pipeline = [ PowerShell ] :: Create ( )
} catch {
$ps_pipeline . RunspacePool = $runspace_pool
Fail-Json -obj $result -message " Failed to create downloader object: $( $_ . Exception . Message ) "
# add the common script functions
$ps_pipeline . AddScript ( $common_functions ) > $null
# add the update script block and required parameters
$ps_pipeline . AddStatement ( ) . AddScript ( $script ) > $null
$ps_pipeline . AddParameter ( " arguments " , @ {
category_guids = $category_guids
log_path = $log_path
state = $state
blacklist = $blacklist
whitelist = $whitelist
check_mode = $check_mode
} ) > $null
$output = $ps_pipeline . Invoke ( )
} finally {
$runspace_pool . Close ( )
}
}
Write-DebugLog -msg " Creating download collection... "
$result = $output [ 0 ] . job_output
try {
if ( $ps_pipeline . HadErrors ) {
$dl . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
$result . failed = $true
} catch {
Fail-Json -obj $result -message " Failed to create download collection object: $( $_ . Exception . Message ) "
# if the msg wasn't set, then add a generic error to at least tell the user something
if ( -not ( $result . ContainsKey ( " msg " ) ) ) {
$result . msg = " Unknown failure when executing native update script block "
$result . errors = $ps_pipeline . Streams . Error
}
}
}
Write-DebugLog -msg " Adding update $update_number $( $update . Identity . UpdateId ) "
Write-DebugLog -msg " Native job completed with output: $( $result | Out-String -Width 300 ) "
$dl . Updates . Add ( $update ) > $null
Write-DebugLog -msg " Downloading $update_number $( $update . Identity . UpdateId ) "
return , $result
try {
}
$download_result = $dl . Download ( )
} catch {
Function Remove-ScheduledJob($name ) {
Fail-Json -obj $result -message " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : $( $_ . Exception . Message ) "
$scheduled_job = Get-ScheduledJob -Name $name -ErrorAction SilentlyContinue
if ( $scheduled_job -ne $null ) {
Write-DebugLog -msg " Scheduled Job $name exists, ensuring it is not running... "
$scheduler = New-Object -ComObject Schedule . Service
Write-DebugLog -msg " Connecting to scheduler service... "
$scheduler . Connect ( )
Write-DebugLog -msg " Getting running tasks named $name "
$running_tasks = @ ( $scheduler . GetRunningTasks ( 0 ) | Where-Object { $_ . Name -eq $name } )
foreach ( $task_to_stop in $running_tasks ) {
Write-DebugLog -msg " Stopping running task $( $task_to_stop . InstanceGuid ) ... "
$task_to_stop . Stop ( )
}
<# FUTURE: add a global waithandle for this to release any other waiters . Wait-Job
and / or polling will block forever , since the killed job object in the parent
session doesn 't know it' s been killed : ( #>
Unregister-ScheduledJob -Name $name
}
}
}
Write-DebugLog -msg " Download result code for $update_number $( $update . Identity . UpdateId ) = $( $download_result . ResultCode ) "
Function Start-AsScheduledTask($common_functions , $script ) {
# FUTURE: configurable download retry
$job_name = " ansible-win-updates "
if ( $download_result . ResultCode -ne 2 ) { # OperationResultCode orcSucceeded
Remove-ScheduledJob -name $job_name
Fail-Json -obj $result -message " Failed to download update $update_number $( $update . Identity . UpdateId ) - $( $update . Title ) : Download Result $( $download_result . ResultCode ) "
$job_args = @ {
ScriptBlock = $script
Name = $job_name
ArgumentList = @ (
@ {
category_guids = $category_guids
log_path = $log_path
state = $state
blacklist = $blacklist
whitelist = $whitelist
check_mode = $check_mode
}
)
ErrorAction = " Stop "
ScheduledJobOption = @ { RunElevated = $True ; StartIfOnBatteries = $True ; StopIfGoingOnBatteries = $False }
InitializationScript = $common_functions
}
}
$result . changed = $true
Write-DebugLog -msg " Registering scheduled job with args $( $job_args | Out-String -Width 300 ) "
$update_index + +
$scheduled_job = Register-ScheduledJob @job_args
}
Write-DebugLog -msg " Installing updates... "
# RunAsTask isn't available in PS3 - fall back to a 2s future trigger
if ( $scheduled_job | Get-Member -Name RunAsTask ) {
Write-DebugLog -msg " Starting scheduled job (PS4+ method) "
$scheduled_job . RunAsTask ( )
} else {
Write-DebugLog -msg " Starting scheduled job (PS3 method) "
Add-JobTrigger -InputObject $scheduled_job -trigger $ ( New-JobTrigger -Once -At $ ( Get-Date ) . AddSeconds ( 2 ) )
}
# install as a batch so the reboot manager will suppress intermediate reboots
$sw = [ System.Diagnostics.Stopwatch ] :: StartNew ( )
Write-DebugLog -msg " Creating installer object... "
$job = $null
try {
$installer = $session . CreateUpdateInstaller ( )
} catch {
Fail-Json -obj $result -message " Failed to create Update Installer object: $( $_ . Exception . Message ) "
}
Write-DebugLog -msg " Creating install collection... "
Write-DebugLog -msg " Waiting for job completion... "
try {
$installer . Updates = New-Object -ComObject Microsoft . Update . UpdateColl
} catch {
Fail-Json -obj $result -message " Failed to create Update Collection object: $( $_ . Exception . Message ) "
}
foreach ( $update in $updates_to_install ) {
# Wait-Job can fail for a few seconds until the scheduled task starts - poll for it...
Write-DebugLog -msg " Adding update $( $update . Identity . UpdateID ) "
while ( $job -eq $null ) {
$installer . Updates . Add ( $update ) > $null
Start-Sleep -Milliseconds 100
}
if ( $sw . ElapsedMilliseconds -ge 30000 ) { # tasks scheduled right after boot on 2008R2 can take awhile to start...
Fail-Json -msg " Timed out waiting for scheduled task to start "
}
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
# FUTURE: configurable timeout so we don't block forever?
try {
# FUTURE: add a global WaitHandle in case another instance kills our job, so we don't block forever
$install_result = $installer . Install ( )
$job = Wait-Job -Name $scheduled_job . Name -ErrorAction SilentlyContinue
} catch {
}
Fail-Json -obj $result -message " Failed to install update from Update Collection: $( $_ . Exception . Message ) "
}
$update_success_count = 0
$sw = [ System.Diagnostics.Stopwatch ] :: StartNew ( )
$update_fail_count = 0
# WU result API requires us to index in to get the install results
# NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available)
$update_index = 0
while ( ( $job . Output -eq $null -or -not ( $job . Output | Get-Member -Name Key -ErrorAction Ignore ) -or -not $job . Output . Key . Contains ( " job_output " ) ) -and $sw . ElapsedMilliseconds -lt 15000 ) {
foreach ( $update in $updates_to_install ) {
Write-DebugLog -msg " Waiting for job output to populate... "
$update_number = " ( $( $update_index + 1 ) of $( $updates_to_install . Count ) ) "
Start-Sleep -Milliseconds 500
try {
$update_result = $install_result . GetUpdateResult ( $update_index )
} catch {
Fail-Json -obj $result -message " Failed to get update result for update $update_number $( $update . Identity . UpdateID ) - $( $update . Title ) : $( $_ . Exception . Message ) "
}
}
$update_resultcode = $update_result . ResultCode
$update_hresult = $update_result . HResult
$update_index + +
# NB: fallthru on both timeout and success
$ret = @ {
ErrorOutput = $job . Error
WarningOutput = $job . Warning
VerboseOutput = $job . Verbose
DebugOutput = $job . Debug
}
$update_dict = $result . updates [ $update . Identity . UpdateID ]
if ( $job . Output -eq $null -or -not $job . Output . Keys . Contains ( 'job_output' ) ) {
if ( $update_resultcode -eq 2 ) { # OperationResultCode orcSucceeded
$ret . Output = @ { failed = $true ; msg = " job output was lost " }
$update_success_count + +
$update_dict . installed = $true
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) succeeded "
} else {
} else {
$update_fail_count + +
$ret . Output = $job . Output . job_output # sub-object returned, can only be accessed as a property for some reason
$update_dict . installed = $false
$update_dict . failed = $true
$update_dict . failure_hresult_code = $update_hresult
Write-DebugLog -msg " Update $update_number $( $update . Identity . UpdateID ) failed, resultcode: $update_resultcode , hresult: $update_hresult "
}
}
}
Write-DebugLog -msg " Performing post-install reboot requirement check... "
try { # this shouldn't be fatal, but can fail with both Powershell errors and COM Exceptions, hence the dual error-handling...
$result . reboot_required = Get-RebootStatus
Unregister-ScheduledJob -Name $job_name -Force -ErrorAction Continue
$result . installed_update_count = $update_success_count
} catch {
$result . failed_update_count = $update_fail_count
Write-DebugLog " Error unregistering job after execution: $( $_ . Exception . ToString ( ) ) $( $_ . ScriptStackTrace ) "
}
Write-DebugLog -msg " Scheduled job completed with output: $( $re . Output | Out-String -Width 300 ) "
if ( $update_fail_count -gt 0 ) {
return $ret . Output
Fail-Json -obj $result -msg " Failed to install one or more updates "
}
}
Write-DebugLog -msg " Return value: `r `n $( ConvertTo-Json -InputObject $result -Depth 99 ) "
# source the common code into the current scope so we can call it
. $common_functions
<# Most of the Windows Update Agent API will not run under a remote token,
which a remote WinRM session always has . Using become can bypass this
limitation but it is not always an option with older hosts . win_updates checks
if WUA is available in the current logon process and does either of the below ;
* If become is used then it will run the windows update process natively
without any of the scheduled task hackery
* If become is not used then it will run the windows update process under
a scheduled job .
#>
try {
( New-Object -ComObject Microsoft . Update . Session ) . CreateUpdateInstaller ( ) . IsBusy > $null
$wua_available = $true
} catch {
$wua_available = $false
}
Exit-Json $result
if ( $wua_available ) {
Write-DebugLog -msg " WUA is available in current logon process, running natively "
$result = Start-Natively -common_functions $common_functions -script $update_script_block
} else {
Write-DebugLog -msg " WUA is not avialable in current logon process, running with scheduled task "
$result = Start-AsScheduledTask -common_functions $common_functions -script $update_script_block
}
Exit-Json -obj $result