├── .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 | [![pwsh ver](https://img.shields.io/powershellgallery/v/PSWinVitals)](https://www.powershellgallery.com/packages/PSWinVitals) 5 | [![pwsh dl](https://img.shields.io/powershellgallery/dt/PSWinVitals)](https://www.powershellgallery.com/packages/PSWinVitals) 6 | [![license](https://img.shields.io/github/license/ralish/PSWinVitals)](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 | --------------------------------------------------------------------------------