├── .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 |
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 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | Waiting for Action...
136 |
137 |
138 |
--------------------------------------------------------------------------------