├── .gitignore ├── OUPicker.xaml ├── README.md ├── Scripts ├── UpdateDownloader.ps1 └── UpdateInstaller.ps1 ├── WUU.ps1 └── WUU.xaml /.gitignore: -------------------------------------------------------------------------------- 1 | [Dd]esktop.ini 2 | *.txt 3 | 4 | PsExec.exe 5 | -------------------------------------------------------------------------------- /OUPicker.xaml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows Update Utility 2 | 3 | Windows Update Utility is a major rewrite of PoshPAIG. The goal is to further increase the agility to patch systems with a simple to use interface. 4 | 5 | ## Description 6 | 7 | This project is a fork of Tyler Siegrist [Windows Update Utility (WUU)](https://gallery.technet.microsoft.com/scriptcenter/Windows-Update-Utility-WUU-1d72e520) project in the Windows scripting center. 8 | 9 | You will need to download [PsExec](https://docs.microsoft.com/en-us/sysinternals/downloads/psexec) and put it in the same directory as the script. 10 | -------------------------------------------------------------------------------- /Scripts/UpdateDownloader.ps1: -------------------------------------------------------------------------------- 1 | $UpdateSession = New-Object -ComObject 'Microsoft.Update.Session' 2 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 3 | $SearchResult = $UpdateSearcher.Search("IsInstalled=0 and IsHidden=0") 4 | $DownloadCount = 0 5 | 6 | if ( $searchResult.Updates.Count -eq 0 ) { 7 | return 0 8 | } 9 | 10 | $UpdatesToDownload = New-Object -ComObject "Microsoft.Update.UpdateColl" 11 | foreach ( $Update in $SearchResult.Updates ) { 12 | if ( $Update.IsDownloaded -eq $false ) { 13 | $UpdatesToDownload.Add( $Update ) | Out-Null 14 | } 15 | } 16 | if ( $UpdatesToDownload.Count -gt 0 ) { 17 | $Downloader = $UpdateSession.CreateUpdateDownloader() 18 | $Downloader.Updates = $UpdatesToDownload 19 | $DownloadResult = $Downloader.Download() 20 | 21 | 22 | 0..( $UpdatesToDownload.Count - 1 ) | ForEach-Object { 23 | $Result = $DownloadResult.GetUpdateResult( $PSItem ).ResultCode 24 | if ( $Result -eq 2 -or $Result -eq 3 ) { 25 | $DownloadCount++ 26 | } 27 | } 28 | } 29 | return $DownloadCount 30 | -------------------------------------------------------------------------------- /Scripts/UpdateInstaller.ps1: -------------------------------------------------------------------------------- 1 | $UpdateSession = New-Object -ComObject 'Microsoft.Update.Session' 2 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 3 | $SearchResult = $UpdateSearcher.Search("IsInstalled=0 and IsHidden=0") 4 | $ErrorCount = 0 5 | 6 | if ( $searchResult.Updates.Count -eq 0 ) { 7 | return 0 8 | } 9 | 10 | $UpdatesToInstall = New-Object -ComObject "Microsoft.Update.UpdateColl" 11 | foreach ( $Update in $SearchResult.Updates ) { 12 | if ( $Update.InstallationBehavior.CanRequestUserInput -eq $true ) { continue } 13 | if ( $Update.IsDownloaded -eq $false ) { continue } 14 | if ( $Update.EulaAccepted -eq $false ) { $Update.AcceptEula() } 15 | $UpdatesToInstall.Add($Update) | Out-Null 16 | } 17 | if ( $UpdatesToInstall.Count -gt 0 ) { 18 | 19 | $Installer = $UpdateSession.CreateUpdateInstaller() 20 | $Installer.Updates = $UpdatesToInstall 21 | $InstallationResult = $Installer.Install() 22 | 23 | 0..( $UpdatesToInstall.Count - 1 ) | ForEach-Object { 24 | $Result = $InstallationResult.GetUpdateResult($PSItem).ResultCode 25 | if ( $Result -ge 4 ) { 26 | $ErrorCount++ 27 | } 28 | } 29 | } 30 | return $ErrorCount 31 | -------------------------------------------------------------------------------- /WUU.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script provides a GUI for remotely managing Windows Updates. 4 | 5 | .DESCRIPTION 6 | This script provides a GUI for remotely managing Windows Updates. You can check for, download, and install updates remotely. There is also an option to automatically reboot the Computer after installing updates if required. 7 | 8 | .EXAMPLE 9 | .\WUU.ps1 10 | 11 | This example open the Windows Update Utility. 12 | 13 | .NOTES 14 | Author: Tyler Siegrist 15 | Date: 12/14/2016 16 | 17 | This script needs to be run as an administrator with the credentials of an administrator on the remote Computers. 18 | 19 | There is limited feedback on the download and install processes due to Microsoft restricting the ability to remotely download or install Windows Updates. This is done by using psexec to run a script locally on the remote machine. 20 | #> 21 | 22 | #region Synchronized collections 23 | $DisplayHash = [hashtable]::Synchronized(@{}) 24 | $runspaceHash = [hashtable]::Synchronized(@{}) 25 | $Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList)) 26 | $JobCleanup = [hashtable]::Synchronized(@{}) 27 | $UpdatesHash = [hashtable]::Synchronized(@{}) 28 | #endregion Synchronized collections 29 | 30 | #region Environment validation 31 | #Validate user is an Administrator 32 | Write-Verbose 'Checking Administrator credentials.' 33 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 34 | Write-Warning "This script must be elevated!`nNow attempting to elevate." 35 | Start-Process -Verb 'Runas' -FilePath 'PowerShell.exe' -ArgumentList "-STA -NoProfile -WindowStyle Hidden -File `"$($MyInvocation.MyCommand.Definition)`"" 36 | Break 37 | } 38 | 39 | #Ensure that we are running the GUI from the correct location so that scripts & psexec can be accessed. 40 | Set-Location $(Split-Path $MyInvocation.MyCommand.Path) 41 | 42 | #Check for PsExec 43 | Write-Verbose 'Checking for psexec.exe.' 44 | if (-Not (Test-Path psexec.exe)) { 45 | Write-Warning ("Psexec.exe missing from {0}!`n Please place file in the path so WUU can work properly" -f (Split-Path $MyInvocation.MyCommand.Path)) 46 | Break 47 | } 48 | 49 | #Determine if this instance of PowerShell can run WPF (required for GUI) 50 | Write-Verbose 'Checking the apartment state.' 51 | if ($host.Runspace.ApartmentState -ne 'STA') { 52 | Write-Warning "This script must be run in PowerShell started using -STA switch!`nScript will attempt to open PowerShell in STA and run re-run script." 53 | Start-Process -File PowerShell.exe -Argument "-STA -NoProfile -WindowStyle Hidden -File `"$($myinvocation.mycommand.definition)`"" 54 | Break 55 | } 56 | #endregion Environment validation 57 | 58 | #region Load required assemblies 59 | Write-Verbose 'Loading required assemblies.' 60 | Add-Type -assemblyName PresentationFramework 61 | Add-Type -assemblyName PresentationCore 62 | Add-Type -assemblyName WindowsBase 63 | Add-Type -assemblyName Microsoft.VisualBasic 64 | Add-Type -assemblyName System.Windows.Forms 65 | #endregion Load required assemblies 66 | 67 | #region Load XAML 68 | Write-Verbose 'Loading XAML data.' 69 | try { 70 | [xml]$xaml = Get-Content .\WUU.xaml 71 | $reader = (New-Object System.Xml.XmlNodeReader $xaml) 72 | $DisplayHash.Window = [Windows.Markup.XamlReader]::Load($reader) 73 | } 74 | catch { 75 | Write-Warning 'Unable to load XAML data!' 76 | Break 77 | } 78 | #endregion 79 | 80 | #region ScriptBlocks 81 | #Add new Computer(s) to list 82 | $AddEntry = { 83 | Param ($ComputerName) 84 | Write-Verbose "Adding $ComputerName." 85 | 86 | if (Test-Path Exempt.txt) { 87 | Write-Verbose 'Collecting systems from exempt list.' 88 | [string[]]$exempt = Get-Content Exempt.txt 89 | } 90 | 91 | #Add to list 92 | foreach ($Computer in $ComputerName) { 93 | $Computer = $Computer.Trim() #Remove any whitspace 94 | if ([System.String]::IsNullOrEmpty($Computer)) {continue} #Do not add if name empty 95 | if ($exempt -contains $Computer) {continue} #Do not add excluded 96 | if (($DisplayHash.Listview.Items | Select-Object -Expand Computer) -contains $Computer) {continue} #Do not add duplicate 97 | 98 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 99 | $DisplayHash.clientObservable.Add(( 100 | New-Object PSObject -Property @{ 101 | Computer = $Computer 102 | Available = 0 -as [int] 103 | Downloaded = 0 -as [int] 104 | InstallErrors = 0 -as [int] 105 | Status = "Initalizing." 106 | RebootRequired = $false -as [bool] 107 | Runspace = $null 108 | })) 109 | $DisplayHash.Listview.Items.CommitEdit() 110 | $DisplayHash.Listview.Items.Refresh() 111 | }) 112 | } 113 | 114 | #Setup runspace 115 | ($DisplayHash.Listview.Items | Where-Object {$_.Runspace -eq $Null}) | % { 116 | $NewRunspace = [runspacefactory]::CreateRunspace() 117 | $NewRunspace.ApartmentState = "STA" 118 | $NewRunspace.ThreadOptions = "ReuseThread" 119 | $NewRunspace.Open() 120 | $NewRunspace.SessionStateProxy.SetVariable("DisplayHash", $DisplayHash) 121 | $NewRunspace.SessionStateProxy.SetVariable("UpdatesHash", $UpdatesHash) 122 | $NewRunspace.SessionStateProxy.SetVariable("path", $pwd) 123 | 124 | $_.Runspace = $NewRunspace 125 | 126 | # $PowerShell = [powershell]::Create().AddScript($GetUpdates).AddArgument($_) 127 | # $PowerShell.Runspace = $_.Runspace 128 | 129 | # #Save handle so we can later end the runspace 130 | # $Temp = New-Object PSObject -Property @{ 131 | # PowerShell = $PowerShell 132 | # Runspace = $PowerShell.BeginInvoke() 133 | # } 134 | 135 | # $Jobs.Add($Temp) | Out-Null 136 | } 137 | } 138 | 139 | #Clear Computer list 140 | $ClearComputerList = { 141 | #Remove Computers & associated updates 142 | &$removeEntry @($DisplayHash.Listview.Items) 143 | 144 | #Update status 145 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 146 | $DisplayHash.StatusTextBox.Foreground = 'Black' 147 | $DisplayHash.StatusTextBox.Text = 'Computer List Cleared!' 148 | }) 149 | } 150 | 151 | #Download available updates 152 | $DownloadUpdates = { 153 | Param ($Computer) 154 | Try { 155 | #Set path for psexec, scripts 156 | Set-Location $Path 157 | 158 | #Check download size 159 | $DownloadStats = ($UpdatesHash[$Computer.Computer] | Where-Object {$_.IsDownloaded -eq $false} | Select-Object -ExpandProperty MaxDownloadSize | Measure-Object -Sum) 160 | 161 | #Update status 162 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 163 | $DisplayHash.Listview.Items.EditItem($Computer) 164 | $Computer.Status = "Downloading $($DownloadStats.Count) Updates ($([math]::Round($DownloadStats.Sum/1MB))MB)." 165 | $DisplayHash.Listview.Items.CommitEdit() 166 | $DisplayHash.Listview.Items.Refresh() 167 | }) 168 | 169 | #Copy script to remote Computer and execute 170 | if ( ! ( Test-Path -Path "\\$($Computer.Computer)\C$\Admin\Scripts") ) { 171 | New-Item -Path "\\$($Computer.Computer)\C$\Admin\Scripts" -ItemType Directory 172 | } 173 | Copy-Item '.\Scripts\UpdateDownloader.ps1' "\\$($Computer.Computer)\c$\Admin\Scripts" -Force 174 | [int]$DownloadCount = .\PsExec.exe -accepteula -nobanner -s "\\$($Computer.Computer)" cmd.exe /c 'echo . | powershell.exe -ExecutionPolicy Bypass -file C:\Admin\Scripts\UpdateDownloader.ps1' 175 | Remove-Item "\\$($Computer.Computer)\c$\Admin\Scripts\UpdateDownloader.ps1" 176 | if ($LASTEXITCODE -ne 0) { 177 | throw "PsExec failed with error code $LASTEXITCODE" 178 | } 179 | 180 | #Update status 181 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 182 | $DisplayHash.Listview.Items.EditItem($Computer) 183 | $Computer.Status = 'Download complete.' 184 | $Computer.Downloaded += $DownloadCount 185 | $DisplayHash.Listview.Items.CommitEdit() 186 | $DisplayHash.Listview.Items.Refresh() 187 | }) 188 | } 189 | catch { 190 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 191 | $DisplayHash.Listview.Items.EditItem($Computer) 192 | $Computer.Status = "Error occured: $($_.Exception.Message)" 193 | $DisplayHash.Listview.Items.CommitEdit() 194 | $DisplayHash.Listview.Items.Refresh() 195 | }) 196 | 197 | #Cancel any remaining actions 198 | exit 199 | } 200 | } 201 | 202 | #Check for available updates 203 | $GetUpdates = { 204 | Param ($Computer) 205 | Try { 206 | #Update status 207 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 208 | $DisplayHash.Listview.Items.EditItem($Computer) 209 | $Computer.Status = 'Checking for updates, this may take some time.' 210 | $DisplayHash.Listview.Items.CommitEdit() 211 | $DisplayHash.Listview.Items.Refresh() 212 | }) 213 | 214 | Set-Location $path 215 | 216 | #Check for updates 217 | $UpdateSession = [activator]::CreateInstance([type]::GetTypeFromProgID('Microsoft.Update.Session', $Computer.Computer)) 218 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 219 | $SearchResult = $UpdateSearcher.Search('IsInstalled=0 and IsHidden=0') 220 | 221 | #Save update info in hash to view with 'Show Available Updates' 222 | $UpdatesHash[$Computer.Computer] = $SearchResult.Updates 223 | 224 | #Update status 225 | $DownloadCount = @($SearchResult.Updates | Where-Object {$_.IsDownloaded -eq $true}).Count 226 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 227 | $DisplayHash.Listview.Items.EditItem($Computer) 228 | $Computer.Available = $SearchResult.Updates.Count 229 | $Computer.Downloaded = $DownloadCount 230 | $DisplayHash.Listview.Items.CommitEdit() 231 | $DisplayHash.Listview.Items.Refresh() 232 | }) 233 | 234 | #Don't bother checking for reboot if there is nothing to be pending. 235 | # if ($DownloadCount -gt 0) { 236 | #Update status 237 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 238 | $DisplayHash.Listview.Items.EditItem($Computer) 239 | $Computer.Status = 'Checking for a pending reboot.' 240 | $DisplayHash.Listview.Items.CommitEdit() 241 | $DisplayHash.Listview.Items.Refresh() 242 | }) 243 | 244 | #Check if there is a pending update 245 | 246 | $rebootRequired = (.\PsExec.exe -accepteula -nobanner -s "\\$($Computer.Computer)" cmd.exe /c 'echo . | powershell.exe -ExecutionPolicy Bypass -Command "&{return (New-Object -ComObject "Microsoft.Update.SystemInfo").RebootRequired}"') -eq $true 247 | 248 | if ($LASTEXITCODE -ne 0) { 249 | throw "PsExec failed with error code $LASTEXITCODE" 250 | } 251 | 252 | #Update status 253 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 254 | $DisplayHash.Listview.Items.EditItem($Computer) 255 | $Computer.RebootRequired = [bool]$rebootRequired 256 | $DisplayHash.Listview.Items.CommitEdit() 257 | $DisplayHash.Listview.Items.Refresh() 258 | }) 259 | # } 260 | 261 | #Update status 262 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 263 | $DisplayHash.Listview.Items.EditItem($Computer) 264 | $Computer.Status = 'Finished checking for updates.' 265 | $DisplayHash.Listview.Items.CommitEdit() 266 | $DisplayHash.Listview.Items.Refresh() 267 | }) 268 | } 269 | catch { 270 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 271 | $DisplayHash.Listview.Items.EditItem($Computer) 272 | $Computer.Status = "Error occured: $($_.Exception.Message)" 273 | $DisplayHash.Listview.Items.CommitEdit() 274 | $DisplayHash.Listview.Items.Refresh() 275 | }) 276 | 277 | #Cancel any remaining actions 278 | exit 279 | } 280 | } 281 | 282 | #Format errors for Out-GridView 283 | $GetErrors = { 284 | foreach ($err in $error) { 285 | Switch ($err) { 286 | {$err -is [System.Management.Automation.ErrorRecord]} { 287 | $hash = @{ 288 | Category = $err.categoryinfo.Category 289 | Activity = $err.categoryinfo.Activity 290 | Reason = $err.categoryinfo.Reason 291 | Type = $err.GetType().ToString() 292 | Exception = ($err.exception -split ': ')[1] 293 | QualifiedError = $err.FullyQualifiedErrorId 294 | CharacterNumber = $err.InvocationInfo.OffsetInLine 295 | LineNumber = $err.InvocationInfo.ScriptLineNumber 296 | Line = $err.InvocationInfo.Line 297 | TargetObject = $err.TargetObject 298 | } 299 | } 300 | Default { 301 | $hash = @{ 302 | Category = $err.errorrecord.categoryinfo.category 303 | Activity = $err.errorrecord.categoryinfo.Activity 304 | Reason = $err.errorrecord.categoryinfo.Reason 305 | Type = $err.GetType().ToString() 306 | Exception = ($err.errorrecord.exception -split ': ')[1] 307 | QualifiedError = $err.errorrecord.FullyQualifiedErrorId 308 | CharacterNumber = $err.errorrecord.InvocationInfo.OffsetInLine 309 | LineNumber = $err.errorrecord.InvocationInfo.ScriptLineNumber 310 | Line = $err.errorrecord.InvocationInfo.Line 311 | TargetObject = $err.errorrecord.TargetObject 312 | } 313 | } 314 | } 315 | $object = New-Object PSObject -Property $hash 316 | $object.PSTypeNames.Insert(0, 'ErrorInformation') 317 | $object 318 | } 319 | } 320 | 321 | #Install downloaded updates 322 | $InstallUpdates = { 323 | Param ($Computer) 324 | Try { 325 | #Set path for psexec, scripts 326 | Set-Location $path 327 | 328 | #Update status 329 | $installCount = ($UpdatesHash[$Computer.Computer] | Where-Object {$_.IsDownloaded -eq $true -and $_.InstallationBehavior.CanRequestUserInput -eq $false} | Measure-Object).Count 330 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 331 | $DisplayHash.Listview.Items.EditItem($Computer) 332 | $Computer.Status = "Installing $installCount Updates, this may take some time." 333 | $Computer.InstallErrors = 0 334 | $DisplayHash.Listview.Items.CommitEdit() 335 | $DisplayHash.Listview.Items.Refresh() 336 | }) 337 | 338 | #Copy script to remote Computer and execute 339 | if ( ! ( Test-Path -Path "\\$($Computer.Computer)\C$\Admin\Scripts") ) { 340 | New-Item -Path "\\$($Computer.Computer)\C$\Admin\Scripts" -ItemType Directory 341 | } 342 | Copy-Item .\Scripts\UpdateInstaller.ps1 "\\$($Computer.Computer)\C$\Admin\Scripts" -Force 343 | [int]$installErrors = .\PsExec.exe -accepteula -nobanner -s "\\$($Computer.Computer)" cmd.exe /c 'echo . | powershell.exe -ExecutionPolicy Bypass -file C:\Admin\Scripts\UpdateInstaller.ps1' 344 | Remove-Item "\\$($Computer.Computer)\C$\Admin\Scripts\UpdateInstaller.ps1" 345 | if ($LASTEXITCODE -ne 0) { 346 | throw "PsExec failed with error code $LASTEXITCODE" 347 | } 348 | 349 | #Update status 350 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 351 | $DisplayHash.Listview.Items.EditItem($Computer) 352 | $Computer.Status = 'Checking if a reboot is required.' 353 | $Computer.InstallErrors = $installErrors 354 | $DisplayHash.Listview.Items.CommitEdit() 355 | $DisplayHash.Listview.Items.Refresh() 356 | }) 357 | 358 | #Check if any updates require reboot 359 | $rebootRequired = (.\PsExec.exe -accepteula -nobanner -s "\\$($Computer.Computer)" cmd.exe /c 'echo . | powershell.exe -ExecutionPolicy Bypass -Command "&{return (New-Object -ComObject "Microsoft.Update.SystemInfo").RebootRequired}"') -eq $true 360 | if ($LASTEXITCODE -ne 0) { 361 | throw "PsExec failed with error code $LASTEXITCODE" 362 | } 363 | 364 | #Update status 365 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 366 | $DisplayHash.Listview.Items.EditItem($Computer) 367 | $Computer.Status = 'Install complete.' 368 | $Computer.RebootRequired = [bool]$rebootRequired 369 | $DisplayHash.Listview.Items.CommitEdit() 370 | $DisplayHash.Listview.Items.Refresh() 371 | }) 372 | } 373 | catch { 374 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 375 | $DisplayHash.Listview.Items.EditItem($Computer) 376 | $Computer.Status = "Error occured: $($_.Exception.Message)" 377 | $DisplayHash.Listview.Items.CommitEdit() 378 | $DisplayHash.Listview.Items.Refresh() 379 | }) 380 | 381 | #Cancel any remaining actions 382 | exit 383 | } 384 | } 385 | 386 | #Remove Computer(s) from list 387 | $RemoveEntry = { 388 | Param ($Computers) 389 | 390 | #Remove Computers from list 391 | foreach ($Computer in $Computers) { 392 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 393 | $DisplayHash.Listview.Items.EditItem($Computer) 394 | $DisplayHash.clientObservable.Remove($Computer) 395 | $DisplayHash.Listview.Items.CommitEdit() 396 | $DisplayHash.Listview.Items.Refresh() 397 | }) 398 | } 399 | 400 | $CleanUp = { 401 | Param($Computers) 402 | foreach ($Computer in $Computers) { 403 | $UpdatesHash.Remove($Computer.Computer) 404 | $Computer.Runspace.Dispose() 405 | } 406 | } 407 | 408 | $NewRunspace = [runspacefactory]::CreateRunspace() 409 | $NewRunspace.ApartmentState = "STA" 410 | $NewRunspace.ThreadOptions = "ReuseThread" 411 | $NewRunspace.Open() 412 | $NewRunspace.SessionStateProxy.SetVariable("DisplayHash", $DisplayHash) 413 | $NewRunspace.SessionStateProxy.SetVariable("UpdatesHash", $UpdatesHash) 414 | 415 | $PowerShell = [powershell]::Create().AddScript($CleanUp).AddArgument($Computers) 416 | $PowerShell.Runspace = $NewRunspace 417 | 418 | #Save handle so we can later end the runspace 419 | $Temp = New-Object PSObject -Property @{ 420 | PowerShell = $PowerShell 421 | Runspace = $PowerShell.BeginInvoke() 422 | } 423 | 424 | $Jobs.Add($Temp) | Out-Null 425 | } 426 | 427 | #Remove Computer that cannot be pinged 428 | $RemoveOfflineComputer = { 429 | Param ($Computer, $RemoveEntry) 430 | try { 431 | #Update status 432 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 433 | $DisplayHash.Listview.Items.EditItem($Computer) 434 | $Computer.Status = 'Testing Connectivity.' 435 | $DisplayHash.Listview.Items.CommitEdit() 436 | $DisplayHash.Listview.Items.Refresh() 437 | }) 438 | #Verify connectivity 439 | if (Test-Connection -Count 1 -ComputerName $Computer.Computer -Quiet) { 440 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 441 | $DisplayHash.Listview.Items.EditItem($Computer) 442 | $Computer.Status = 'Online.' 443 | $DisplayHash.Listview.Items.CommitEdit() 444 | $DisplayHash.Listview.Items.Refresh() 445 | }) 446 | } 447 | else { 448 | #Remove unreachable Computers 449 | $UpdatesHash.Remove($Computer.Computer) 450 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 451 | $DisplayHash.Listview.Items.EditItem($Computer) 452 | $DisplayHash.clientObservable.Remove($Computer) 453 | $DisplayHash.Listview.Items.CommitEdit() 454 | $DisplayHash.Listview.Items.Refresh() 455 | }) 456 | } 457 | } 458 | catch { 459 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 460 | $DisplayHash.Listview.Items.EditItem($Computer) 461 | $Computer.Status = "Error occured: $($_.Exception.Message)" 462 | $DisplayHash.Listview.Items.CommitEdit() 463 | $DisplayHash.Listview.Items.Refresh() 464 | }) 465 | 466 | #Cancel any remaining actions 467 | exit 468 | } 469 | } 470 | 471 | #Report status to WSUS server 472 | $ReportStatus = { 473 | Param ($Computer) 474 | try { 475 | #Set path for psexec, scripts 476 | Set-Location $Path 477 | 478 | #Update status 479 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 480 | $DisplayHash.Listview.Items.EditItem($Computer) 481 | $Computer.Status = 'Reporting status to WSUS server.' 482 | $DisplayHash.Listview.Items.CommitEdit() 483 | $DisplayHash.Listview.Items.Refresh() 484 | }) 485 | 486 | $ExecStatus = .\PsExec.exe -accepteula -nobanner -s "\\$($Computer.Computer)" cmd.exe /c 'echo . | wuauclt /reportnow' 487 | if ($LASTEXITCODE -ne 0) { 488 | throw "PsExec failed with error code $LASTEXITCODE" 489 | } 490 | 491 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 492 | $DisplayHash.Listview.Items.EditItem($Computer) 493 | $Computer.Status = 'Finished updating status.' 494 | $DisplayHash.Listview.Items.CommitEdit() 495 | $DisplayHash.Listview.Items.Refresh() 496 | }) 497 | } 498 | catch { 499 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 500 | $DisplayHash.Listview.Items.EditItem($Computer) 501 | $Computer.Status = "Error occured: $($_.Exception.Message)" 502 | $DisplayHash.Listview.Items.CommitEdit() 503 | $DisplayHash.Listview.Items.Refresh() 504 | }) 505 | 506 | #Cancel any remaining actions 507 | exit 508 | } 509 | } 510 | 511 | #Reboot remote Computer 512 | $RestartComputer = { 513 | Param ($Computer, $afterInstall) 514 | try { 515 | #Avoid auto reboot if not enabled and required 516 | if ($afterInstall -and (-not $Computer.RebootRequired -or -not $DisplayHash.AutoRebootCheckBox.IsChecked)) {return} 517 | #Update status 518 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 519 | $DisplayHash.Listview.Items.EditItem($Computer) 520 | $Computer.Status = 'Restarting... Waiting for Computer to shutdown.' 521 | $DisplayHash.Listview.Items.CommitEdit() 522 | $DisplayHash.Listview.Items.Refresh() 523 | }) 524 | 525 | #Restart and wait until remote COM can be connected 526 | if (Get-Command -Name manage-bde.exe) { 527 | manage-bde.exe -protectors c: -disable -rc 1 -cn $Computer.Computer 528 | } else { 529 | Invoke-Command -ComputerName $Computer.Computer -ScriptBlock { Suspend-BitLocker -MountPoint C: -RebootCount 1 } 530 | } 531 | Restart-Computer $Computer.Computer -Force 532 | while (Test-Connection -Count 1 -ComputerName $Computer.Computer -Quiet) { Start-Sleep -Milliseconds 500 } #Wait for Computer to go offline 533 | 534 | #Update status 535 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 536 | $DisplayHash.Listview.Items.EditItem($Computer) 537 | $Computer.Status = 'Restarting... Waiting for Computer to come online.' 538 | $DisplayHash.Listview.Items.CommitEdit() 539 | $DisplayHash.Listview.Items.Refresh() 540 | }) 541 | 542 | while ($true) { 543 | #Wait for Computer to come online 544 | Start-Sleep -Seconds 5 545 | try { 546 | [activator]::CreateInstance([type]::GetTypeFromProgID('Microsoft.Update.Session', $Computer.Computer)) 547 | Break 548 | } 549 | catch { 550 | Start-Sleep -Seconds 5 551 | } 552 | } 553 | } 554 | catch { 555 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 556 | $DisplayHash.Listview.Items.EditItem($Computer) 557 | $Computer.Status = 'Error occured: $($_.Exception.Message)' 558 | $DisplayHash.Listview.Items.CommitEdit() 559 | $DisplayHash.Listview.Items.Refresh() 560 | }) 561 | 562 | #Cancel any remaining actions 563 | exit 564 | } 565 | } 566 | 567 | #Start, stop, or restart Windows Update Service 568 | $WUServiceAction = { 569 | Param($Computer, $Action) 570 | try { 571 | #Start Windows Update Service 572 | if ($Action -eq 'Start') { 573 | #Update status 574 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 575 | $DisplayHash.Listview.Items.EditItem($Computer) 576 | $Computer.Status = 'Starting Windows Update Service' 577 | $DisplayHash.Listview.Items.CommitEdit() 578 | $DisplayHash.Listview.Items.Refresh() 579 | }) 580 | 581 | #Start service 582 | Get-Service -ComputerName $($Computer.Computer) -Name 'wuauserv' -ErrorAction Stop | Start-Service -ErrorAction Stop 583 | 584 | #Update status 585 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 586 | $DisplayHash.Listview.Items.EditItem($Computer) 587 | $Computer.Status = 'Windows Update Service Started' 588 | $DisplayHash.Listview.Items.CommitEdit() 589 | $DisplayHash.Listview.Items.Refresh() 590 | }) 591 | } 592 | 593 | #Stop Windows Update Service 594 | elseif ($Action -eq 'Stop') { 595 | #Update status 596 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 597 | $DisplayHash.Listview.Items.EditItem($Computer) 598 | $Computer.Status = 'Stopping Windows Update Service' 599 | $DisplayHash.Listview.Items.CommitEdit() 600 | $DisplayHash.Listview.Items.Refresh() 601 | }) 602 | 603 | #Stop service 604 | Get-Service -ComputerName $Computer.Computer -Name wuauserv -ErrorAction Stop | Stop-Service -ErrorAction Stop 605 | 606 | #Update status 607 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 608 | $DisplayHash.Listview.Items.EditItem($Computer) 609 | $Computer.Status = 'Windows Update Service Stopped' 610 | $DisplayHash.Listview.Items.CommitEdit() 611 | $DisplayHash.Listview.Items.Refresh() 612 | }) 613 | } 614 | 615 | #Restart Windows Update Service 616 | elseif ($Action -eq 'Restart') { 617 | #Update status 618 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 619 | $DisplayHash.Listview.Items.EditItem($Computer) 620 | $Computer.Status = 'Restarting Windows Update Service' 621 | $DisplayHash.Listview.Items.CommitEdit() 622 | $DisplayHash.Listview.Items.Refresh() 623 | }) 624 | 625 | #Restart service 626 | Get-Service -ComputerName $Computer.Computer -Name wuauserv -ErrorAction Stop | Restart-Service -ErrorAction Stop 627 | 628 | #Update status 629 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 630 | $DisplayHash.Listview.Items.EditItem($Computer) 631 | $Computer.Status = 'Windows Update Service Restarted' 632 | $DisplayHash.Listview.Items.CommitEdit() 633 | $DisplayHash.Listview.Items.Refresh() 634 | }) 635 | } 636 | 637 | #Invalid action 638 | else { 639 | Write-Error 'Invalid action specified.' 640 | } 641 | } 642 | catch { 643 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 644 | $DisplayHash.Listview.Items.EditItem($Computer) 645 | $Computer.Status = "Error occured: $($_.Exception.Message)" 646 | $DisplayHash.Listview.Items.CommitEdit() 647 | $DisplayHash.Listview.Items.Refresh() 648 | }) 649 | 650 | #Cancel any remaining actions 651 | exit 652 | } 653 | } 654 | #endregion ScriptBlocks 655 | 656 | #region Background runspace to clean up Jobs 657 | $JobCleanup.Flag = $true 658 | $NewRunspace = [runspacefactory]::CreateRunspace() 659 | $NewRunspace.ApartmentState = 'STA' 660 | $NewRunspace.ThreadOptions = 'ReuseThread' 661 | $NewRunspace.Open() 662 | $NewRunspace.SessionStateProxy.SetVariable('JobCleanup', $JobCleanup) 663 | $NewRunspace.SessionStateProxy.SetVariable('Jobs', $Jobs) 664 | $JobCleanup.PowerShell = [PowerShell]::Create().AddScript( { 665 | #Routine to handle completed runspaces 666 | do { 667 | foreach ($runspace in $Jobs) { 668 | if ($runspace.Runspace.isCompleted) { 669 | $runspace.powershell.EndInvoke($runspace.Runspace) | Out-Null 670 | $runspace.powershell.dispose() 671 | $runspace.Runspace = $null 672 | $runspace.powershell = $null 673 | $Jobs.remove($runspace) 674 | } 675 | } 676 | Start-Sleep -Seconds 1 677 | } while ($JobCleanup.Flag) 678 | }) 679 | $JobCleanup.PowerShell.Runspace = $NewRunspace 680 | $JobCleanup.Thread = $JobCleanup.PowerShell.BeginInvoke() 681 | #endregion 682 | 683 | #region Connect to controls 684 | $DisplayHash.ActionMenu = $DisplayHash.Window.FindName('ActionMenu') 685 | $DisplayHash.AddADContext = $DisplayHash.Window.FindName('AddADContext') 686 | $DisplayHash.AddADMenu = $DisplayHash.Window.FindName('AddADMenu') 687 | $DisplayHash.AddComputerContext = $DisplayHash.Window.FindName('AddComputerContext') 688 | $DisplayHash.AddComputerMenu = $DisplayHash.Window.FindName('AddComputerMenu') 689 | $DisplayHash.AddFileContext = $DisplayHash.Window.FindName('AddFileContext') 690 | $DisplayHash.EnableRebootCheckBox = $DisplayHash.Window.FindName('EnableRebootCheckBox') 691 | $DisplayHash.AutoRebootCheckBox = $DisplayHash.Window.FindName('AutoRebootCheckBox') 692 | $DisplayHash.BrowseFileMenu = $DisplayHash.Window.FindName('BrowseFileMenu') 693 | $DisplayHash.CheckUpdatesContext = $DisplayHash.Window.FindName('CheckUpdatesContext') 694 | $DisplayHash.ClearComputerListMenu = $DisplayHash.Window.FindName('ClearComputerListMenu') 695 | $DisplayHash.DownloadUpdatesContext = $DisplayHash.Window.FindName('DownloadUpdatesContext') 696 | $DisplayHash.ExitMenu = $DisplayHash.Window.FindName('ExitMenu') 697 | $DisplayHash.ExportListMenu = $DisplayHash.Window.FindName('ExportListMenu') 698 | $DisplayHash.GridView = $DisplayHash.Window.FindName('GridView') 699 | $DisplayHash.InstallUpdatesContext = $DisplayHash.Window.FindName('InstallUpdatesContext') 700 | $DisplayHash.Listview = $DisplayHash.Window.FindName('Listview') 701 | $DisplayHash.ListviewContextMenu = $DisplayHash.Window.FindName('ListViewContextMenu') 702 | $DisplayHash.OfflineHostsMenu = $DisplayHash.Window.FindName('OfflineHostsMenu') 703 | $DisplayHash.RemoteDesktopContext = $DisplayHash.Window.FindName('RemoteDesktopContext') 704 | $DisplayHash.RemoveComputerContext = $DisplayHash.Window.FindName('RemoveComputerContext') 705 | $DisplayHash.ReportStatusContext = $DisplayHash.Window.FindName('ReportStatusContext') 706 | $DisplayHash.RestartContext = $DisplayHash.Window.FindName('RestartContext') 707 | $DisplayHash.SelectAllMenu = $DisplayHash.Window.FindName('SelectAllMenu') 708 | $DisplayHash.ShowInstalledContext = $DisplayHash.Window.FindName('ShowInstalledContext') 709 | $DisplayHash.ShowUpdatesContext = $DisplayHash.Window.FindName('ShowUpdatesContext') 710 | $DisplayHash.StatusTextBox = $DisplayHash.Window.FindName('StatusTextBox') 711 | $DisplayHash.UpdateHistoryMenu = $DisplayHash.Window.FindName('UpdateHistoryMenu') 712 | $DisplayHash.ViewErrorMenu = $DisplayHash.Window.FindName('ViewErrorMenu') 713 | $DisplayHash.ViewUpdateLogContext = $DisplayHash.Window.FindName('ViewUpdateLogContext') 714 | $DisplayHash.WindowsUpdateServiceMenu = $DisplayHash.Window.FindName('WindowsUpdateServiceMenu') 715 | $DisplayHash.WURestartServiceMenu = $DisplayHash.Window.FindName('WURestartServiceMenu') 716 | $DisplayHash.WUStartServiceMenu = $DisplayHash.Window.FindName('WUStartServiceMenu') 717 | $DisplayHash.WUStopServiceMenu = $DisplayHash.Window.FindName('WUStopServiceMenu') 718 | #endregion Connect to controls 719 | 720 | #region Event ScriptBlocks 721 | $eventWindowInit = { #Runs before opening window 722 | $Script:SortHash = @{} 723 | 724 | #Sort event handler 725 | [System.Windows.RoutedEventHandler]$Global:ColumnSortHandler = { 726 | if ($_.OriginalSource -is [System.Windows.Controls.GridViewColumnHeader]) { 727 | Write-Verbose ('{0}' -f $_.Originalsource.getType().FullName) 728 | if ($_.OriginalSource -AND $_.OriginalSource.Role -ne 'Padding') { 729 | $Column = $_.Originalsource.Column.DisplayMemberBinding.Path.Path 730 | Write-Debug ('Sort: {0}' -f $Column) 731 | if ($SortHash[$Column] -eq 'Ascending') { 732 | $SortHash[$Column] = 'Descending' 733 | } 734 | else { 735 | $SortHash[$Column] = 'Ascending' 736 | } 737 | $lastColumnsort = $Column 738 | $DisplayHash.Listview.Items.SortDescriptions.clear() 739 | Write-Verbose ('Sorting {0} by {1}' -f $Column, $SortHash[$Column]) 740 | $DisplayHash.Listview.Items.SortDescriptions.Add((New-Object System.ComponentModel.SortDescription $Column, $SortHash[$Column])) 741 | $DisplayHash.Listview.Items.Refresh() 742 | } 743 | } 744 | } 745 | $DisplayHash.Listview.AddHandler([System.Windows.Controls.GridViewColumnHeader]::ClickEvent, $ColumnSortHandler) 746 | 747 | #Create and bind the observable collection to the GridView 748 | $DisplayHash.clientObservable = New-Object System.Collections.ObjectModel.ObservableCollection[object] 749 | $DisplayHash.ListView.ItemsSource = $DisplayHash.clientObservable 750 | } 751 | $eventWindowClose = { #Runs when WUU closes 752 | #Halt job processing 753 | $JobCleanup.Flag = $false 754 | 755 | #Stop all runspaces 756 | $JobCleanup.PowerShell.Dispose() 757 | 758 | #Cleanup 759 | [gc]::Collect() 760 | [gc]::WaitForPendingFinalizers() 761 | } 762 | $eventActionMenu = { #Enable/disable action menu items 763 | $DisplayHash.ClearComputerListMenu.IsEnabled = ($DisplayHash.Listview.Items.Count -gt 0) 764 | $DisplayHash.OfflineHostsMenu.IsEnabled = ($DisplayHash.Listview.Items.Count -gt 0) 765 | $DisplayHash.ViewErrorMenu.IsEnabled = ($Error.Count -gt 0) 766 | } 767 | $eventAddAD = { #Add Computers from Active Directory 768 | #region OUPicker 769 | $OUPickerHash = [hashtable]::Synchronized(@{}) 770 | try { 771 | [xml]$xaml = Get-Content .\OUPicker.xaml 772 | $reader = (New-Object System.Xml.XmlNodeReader $xaml) 773 | $OUPickerHash.Window = [Windows.Markup.XamlReader]::Load($reader) 774 | } 775 | catch { 776 | Write-Warning 'Unable to load XAML data for OUPicker!' 777 | return 778 | } 779 | 780 | $OUPickerHash.OKButton = $OUPickerHash.Window.FindName('OKButton') 781 | $OUPickerHash.CancelButton = $OUPickerHash.Window.FindName('CancelButton') 782 | $OUPickerHash.OUTree = $OUPickerHash.Window.FindName('OUTree') 783 | 784 | $OUPickerHash.OKButton.Add_Click( {$OUPickerHash.SelectedOU = $OUPickerHash.OUTree.SelectedItem.Tag; $OUPickerHash.Window.Close()}) 785 | $OUPickerHash.CancelButton.Add_Click( {$OUPickerHash.Window.Close()}) 786 | 787 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher 788 | $Searcher.Filter = "(objectCategory=organizationalUnit)" 789 | $Searcher.SearchScope = "OneLevel" 790 | 791 | $rootItem = New-Object System.Windows.Controls.TreeViewItem 792 | $rootItem.Header = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name 793 | $rootItem.Tag = $Searcher.SearchRoot.distinguishedName 794 | 795 | function Populate-Children($node) { 796 | $Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($node.Tag)") 797 | $Searcher.FindAll() | % { 798 | $childItem = New-Object System.Windows.Controls.TreeViewItem 799 | $childItem.Header = $_.Properties.name[0] 800 | $childItem.Tag = $_.Properties.distinguishedname 801 | Populate-Children($childItem) 802 | $node.AddChild($childItem) 803 | } 804 | } 805 | Populate-Children($rootItem) 806 | $OUPickerHash.OUTree.AddChild($rootItem) 807 | 808 | $OUPickerHash.Window.ShowDialog() | Out-Null 809 | #endregion 810 | 811 | #Verify user didn't hit 'cancel' before processing 812 | if ($OUPickerHash.SelectedOU) { 813 | #Update status 814 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 815 | $DisplayHash.StatusTextBox.Foreground = 'Black' 816 | $DisplayHash.StatusTextBox.Text = 'Querying Active Directory for Computers...' 817 | }) 818 | 819 | #Search LDAP path 820 | $Searcher = [adsisearcher]'' 821 | $Searcher.SearchRoot = [adsi]"LDAP://$($OUPickerHash.SelectedOU)" 822 | $Searcher.Filter = ('(&(objectCategory=Computer)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))') 823 | $Searcher.PropertiesToLoad.Add('name') | Out-Null 824 | $Results = $Searcher.FindAll() 825 | if ($Results) { 826 | #Add Computers found 827 | &$AddEntry ($Results | % {$_.Properties.name}) 828 | 829 | #Update status 830 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 831 | $DisplayHash.StatusTextBox.Text = "Successfully Imported $($Results.Count) Computers from Active Directory." 832 | }) 833 | } 834 | else { 835 | #Update status 836 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 837 | $DisplayHash.StatusTextBox.Foreground = 'Red' 838 | $DisplayHash.StatusTextBox.Text = 'No Computers found, verify LDAP path...' 839 | }) 840 | } 841 | } 842 | } 843 | $eventAddComputer = { #Add Computers by typing them in manually 844 | #Open prompt 845 | $Computer = [Microsoft.VisualBasic.Interaction]::InputBox('Enter a Computer name or names. Separate Computers with a comma (,) or semi-colon (;).', 'Add Computer(s)') 846 | 847 | #Verify Computers were input 848 | if (-Not [System.String]::IsNullOrEmpty($Computer)) { 849 | [string[]]$Computername = $Computer -split ',|;' #Parse 850 | } 851 | if ($Computername) {&$AddEntry $Computername} #Add Computers 852 | } 853 | $eventAddFile = { #Add Computers from a file 854 | #Open file dialog 855 | $dlg = new-object microsoft.win32.OpenFileDialog 856 | $dlg.DefaultExt = '*.txt' 857 | $dlg.Filter = 'Text Files |*.txt;*.csv' 858 | $dlg.Multiselect = $true 859 | $dlg.InitialDirectory = $pwd 860 | [void]$dlg.showdialog() 861 | $Files = $dlg.FileNames 862 | 863 | foreach ($File in $Files) 864 | { 865 | #Verify file was selected 866 | if (-Not ([system.string]::IsNullOrEmpty($File))) { 867 | $entries = (Get-Content $File | Where {$_ -ne ''}) #Parse 868 | &$AddEntry $entries #Add Computers 869 | 870 | #Update Status 871 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 872 | $DisplayHash.StatusTextBox.Foreground = 'Black' 873 | $DisplayHash.StatusTextBox.Text = "Successfully Added $($entries.Count) Computers from $File." 874 | }) 875 | } 876 | } 877 | } 878 | $eventGetUpdates = { 879 | $DisplayHash.Listview.SelectedItems | % { 880 | $Temp = "" | Select-Object PowerShell, Runspace 881 | $Temp.PowerShell = [powershell]::Create().AddScript($GetUpdates).AddArgument($_) 882 | $Temp.PowerShell.Runspace = $_.Runspace 883 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 884 | $Jobs.Add($Temp) | Out-Null 885 | } 886 | } 887 | $eventDownloadUpdates = { 888 | $DisplayHash.Listview.SelectedItems | % { 889 | #Don't bother downloading if nothing available. 890 | if ($_.Available -eq $_.Downloaded) { 891 | #Update status 892 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 893 | $DisplayHash.Listview.Items.EditItem($_) 894 | $_.Status = 'There are no updates available to download.' 895 | $DisplayHash.Listview.Items.CommitEdit() 896 | $DisplayHash.Listview.Items.Refresh() 897 | }) 898 | return 899 | } 900 | 901 | $Temp = "" | Select-Object PowerShell, Runspace 902 | $Temp.PowerShell = [powershell]::Create().AddScript($DownloadUpdates).AddArgument($_) 903 | $Temp.PowerShell.Runspace = $_.Runspace 904 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 905 | $Jobs.Add($Temp) | Out-Null 906 | } 907 | } 908 | $eventInstallUpdates = { 909 | $DisplayHash.Listview.SelectedItems | % { 910 | #Check if there are any updates that are downloaded and don't require user input 911 | if (-not ($UpdatesHash[$_.Computer] | Where-Object {$_.IsDownloaded -and $_.InstallationBehavior.CanRequestUserInput -eq $false})) { 912 | #Update status 913 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 914 | $DisplayHash.Listview.Items.EditItem($_) 915 | $_.Status = 'There are no updates available that can be installed remotely.' 916 | $DisplayHash.Listview.Items.CommitEdit() 917 | $DisplayHash.Listview.Items.Refresh() 918 | }) 919 | 920 | #No need to continue if there are no updates to install. 921 | return 922 | } 923 | 924 | $Temp = "" | Select-Object PowerShell, Runspace 925 | $Temp.PowerShell = [powershell]::Create().AddScript($InstallUpdates).AddArgument($_) 926 | # $Temp.PowerShell.AddScript($RestartComputer).AddArgument($_).AddArgument($true) 927 | # $Temp.PowerShell.AddScript($GetUpdates).AddArgument($_) 928 | $Temp.PowerShell.Runspace = $_.Runspace 929 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 930 | $Jobs.Add($Temp) | Out-Null 931 | } 932 | } 933 | $eventRemoveOfflineComputer = { 934 | $DisplayHash.Listview.Items | % { 935 | $Temp = "" | Select-Object PowerShell, Runspace 936 | $Temp.PowerShell = [powershell]::Create().AddScript($RemoveOfflineComputer).AddArgument($_).AddArgument($RemoveEntry) 937 | $Temp.PowerShell.Runspace = $_.Runspace 938 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 939 | $Jobs.Add($Temp) | Out-Null 940 | } 941 | } 942 | $eventRestartComputer = { 943 | $DisplayHash.Listview.SelectedItems | % { 944 | $Temp = "" | Select-Object PowerShell, Runspace 945 | $Temp.PowerShell = [powershell]::Create().AddScript($RestartComputer).AddArgument($_).AddArgument($false) 946 | $Temp.PowerShell.AddScript($GetUpdates).AddArgument($_) 947 | $Temp.PowerShell.Runspace = $_.Runspace 948 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 949 | $Jobs.Add($Temp) | Out-Null 950 | } 951 | } 952 | $eventReportStatus = { 953 | $DisplayHash.Listview.SelectedItems | % { 954 | $Temp = "" | Select-Object PowerShell, Runspace 955 | $Temp.PowerShell = [powershell]::Create().AddScript($ReportStatus).AddArgument($_) 956 | $Temp.PowerShell.Runspace = $_.Runspace 957 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 958 | $Jobs.Add($Temp) | Out-Null 959 | } 960 | } 961 | $eventKeyDown = { 962 | if ([System.Windows.Input.Keyboard]::IsKeyDown('RightCtrl') -OR [System.Windows.Input.Keyboard]::IsKeyDown('LeftCtrl')) { 963 | Switch ($_.Key) { 964 | 'A' {$DisplayHash.Listview.SelectAll()} 965 | 'O' {&$eventAddFile} 966 | 'S' {&$eventSaveComputerList} 967 | Default {$Null} 968 | } 969 | } 970 | elseif ($_.Key -eq 'Delete') {&$removeEntry @($DisplayHash.Listview.SelectedItems)} 971 | } 972 | $eventRightClick = { 973 | #Set default values 974 | $DisplayHash.RemoveComputerContext.IsEnabled = $false 975 | $DisplayHash.RemoteDesktopContext.IsEnabled = $false 976 | $DisplayHash.CheckUpdatesContext.IsEnabled = $false 977 | $DisplayHash.DownloadUpdatesContext.IsEnabled = $false 978 | $DisplayHash.InstallUpdatesContext.IsEnabled = $false 979 | $DisplayHash.RestartContext.IsEnabled = $false 980 | $DisplayHash.ReportStatusContext.IsEnabled = $false 981 | $DisplayHash.ShowInstalledContext.IsEnabled = $false 982 | $DisplayHash.ShowUpdatesContext.IsEnabled = $false 983 | $DisplayHash.UpdateHistoryMenu.IsEnabled = $false 984 | $DisplayHash.ViewUpdateLogContext.IsEnabled = $false 985 | $DisplayHash.WindowsUpdateServiceMenu.IsEnabled = $false 986 | if ($DisplayHash.Listview.SelectedItems.count -eq 1) { 987 | $DisplayHash.RemoteDesktopContext.IsEnabled = $true 988 | $DisplayHash.ShowInstalledContext.IsEnabled = $true 989 | $DisplayHash.ShowUpdatesContext.IsEnabled = $true 990 | $DisplayHash.UpdateHistoryMenu.IsEnabled = $true 991 | $DisplayHash.ViewUpdateLogContext.IsEnabled = $true 992 | } 993 | if ($DisplayHash.Listview.SelectedItems.count -ge 1) { 994 | $DisplayHash.RemoveComputerContext.IsEnabled = $true 995 | $DisplayHash.CheckUpdatesContext.IsEnabled = $true 996 | $DisplayHash.DownloadUpdatesContext.IsEnabled = $true 997 | $DisplayHash.ReportStatusContext.IsEnabled = $true 998 | $DisplayHash.WindowsUpdateServiceMenu.IsEnabled = $true 999 | } 1000 | 1001 | if ($DisplayHash.Listview.SelectedItems.count -ge 1 -and 1002 | $DisplayHash.EnableRebootCheckBox.IsChecked -eq $true) { 1003 | $DisplayHash.InstallUpdatesContext.IsEnabled = $true 1004 | $DisplayHash.RestartContext.IsEnabled = $true 1005 | } 1006 | } 1007 | $eventSaveComputerList = { 1008 | if ($DisplayHash.Listview.Items.count -gt 0) { 1009 | #Save dialog 1010 | $dlg = new-object Microsoft.Win32.SaveFileDialog 1011 | $dlg.FileName = 'Computer List' 1012 | $dlg.DefaultExt = '*.txt' 1013 | $dlg.Filter = 'Text files (*.txt)|*.txt|CSV files (*.csv)|*.csv' 1014 | $dlg.InitialDirectory = $pwd 1015 | [void]$dlg.showdialog() 1016 | $filePath = $dlg.FileName 1017 | 1018 | #Verify file was selected 1019 | if (-Not ([system.string]::IsNullOrEmpty($filepath))) { 1020 | #Save file 1021 | $DisplayHash.Listview.Items | Select -Expand Computer | Out-File $filePath -Force 1022 | 1023 | #Update status 1024 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 1025 | $DisplayHash.StatusTextBox.Foreground = 'Black' 1026 | $DisplayHash.StatusTextBox.Text = "Computer List saved to $filePath" 1027 | }) 1028 | } 1029 | } 1030 | else { 1031 | #No items selected 1032 | #Update status 1033 | $DisplayHash.StatusTextBox.Dispatcher.Invoke('Background', [action] { 1034 | $DisplayHash.StatusTextBox.Foreground = 'Red' 1035 | $DisplayHash.StatusTextBox.Text = 'Computer List not saved, there are no Computers in the list!' 1036 | }) 1037 | } 1038 | } 1039 | $eventShowAvailableUpdates = { 1040 | foreach ($Computer in $DisplayHash.Listview.SelectedItems) { 1041 | $UpdatesHash[$Computer.Computer] | Select Title, Description, IsDownloaded, IsMandatory, IsUninstallable, @{n = 'CanRequestUserInput'; e = {$_.InstallationBehavior.CanRequestUserInput}}, LastDeploymentChangeTime, @{n = 'MaxDownloadSize (MB)'; e = {'{0:N2}' -f ($_.MaxDownloadSize / 1MB)}}, @{n = 'MinDownloadSize (MB)'; e = {'{0:N2}' -f ($_.MinDownloadSize / 1MB)}}, RecommendedCpuSpeed, RecommendedHardDiskSpace, RecommendedMemory, DriverClass, DriverManufacturer, DriverModel, DriverProvider, DriverVerDate | Out-GridView -Title "$($Computer.Computer)'s Available Updates" 1042 | } 1043 | } 1044 | $eventShowInstalledUpdates = { 1045 | foreach ($Computer in $DisplayHash.Listview.SelectedItems) { 1046 | $UpdateSession = [activator]::CreateInstance([type]::GetTypeFromProgID('Microsoft.Update.Session', $Computer.Computer)) 1047 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 1048 | $UpdateSearcher.Search('IsInstalled=1').Updates | Select Title, Description, IsUninstallable, SupportUrl | Out-GridView -Title "$($Computer.Computer)'s Installed Updates" 1049 | } 1050 | } 1051 | $eventShowUpdateHistory = { 1052 | Try { 1053 | $Computer = $DisplayHash.Listview.SelectedItems | Select -First 1 1054 | #Get installed hotfix, create popup 1055 | $UpdateSession = [activator]::CreateInstance([type]::GetTypeFromProgID('Microsoft.Update.Session', $Computer.Computer)) 1056 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 1057 | $updates = $updateSearcher.QueryHistory(1, $updateSearcher.GetTotalHistoryCount()) 1058 | $updates | Select-Object -Property ` 1059 | @{name = "Operation"; expression = {switch ($_.Operation) {1 {"Installation"}; 2 {"Uninstallation"}; 3 {"Other"}}}}, ` 1060 | @{name = "Result"; expression = {switch ($_.ResultCode) {1 {"Success"}; 2 {"Success (reboot required)"}; 4 {"Failure"}}}}, ` 1061 | @{n = 'HResult'; e = {'0x' + [Convert]::ToString($_.HResult, 16)}}, ` 1062 | Date, Title, Description, SupportUrl | Out-GridView -Title "$($Computer.Computer)'s Update History" 1063 | } 1064 | catch { 1065 | $DisplayHash.ListView.Dispatcher.Invoke('Background', [action] { 1066 | $DisplayHash.Listview.Items.EditItem($Computer) 1067 | $Computer.Status = "Error Occured: $($_.exception.Message)" 1068 | $DisplayHash.Listview.Items.CommitEdit() 1069 | $DisplayHash.Listview.Items.Refresh() 1070 | }) 1071 | } 1072 | } 1073 | $eventViewUpdateLog = { 1074 | $DisplayHash.Listview.SelectedItems | % { 1075 | &"\\$($_.Computer)\c$\windows\windowsupdate.log" 1076 | } 1077 | } 1078 | $eventWUServiceAction = { 1079 | Param ($Action) 1080 | $DisplayHash.Listview.SelectedItems | % { 1081 | $Temp = "" | Select-Object PowerShell, Runspace 1082 | $Temp.PowerShell = [powershell]::Create().AddScript($WUServiceAction).AddArgument($_).AddArgument($Action) 1083 | $Temp.PowerShell.Runspace = $_.Runspace 1084 | $Temp.Runspace = $Temp.PowerShell.BeginInvoke() 1085 | $Jobs.Add($Temp) | Out-Null 1086 | } 1087 | } 1088 | #endregion Event ScriptBlocks 1089 | 1090 | #region Event Handlers 1091 | $DisplayHash.ActionMenu.Add_SubmenuOpened($eventActionMenu) #Action Menu 1092 | $DisplayHash.AddADContext.Add_Click($eventAddAD) #Add Computers From AD (Context) 1093 | $DisplayHash.AddADMenu.Add_Click($eventAddAD) #Add Computers From AD (Menu) 1094 | $DisplayHash.AddComputerContext.Add_Click($eventAddComputer) #Add Computers (Context) 1095 | $DisplayHash.AddComputerMenu.Add_Click($eventAddComputer) #Add Computers (Menu) 1096 | $DisplayHash.AddFileContext.Add_Click($eventAddFile) #Add Computers From File (Context) 1097 | $DisplayHash.BrowseFileMenu.Add_Click($eventAddFile) #Add Computers From File (Menu) 1098 | $DisplayHash.CheckUpdatesContext.Add_Click($eventGetUpdates) #Check For Updates (Context) 1099 | $DisplayHash.ClearComputerListMenu.Add_Click($clearComputerList) #Clear Computer List 1100 | $DisplayHash.DownloadUpdatesContext.Add_Click($eventDownloadUpdates) #Download Updates 1101 | $DisplayHash.ExitMenu.Add_Click( {$DisplayHash.Window.Close()}) #Exit 1102 | $DisplayHash.UpdateHistoryMenu.Add_Click($eventShowUpdateHistory) #Get Update History 1103 | $DisplayHash.ExportListMenu.Add_Click($eventSaveComputerList) #Exports Computer To File 1104 | $DisplayHash.InstallUpdatesContext.Add_Click($eventInstallUpdates) #Install Updates 1105 | $DisplayHash.Listview.Add_MouseRightButtonUp($eventRightClick) #On Right Click 1106 | $DisplayHash.OfflineHostsMenu.Add_Click($eventRemoveOfflineComputer) #Remove Offline Computers 1107 | $DisplayHash.RemoteDesktopContext.Add_Click( {mstsc.exe /v $DisplayHash.Listview.SelectedItems.Computer}) #RDP 1108 | $DisplayHash.RemoveComputerContext.Add_Click( {&$removeEntry @($DisplayHash.Listview.SelectedItems)}) #Delete Computers 1109 | $DisplayHash.RestartContext.Add_Click($eventRestartComputer) #Restart Computer 1110 | $DisplayHash.ReportStatusContext.Add_Click($eventReportStatus) #Report to WSUS 1111 | $DisplayHash.SelectAllMenu.Add_Click( {$DisplayHash.Listview.SelectAll()}) #Select All 1112 | $DisplayHash.ShowUpdatesContext.Add_Click($eventShowAvailableUpdates) #Show Available Updates 1113 | $DisplayHash.ShowInstalledContext.Add_Click($eventShowInstalledUpdates) #Show Installed Updates 1114 | $DisplayHash.ViewUpdateLogContext.Add_Click($eventViewUpdateLog) #Show Installed Updates 1115 | $DisplayHash.Window.Add_Closed($eventWindowClose) #On Window Close 1116 | $DisplayHash.Window.Add_SourceInitialized($eventWindowInit) #On Window Open 1117 | $DisplayHash.Window.Add_KeyDown($eventKeyDown) #On key down 1118 | $DisplayHash.WURestartServiceMenu.Add_Click( {&$eventWUServiceAction 'Restart'}) #Restart Windows Update Service 1119 | $DisplayHash.WUStartServiceMenu.Add_Click( {&$eventWUServiceAction 'Start'}) #Start Windows Update Service 1120 | $DisplayHash.WUStopServiceMenu.Add_Click( {&$eventWUServiceAction 'Stop'}) #Stop Windows Update Service 1121 | $DisplayHash.ViewErrorMenu.Add_Click( {&$GetErrors | Out-GridView}) #View Errors 1122 | #endregion 1123 | 1124 | #Start the GUI 1125 | $DisplayHash.Window.ShowDialog() | Out-Null 1126 | -------------------------------------------------------------------------------- /WUU.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | Enable Install / Reboot 54 | Auto Reboot After Updates 55 | 56 | 57 | 58 | 59 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Waiting for Action... 136 | 137 | 138 | --------------------------------------------------------------------------------