├── .editorconfig
├── .markdownlint.json
├── .vscode
├── extensions.json
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── PSScriptAnalyzerSettings.psd1
├── PSWinVitals.format.ps1xml
├── PSWinVitals.psd1
├── PSWinVitals.psm1
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig
2 | # http://EditorConfig.org
3 |
4 | # Don't search any further up the directory tree
5 | root = true
6 |
7 | # Baseline
8 | [*]
9 | charset = utf-8
10 | indent_style = space
11 | indent_size = 4
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
15 | # Markdown
16 | [*.md]
17 | trim_trailing_whitespace = false
18 |
19 | # XML
20 | [*.ps1xml]
21 | insert_final_newline = false
22 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "heading-style": { "style": "setext_with_atx" },
4 | "no-trailing-spaces": { "br_spaces": 2 },
5 | "line-length": false,
6 | "no-trailing-punctuation": { "punctuation": ".,;:!" }
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "davidanson.vscode-markdownlint",
4 | "editorconfig.editorconfig",
5 | "ms-vscode.powershell",
6 | "yzhang.markdown-all-in-one"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // Important to disable for EditorConfig to apply correctly
3 | // See: https://github.com/editorconfig/editorconfig-vscode/issues/153
4 | "files.trimTrailingWhitespace": false,
5 |
6 | "devskim.ignoreRulesList": [
7 | // Restricted functions
8 | "DS104456"
9 | ],
10 |
11 | // Markdown table of contents generation
12 | "markdown.extension.toc.levels": "2..2",
13 | "markdown.extension.toc.slugifyMode": "github",
14 |
15 | // PowerShell script analysis
16 | "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1",
17 |
18 | // PowerShell code formatting
19 | "powershell.codeFormatting.autoCorrectAliases": true,
20 | "powershell.codeFormatting.newLineAfterCloseBrace": false,
21 | "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
22 | "powershell.codeFormatting.trimWhitespaceAroundPipe": true,
23 | "powershell.codeFormatting.useCorrectCasing": true,
24 | "powershell.codeFormatting.whitespaceBetweenParameters": true,
25 |
26 | "[powershell]": {
27 | "editor.formatOnSave": true,
28 | "editor.rulers": [79]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | v0.7.0
5 | ------
6 |
7 | - `Invoke-VitalMaintenance`: Add support for running NGEN with new `DotNetQueuedItems` task
8 |
9 | v0.6.9
10 | ------
11 |
12 | - `Get-InstalledPrograms`: Fix invalid syntax (affects PowerShell 7.4)
13 |
14 | v0.6.8
15 | ------
16 |
17 | - Fix incorrect count of crash dumps in type formatting
18 |
19 | v0.6.7
20 | ------
21 |
22 | - Fix compatibility due to earlier PowerShell releases not having a type accelerator for `UInt`
23 |
24 | v0.6.6
25 | ------
26 |
27 | - Minor code clean-up & developer tooling improvements
28 |
29 | v0.6.5
30 | ------
31 |
32 | - `Invoke-VitalMaintenance`: Fix divide by zero error due to progress bar handling bug
33 |
34 | v0.6.4
35 | ------
36 |
37 | - Add progress bar support to all commands
38 |
39 | v0.6.3
40 | ------
41 |
42 | - `Get-VitalInformation`: Fix incorrect crash dumps count in output formatting
43 |
44 | v0.6.2
45 | ------
46 |
47 | - `Get-VitalInformation`: The `CrashDumps` task now also checks for crashdumps under each user profile
48 | - `Get-VitalInformation`: The `EnvironmentVariables` task now returns results sorted by variable name
49 | - `Get-VitalInformation`: Smarter handling of tasks requiring administrator privileges (see help)
50 | - Added additional type format data & tweaks to existing formats
51 | - Minor documentation updates & miscellaneous fixes
52 |
53 | v0.6.1
54 | ------
55 |
56 | - `Get-VitalInformation`: Fix array passing bug to the `ArrayList` constructor on retrieving Windows Updates
57 | - `Invoke-VitalMaintenance`: Fix array passing bug to the `ArrayList` constructor on installing Windows Updates
58 |
59 | v0.6.0
60 | ------
61 |
62 | - **All commands**: Add type format data so default output is much easier to parse
63 |
64 | v0.5.1
65 | ------
66 |
67 | - `Invoke-VitalMaintenance`: Permit overwriting files during Sysinternals extraction due to files in archive differing only by case (*upstream issue*)
68 |
69 | v0.5.0
70 | ------
71 |
72 | - `Get-VitalInformation`: Add `-WUParameters` for passing arbitrary parameters to `Get-WindowsUpdate`
73 | - `Invoke-VitalMaintenance`: Replace `-WUTitleExclude` with `-WUParameters` for passing arbitrary parameters to `Install-WindowsUpdate` (**Breaking change**)
74 |
75 | v0.4.7
76 | ------
77 |
78 | - `Invoke-VitalChecks`: Split SFC output on newlines so the output is a string array
79 |
80 | v0.4.6
81 | ------
82 |
83 | - `Invoke-VitalChecks`: Fix SFC output under sessions without a console (e.g. WinRM)
84 | - `Invoke-VitalChecks`: Remove extra newlines in SFC output due to `\r\r\n` sequences
85 |
86 | v0.4.5
87 | ------
88 |
89 | - `Invoke-VitalChecks`: Run SFC scan after component store scan as the more correct ordering
90 | - `Invoke-VitalMaintenance`: Explicitly import `PSWindowsUpdate` module to fix PowerShell Core issue
91 |
92 | v0.4.4
93 | ------
94 |
95 | - `Get-VitalInformation`: Fall back to last write time of uninstall key for installed programs
96 |
97 | v0.4.3
98 | ------
99 |
100 | - `Invoke-VitalChecks`: Skip `ChkDsk` scan for *PortableBaseLayer* volume
101 | - `Invoke-VitalMaintenance`: Add `-WUTitleExclude` parameter for excluding updates by title
102 | - `Invoke-VitalMaintenance`: Add missing help information for parameters
103 | - Apply code formatting
104 |
105 | v0.4.2
106 | ------
107 |
108 | - Syntax fixes for older PowerShell versions
109 | - Performance optimisations around array use
110 |
111 | v0.4.1
112 | ------
113 |
114 | - Remove unneeded files from published package
115 | - Minor documentation updates & miscellaneous fixes
116 |
117 | v0.4.0
118 | ------
119 |
120 | - **Breaking change**: Parameters for all functions have been reworked for more flexible task selection
121 | - `Get-VitalInformation`: The `CrashDumps` task checks we're running with Administrator privileges
122 | - `Get-VitalInformation`: The `InstalledFeatures` task checks if we're running on Windows Server
123 | - `Get-VitalInformation`: The `WindowsUpdates` task checks we're running with Administrator privileges
124 |
125 | v0.3.7
126 | ------
127 |
128 | - `Get-VitalInformation`: Installed programs now include `PSPath` and default to a friendly table view
129 |
130 | v0.3.6
131 | ------
132 |
133 | - `Get-VitalInformation`: Check we're running on Windows 8/Server 2012 or newer for storage volumes summary
134 | - `Invoke-VitalChecks`: Add exFAT to supported file systems for scanning
135 | - `Invoke-VitalChecks`: Check we're running on Windows 8/Server 2012 or newer for file system scans
136 | - `Invoke-VitalChecks`: Skip fix operations on FAT volumes due to lack of online repair support
137 | - Minor documentation updates & miscellaneous code clean-up
138 |
139 | v0.3.5
140 | ------
141 |
142 | - Return the exception message and fail immediately if the Sysinternals download fails
143 |
144 | v0.3.4
145 | ------
146 |
147 | - Add PSScriptAnalyzer linting configuration
148 |
149 | v0.3.3
150 | ------
151 |
152 | - `Get-VitalInformation`: Use the `Version.txt` file for checking the installed Sysinternals Suite version
153 | - `Invoke-VitalMaintenance`: Create a `Version.txt` file when installing or updating Sysinternals Suite
154 |
155 | v0.3.2
156 | ------
157 |
158 | - `Invoke-VitalChecks`: Capture SFC output correctly by using UTF-16 encoding
159 | - `Invoke-VitalMaintenance`: Use the `TEMP` environment variable directly when deleting the current user's temporary files
160 | - Minor documentation updates
161 |
162 | v0.3.1
163 | ------
164 |
165 | - Add built-in help for all exported functions
166 |
167 | 0.3.0
168 | -----
169 |
170 | - Cmdlets now default to running with all options
171 | - Rename `Get-VitalStatistics` to `Get-VitalInformation`
172 | - `Get-VitalInformation`: Add `-DevicesNotPresent` to retrieve devices with a status of `UNKNOWN`
173 | - `Get-VitalInformation`: Add `-HypervisorInfo` to retrieve details on the hypervisor we're running in (if any)
174 | - `Get-VitalInformation`: Add `-SysinternalsSuite` to retrieve version of Sysinternals Suite that's currently installed
175 | - `Get-VitalInformation`: Change `-DevicesWithBadStatus` to only retrieves devices with a status of `ERROR` or `DEGRADED`
176 | - `Get-VitalInformation`: Rename `-VolumeSummary` to `-StorageVolumes`
177 | - `Get-VitalInformation`: Remove `-AllStatistics` (this is now the default when no other parameters are specified)
178 | - `Invoke-VitalChecks`: Remove `-AllChecks` (this is now the default when no other parameters are specified)
179 | - `Invoke-VitalMaintenance`: Add `-ClearInternetExplorerCache` to clear all cached Internet Explorer browser data
180 | - `Invoke-VitalMaintenance`: Add `-DeleteErrorReports` to clear all error reports for the system & current user
181 | - `Invoke-VitalMaintenance`: Add `-DeleteTemporaryFiles` to clear all temporary files for the system & current user
182 | - `Invoke-VitalMaintenance`: Remove `-AllMaintenance` (this is now the default when no other parameters are specified)
183 | - Enabled Strict Mode set to version 2.0 (latest at time of writing)
184 | - Major refactoring & clean-up of the codebase to conform to best practices
185 |
186 | v0.2.8
187 | ------
188 |
189 | - Set `PSCustomObject` attributes to `False` to indicate a requested operation didn't run
190 |
191 | v0.2.7
192 | ------
193 |
194 | - Remove assumptions that we're running on 64-bit Windows (should work correctly on 32-bit)
195 |
196 | v0.2.6
197 | ------
198 |
199 | - Fix a stupid bug due to lack of testing that broke updating the *Sysinternals Suite* files
200 |
201 | v0.2.5
202 | ------
203 |
204 | - Module now supports *PowerShell 4.0* and newer (previously required *PowerShell 5.0* or newer)
205 |
206 | v0.2.4
207 | ------
208 |
209 | - Test for `Get-ComputerInfo` & `Get-PnpDevice` cmdlets (only available on **Windows 10** or newer)
210 |
211 | v0.2.3
212 | ------
213 |
214 | - Fix exception handling changes introduced in previous version to actually trigger the `Catch` block
215 |
216 | v0.2.2
217 | ------
218 |
219 | - Improved exception handling for `-EmptyRecycleBin` and `-PowerShellHelp` options of `Invoke-VitalMaintenance`
220 |
221 | v0.2.1
222 | ------
223 |
224 | - Updated the module manifest to reflect renaming of the `Invoke-VitalMaintenance` function
225 |
226 | v0.2
227 | ----
228 |
229 | - Cmdlets now return a suitable `PSCustomObject` with categorised output
230 | - Add support for retrieving consolidated computer & operating system info (via `Get-ComputerInfo`)
231 | - Add support for checking for & installing Windows updates (requires `PSWindowsUpdate` module)
232 | - Add support for retrieving devices with a status other than 'OK' (**Windows 10/Server 2016 only**)
233 | - Add support for checking for kernel & service profile crash dumps (`LocalSystem`, `LocalService` & `NetworkService`)
234 | - Add support for emptying the Recycle Bin (via `Clear-RecycleBin`)
235 | - Major clean-up of code (stylistic improvements, stop using `Write-Host`, etc...)
236 |
237 | v0.1
238 | ----
239 |
240 | - Initial stable release
241 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Samuel Leslie
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PSScriptAnalyzerSettings.psd1:
--------------------------------------------------------------------------------
1 | # PSScriptAnalyzer settings
2 | #
3 | # Last reviewed release: v1.22.0
4 |
5 | @{
6 | IncludeRules = @('*')
7 |
8 | ExcludeRules = @(
9 | 'PSReviewUnusedParameter'
10 | )
11 |
12 | Rules = @{
13 | # Compatibility rules
14 | PSUseCompatibleSyntax = @{
15 | Enable = $true
16 | # Only major versions from v3.0 are supported
17 | TargetVersions = @('4.0', '5.0', '6.0', '7.0')
18 | }
19 |
20 | # General rules
21 | PSAlignAssignmentStatement = @{
22 | Enable = $true
23 | CheckHashtable = $true
24 | }
25 |
26 | PSAvoidUsingPositionalParameters = @{
27 | Enable = $true
28 | CommandAllowList = @()
29 | }
30 |
31 | PSPlaceCloseBrace = @{
32 | Enable = $true
33 | IgnoreOneLineBlock = $true
34 | NewLineAfter = $false
35 | NoEmptyLineBefore = $false
36 | }
37 |
38 | PSPlaceOpenBrace = @{
39 | Enable = $true
40 | IgnoreOneLineBlock = $true
41 | NewLineAfter = $true
42 | OnSameLine = $true
43 | }
44 |
45 | PSProvideCommentHelp = @{
46 | Enable = $true
47 | BlockComment = $true
48 | ExportedOnly = $true
49 | Placement = 'begin'
50 | VSCodeSnippetCorrection = $false
51 | }
52 |
53 | PSUseConsistentIndentation = @{
54 | Enable = $true
55 | IndentationSize = 4
56 | Kind = 'space'
57 | PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
58 | }
59 |
60 | PSUseConsistentWhitespace = @{
61 | Enable = $true
62 | CheckInnerBrace = $true
63 | CheckOpenBrace = $true
64 | CheckOpenParen = $true
65 | CheckOperator = $true
66 | CheckParameter = $true
67 | CheckPipe = $true
68 | CheckPipeForRedundantWhitespace = $true
69 | CheckSeparator = $true
70 | IgnoreAssignmentOperatorInsideHashTable = $true
71 | }
72 |
73 | PSUseSingularNouns = @{
74 | Enable = $true
75 | # If unset, defaults to: Data, Windows
76 | NounAllowList = @()
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/PSWinVitals.format.ps1xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PSWinVitals.CHKDSK
6 |
7 | PSWinVitals.CHKDSK
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Operation
23 |
24 |
25 | VolumePath
26 |
27 |
28 | '{0} lines' -f $_.Output.Count
29 |
30 |
31 | ExitCode
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | PSWinVitals.CrashDumps
40 |
41 | PSWinVitals.CrashDumps
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | $CrashDumps = $_.Kernel.Minidumps.Count
51 | if ($_.Kernel.MemoryDump) { $CrashDumps++ }
52 | return '{0} dump(s)' -f $CrashDumps
53 |
54 |
55 |
56 |
57 |
58 | $CrashDumps = 0
59 | foreach ($Service in $_.Service) { $CrashDumps += $Service.CrashDumps.Count }
60 | return '{0} dump(s)' -f $CrashDumps
61 |
62 |
63 |
64 |
65 |
66 | $CrashDumps = 0
67 | foreach ($User in $_.User) { $CrashDumps += $User.CrashDumps.Count }
68 | return '{0} dump(s)' -f $CrashDumps
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | PSWinVitals.DISM
78 |
79 | PSWinVitals.DISM
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | Operation
94 |
95 |
96 | '{0} lines' -f $_.Output.Count
97 |
98 |
99 | ExitCode
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | PSWinVitals.EnvironmentVariables
108 |
109 | PSWinVitals.EnvironmentVariables
110 |
111 |
112 |
113 |
114 |
115 |
116 | Machine
117 |
118 |
119 | User
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | PSWinVitals.InstalledProgram
128 |
129 | PSWinVitals.InstalledProgram
130 |
131 |
132 |
133 |
134 |
135 |
136 | Name
137 |
138 |
139 | Publisher
140 |
141 |
142 | Version
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | PSWinVitals.KernelCrashDumps
151 |
152 | PSWinVitals.KernelCrashDumps
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | if ($_.MemoryDump) { $_.MemoryDump } else { 'Absent' }
166 |
167 |
168 | Minidumps
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | PSWinVitals.NGEN
177 |
178 | PSWinVitals.NGEN
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 | Name
193 |
194 |
195 |
196 | if ($_.Output.Count -eq 0) { return 'No output' }
197 | if ($_.Output.Count -eq 1) { return $_.Output }
198 | return '{0} lines' -f $_.Output.Count
199 |
200 |
201 |
202 | ExitCode
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | PSWinVitals.SFC
211 |
212 | PSWinVitals.SFC
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | Operation
227 |
228 |
229 | '{0} lines' -f $_.Output.Count
230 |
231 |
232 | ExitCode
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | PSWinVitals.UserProfileCrashDumps
241 |
242 | PSWinVitals.UserProfileCrashDumps
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | Name
257 |
258 |
259 | $_.CrashDumps.Count
260 |
261 |
262 | CrashDumps
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 | PSWinVitals.VitalChecks
271 |
272 | PSWinVitals.VitalChecks
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 | if ($null -eq $_.FileSystemScans) { return 'Skipped' }
282 | if ($_.FileSystemScans -eq $false) { return 'Missing dependency: Storage module' }
283 |
284 | $ScanErrors = 0
285 | foreach ($Scan in $_.FileSystemScans) {
286 | if ($Scan.ExitCode -ne 0) {
287 | $ScanErrors++
288 | }
289 | }
290 |
291 | if ($ScanErrors -eq 0) { return 'Healthy' }
292 | return '{0} error(s)' -f $ScanErrors
293 |
294 |
295 |
296 |
297 |
298 | if ($null -eq $_.ComponentStoreScan) { return 'Skipped' }
299 | if ($_.ComponentStoreScan.ExitCode -eq 0) { return 'Healthy' }
300 | return 'Error (RC: {0})' -f $_.ComponentStoreScan.ExitCode
301 |
302 |
303 |
304 |
305 |
306 | if ($null -eq $_.SystemFileChecker) { return 'Skipped' }
307 | if ($_.SystemFileChecker.ExitCode -eq 0) { return 'Healthy' }
308 | return 'Error (RC: {0})' -f $_.SystemFileChecker.ExitCode
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | PSWinVitals.VitalInformation
318 |
319 | PSWinVitals.VitalInformation
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 | if ($null -eq $_.ComputerInfo) { return 'Skipped' }
329 | if (!$_.ComputerInfo) { return 'Missing dependency: Get-ComputerInfo cmdlet' }
330 | return '{0} {1}' -f $_.ComputerInfo.WindowsProductName, $_.ComputerInfo.OSVersion
331 |
332 |
333 |
334 |
335 |
336 | if ($null -eq $_.HypervisorInfo) { return 'Skipped' }
337 | if (!$_.HypervisorInfo) { return 'Not present or unknown' }
338 | return $_.HypervisorInfo.Vendor
339 |
340 |
341 |
342 |
343 |
344 | if ($null -eq $_.DevicesWithBadStatus) { return 'Skipped' }
345 | if ($_.DevicesWithBadStatus -eq $false) { return 'Missing dependency: PnpDevice module' }
346 | return '{0} device(s)' -f $_.DevicesWithBadStatus.Count
347 |
348 |
349 |
350 |
351 |
352 | if ($null -eq $_.DevicesNotPresent) { return 'Skipped' }
353 | if ($_.DevicesNotPresent -eq $false) { return 'Missing dependency: PnpDevice module' }
354 | return '{0} device(s)' -f $_.DevicesNotPresent.Count
355 |
356 |
357 |
358 |
359 |
360 | if ($null -eq $_.StorageVolumes) { return 'Skipped' }
361 | if ($_.StorageVolumes -eq $false) { return 'Missing dependency: Storage module' }
362 | return '{0} volume(s)' -f $_.StorageVolumes.Count
363 |
364 |
365 |
366 |
367 |
368 | if ($null -eq $_.CrashDumps) { return 'Skipped' }
369 |
370 | $KernelDumps = $_.CrashDumps.Kernel.Minidumps.Count
371 | if ($_.CrashDumps.Kernel.MemoryDump) { $KernelDumps++ }
372 |
373 | $ServiceDumps = 0
374 | foreach ($Service in $_.CrashDumps.Service) { $ServiceDumps += $Service.CrashDumps.Count }
375 |
376 | $UserDumps = 0
377 | foreach ($User in $_.CrashDumps.User) { $UserDumps += $User.CrashDumps.Count }
378 |
379 | return '{0} kernel dump(s), {1} service dump(s), {2} user dump(s)' -f $KernelDumps, $ServiceDumps, $UserDumps
380 |
381 |
382 |
383 |
384 |
385 | if ($null -eq $_.ComponentStoreAnalysis) { return 'Skipped' }
386 | if ($_.ComponentStoreAnalysis.ExitCode -eq 0) { return 'Completed' }
387 | return 'Error (RC: {0})' -f $_.ComponentStoreAnalysis.ExitCode
388 |
389 |
390 |
391 |
392 |
393 | if ($null -eq $_.InstalledFeatures) { return 'Skipped' }
394 |
395 | if ($_.InstalledFeatures -eq $false) {
396 | if ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -gt 1) {
397 | return 'Missing dependency: ServerManager module'
398 | }
399 | return 'N/A for Windows clients'
400 | }
401 |
402 | return '{0} feature(s)' -f $_.InstalledFeatures.Count
403 |
404 |
405 |
406 |
407 |
408 | if ($null -eq $_.InstalledPrograms) { return 'Skipped' }
409 | return '{0} programs(s)' -f $_.InstalledPrograms.Count
410 |
411 |
412 |
413 |
414 |
415 | if ($null -eq $_.EnvironmentVariables) { return 'Skipped' }
416 | return '{0} system variable(s), {1} user variable(s)' -f $_.EnvironmentVariables.Machine.Count, $_.EnvironmentVariables.User.Count
417 |
418 |
419 |
420 |
421 |
422 | if ($null -eq $_.WindowsUpdates) { return 'Skipped' }
423 | if ($_.WindowsUpdates -eq $false) { return 'Missing dependency: PSWindowsUpdate module' }
424 | return 'Found {0} update(s)' -f $_.WindowsUpdates.Count
425 |
426 |
427 |
428 |
429 |
430 | if ($null -eq $_.SysinternalsSuite) { return 'Skipped' }
431 | if (!$_.SysinternalsSuite) { return 'Not present' }
432 | return 'Present (Version: {0})' -f $_.SysinternalsSuite.Version
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 | PSWinVitals.VitalMaintenance
442 |
443 | PSWinVitals.VitalMaintenance
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 | if ($null -eq $_.WindowsUpdates) { return 'Skipped' }
453 | if ($_.WindowsUpdates -eq $false) { return 'Missing dependency: PSWindowsUpdate module' }
454 | return 'Installed {0} update(s)' -f $_.WindowsUpdates.Count
455 |
456 |
457 |
458 |
459 |
460 | if ($null -eq $_.ComponentStoreCleanup) { return 'Skipped' }
461 | if ($_.ComponentStoreCleanup.ExitCode -eq 0) { return 'Completed' }
462 | return 'Error (RC: {0})' -f $_.ComponentStoreCleanup.ExitCode
463 |
464 |
465 |
466 |
467 |
468 | if ($null -eq $_.DotNetQueuedItems) { return 'Skipped' }
469 | return 'Invoked for {0} .NET Framework runtimes' -f $_.DotNetQueuedItems.Count
470 |
471 |
472 |
473 |
474 |
475 | if ($null -eq $_.PowerShellHelp) { return 'Skipped' }
476 | if ($_.PowerShellHelp -eq $true) { return 'Completed' }
477 | return 'Completed with some errors'
478 |
479 |
480 |
481 |
482 |
483 | if ($null -eq $_.SysinternalsSuite) { return 'Skipped' }
484 | if ($_.SysinternalsSuite -is [String]) { return 'Failed: {0}' -f $_.SysinternalsSuite }
485 |
486 | if ($_.SysinternalsSuite.Updated) {
487 | $Status = 'Updated'
488 | } else {
489 | $Status = 'Up-to-date'
490 | }
491 |
492 | return '{0} (Version: {1})' -f $Status, $_.SysinternalsSuite.Version
493 |
494 |
495 |
496 |
497 |
498 | if ($null -eq $_.ClearInternetExplorerCache) { return 'Skipped' }
499 | if (!$_.ClearInternetExplorerCache) { return 'Missing dependency: inetcpl.cpl applet' }
500 | return 'Completed'
501 |
502 |
503 |
504 |
505 |
506 | if ($null -eq $_.DeleteErrorReports) { return 'Skipped' }
507 | return 'Completed'
508 |
509 |
510 |
511 |
512 |
513 | if ($null -eq $_.DeleteTemporaryFiles) { return 'Skipped' }
514 | return 'Completed'
515 |
516 |
517 |
518 |
519 |
520 | if ($null -eq $_.EmptyRecycleBin) { return 'Skipped' }
521 | if ($_.EmptyRecycleBin -eq $false) { return 'Missing dependency: Clear-RecycleBin cmdlet' }
522 | if ($_.EmptyRecycleBin -ne $true) { return 'Failed: {0}' -f $_.EmptyRecycleBin }
523 | return 'Completed'
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
--------------------------------------------------------------------------------
/PSWinVitals.psd1:
--------------------------------------------------------------------------------
1 | #
2 | # Module manifest for module 'PSWinVitals'
3 | #
4 |
5 | @{
6 |
7 | # Script module or binary module file associated with this manifest.
8 | RootModule = 'PSWinVitals.psm1'
9 |
10 | # Version number of this module.
11 | ModuleVersion = '0.7.0'
12 |
13 | # Supported PSEditions
14 | # CompatiblePSEditions = @()
15 |
16 | # ID used to uniquely identify this module
17 | GUID = 'd296ad05-f4c9-4d61-b37a-014d03cb034d'
18 |
19 | # Author of this module
20 | Author = 'Samuel Leslie'
21 |
22 | # Company or vendor of this module
23 | # CompanyName = ''
24 |
25 | # Copyright statement for this module
26 | Copyright = '(c) Samuel Leslie. All rights reserved.'
27 |
28 | # Description of the functionality provided by this module
29 | Description = 'Consolidate common system health checks, maintenance tasks & inventory retrieval'
30 |
31 | # Minimum version of the PowerShell engine required by this module
32 | PowerShellVersion = '4.0'
33 |
34 | # Name of the PowerShell host required by this module
35 | # PowerShellHostName = ''
36 |
37 | # Minimum version of the PowerShell host required by this module
38 | # PowerShellHostVersion = ''
39 |
40 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
41 | # DotNetFrameworkVersion = ''
42 |
43 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
44 | # ClrVersion = ''
45 |
46 | # Processor architecture (None, X86, Amd64) required by this module
47 | # ProcessorArchitecture = ''
48 |
49 | # Modules that must be imported into the global environment prior to importing this module
50 | # RequiredModules = @()
51 |
52 | # Assemblies that must be loaded prior to importing this module
53 | # RequiredAssemblies = @()
54 |
55 | # Script files (.ps1) that are run in the caller's environment prior to importing this module.
56 | # ScriptsToProcess = @()
57 |
58 | # Type files (.ps1xml) to be loaded when importing this module
59 | # TypesToProcess = @()
60 |
61 | # Format files (.ps1xml) to be loaded when importing this module
62 | FormatsToProcess = @('PSWinVitals.format.ps1xml')
63 |
64 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
65 | # NestedModules = @()
66 |
67 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
68 | FunctionsToExport = @('Get-VitalInformation', 'Invoke-VitalChecks', 'Invoke-VitalMaintenance')
69 |
70 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
71 | CmdletsToExport = @()
72 |
73 | # Variables to export from this module
74 | VariablesToExport = '*'
75 |
76 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
77 | AliasesToExport = @()
78 |
79 | # DSC resources to export from this module
80 | # DscResourcesToExport = @()
81 |
82 | # List of all modules packaged with this module
83 | # ModuleList = @()
84 |
85 | # List of all files packaged with this module
86 | # FileList = @()
87 |
88 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
89 | PrivateData = @{
90 |
91 | PSData = @{
92 |
93 | # Tags applied to this module. These help with module discovery in online galleries.
94 | Tags = @(
95 | 'maintenance', 'sysadmin'
96 | 'Windows',
97 | 'PSEdition_Desktop'
98 | )
99 |
100 | # A URL to the license for this module.
101 | LicenseUri = 'https://github.com/ralish/PSWinVitals/blob/stable/LICENSE'
102 |
103 | # A URL to the main website for this project.
104 | ProjectUri = 'https://github.com/ralish/PSWinVitals'
105 |
106 | # A URL to an icon representing this module.
107 | # IconUri = ''
108 |
109 | # ReleaseNotes of this module
110 | ReleaseNotes = 'https://github.com/ralish/PSWinVitals/blob/stable/CHANGELOG.md'
111 |
112 | # Prerelease string of this module
113 | # Prerelease = ''
114 |
115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save
116 | # RequireLicenseAcceptance = $false
117 |
118 | # External dependent modules of this module
119 | # ExternalModuleDependencies = @()
120 |
121 | } # End of PSData hashtable
122 |
123 | } # End of PrivateData hashtable
124 |
125 | # HelpInfo URI of this module
126 | # HelpInfoURI = ''
127 |
128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
129 | # DefaultCommandPrefix = ''
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/PSWinVitals.psm1:
--------------------------------------------------------------------------------
1 | # See the help for Set-StrictMode for what this enables
2 | Set-StrictMode -Version 3.0
3 |
4 | Function Get-VitalInformation {
5 | <#
6 | .SYNOPSIS
7 | Retrieves system information and inventory
8 |
9 | .DESCRIPTION
10 | The following tasks are available:
11 | - ComponentStoreAnalysis
12 | Performs a component store analysis to determine current statistics and reclaimable space.
13 |
14 | This task requires administrator privileges.
15 |
16 | - ComputerInfo
17 | Retrieves baseline system hardware and operating system information.
18 |
19 | This task requires Windows PowerShell 5.1 or newer.
20 |
21 | - CrashDumps
22 | Checks for any kernel, service, or user crash dumps.
23 |
24 | This task requires administrator privileges.
25 |
26 | - DevicesNotPresent
27 | Retrieves any PnP devices which are not present.
28 |
29 | Devices which are not present are those with an "Unknown" state.
30 |
31 | This task requires Windows 10, Windows Server 2016, or newer.
32 |
33 | - DevicesWithBadStatus
34 | Retrieves any PnP devices with a bad status.
35 |
36 | A bad status corresponds to any device in an "Error" or "Degraded" state.
37 |
38 | This task requires Windows 10, Windows Server 2016, or newer.
39 |
40 | - EnvironmentVariables
41 | Retrieves environment variables for the system and current user.
42 |
43 | - HypervisorInfo
44 | Attempts to detect if the system is running under a hypervisor.
45 |
46 | Currently only Microsoft Hyper-V and VMware hypervisors are detected.
47 |
48 | - InstalledFeatures
49 | Retrieves information on installed Windows features.
50 |
51 | This task requires a Window Server operating system.
52 |
53 | - InstalledPrograms
54 | Retrieves information on installed programs.
55 |
56 | Only programs installed system-wide are retrieved.
57 |
58 | - StorageVolumes
59 | Retrieves information on fixed storage volumes.
60 |
61 | This task requires Windows 8, Windows Server 2012, or newer.
62 |
63 | - SysinternalsSuite
64 | Retrieves the version of the installed Sysinternals Suite if any.
65 |
66 | The version is retrieved from the Version.txt file created by Invoke-VitalMaintenance.
67 |
68 | The location to check if the utilities are installed depends on the OS architecture:
69 | * 32-bit: The "Sysinternals" folder in the "Program Files" directory
70 | * 64-bit: The "Sysinternals" folder in the "Program Files (x86)" directory
71 |
72 | - WindowsUpdates
73 | Scans for any available Windows updates.
74 |
75 | Updates from Microsoft Update are also included if opted-in via the Windows Update configuration.
76 |
77 | This task requires administrator privileges and the PSWindowsUpdate module.
78 |
79 | The default is to run all tasks.
80 |
81 | .PARAMETER ExcludeTasks
82 | Array of tasks to exclude. The default is an empty array (i.e. run all tasks).
83 |
84 | .PARAMETER IncludeTasks
85 | Array of tasks to include. At least one task must be specified.
86 |
87 | .PARAMETER WUParameters
88 | Hashtable of additional parameters to pass to Get-WindowsUpdate.
89 |
90 | Only used if the WindowsUpdates task is selected.
91 |
92 | .EXAMPLE
93 | Get-VitalInformation -IncludeTasks StorageVolumes, InstalledPrograms
94 |
95 | Only retrieves information on storage volumes and installed programs.
96 |
97 | .NOTES
98 | Selected inventory information is retrieved in the following order:
99 | - ComputerInfo
100 | - HypervisorInfo
101 | - DevicesWithBadStatus
102 | - DevicesNotPresent
103 | - StorageVolumes
104 | - CrashDumps
105 | - ComponentStoreAnalysis
106 | - InstalledFeatures
107 | - InstalledPrograms
108 | - EnvironmentVariables
109 | - WindowsUpdates
110 | - SysinternalsSuite
111 |
112 | When running without administrator privileges, the handling of tasks which require administrator privileges differs by selection method:
113 | - ExcludeTasks (default)
114 | Administrator tasks which were not explicitly excluded will be automatically excluded and a warning displayed.
115 | - IncludeTasks
116 | Administrator tasks will result in the command exiting with an error.
117 |
118 | .LINK
119 | https://github.com/ralish/PSWinVitals
120 | #>
121 |
122 | [CmdletBinding(DefaultParameterSetName = 'OptOut')]
123 | [OutputType([PSCustomObject])]
124 | Param(
125 | [Parameter(ParameterSetName = 'OptOut')]
126 | [ValidateSet(
127 | 'ComponentStoreAnalysis',
128 | 'ComputerInfo',
129 | 'CrashDumps',
130 | 'DevicesNotPresent',
131 | 'DevicesWithBadStatus',
132 | 'EnvironmentVariables',
133 | 'HypervisorInfo',
134 | 'InstalledFeatures',
135 | 'InstalledPrograms',
136 | 'StorageVolumes',
137 | 'SysinternalsSuite',
138 | 'WindowsUpdates'
139 | )]
140 | [String[]]$ExcludeTasks,
141 |
142 | [Parameter(ParameterSetName = 'OptIn', Mandatory)]
143 | [ValidateSet(
144 | 'ComponentStoreAnalysis',
145 | 'ComputerInfo',
146 | 'CrashDumps',
147 | 'DevicesNotPresent',
148 | 'DevicesWithBadStatus',
149 | 'EnvironmentVariables',
150 | 'HypervisorInfo',
151 | 'InstalledFeatures',
152 | 'InstalledPrograms',
153 | 'StorageVolumes',
154 | 'SysinternalsSuite',
155 | 'WindowsUpdates'
156 | )]
157 | [String[]]$IncludeTasks,
158 |
159 | [ValidateNotNull()]
160 | [Hashtable]$WUParameters = @{}
161 | )
162 |
163 | $Tasks = @{
164 | ComponentStoreAnalysis = $null
165 | ComputerInfo = $null
166 | CrashDumps = $null
167 | DevicesNotPresent = $null
168 | DevicesWithBadStatus = $null
169 | EnvironmentVariables = $null
170 | HypervisorInfo = $null
171 | InstalledFeatures = $null
172 | InstalledPrograms = $null
173 | StorageVolumes = $null
174 | SysinternalsSuite = $null
175 | WindowsUpdates = $null
176 | }
177 |
178 | $TasksDone = 0
179 | $TasksTotal = 0
180 |
181 | foreach ($Task in @($Tasks.Keys)) {
182 | if ($PSCmdlet.ParameterSetName -eq 'OptOut') {
183 | if ($ExcludeTasks -contains $Task) {
184 | $Tasks[$Task] = $false
185 | } else {
186 | $Tasks[$Task] = $true
187 | $TasksTotal++
188 | }
189 | } else {
190 | if ($IncludeTasks -contains $Task) {
191 | $Tasks[$Task] = $true
192 | $TasksTotal++
193 | } else {
194 | $Tasks[$Task] = $false
195 | }
196 | }
197 | }
198 |
199 | if (!(Test-IsAdministrator)) {
200 | $AdminTasks = 'ComponentStoreAnalysis', 'CrashDumps', 'WindowsUpdates'
201 | $SelectedAdminTasks = New-Object -TypeName 'Collections.Generic.List[String]'
202 |
203 | if ($PSCmdlet.ParameterSetName -eq 'OptOut') {
204 | foreach ($AdminTask in $AdminTasks) {
205 | if ($Tasks[$AdminTask]) {
206 | $Tasks[$AdminTask] = $false
207 | $SelectedAdminTasks.Add($AdminTask)
208 | }
209 | }
210 |
211 | if ($SelectedAdminTasks.Count -gt 0) {
212 | Write-Warning -Message ('Skipping tasks which require administrator privileges: {0}' -f [String]::Join(', ', $SelectedAdminTasks.ToArray()))
213 | }
214 | } else {
215 | foreach ($AdminTask in $AdminTasks) {
216 | if ($Tasks[$AdminTask]) {
217 | $SelectedAdminTasks.Add($AdminTask)
218 | }
219 | }
220 |
221 | if ($SelectedAdminTasks.Count -gt 0) {
222 | throw 'Some selected tasks require administrator privileges: {0}' -f [String]::Join(', ', $SelectedAdminTasks.ToArray())
223 | }
224 | }
225 | }
226 |
227 | $VitalInformation = [PSCustomObject]@{
228 | ComponentStoreAnalysis = $null
229 | ComputerInfo = $null
230 | CrashDumps = $null
231 | DevicesNotPresent = $null
232 | DevicesWithBadStatus = $null
233 | EnvironmentVariables = $null
234 | HypervisorInfo = $null
235 | InstalledFeatures = $null
236 | InstalledPrograms = $null
237 | StorageVolumes = $null
238 | SysinternalsSuite = $null
239 | WindowsUpdates = $null
240 | }
241 | $VitalInformation.PSObject.TypeNames.Insert(0, 'PSWinVitals.VitalInformation')
242 |
243 | $WriteProgressParams = @{
244 | Activity = 'Retrieving vital information'
245 | }
246 |
247 | if ($Tasks['ComputerInfo']) {
248 | if (Get-Command -Name 'Get-ComputerInfo' -ErrorAction Ignore) {
249 | Write-Progress @WriteProgressParams -Status 'Retrieving computer info' -PercentComplete ($TasksDone / $TasksTotal * 100)
250 | $VitalInformation.ComputerInfo = Get-ComputerInfo
251 | } else {
252 | Write-Warning -Message 'Unable to retrieve computer info as Get-ComputerInfo cmdlet not available.'
253 | $VitalInformation.ComputerInfo = $false
254 | }
255 | $TasksDone++
256 | }
257 |
258 | if ($Tasks['HypervisorInfo']) {
259 | Write-Progress @WriteProgressParams -Status 'Retrieving hypervisor info' -PercentComplete ($TasksDone / $TasksTotal * 100)
260 | $VitalInformation.HypervisorInfo = Get-HypervisorInfo
261 | $TasksDone++
262 | }
263 |
264 | if ($Tasks['DevicesWithBadStatus']) {
265 | if (Get-Module -Name 'PnpDevice' -ListAvailable -Verbose:$false) {
266 | Write-Progress @WriteProgressParams -Status 'Retrieving problem devices' -PercentComplete ($TasksDone / $TasksTotal * 100)
267 | $VitalInformation.DevicesWithBadStatus = @(Get-PnpDevice | Where-Object Status -In 'Degraded', 'Error')
268 | } else {
269 | Write-Warning -Message 'Unable to retrieve problem devices as PnpDevice module not available.'
270 | $VitalInformation.DevicesWithBadStatus = $false
271 | }
272 | $TasksDone++
273 | }
274 |
275 | if ($Tasks['DevicesNotPresent']) {
276 | if (Get-Module -Name 'PnpDevice' -ListAvailable -Verbose:$false) {
277 | Write-Progress @WriteProgressParams -Status 'Retrieving not present devices' -PercentComplete ($TasksDone / $TasksTotal * 100)
278 | $VitalInformation.DevicesNotPresent = @(Get-PnpDevice | Where-Object Status -EQ 'Unknown')
279 | } else {
280 | Write-Warning -Message 'Unable to retrieve not present devices as PnpDevice module not available.'
281 | $VitalInformation.DevicesNotPresent = $false
282 | }
283 | $TasksDone++
284 | }
285 |
286 | if ($Tasks['StorageVolumes']) {
287 | if (Get-Module -Name 'Storage' -ListAvailable -Verbose:$false) {
288 | Write-Progress @WriteProgressParams -Status 'Retrieving storage volumes summary' -PercentComplete ($TasksDone / $TasksTotal * 100)
289 | $VitalInformation.StorageVolumes = @(Get-Volume | Where-Object DriveType -EQ 'Fixed')
290 | } else {
291 | Write-Warning -Message 'Unable to retrieve storage volumes summary as Storage module not available.'
292 | $VitalInformation.StorageVolumes = $false
293 | }
294 | $TasksDone++
295 | }
296 |
297 | if ($Tasks['CrashDumps']) {
298 | Write-Progress @WriteProgressParams -Status 'Retrieving crash dumps' -PercentComplete ($TasksDone / $TasksTotal * 100)
299 |
300 | $CrashDumps = [PSCustomObject]@{
301 | Kernel = $null
302 | Service = $null
303 | User = $null
304 | }
305 | $CrashDumps.PSObject.TypeNames.Insert(0, 'PSWinVitals.CrashDumps')
306 |
307 | $CrashDumps.Kernel = Get-KernelCrashDumps
308 | $CrashDumps.Service = Get-ServiceCrashDumps
309 | $CrashDumps.User = Get-UserCrashDumps
310 |
311 | $VitalInformation.CrashDumps = $CrashDumps
312 | $TasksDone++
313 | }
314 |
315 | if ($Tasks['ComponentStoreAnalysis']) {
316 | Write-Progress @WriteProgressParams -Status 'Running component store analysis' -PercentComplete ($TasksDone / $TasksTotal * 100)
317 | $VitalInformation.ComponentStoreAnalysis = Invoke-DISM -Operation AnalyzeComponentStore
318 | $TasksDone++
319 | }
320 |
321 | if ($Tasks['InstalledFeatures']) {
322 | if ((Get-WindowsProductType) -gt 1) {
323 | if (Get-Module -Name 'ServerManager' -ListAvailable -Verbose:$false) {
324 | Write-Progress @WriteProgressParams -Status 'Retrieving installed features' -PercentComplete ($TasksDone / $TasksTotal * 100)
325 | $VitalInformation.InstalledFeatures = @(Get-WindowsFeature | Where-Object Installed)
326 | } else {
327 | Write-Warning -Message 'Unable to retrieve installed features as ServerManager module not available.'
328 | $VitalInformation.InstalledFeatures = $false
329 | }
330 | } else {
331 | Write-Verbose -Message 'Unable to retrieve installed features as not running on Windows Server.'
332 | $VitalInformation.InstalledFeatures = $false
333 | }
334 | $TasksDone++
335 | }
336 |
337 | if ($Tasks['InstalledPrograms']) {
338 | Write-Progress @WriteProgressParams -Status 'Retrieving installed programs' -PercentComplete ($TasksDone / $TasksTotal * 100)
339 | $VitalInformation.InstalledPrograms = Get-InstalledPrograms
340 | $TasksDone++
341 | }
342 |
343 | if ($Tasks['EnvironmentVariables']) {
344 | Write-Progress @WriteProgressParams -Status 'Retrieving environment variables' -PercentComplete ($TasksDone / $TasksTotal * 100)
345 |
346 | $EnvironmentVariables = [PSCustomObject]@{
347 | Machine = $null
348 | User = $null
349 | }
350 | $EnvironmentVariables.PSObject.TypeNames.Insert(0, 'PSWinVitals.EnvironmentVariables')
351 |
352 | $Machine = [Ordered]@{}
353 | $MachineVariables = [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::Machine)
354 | foreach ($Variable in ($MachineVariables.Keys | Sort-Object)) {
355 | $Machine[$Variable] = $MachineVariables[$Variable]
356 | }
357 | $EnvironmentVariables.Machine = $Machine
358 |
359 | $User = [Ordered]@{}
360 | $UserVariables = [Environment]::GetEnvironmentVariables([EnvironmentVariableTarget]::User)
361 | foreach ($Variable in ($UserVariables.Keys | Sort-Object)) {
362 | $User[$Variable] = $UserVariables[$Variable]
363 | }
364 | $EnvironmentVariables.User = $User
365 |
366 | $VitalInformation.EnvironmentVariables = $EnvironmentVariables
367 | $TasksDone++
368 | }
369 |
370 | if ($Tasks['WindowsUpdates']) {
371 | if (Get-Module -Name 'PSWindowsUpdate' -ListAvailable -Verbose:$false) {
372 | Write-Progress @WriteProgressParams -Status 'Retrieving Windows updates' -PercentComplete ($TasksDone / $TasksTotal * 100)
373 | $WindowsUpdates = Get-WindowsUpdate @WUParameters
374 |
375 | if ($null -ne $WindowsUpdates -and $WindowsUpdates.Count -gt 0) {
376 | $VitalInformation.WindowsUpdates = [Array]$WindowsUpdates
377 | } else {
378 | $VitalInformation.WindowsUpdates = @()
379 | }
380 | } else {
381 | Write-Warning -Message 'Unable to retrieve Windows updates as PSWindowsUpdate module not available.'
382 | $VitalInformation.WindowsUpdates = $false
383 | }
384 | $TasksDone++
385 | }
386 |
387 | if ($Tasks['SysinternalsSuite']) {
388 | if (Test-IsWindows64bit) {
389 | $InstallDir = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Sysinternals'
390 | } else {
391 | $InstallDir = Join-Path -Path $env:ProgramFiles -ChildPath 'Sysinternals'
392 | }
393 |
394 | if (Test-Path -Path $InstallDir -PathType Container) {
395 | Write-Progress @WriteProgressParams -Status 'Retrieving Sysinternals Suite version' -PercentComplete ($TasksDone / $TasksTotal * 100)
396 |
397 | $Sysinternals = [PSCustomObject]@{
398 | Path = $InstallDir
399 | Version = $null
400 | }
401 |
402 | $VersionFile = Join-Path -Path $InstallDir -ChildPath 'Version.txt'
403 | if (Test-Path -Path $VersionFile -PathType Leaf) {
404 | $Sysinternals.Version = Get-Content -Path $VersionFile
405 | } else {
406 | Write-Warning -Message 'Unable to retrieve Sysinternals Suite version as version file is not present.'
407 | $Sysinternals.Version = 'Unknown'
408 | }
409 |
410 | $VitalInformation.SysinternalsSuite = $Sysinternals
411 | } else {
412 | Write-Warning -Message 'Unable to retrieve Sysinternals Suite version as it does not appear to be installed.'
413 | $VitalInformation.SysinternalsSuite = $false
414 | }
415 | $TasksDone++
416 | }
417 |
418 | Write-Progress @WriteProgressParams -Completed
419 | return $VitalInformation
420 | }
421 |
422 | Function Invoke-VitalChecks {
423 | <#
424 | .SYNOPSIS
425 | Performs system health checks
426 |
427 | .DESCRIPTION
428 | The following tasks are available:
429 | - ComponentStoreScan
430 | Scans the component store and repairs any corruption.
431 |
432 | If the -VerifyOnly parameter is specified then no repairs will be performed.
433 |
434 | This task requires administrator privileges.
435 |
436 | - FileSystemScans
437 | Scans all non-removable storage volumes with supported file systems and repairs any corruption.
438 |
439 | If the -VerifyOnly parameter is specified then no repairs will be performed.
440 |
441 | Volumes using FAT file systems are only supported with -VerifyOnly as they do not support online repair.
442 |
443 | This task requires administrator privileges and Windows 8, Windows Server 2012, or newer.
444 |
445 | - SystemFileChecker
446 | Scans system files and repairs any corruption.
447 |
448 | If the -VerifyOnoly parameter is specified then no repairs will be performed.
449 |
450 | This task requires administrator privileges.
451 |
452 | The default is to run all tasks.
453 |
454 | .PARAMETER ExcludeTasks
455 | Array of tasks to exclude. The default is an empty array (i.e. run all tasks).
456 |
457 | .PARAMETER IncludeTasks
458 | Array of tasks to include. At least one task must be specified.
459 |
460 | .PARAMETER VerifyOnly
461 | Modifies the behaviour of health checks to not repair any issues.
462 |
463 | .EXAMPLE
464 | Invoke-VitalChecks -IncludeTasks FileSystemScans -VerifyOnly
465 |
466 | Only runs file system scans without performing any repairs.
467 |
468 | .NOTES
469 | Selected health checks are run in the following order:
470 | - FileSystemScans
471 | - SystemFileChecker
472 | - ComponentStoreScan
473 |
474 | .LINK
475 | https://github.com/ralish/PSWinVitals
476 | #>
477 |
478 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
479 | [CmdletBinding(DefaultParameterSetName = 'OptOut')]
480 | [OutputType([PSCustomObject])]
481 | Param(
482 | [Parameter(ParameterSetName = 'OptOut')]
483 | [ValidateSet(
484 | 'ComponentStoreScan',
485 | 'FileSystemScans',
486 | 'SystemFileChecker'
487 | )]
488 | [String[]]$ExcludeTasks,
489 |
490 | [Parameter(ParameterSetName = 'OptIn', Mandatory)]
491 | [ValidateSet(
492 | 'ComponentStoreScan',
493 | 'FileSystemScans',
494 | 'SystemFileChecker'
495 | )]
496 | [String[]]$IncludeTasks,
497 |
498 | [Switch]$VerifyOnly
499 | )
500 |
501 | if (!(Test-IsAdministrator)) {
502 | throw 'You must have administrator privileges to perform system health checks.'
503 | }
504 |
505 | $Tasks = @{
506 | ComponentStoreScan = $null
507 | FileSystemScans = $null
508 | SystemFileChecker = $null
509 | }
510 |
511 | $TasksDone = 0
512 | $TasksTotal = 0
513 |
514 | foreach ($Task in @($Tasks.Keys)) {
515 | if ($PSCmdlet.ParameterSetName -eq 'OptOut') {
516 | if ($ExcludeTasks -contains $Task) {
517 | $Tasks[$Task] = $false
518 | } else {
519 | $Tasks[$Task] = $true
520 | $TasksTotal++
521 | }
522 | } else {
523 | if ($IncludeTasks -contains $Task) {
524 | $Tasks[$Task] = $true
525 | $TasksTotal++
526 | } else {
527 | $Tasks[$Task] = $false
528 | }
529 | }
530 | }
531 |
532 | $VitalChecks = [PSCustomObject]@{
533 | ComponentStoreScan = $null
534 | FileSystemScans = $null
535 | SystemFileChecker = $null
536 | }
537 | $VitalChecks.PSObject.TypeNames.Insert(0, 'PSWinVitals.VitalChecks')
538 |
539 | $WriteProgressParams = @{
540 | Activity = 'Running vital checks'
541 | }
542 |
543 | if ($Tasks['FileSystemScans']) {
544 | if (Get-Module -Name 'Storage' -ListAvailable -Verbose:$false) {
545 | Write-Progress @WriteProgressParams -Status 'Running file system scans' -PercentComplete ($TasksDone / $TasksTotal * 100)
546 | if ($VerifyOnly) {
547 | $VitalChecks.FileSystemScans = Invoke-CHKDSK -Operation Verify
548 | } else {
549 | $VitalChecks.FileSystemScans = Invoke-CHKDSK -Operation Scan
550 | }
551 | } else {
552 | Write-Warning -Message 'Unable to run file system scans as Storage module not available.'
553 | $VitalChecks.FileSystemScans = $false
554 | }
555 | $TasksDone++
556 | }
557 |
558 | if ($Tasks['ComponentStoreScan']) {
559 | Write-Progress @WriteProgressParams -Status 'Running component store scan' -PercentComplete ($TasksDone / $TasksTotal * 100)
560 | if ($VerifyOnly) {
561 | $VitalChecks.ComponentStoreScan = Invoke-DISM -Operation ScanHealth
562 | } else {
563 | $VitalChecks.ComponentStoreScan = Invoke-DISM -Operation RestoreHealth
564 | }
565 | $TasksDone++
566 | }
567 |
568 | if ($Tasks['SystemFileChecker']) {
569 | Write-Progress @WriteProgressParams -Status 'Running System File Checker' -PercentComplete ($TasksDone / $TasksTotal * 100)
570 | if ($VerifyOnly) {
571 | $VitalChecks.SystemFileChecker = Invoke-SFC -Operation Verify
572 | } else {
573 | $VitalChecks.SystemFileChecker = Invoke-SFC -Operation Scan
574 | }
575 | $TasksDone++
576 | }
577 |
578 | Write-Progress @WriteProgressParams -Completed
579 | return $VitalChecks
580 | }
581 |
582 | Function Invoke-VitalMaintenance {
583 | <#
584 | .SYNOPSIS
585 | Performs system maintenance tasks
586 |
587 | .DESCRIPTION
588 | The following tasks are available:
589 | - ClearInternetExplorerCache
590 | Clears all cached Internet Explorer data for the user.
591 |
592 | - ComponentStoreCleanup
593 | Performs a component store clean-up to remove obsolete Windows updates.
594 |
595 | This task requires administrator privileges.
596 |
597 | - DotNetQueuedItems
598 | Executes queued compilation jobs for all installed .NET Framework versions.
599 |
600 | This task requires administrator privileges.
601 |
602 | - DeleteErrorReports
603 | Deletes all error reports (queued & archived) for the system and user.
604 |
605 | This task requires administrator privileges.
606 |
607 | - DeleteTemporaryFiles
608 | Recursively deletes all data in the following locations:
609 | * The "TEMP" environment variable path for the system
610 | * The "TEMP" environment variable path for the user
611 |
612 | This task requires administrator privileges.
613 |
614 | - EmptyRecycleBin
615 | Empties the Recycle Bin for the user.
616 |
617 | This task requires Windows 10, Windows Server 2016, or newer.
618 |
619 | - PowerShellHelp
620 | Updates PowerShell help for all modules.
621 |
622 | This task requires administrator privileges.
623 |
624 | - SysinternalsSuite
625 | Downloads and installs the latest Sysinternals Suite.
626 |
627 | The installation process itself consists of the following steps:
628 | * Download the latest Sysinternals Suite archive from download.sysinternals.com
629 | * Determine the version based off the date of the most recently modified file in the archive
630 | * If the downloaded version is newer than the installed version (if any is present) then:
631 | | * Remove any existing files in the installation directory and decompress the downloaded archive
632 | | * Write a Version.txt file in the installation directory with earlier determined version date
633 | * Add the installation directory to the system path environment variable if it's not already present
634 |
635 | The location where the utilities will be installed depends on the OS architecture:
636 | * 32-bit: The "Sysinternals" folder in the "Program Files" directory
637 | * 64-bit: The "Sysinternals" folder in the "Program Files (x86)" directory
638 |
639 | This task requires administrator privileges.
640 |
641 | - WindowsUpdates
642 | Downloads and installs all available Windows updates.
643 |
644 | Updates from Microsoft Update are also included if opted-in via the Windows Update configuration.
645 |
646 | This task requires administrator privileges and the PSWindowsUpdate module.
647 |
648 | The default is to run all tasks.
649 |
650 | .PARAMETER ExcludeTasks
651 | Array of tasks to exclude. The default is an empty array (i.e. run all tasks).
652 |
653 | .PARAMETER IncludeTasks
654 | Array of tasks to include. At least one task must be specified.
655 |
656 | .PARAMETER WUParameters
657 | Hashtable of additional parameters to pass to Install-WindowsUpdate.
658 |
659 | The -IgnoreReboot and -AcceptAll parameters are set by default.
660 |
661 | Only used if the WindowsUpdates task is selected.
662 |
663 | .EXAMPLE
664 | Invoke-VitalMaintenance -IncludeTasks WindowsUpdates, SysinternalsSuite -WUParameters @{NotTitle = 'Silverlight'}
665 |
666 | Only install Windows updates and the latest Sysinternals utilities. Exclude updates with Silverlight in the title.
667 |
668 | .NOTES
669 | Selected maintenance tasks are run in the following order:
670 | - WindowsUpdates
671 | - ComponentStoreCleanup
672 | - DotNetQueuedItems
673 | - PowerShellHelp
674 | - SysinternalsSuite
675 | - ClearInternetExplorerCache
676 | - DeleteErrorReports
677 | - DeleteTemporaryFiles
678 | - EmptyRecycleBin
679 |
680 | .LINK
681 | https://github.com/ralish/PSWinVitals
682 | #>
683 |
684 | [CmdletBinding(DefaultParameterSetName = 'OptOut')]
685 | [OutputType([PSCustomObject])]
686 | Param(
687 | [Parameter(ParameterSetName = 'OptOut')]
688 | [ValidateSet(
689 | 'ComponentStoreCleanup',
690 | 'ClearInternetExplorerCache',
691 | 'DeleteErrorReports',
692 | 'DeleteTemporaryFiles',
693 | 'DotNetQueuedItems',
694 | 'EmptyRecycleBin',
695 | 'PowerShellHelp',
696 | 'SysinternalsSuite',
697 | 'WindowsUpdates'
698 | )]
699 | [String[]]$ExcludeTasks,
700 |
701 | [Parameter(ParameterSetName = 'OptIn', Mandatory)]
702 | [ValidateSet(
703 | 'ComponentStoreCleanup',
704 | 'ClearInternetExplorerCache',
705 | 'DeleteErrorReports',
706 | 'DeleteTemporaryFiles',
707 | 'DotNetQueuedItems',
708 | 'EmptyRecycleBin',
709 | 'PowerShellHelp',
710 | 'SysinternalsSuite',
711 | 'WindowsUpdates'
712 | )]
713 | [String[]]$IncludeTasks,
714 |
715 | [ValidateNotNull()]
716 | [Hashtable]$WUParameters = @{}
717 | )
718 |
719 | if (!(Test-IsAdministrator)) {
720 | throw 'You must have administrator privileges to perform system maintenance.'
721 | }
722 |
723 | $Tasks = @{
724 | ClearInternetExplorerCache = $null
725 | ComponentStoreCleanup = $null
726 | DeleteErrorReports = $null
727 | DeleteTemporaryFiles = $null
728 | DotNetQueuedItems = $null
729 | EmptyRecycleBin = $null
730 | PowerShellHelp = $null
731 | SysinternalsSuite = $null
732 | WindowsUpdates = $null
733 | }
734 |
735 | $TasksDone = 0
736 | $TasksTotal = 0
737 |
738 | foreach ($Task in @($Tasks.Keys)) {
739 | if ($PSCmdlet.ParameterSetName -eq 'OptOut') {
740 | if ($ExcludeTasks -contains $Task) {
741 | $Tasks[$Task] = $false
742 | } else {
743 | $Tasks[$Task] = $true
744 | $TasksTotal++
745 | }
746 | } else {
747 | if ($IncludeTasks -contains $Task) {
748 | $Tasks[$Task] = $true
749 | $TasksTotal++
750 | } else {
751 | $Tasks[$Task] = $false
752 | }
753 | }
754 | }
755 |
756 | $VitalMaintenance = [PSCustomObject]@{
757 | ClearInternetExplorerCache = $null
758 | ComponentStoreCleanup = $null
759 | DeleteErrorReports = $null
760 | DeleteTemporaryFiles = $null
761 | DotNetQueuedItems = $null
762 | EmptyRecycleBin = $null
763 | PowerShellHelp = $null
764 | SysinternalsSuite = $null
765 | WindowsUpdates = $null
766 | }
767 | $VitalMaintenance.PSObject.TypeNames.Insert(0, 'PSWinVitals.VitalMaintenance')
768 |
769 | $WriteProgressParams = @{
770 | Activity = 'Running vital maintenance'
771 | }
772 |
773 | if ($Tasks['WindowsUpdates']) {
774 | try {
775 | Import-Module -Name 'PSWindowsUpdate' -ErrorAction Stop -Verbose:$false
776 | } catch [IO.FileNotFoundException] {
777 | Write-Warning -Message 'Unable to install Windows updates as PSWindowsUpdate module not available.'
778 | $VitalMaintenance.WindowsUpdates = $false
779 | }
780 |
781 | if ($null -eq $VitalMaintenance.WindowsUpdates) {
782 | Write-Progress @WriteProgressParams -Status 'Installing Windows updates' -PercentComplete ($TasksDone / $TasksTotal * 100)
783 | $WindowsUpdates = Install-WindowsUpdate -IgnoreReboot -AcceptAll @WUParameters
784 |
785 | if ($null -ne $WindowsUpdates -and $WindowsUpdates.Count -gt 0) {
786 | $VitalMaintenance.WindowsUpdates = [Array]$WindowsUpdates
787 | } else {
788 | $VitalMaintenance.WindowsUpdates = @()
789 | }
790 | }
791 |
792 | $TasksDone++
793 | }
794 |
795 | if ($Tasks['ComponentStoreCleanup']) {
796 | Write-Progress @WriteProgressParams -Status 'Running component store clean-up' -PercentComplete ($TasksDone / $TasksTotal * 100)
797 | $VitalMaintenance.ComponentStoreCleanup = Invoke-DISM -Operation StartComponentCleanup
798 | $TasksDone++
799 | }
800 |
801 | if ($Tasks['DotNetQueuedItems']) {
802 | Write-Progress @WriteProgressParams -Status 'Running .NET Framework queued compilation jobs' -PercentComplete ($TasksDone / $TasksTotal * 100)
803 | $VitalMaintenance.DotNetQueuedItems = Invoke-NGEN
804 | $TasksDone++
805 | }
806 |
807 | if ($Tasks['PowerShellHelp']) {
808 | Write-Progress @WriteProgressParams -Status 'Updating PowerShell help' -PercentComplete ($TasksDone / $TasksTotal * 100)
809 | try {
810 | Update-Help -Force -ErrorAction Stop
811 | $VitalMaintenance.PowerShellHelp = $true
812 | } catch {
813 | # Many modules don't define the HelpInfoUri key in their manifest,
814 | # which will cause Update-Help to log an error. This should really
815 | # be treated as a warning.
816 | $VitalMaintenance.PowerShellHelp = $_.Exception.Message
817 | }
818 | $TasksDone++
819 | }
820 |
821 | if ($Tasks['SysinternalsSuite']) {
822 | Write-Progress @WriteProgressParams -Status 'Updating Sysinternals suite' -PercentComplete ($TasksDone / $TasksTotal * 100)
823 | $VitalMaintenance.SysinternalsSuite = Update-Sysinternals
824 | $TasksDone++
825 | }
826 |
827 | if ($Tasks['ClearInternetExplorerCache']) {
828 | if (Get-Command -Name 'inetcpl.cpl' -ErrorAction Ignore) {
829 | Write-Progress @WriteProgressParams -Status 'Clearing Internet Explorer cache' -PercentComplete ($TasksDone / $TasksTotal * 100)
830 | # More details on the bitmask here:
831 | # https://github.com/SeleniumHQ/selenium/blob/master/cpp/iedriver/BrowserFactory.cpp
832 | $RunDll32Path = Join-Path -Path $env:SystemRoot -ChildPath 'System32\rundll32.exe'
833 | Start-Process -FilePath $RunDll32Path -ArgumentList 'inetcpl.cpl,ClearMyTracksByProcess', '9FF' -Wait
834 | $VitalMaintenance.ClearInternetExplorerCache = $true
835 | } else {
836 | Write-Warning -Message 'Unable to clear Internet Explorer cache as Control Panel applet not available.'
837 | $VitalMaintenance.ClearInternetExplorerCache = $false
838 | }
839 | $TasksDone++
840 | }
841 |
842 | if ($Tasks['DeleteErrorReports']) {
843 | Write-Progress @WriteProgressParams -Status 'Deleting error reports' -PercentComplete ($TasksDone / $TasksTotal * 100)
844 |
845 | $SystemReports = Join-Path -Path $env:ProgramData -ChildPath 'Microsoft\Windows\WER'
846 | $SystemQueue = Join-Path -Path $SystemReports -ChildPath 'ReportQueue'
847 | $SystemArchive = Join-Path -Path $SystemReports -ChildPath 'ReportArchive'
848 | foreach ($Path in @($SystemQueue, $SystemArchive)) {
849 | if (Test-Path -Path $Path -PathType Container) {
850 | Remove-Item -Path "$Path\*" -Recurse -ErrorAction Ignore
851 | }
852 | }
853 |
854 | $UserReports = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\Windows\WER'
855 | $UserQueue = Join-Path -Path $UserReports -ChildPath 'ReportQueue'
856 | $UserArchive = Join-Path -Path $UserReports -ChildPath 'ReportArchive'
857 | foreach ($Path in @($UserQueue, $UserArchive)) {
858 | if (Test-Path -Path $Path -PathType Container) {
859 | Remove-Item -Path "$Path\*" -Recurse -ErrorAction Ignore
860 | }
861 | }
862 |
863 | $VitalMaintenance.DeleteErrorReports = $true
864 | $TasksDone++
865 | }
866 |
867 | if ($Tasks['DeleteTemporaryFiles']) {
868 | Write-Progress @WriteProgressParams -Status 'Deleting temporary files' -PercentComplete ($TasksDone / $TasksTotal * 100)
869 |
870 | $SystemTemp = [Environment]::GetEnvironmentVariable('Temp', [EnvironmentVariableTarget]::Machine)
871 | Remove-Item -Path "$SystemTemp\*" -Recurse -ErrorAction Ignore
872 | Remove-Item -Path "$env:TEMP\*" -Recurse -ErrorAction Ignore
873 |
874 | $VitalMaintenance.DeleteTemporaryFiles = $true
875 | $TasksDone++
876 | }
877 |
878 | if ($Tasks['EmptyRecycleBin']) {
879 | if (Get-Command -Name 'Clear-RecycleBin' -ErrorAction Ignore) {
880 | Write-Progress @WriteProgressParams -Status 'Emptying Recycle Bin' -PercentComplete ($TasksDone / $TasksTotal * 100)
881 | try {
882 | Clear-RecycleBin -Force -ErrorAction Stop
883 | $VitalMaintenance.EmptyRecycleBin = $true
884 | } catch [ComponentModel.Win32Exception] {
885 | # Sometimes clearing the Recycle Bin fails with an exception
886 | # indicating the Recycle Bin directory doesn't exist. Only a
887 | # generic E_FAIL exception is thrown though, so inspect the
888 | # actual exception message to be sure.
889 | if ($_.Exception.Message -eq 'The system cannot find the path specified') {
890 | $VitalMaintenance.EmptyRecycleBin = $true
891 | } else {
892 | $VitalMaintenance.EmptyRecycleBin = $_.Exception.Message
893 | }
894 | }
895 | } else {
896 | Write-Warning -Message 'Unable to empty Recycle Bin as Clear-RecycleBin cmdlet not available.'
897 | $VitalMaintenance.EmptyRecycleBin = $false
898 | }
899 | $TasksDone++
900 | }
901 |
902 | Write-Progress @WriteProgressParams -Completed
903 | return $VitalMaintenance
904 | }
905 |
906 | Function Get-HypervisorInfo {
907 | [CmdletBinding()]
908 | [OutputType([Boolean], [PSCustomObject])]
909 | Param()
910 |
911 | $LogPrefix = 'HypervisorInfo'
912 | $HypervisorInfo = [PSCustomObject]@{
913 | Vendor = $null
914 | Hypervisor = $null
915 | ToolsVersion = $null
916 | }
917 |
918 | $ComputerSystem = Get-CimInstance -ClassName 'Win32_ComputerSystem' -Verbose:$false
919 | $Manufacturer = $ComputerSystem.Manufacturer
920 | $Model = $ComputerSystem.Model
921 |
922 | # Useful:
923 | # http://git.annexia.org/?p=virt-what.git;a=blob_plain;f=virt-what.in;hb=HEAD
924 | if ($Manufacturer -eq 'Microsoft Corporation' -and $Model -eq 'Virtual Machine') {
925 | $HypervisorInfo.Vendor = 'Microsoft'
926 | $HypervisorInfo.Hypervisor = 'Hyper-V'
927 |
928 | $IntegrationServicesVersion = $false
929 | $VMInfoRegPath = 'HKLM:\Software\Microsoft\Virtual Machine\Auto'
930 | if (Test-Path -Path $VMInfoRegPath -PathType Container) {
931 | $VMInfo = Get-ItemProperty -Path $VMInfoRegPath
932 | if ($VMInfo.PSObject.Properties['IntegrationServicesVersion']) {
933 | $IntegrationServicesVersion = $VMInfo.IntegrationServicesVersion
934 | }
935 | }
936 |
937 | if ($IntegrationServicesVersion) {
938 | $HypervisorInfo.ToolsVersion = $VMinfo.IntegrationServicesVersion
939 | } else {
940 | Write-Warning -Message ('[{0}] Detected Microsoft Hyper-V but unable to determine Integration Services version.' -f $LogPrefix)
941 | }
942 | } elseif ($Manufacturer -eq 'VMware, Inc.' -and $Model -match '^VMware') {
943 | $HypervisorInfo.Vendor = 'VMware'
944 | $HypervisorInfo.Hypervisor = 'Unknown'
945 |
946 | $VMwareToolboxCmd = Join-Path -Path $env:ProgramFiles -ChildPath 'VMware\VMware Tools\VMwareToolboxCmd.exe'
947 | if (Test-Path -Path $VMwareToolboxCmd -PathType Leaf) {
948 | $HypervisorInfo.ToolsVersion = & $VMwareToolboxCmd -v
949 | } else {
950 | Write-Warning -Message ('[{0}] Detected a VMware hypervisor but unable to determine VMware Tools version.' -f $LogPrefix)
951 | }
952 | } else {
953 | Write-Verbose -Message ('[{0}] Either not running in a hypervisor or hypervisor not recognised.' -f $LogPrefix)
954 | return $false
955 | }
956 |
957 | return $HypervisorInfo
958 | }
959 |
960 | Function Get-InstalledPrograms {
961 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')]
962 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
963 | [CmdletBinding()]
964 | [OutputType([Object[]])]
965 | Param()
966 |
967 | Add-NativeMethods
968 |
969 | # System-wide in native bitness
970 | $ComputerNativeRegPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
971 | # System-wide under the 32-bit emulation layer (64-bit Windows only)
972 | $ComputerWow64RegPath = 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
973 |
974 | # Retrieve all installed programs from available keys
975 | $UninstallKeys = Get-ChildItem -Path $ComputerNativeRegPath
976 | if (Test-Path -Path $ComputerWow64RegPath -PathType Container) {
977 | $UninstallKeys += Get-ChildItem -Path $ComputerWow64RegPath
978 | }
979 |
980 | # Filter out all the uninteresting installations
981 | $InstalledPrograms = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
982 | foreach ($UninstallKey in $UninstallKeys) {
983 | $Program = Get-ItemProperty -Path $UninstallKey.PSPath
984 |
985 | # Skip any program which doesn't define a display name
986 | if (!$Program.PSObject.Properties['DisplayName']) {
987 | continue
988 | }
989 |
990 | # Skip any program without an uninstall command which is not marked non-removable
991 | if (!($Program.PSObject.Properties['UninstallString'] -or ($Program.PSObject.Properties['NoRemove'] -and $Program.NoRemove -eq 1))) {
992 | continue
993 | }
994 |
995 | # Skip any program which defines a parent program
996 | if ($Program.PSObject.Properties['ParentKeyName'] -or $Program.PSObject.Properties['ParentDisplayName']) {
997 | continue
998 | }
999 |
1000 | # Skip any program marked as a system component
1001 | if ($Program.PSObject.Properties['SystemComponent'] -and $Program.SystemComponent -eq 1) {
1002 | continue
1003 | }
1004 |
1005 | # Skip any program which defines a release type
1006 | if ($Program.PSObject.Properties['ReleaseType']) {
1007 | continue
1008 | }
1009 |
1010 | $InstalledProgram = [PSCustomObject]@{
1011 | PSPath = $Program.PSPath
1012 | Name = $Program.DisplayName
1013 | Publisher = $null
1014 | InstallDate = $null
1015 | EstimatedSize = $null
1016 | Version = $null
1017 | Location = $null
1018 | Uninstall = $null
1019 | }
1020 | $InstalledProgram.PSObject.TypeNames.Insert(0, 'PSWinVitals.InstalledProgram')
1021 |
1022 | if ($Program.PSObject.Properties['Publisher']) {
1023 | $InstalledProgram.Publisher = $Program.Publisher
1024 | }
1025 |
1026 | # Try and convert the InstallDate value to a DateTime
1027 | if ($Program.PSObject.Properties['InstallDate']) {
1028 | $RegInstallDate = $Program.InstallDate
1029 | if ($RegInstallDate -match '^[0-9]{8}') {
1030 | try {
1031 | $InstalledProgram.InstallDate = New-Object -TypeName 'DateTime' -ArgumentList $RegInstallDate.Substring(0, 4), $RegInstallDate.Substring(4, 2), $RegInstallDate.Substring(6, 2)
1032 | } catch { }
1033 | }
1034 |
1035 | if (!$InstalledProgram.InstallDate) {
1036 | Write-Warning -Message ('[{0}] Registry key has invalid value for InstallDate: {1}' -f $Program.DisplayName, $RegInstallDate)
1037 | }
1038 | }
1039 |
1040 | # Fall back to the last write time of the registry key
1041 | if (!$InstalledProgram.InstallDate) {
1042 | [UInt64]$RegLastWriteTime = 0
1043 | $Status = [PSWinVitals.NativeMethods]::RegQueryInfoKey($UninstallKey.Handle, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, [Ref]$RegLastWriteTime)
1044 |
1045 | if ($Status -eq 0) {
1046 | $InstalledProgram.InstallDate = [DateTime]::FromFileTime($RegLastWriteTime)
1047 | } else {
1048 | Write-Warning -Message ('[{0}] Retrieving registry key last write time failed with status: {1}' -f $Program.DisplayName, $Status)
1049 | }
1050 | }
1051 |
1052 | if ($Program.PSObject.Properties['EstimatedSize']) {
1053 | $InstalledProgram.EstimatedSize = $Program.EstimatedSize
1054 | }
1055 |
1056 | if ($Program.PSObject.Properties['DisplayVersion']) {
1057 | $InstalledProgram.Version = $Program.DisplayVersion
1058 | }
1059 |
1060 | if ($Program.PSObject.Properties['InstallLocation']) {
1061 | $InstalledProgram.Location = $Program.InstallLocation
1062 | }
1063 |
1064 | if ($Program.PSObject.Properties['UninstallString']) {
1065 | $InstalledProgram.Uninstall = $Program.UninstallString
1066 | }
1067 |
1068 | $InstalledPrograms.Add($InstalledProgram)
1069 | }
1070 |
1071 | return , @($InstalledPrograms.ToArray() | Sort-Object -Property Name)
1072 | }
1073 |
1074 | Function Get-KernelCrashDumps {
1075 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1076 | [CmdletBinding()]
1077 | [OutputType([PSCustomObject])]
1078 | Param()
1079 |
1080 | $LogPrefix = 'KernelCrashDumps'
1081 | $KernelCrashDumps = [PSCustomObject]@{
1082 | MemoryDump = $null
1083 | Minidumps = $null
1084 | }
1085 | $KernelCrashDumps.PSObject.TypeNames.Insert(0, 'PSWinVitals.KernelCrashDumps')
1086 |
1087 | $CrashControlRegPath = 'HKLM:\System\CurrentControlSet\Control\CrashControl'
1088 |
1089 | if (Test-Path -Path $CrashControlRegPath -PathType Container) {
1090 | $CrashControl = Get-ItemProperty -Path $CrashControlRegPath
1091 |
1092 | if ($CrashControl.PSObject.Properties['DumpFile']) {
1093 | $DumpFile = $CrashControl.DumpFile
1094 | } else {
1095 | $DumpFile = Join-Path -Path $env:SystemRoot -ChildPath 'MEMORY.DMP'
1096 | Write-Warning -Message ("[{0}] Guessing the location as DumpFile value doesn't exist under the CrashControl registry key." -f $LogPrefix)
1097 | }
1098 |
1099 | if ($CrashControl.PSObject.Properties['MinidumpDir']) {
1100 | $MinidumpDir = $CrashControl.MinidumpDir
1101 | } else {
1102 | $DumpFile = Join-Path -Path $env:SystemRoot -ChildPath 'Minidump'
1103 | Write-Warning -Message ("[{0}] Guessing the location as MinidumpDir value doesn't exist under CrashControl registry key." -f $LogPrefix)
1104 | }
1105 | } else {
1106 | Write-Warning -Message ("[{0}] Guessing dump locations as the CrashControl registry key doesn't exist." -f $LogPrefix)
1107 | }
1108 |
1109 | if (Test-Path -Path $DumpFile -PathType Leaf) {
1110 | $KernelCrashDumps.MemoryDump = Get-Item -Path $DumpFile
1111 | }
1112 |
1113 | if (Test-Path -Path $MinidumpDir -PathType Container) {
1114 | $KernelCrashDumps.Minidumps = @(Get-ChildItem -Path $MinidumpDir)
1115 | }
1116 |
1117 | return $KernelCrashDumps
1118 | }
1119 |
1120 | Function Get-ServiceCrashDumps {
1121 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1122 | [CmdletBinding()]
1123 | [OutputType([Object[]])]
1124 | Param()
1125 |
1126 | $LogPrefix = 'ServiceCrashDumps'
1127 | $ServiceCrashDumps = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
1128 |
1129 | $ServiceCrashDumps.Add((Get-UserProfileCrashDumps -Sid 'S-1-5-18' -Name 'LocalSystem' -LogPrefix $LogPrefix))
1130 | $ServiceCrashDumps.Add((Get-UserProfileCrashDumps -Sid 'S-1-5-19' -Name 'LocalService' -LogPrefix $LogPrefix))
1131 | $ServiceCrashDumps.Add((Get-UserProfileCrashDumps -Sid 'S-1-5-20' -Name 'NetworkService' -LogPrefix $LogPrefix))
1132 |
1133 | return , $ServiceCrashDumps.ToArray()
1134 | }
1135 |
1136 | Function Get-UserCrashDumps {
1137 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1138 | [CmdletBinding()]
1139 | [OutputType([Object[]])]
1140 | Param()
1141 |
1142 | $LogPrefix = 'UserCrashDumps'
1143 | $UserCrashDumps = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
1144 |
1145 | $ProfileList = Get-Item -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList'
1146 | $UserSids = $ProfileList.GetSubKeyNames() | Where-Object { $_ -match '^S-1-5-21-' }
1147 | foreach ($UserSid in $UserSids) {
1148 | $UserCrashDumps.Add((Get-UserProfileCrashDumps -Sid $UserSid -LogPrefix $LogPrefix))
1149 | }
1150 |
1151 | return , $UserCrashDumps.ToArray()
1152 | }
1153 |
1154 | Function Get-UserProfileCrashDumps {
1155 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1156 | [CmdletBinding()]
1157 | [OutputType([Void], [PSCustomObject])]
1158 | Param(
1159 | [Parameter(Mandatory)]
1160 | [String]$Sid,
1161 |
1162 | [ValidateNotNullOrEmpty()]
1163 | [String]$Name,
1164 |
1165 | [ValidateNotNullOrEmpty()]
1166 | [String]$LogPrefix = 'UserProfileCrashDumps'
1167 | )
1168 |
1169 | $UserProfileRegPath = Join-Path -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList' -ChildPath $Sid
1170 | try {
1171 | $UserProfile = Get-ItemProperty -Path $UserProfileRegPath -ErrorAction Stop
1172 | } catch {
1173 | Write-Warning -Message ('[{0}] Failed to retrieve user profile information for SID: {1}' -f $LogPrefix, $Sid)
1174 | return
1175 | }
1176 |
1177 | if ($UserProfile.PSObject.Properties['ProfileImagePath']) {
1178 | $ProfileImagePath = $UserProfile.ProfileImagePath
1179 | } else {
1180 | Write-Warning -Message ('[{0}] User profile information has no ProfileImagePath for SID: {1}' -f $LogPrefix, $Sid)
1181 | return
1182 | }
1183 |
1184 | if (!$Name) {
1185 | $Name = Split-Path -Path $ProfileImagePath -Leaf
1186 | }
1187 |
1188 | $CrashDumps = [PSCustomObject]@{
1189 | Name = $Name
1190 | Crashdumps = $null
1191 | }
1192 | $CrashDumps.PSObject.TypeNames.Insert(0, 'PSWinVitals.UserProfileCrashDumps')
1193 |
1194 | $CrashDumpsPath = Join-Path -Path $ProfileImagePath -ChildPath 'AppData\Local\CrashDumps'
1195 | try {
1196 | $CrashDumps.CrashDumps = @(Get-ChildItem -Path $CrashDumpsPath -ErrorAction Stop)
1197 | } catch {
1198 | Write-Verbose -Message ('[{0}] The crash dumps path for the user does not exist: {1}' -f $LogPrefix, $Name)
1199 | }
1200 |
1201 | return $CrashDumps
1202 | }
1203 |
1204 | Function Invoke-CHKDSK {
1205 | [CmdletBinding()]
1206 | [OutputType([Object[]])]
1207 | Param(
1208 | [ValidateSet('Scan', 'Verify')]
1209 | [String]$Operation = 'Scan'
1210 | )
1211 |
1212 | # We could use the Repair-Volume cmdlet introduced in Windows 8 and Server
1213 | # 2012, but it's just a thin wrapper around CHKDSK and only exposes a small
1214 | # subset of its underlying functionality.
1215 | $LogPrefix = 'CHKDSK'
1216 |
1217 | # Supported file systems for scanning for errors (Verify)
1218 | $SupportedFileSystems = @('exFAT', 'FAT', 'FAT16', 'FAT32', 'NTFS', 'NTFS4', 'NTFS5')
1219 | # Supported file system for scanning for errors and fixing (Scan)
1220 | #
1221 | # FAT volumes don't support online repair so fixing errors means
1222 | # dismounting the volume. No parameter equivalent to "dismount only if
1223 | # safe" exists so for now we don't support reparing these volumes.
1224 | $ScanSupportedFileSystems = @('NTFS', 'NTFS4', 'NTFS5')
1225 |
1226 | $Volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.FileSystem -in $SupportedFileSystems }
1227 |
1228 | $Results = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
1229 | foreach ($Volume in $Volumes) {
1230 | $VolumePath = $Volume.Path.TrimEnd('\')
1231 |
1232 | if ($Operation -eq 'Scan' -and $Volume.FileSystem -notin $ScanSupportedFileSystems) {
1233 | Write-Warning -Message ('[{0}] Skipping volume as non-interactive repair of {1} file systems is unsupported: {2}' -f $LogPrefix, $Volume.FileSystem, $VolumePath)
1234 | continue
1235 | }
1236 |
1237 | if ($Operation -eq 'Scan' -and $VolumePath -eq '\\?\Volume{629458e4-0000-0000-0000-010000000000}') {
1238 | Write-Warning -Message ('[{0}] Skipping {1} volume as shadow copying the volume is not supported.' -f $LogPrefix, $Volume.FileSystemLabel)
1239 | continue
1240 | }
1241 |
1242 | $CHKDSK = [PSCustomObject]@{
1243 | Operation = $Operation
1244 | VolumePath = $VolumePath
1245 | Output = $null
1246 | ExitCode = $null
1247 | }
1248 | $CHKDSK.PSObject.TypeNames.Insert(0, 'PSWinVitals.CHKDSK')
1249 |
1250 | Write-Verbose -Message ('[{0}] Running {1} operation on: {2}' -f $LogPrefix, $Operation.ToLower(), $VolumePath)
1251 | $ChkDskPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\chkdsk.exe'
1252 | if ($Operation -eq 'Scan') {
1253 | $CHKDSK.Output = & $ChkDskPath $VolumePath /scan
1254 | } else {
1255 | $CHKDSK.Output = & $ChkDskPath $VolumePath
1256 | }
1257 | $CHKDSK.ExitCode = $LASTEXITCODE
1258 |
1259 | switch ($CHKDSK.ExitCode) {
1260 | 0 { continue }
1261 | 2 { Write-Warning -Message ('[{0}] Volume requires cleanup: {1}' -f $LogPrefix, $VolumePath) }
1262 | 3 { Write-Warning -Message ('[{0}] Volume contains errors: {1}' -f $LogPrefix, $VolumePath) }
1263 | default { Write-Error -Message ('[{0}] Unexpected exit code: {1}' -f $LogPrefix, $CHKDSK.ExitCode) }
1264 | }
1265 |
1266 | $Results.Add($CHKDSK)
1267 | }
1268 |
1269 | return , $Results.ToArray()
1270 | }
1271 |
1272 | Function Invoke-DISM {
1273 | [CmdletBinding()]
1274 | [OutputType([PSCustomObject])]
1275 | Param(
1276 | [Parameter(Mandatory)]
1277 | [ValidateSet('AnalyzeComponentStore', 'RestoreHealth', 'ScanHealth', 'StartComponentCleanup')]
1278 | [String]$Operation
1279 | )
1280 |
1281 | # The Dism module doesn't include cmdlets which map to the /Cleanup-Image
1282 | # functionality in the underlying Dism.exe utility, so it's necessary to
1283 | # invoke it directly.
1284 | $LogPrefix = 'DISM'
1285 | $DISM = [PSCustomObject]@{
1286 | Operation = $Operation
1287 | Output = $null
1288 | ExitCode = $null
1289 | }
1290 | $DISM.PSObject.TypeNames.Insert(0, 'PSWinVitals.DISM')
1291 |
1292 | Write-Verbose -Message ('[{0}] Running {1} operation ...' -f $LogPrefix, $Operation)
1293 | $DismPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\dism.exe'
1294 | $DISM.Output = & $DismPath /Online /Cleanup-Image /$Operation
1295 | $DISM.ExitCode = $LASTEXITCODE
1296 |
1297 | switch ($DISM.ExitCode) {
1298 | 0 { continue }
1299 | -2146498554 { Write-Warning -Message ('[{0}] The operation could not be completed due to pending operations.' -f $LogPrefix, $DISM.ExitCode) }
1300 | default { Write-Error -Message ('[{0}] Returned non-zero exit code: {1}' -f $LogPrefix, $DISM.ExitCode) }
1301 | }
1302 |
1303 | return $DISM
1304 | }
1305 |
1306 | Function Invoke-NGEN {
1307 | [CmdletBinding()]
1308 | [OutputType([PSCustomObject[]])]
1309 | Param()
1310 |
1311 | $LogPrefix = 'NGEN'
1312 | $Versions = @('v2.0.50727', 'v4.0.30319')
1313 |
1314 | $Architectures = @('Framework')
1315 | if (Test-IsWindows64bit) {
1316 | $Architectures += @('Framework64')
1317 | }
1318 |
1319 | $Frameworks = New-Object -TypeName 'Collections.Generic.List[PSCustomObject]'
1320 | foreach ($Version in $Versions) {
1321 | foreach ($Architecture in $Architectures) {
1322 | $NgenPath = Join-Path -Path $env:windir -ChildPath ('Microsoft.NET\{0}\{1}\ngen.exe' -f $Architecture, $Version)
1323 | if (!(Test-Path -LiteralPath $NgenPath -PathType Leaf)) {
1324 | continue
1325 | }
1326 |
1327 | $Framework = [PSCustomObject]@{
1328 | Name = ('.NET Framework {0}' -f $Version.Substring(0, 4))
1329 | NgenPath = $NgenPath
1330 | Output = $null
1331 | ExitCode = $null
1332 | }
1333 |
1334 | if ($Architecture -eq 'Framework64') {
1335 | $Framework.Name += ' (x64)'
1336 | } else {
1337 | $Framework.Name += ' (x86)'
1338 | }
1339 |
1340 | $Framework.PSObject.TypeNames.Insert(0, 'PSWinVitals.NGEN')
1341 | $Frameworks.Add($Framework)
1342 | }
1343 | }
1344 |
1345 | foreach ($Framework in $Frameworks) {
1346 | Write-Verbose -Message ('[{0}] Running for {1} ...' -f $LogPrefix, $Framework.Name)
1347 | $Framework.Output = @(& $Framework.NgenPath executeQueuedItems /nologo)
1348 | $Framework.ExitCode = $LASTEXITCODE
1349 |
1350 | switch ($Framework.ExitCode) {
1351 | 0 { continue }
1352 | default { Write-Error -Message ('[{0}] Running for {1} returned non-zero exit code: {2}' -f $LogPrefix, , $Framework.Name, $Framework.ExitCode) }
1353 | }
1354 | }
1355 |
1356 | return $Frameworks
1357 | }
1358 |
1359 | Function Invoke-SFC {
1360 | [CmdletBinding()]
1361 | [OutputType([PSCustomObject])]
1362 | Param(
1363 | [ValidateSet('Scan', 'Verify')]
1364 | [String]$Operation = 'Scan'
1365 | )
1366 |
1367 | <#
1368 | SFC is a horror show when it comes to capturing its output:
1369 |
1370 | 1. In contrast to most (every?) other built-in Windows console app, SFC
1371 | output is UTF-16LE. PowerShell is probably expecting windows-1252 (a
1372 | superset of ASCII), so the output will be decoded incorrectly. Fix
1373 | this by temporarily setting the OutputEncoding property of the
1374 | Console static class to Unicode, which specifies the character
1375 | encoding used by native applications.
1376 |
1377 | 2. It outputs \r\r\n sequences for newlines (yes, really). PowerShell
1378 | interprets this character sequence as two newlines so the output
1379 | must be filtered to remove the extras.
1380 |
1381 | 3. When running in a remote session via WinRM we're not running under a
1382 | console, which results in an invalid handle exception on setting
1383 | [Console]::OutputEncoding. Actually, that's not entirely true; it
1384 | works when setting it to [Text.Encoding]::Unicode (i.e. UTF-16LE),
1385 | which is what we want, but will throw an exception on changing it
1386 | back to anything else (including the original value). This causes
1387 | broken output for any subsequent app that's called (except SFC).
1388 |
1389 | The solution to this craziness is to manually allocate a console
1390 | with AllocConsole() and free it with FreeConsole(). This spawns a
1391 | ConsoleHost.exe process allowing us to set [Console]::OutputEncoding
1392 | without hitting an invalid handle exception. SFC spawns a Console
1393 | Host itself anyway if it doesn't inherit a console from the parent
1394 | process, so this happens regardless; we're just attaching a console
1395 | to PowerShell directly instead.
1396 |
1397 | Bonus extra confusion: you'll probably find SFC works just fine if you
1398 | invoke it directly in PowerShell. That's because the problem happens
1399 | when *redirecting* the output. It seems that if SFC output is not being
1400 | redirected it just directly writes to the console via WriteConsole().
1401 | Except under WinRM, where it's always broken, presumably because its
1402 | output is being redirected at some level being under a remote session.
1403 |
1404 | Useful references:
1405 | - https://stackoverflow.com/a/57751203/8787985
1406 | - https://computerexpress1.blogspot.com/2017/11/powershell-and-cyrillic-in-console.html
1407 | #>
1408 |
1409 | Add-NativeMethods
1410 |
1411 | $LogPrefix = 'SFC'
1412 | $SFC = [PSCustomObject]@{
1413 | Operation = $Operation
1414 | Output = $null
1415 | ExitCode = $null
1416 | }
1417 | $SFC.PSObject.TypeNames.Insert(0, 'PSWinVitals.SFC')
1418 |
1419 | $AllocatedConsole = $false
1420 | $DefaultOutputEncoding = [Console]::OutputEncoding
1421 |
1422 | # If AllocConsole() returns false a console is probably already attached
1423 | if ([PSWinVitals.NativeMethods]::AllocConsole()) {
1424 | Write-Debug -Message ('[{0}] Allocated a new console.' -f $LogPrefix, $Operation.ToLower())
1425 | $AllocatedConsole = $true
1426 | }
1427 |
1428 | Write-Debug -Message ('[{0}] Setting console output encoding to Unicode.' -f $LogPrefix, $Operation.ToLower())
1429 | [Console]::OutputEncoding = [Text.Encoding]::Unicode
1430 |
1431 | Write-Verbose -Message ('[{0}] Running {1} operation ...' -f $LogPrefix, $Operation.ToLower())
1432 | $SfcPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\sfc.exe'
1433 | if ($Operation -eq 'Scan') {
1434 | $SfcParam = '/SCANNOW'
1435 | } else {
1436 | $SfcParam = '/VERIFYONLY'
1437 | }
1438 | # Remove the duplicate newlines and split on them for a string array output
1439 | $SFC.Output = ((& $SfcPath $SfcParam) -join "`r`n" -replace "`r`n`r`n", "`r`n") -split "`r`n"
1440 | $SFC.ExitCode = $LASTEXITCODE
1441 |
1442 | Write-Debug -Message ('[{0}] Restoring original console output encoding.' -f $LogPrefix, $Operation.ToLower())
1443 | [Console]::OutputEncoding = $DefaultOutputEncoding
1444 |
1445 | if ($AllocatedConsole) {
1446 | Write-Debug -Message ('[{0}] Freeing allocated console.' -f $LogPrefix, $Operation.ToLower())
1447 | if (![PSWinVitals.NativeMethods]::FreeConsole()) {
1448 | $Win32Error = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
1449 | Write-Error -Message ('Failed to free allocated console with error: {0}' -f $Win32Error)
1450 | }
1451 | }
1452 |
1453 | switch ($SFC.ExitCode) {
1454 | 0 { continue }
1455 | default { Write-Error -Message ('[{0}] Returned non-zero exit code: {1}' -f $LogPrefix, $SFC.ExitCode) }
1456 | }
1457 |
1458 | return $SFC
1459 | }
1460 |
1461 | Function Update-Sysinternals {
1462 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
1463 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1464 | [CmdletBinding()]
1465 | [OutputType([String])]
1466 | Param(
1467 | [ValidatePattern('^http[Ss]?://.*')]
1468 | [String]$DownloadUrl = 'https://download.sysinternals.com/files/SysinternalsSuite.zip'
1469 | )
1470 |
1471 | $LogPrefix = 'Sysinternals'
1472 | $Sysinternals = [PSCustomObject]@{
1473 | Path = $null
1474 | Version = $null
1475 | Updated = $false
1476 | }
1477 |
1478 | $DownloadDir = $env:TEMP
1479 | $DownloadFile = Split-Path -Path $DownloadUrl -Leaf
1480 | $DownloadPath = Join-Path -Path $DownloadDir -ChildPath $DownloadFile
1481 |
1482 | if (Test-IsWindows64bit) {
1483 | $InstallDir = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'Sysinternals'
1484 | } else {
1485 | $InstallDir = Join-Path -Path $env:ProgramFiles -ChildPath 'Sysinternals'
1486 | }
1487 | $Sysinternals.Path = $InstallDir
1488 |
1489 | $ExistingVersion = $false
1490 | $VersionFile = Join-Path -Path $InstallDir -ChildPath 'Version.txt'
1491 | if (Test-Path -Path $VersionFile -PathType Leaf) {
1492 | $ExistingVersion = Get-Content -Path $VersionFile
1493 | }
1494 |
1495 | Write-Verbose -Message ('[{0}] Downloading latest version from: {1}' -f $LogPrefix, $DownloadUrl)
1496 | $null = New-Item -Path $DownloadDir -ItemType Directory -ErrorAction Ignore
1497 | $WebClient = New-Object -TypeName 'Net.WebClient'
1498 | try {
1499 | $WebClient.DownloadFile($DownloadUrl, $DownloadPath)
1500 | } catch {
1501 | # Return immediately with the error message if the download fails
1502 | return $_.Exception.Message
1503 | }
1504 |
1505 | Write-Verbose -Message ('[{0}] Determining downloaded version ...' -f $LogPrefix)
1506 | Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
1507 | $Archive = [IO.Compression.ZipFile]::OpenRead($DownloadPath)
1508 | $DownloadedVersion = ($Archive.Entries.LastWriteTime | Sort-Object | Select-Object -Last 1).ToString('yyyyMMdd')
1509 | $Archive.Dispose()
1510 |
1511 | if (!$ExistingVersion -or ($DownloadedVersion -gt $ExistingVersion)) {
1512 | Write-Verbose -Message ('[{0}] Extracting archive to: {1}' -f $LogPrefix, $InstallDir)
1513 | Remove-Item -Path $InstallDir -Recurse -ErrorAction Ignore
1514 | # The -Force parameter shouldn't be necessary given we've removed any
1515 | # existing files, except sometimes the archive has files differing only
1516 | # by case. Permit overwriting of files as a workaround and we just have
1517 | # to hope any overwritten files were older.
1518 | Expand-ZipFile -FilePath $DownloadPath -DestinationPath $InstallDir -Force
1519 | Set-Content -Path $VersionFile -Value $DownloadedVersion
1520 | Remove-Item -Path $DownloadPath
1521 |
1522 | $Sysinternals.Version = $DownloadedVersion
1523 | $Sysinternals.Updated = $true
1524 | } elseif ($DownloadedVersion -eq $ExistingVersion) {
1525 | Write-Verbose -Message ('[{0}] Not updating as existing version is latest: {1}' -f $LogPrefix, $ExistingVersion)
1526 | $Sysinternals.Version = $ExistingVersion
1527 | } else {
1528 | Write-Warning -Message ('[{0}] Installed version newer than downloaded version: {1}' -f $LogPrefix, $ExistingVersion)
1529 | $Sysinternals.Version = $ExistingVersion
1530 | }
1531 |
1532 | $SystemPath = [Environment]::GetEnvironmentVariable('Path', [EnvironmentVariableTarget]::Machine)
1533 | $RegEx = [Regex]::Escape($InstallDir)
1534 | if (!($SystemPath -match "^;*$RegEx;" -or $SystemPath -match ";$RegEx;" -or $SystemPath -match ";$RegEx;*$")) {
1535 | Write-Verbose -Message ('[{0}] Updating system path ...' -f $LogPrefix)
1536 | if (!$SystemPath.EndsWith(';')) {
1537 | $SystemPath += ';'
1538 | }
1539 | $SystemPath += $InstallDir
1540 | [Environment]::SetEnvironmentVariable('Path', $SystemPath, [EnvironmentVariableTarget]::Machine)
1541 | }
1542 |
1543 | return $Sysinternals
1544 | }
1545 |
1546 | Function Add-NativeMethods {
1547 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
1548 | [CmdletBinding()]
1549 | [OutputType([Void])]
1550 | Param()
1551 |
1552 | if (!('PSWinVitals.NativeMethods' -as [Type])) {
1553 | $NativeMethods = @'
1554 | [DllImport("kernel32.dll", SetLastError = true)]
1555 | public extern static bool AllocConsole();
1556 |
1557 | [DllImport("kernel32.dll", SetLastError = true)]
1558 | public extern static bool FreeConsole();
1559 |
1560 | [DllImport("advapi32.dll", EntryPoint = "RegQueryInfoKeyW")]
1561 | public static extern int RegQueryInfoKey(Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey,
1562 | IntPtr lpClass,
1563 | IntPtr lpcchClass,
1564 | IntPtr lpReserved,
1565 | IntPtr lpcSubKeys,
1566 | IntPtr lpcbMaxSubKeyLen,
1567 | IntPtr lpcbMaxClassLen,
1568 | IntPtr lpcValues,
1569 | IntPtr lpcbMaxValueNameLen,
1570 | IntPtr lpcbMaxValueLen,
1571 | IntPtr lpcbSecurityDescriptor,
1572 | out UInt64 lpftLastWriteTime);
1573 | '@
1574 |
1575 | $AddTypeParams = @{}
1576 |
1577 | if ($PSVersionTable['PSEdition'] -eq 'Core') {
1578 | $AddTypeParams['ReferencedAssemblies'] = 'Microsoft.Win32.Registry'
1579 | }
1580 |
1581 | Add-Type -Namespace 'PSWinVitals' -Name 'NativeMethods' -MemberDefinition $NativeMethods @AddTypeParams
1582 | }
1583 | }
1584 |
1585 | Function Expand-ZipFile {
1586 | [CmdletBinding()]
1587 | [OutputType([Void])]
1588 | Param(
1589 | [Parameter(Mandatory)]
1590 | [String]$FilePath,
1591 |
1592 | [Parameter(Mandatory)]
1593 | [String]$DestinationPath,
1594 |
1595 | [Switch]$Force
1596 | )
1597 |
1598 | # The Expand-Archive cmdlet is only available from PowerShell v5.0
1599 | if ($PSVersionTable.PSVersion.Major -ge 5) {
1600 | Expand-Archive -Path $FilePath -DestinationPath $DestinationPath -Force:$Force
1601 | } else {
1602 | Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
1603 | [IO.Compression.ZipFile]::ExtractToDirectory($FilePath, $DestinationPath, $Force)
1604 | }
1605 | }
1606 |
1607 | Function Get-WindowsProductType {
1608 | [CmdletBinding()]
1609 | [OutputType([UInt32])]
1610 | Param()
1611 |
1612 | return (Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).ProductType
1613 | }
1614 |
1615 | Function Test-IsAdministrator {
1616 | [CmdletBinding()]
1617 | [OutputType([Boolean])]
1618 | Param()
1619 |
1620 | $User = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
1621 | if ($User.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
1622 | return $true
1623 | }
1624 |
1625 | return $false
1626 | }
1627 |
1628 | Function Test-IsWindows64bit {
1629 | [CmdletBinding()]
1630 | [OutputType([Boolean])]
1631 | Param()
1632 |
1633 | if ((Get-CimInstance -ClassName 'Win32_OperatingSystem' -Verbose:$false).OSArchitecture -eq '64-bit') {
1634 | return $true
1635 | }
1636 |
1637 | return $false
1638 | }
1639 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PSWinVitals
2 | ===========
3 |
4 | [](https://www.powershellgallery.com/packages/PSWinVitals)
5 | [](https://www.powershellgallery.com/packages/PSWinVitals)
6 | [](https://choosealicense.com/licenses/mit/)
7 |
8 | A PowerShell module to consolidate common system health checks, maintenance tasks & inventory retrieval.
9 |
10 | - [Requirements](#requirements)
11 | - [Installing](#installing)
12 | - [Usage](#usage)
13 | - [License](#license)
14 |
15 | Requirements
16 | ------------
17 |
18 | - PowerShell 4.0 (or later)
19 |
20 | Installing
21 | ----------
22 |
23 | ### PowerShellGet (included with PowerShell 5.0)
24 |
25 | The module is published to the [PowerShell Gallery](https://www.powershellgallery.com/packages/PSWinVitals):
26 |
27 | ```posh
28 | Install-Module -Name PSWinVitals
29 | ```
30 |
31 | ### ZIP File
32 |
33 | Download the [ZIP file](https://github.com/ralish/PSWinVitals/archive/stable.zip) of the latest release and unpack it to one of the following locations:
34 |
35 | - Current user: `C:\Users\\Documents\WindowsPowerShell\Modules\PSWinVitals`
36 | - All users: `C:\Program Files\WindowsPowerShell\Modules\PSWinVitals`
37 |
38 | ### Git Clone
39 |
40 | You can also clone the repository into one of the above locations if you'd like the ability to easily update it via Git.
41 |
42 | ### Did it work?
43 |
44 | You can check that PowerShell is able to locate the module by running the following at a PowerShell prompt:
45 |
46 | ```posh
47 | Get-Module PSWinVitals -ListAvailable
48 | ```
49 |
50 | Usage
51 | -----
52 |
53 | The module exports three functions which handle inventory retrieval, health checks, and maintenance tasks respectively. Each function returns a `PSCustomObject` with the results of the command. A summary of the capabilities of each command follows, however, please consult the built-in help of each function for comprehensive details.
54 |
55 | ### Get-VitalInformation
56 |
57 | - Retrieval of computer & operating system info
58 | - Retrieval of hypervisor details (if present)
59 | - Retrieval of hardware devices with errors
60 | - Retrieval of hardware devices which are absent
61 | - Retrieval of fixed storage volume details
62 | - Check for kernel, service, or user crash dumps
63 | - Analysis of the Windows component store
64 | - Retrieval of installed Windows features (Server SKUs only)
65 | - Retrieval of installed programs
66 | - Retrieval of environment variables
67 | - Retrieval of available Windows updates
68 | - Retrieval of installed Sysinternals version
69 |
70 | ### Invoke-VitalChecks
71 |
72 | - Run file system scans against all fixed volumes
73 | - Run Windows component store scan
74 | - Run Windows System File Checker (SFC)
75 |
76 | ### Invoke-VitalMaintenance
77 |
78 | - Install all available Windows updates
79 | - Perform Windows component store clean-up
80 | - Executes queued .NET Framework compilation jobs
81 | - Update help for all PowerShell modules
82 | - Install latest Sysinternals Suite tools
83 | - Clear Internet Explorer cache
84 | - Delete Windows Error Report files
85 | - Delete temporary files
86 | - Empty Recycle Bin
87 |
88 | License
89 | -------
90 |
91 | All content is licensed under the terms of [The MIT License](LICENSE).
92 |
--------------------------------------------------------------------------------