Dark mode

Dark mode

There are 0 results matching

article card image dark article card image light

Published by · Jan 23, 2024 tools · 3 mins read

Introducing: Intune Device Renaming Tool

Rename Intune Devices by setting a Prefix or using a User Attribute as Prefix. Supports Windows, macOS, and Linux ...

See More
article card image dark article card image light

Published by · Dec 8, 2023 intune · 5 mins read

Intune Logs: A Deep Dive into Locations, Interpretation, and Configuration

A Comprehensive Guide to Locations, Interpretation, and Configuration of Intune Logs ...

See More
article card image dark article card image light

Published by · Aug 14, 2023 configmgr · 2 mins read

Configuration Manager Console Extension to show Device Collection Membership with Console Builder

Use the Configuration Manager Console Builder, to add Collection Membership View to the Device Node ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 3 mins read

Introducing: Configuration Manager SSRS Dashboards

A Configuration Manager Dashboards solution with Reports for Software Updates, Bitlocker and more ...

See More
article card image dark article card image light

Published by · Aug 3, 2023 tools · 2 mins read

Introducing: PowerShell WMI Management Toolkit Module

Streamline your WMI Namespace, Class, and Instance Management with our PowerShell Module ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 1 mins read

Configuration Manager detailed, filterable Port Documentation

Configuration Manager detailed, filterable port documentation as an excel document ...

See More
article card image dark article card image light

Published by · Jul 14, 2023 configmgr · 3 mins read

Configuration Manager PXE TFTP Window Size Bug

Configuration Manager TFTP Block Size and TFTP Window Size Correct Configuration ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 4 mins read

Introducing: Configuration Manager Client Cache Cleanup Tool

Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 2 mins read

Introducing: Windows Cache Cleanup Tool

Cleaning Windows and Configuration Manager Caches for Configuration Manager Build and Capture Task Sequence or Standalone Use ...

See More
article card image dark article card image light

Published by · Jun 17, 2023 tools · 1 mins read

Introducing: Windows Update Database Reinitialization Tool

Proactively repair corrupted Windows Update Database with Powershell and Configuration Manager ...

See More
article card image dark article card image light

Published by · Mar 31, 2023 tools · 3 mins read

Introducing: Configuration Manager SQL Products Reporting

A Complete SQL Products reporting solution using Configuration Manager ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 configmgr · 1 mins read

Application Detection Method using the Configuration Manager Application Version

Replace hardcoded application version in scripts, with the Configuration Manager Application Version ...

See More
article card image dark article card image light

Published by · Jan 28, 2023 tools · 3 mins read

Introducing: Certificate Management Toolkit

Managing Certificates with Configuration Manager and PowerShell by using just the Public Key ...

See More
article card image dark article card image light

Published by · Jan 7, 2019 reports · 2 mins read

Configuration Manager Device Boundary and Network Information Report

List Device Boundaries and Network Information with Configuration Manager ...

See More
article card image dark article card image light

Published by · Sep 9, 1980 help · 5 mins read

MEM.Zone Blog Publishing Documentation

Publishing Documentation for MEM.Zone ...

See More

We couldn’t find anything related to

“SCCM”

BLOG / tools zone

Introducing: Windows Cache Cleanup Tool

Published by Popovici Ioan · Jun 18, 2023 · 2 mins read
article card image dark article card image light

Quick Summary

Update, Component, Volume, Configuration Manager Client, and Shadow Copy all create caches that take up a lot of space.

All these caches can be safely purged without major issues. One exception is the update cache which when cleaned will make all installed updates non-removable.

This tool was specifically created for cleaning up the Windows Image during a Build and Capture task sequence, but can also be used as a standalone cleanup tool or in a Configuration Baseline.

Prerequisites


Recommendations

  • Always use a test environment to validate your configuration.
  • History is available in the Endpoint Management log, or %SystemRoot%\Logs\Start-WindowsCleanup\ folder.

Parameters

CleanupOptions

  • comCacheRepair
    Performs a Component Cache Repair.
  • comCacheCleanup
    Performs a Component Cache Cleanup.
  • volCacheCleanup
    Performs a Volume Cache Cleanup.
  • volShadowCleanup
    Performs a Volume Shadow Copy Cleanup.
  • updCacheCleanup
    Performs an Update Cache Cleanup.
  • ccmCacheCleanup
    Performs a Configuration Manager Client Cache Cleanup.
  • All
    Performs all the above-mentioned cleanup operations.
  • Recommended
    Performs some or all of the above-mentioned cleanup operations in a specific order depending on the operating system.
Notes

By using the Update Caches cleanup option, all installed updates will become NON-REMOVABLE!

Examples

Cleanup Options

Start-WindowsCleanup.ps1 -CleanupOptions "comCacheRepair", "comCacheCleanup", "updCacheCleanup", "volCacheCleanup", "ccmCacheCleanup"
Start-WindowsCleanup.ps1 -CleanupOptions "Recommended"

Full Cleanup

Start-WindowsCleanup.ps1 -CleanupOptions "All"

Preview

article card image powershell-windows-cleanup-in-progress.gif
Cleanup in Progress
article card image configmgr-build-and-caputure-task-sequence.png
Build and Capture Task Sequence Step

Code

   1<#
   2.SYNOPSIS
   3    Performs a Windows cleanup.
   4.DESCRIPTION
   5    Performs a Windows cleanup by removing volume caches, update backups, updates and CCM caches.
   6.PARAMETER CleanupOptions
   7    Supported options:
   8        "comCacheRepair"   # Component Cache Repair
   9        "comCacheCleanup"  # Component Cache Cleanup
  10        "volCacheCleanup"  # Volume Cache Cleanup
  11        "volShadowCleanup" # Volume Shadow Copy Cleanup
  12        "updCacheCleanup"  # Update Cache Cleanup
  13        "ccmCacheCleanup"  # CCM Cache Cleanup
  14        "Recommended"      # Performs some or all of the above-mentioned cleanup operations in a specific order depending on the operating system.
  15        "All"              # Performs all the above-mentioned cleanup operations.
  16    If set to "Recommended", the cleanup will be performed in the recommended order.
  17    Default is: "Recommended".
  18.EXAMPLE
  19    Start-WindowsCleanup.ps1 -CleanupOptions "comCacheRepair", "comCacheCleanup", "updCacheCleanup", "volCacheCleanup", "ccmCacheCleanup"
  20.EXAMPLE
  21    Start-WindowsCleanup.ps1 -CleanupOptions "Recommended"
  22.EXAMPLE
  23    Start-WindowsCleanup.ps1 -CleanupOptions "All"
  24.INPUTS
  25    None.
  26.OUTPUTS
  27    None.
  28.NOTES
  29    Created by Ioan Popovici
  30.LINK
  31    https://MEMZ.one/Start-WindowsCleanup-CREDIT (@mikael_nystrom [Deplyment Bunny] - Original VB Script)
  32.LINK
  33    https://MEMZ.one/Start-WindowsCleanup
  34.LINK
  35    https://MEMZ.one/Start-WindowsCleanup-CHANGELOG
  36.LINK
  37    https://MEMZ.one/Start-WindowsCleanup-GIT
  38.LINK
  39    https://MEM.Zone/ISSUES
  40.COMPONENT
  41    Windows Cleanup
  42.FUNCTIONALITY
  43    Clean Windows data and caches.
  44#>
  45
  46##*=============================================
  47##* VARIABLE DECLARATION
  48##*=============================================
  49#region VariableDeclaration
  50
  51## Set script requirements
  52#Requires -Version 3.0
  53
  54## Get script parameters
  55Param (
  56    [Parameter(Mandatory = $false)]
  57    [ValidateNotNullorEmpty()]
  58    [string[]]$CleanupOptions = "Recommended"
  59)
  60
  61## Get script path and name
  62[string]$ScriptName     = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Definition)
  63[string]$ScriptFullName = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Definition)
  64
  65## Get Show-Progress steps
  66$ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
  67$ForEachSteps  = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Keyword' -and $_.Content -eq 'ForEach' }).Count)
  68
  69## Set Show-Progress steps
  70$Script:Steps = $ProgressSteps - $ForEachSteps
  71$Script:Step  = 1
  72
  73## Set script global variables
  74$script:LoggingOptions   = 'EventLog'
  75$script:LogName          = 'Endpoint Management'
  76$script:LogSource        = $ScriptName
  77$script:LogDebugMessages = $false
  78$script:LogFileDirectory = If ($LogPath) { Join-Path -Path $LogPath -ChildPath $script:LogName } Else { $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)) }
  79
  80
  81## Initialize variables
  82[string]$StartWindowsCleanup = $null
  83
  84#endregion
  85##*=============================================
  86##* END VARIABLE DECLARATION
  87##*=============================================
  88
  89##*=============================================
  90##* FUNCTION LISTINGS
  91##*=============================================
  92#region FunctionListings
  93
  94#region Function Resolve-Error
  95Function Resolve-Error {
  96<#
  97.SYNOPSIS
  98    Enumerate error record details.
  99.DESCRIPTION
 100    Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated.
 101.PARAMETER ErrorRecord
 102    The error record to resolve. The default error record is the latest one: $global:Error[0]. This parameter will also accept an array of error records.
 103.PARAMETER Property
 104    The list of properties to display from the error record. Use "*" to display all properties.
 105    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
 106.PARAMETER GetErrorRecord
 107    Get error record details as represented by $_.
 108.PARAMETER GetErrorInvocation
 109    Get error record invocation information as represented by $_.InvocationInfo.
 110.PARAMETER GetErrorException
 111    Get error record exception details as represented by $_.Exception.
 112.PARAMETER GetErrorInnerException
 113    Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one.
 114.EXAMPLE
 115    Resolve-Error
 116.EXAMPLE
 117    Resolve-Error -Property *
 118.EXAMPLE
 119    Resolve-Error -Property InnerException
 120.EXAMPLE
 121    Resolve-Error -GetErrorInvocation:$false
 122.NOTES
 123    Unmodified version of the PADT error resolving cmdlet. I did not write the original cmdlet, please do not credit me for it!
 124.LINK
 125    https://psappdeploytoolkit.com
 126#>
 127    [CmdletBinding()]
 128    Param (
 129        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 130        [AllowEmptyCollection()]
 131        [array]$ErrorRecord,
 132        [Parameter(Mandatory = $false, Position = 1)]
 133        [ValidateNotNullorEmpty()]
 134        [string[]]$Property = ('Message', 'InnerException', 'FullyQualifiedErrorId', 'ScriptStackTrace', 'PositionMessage'),
 135        [Parameter(Mandatory = $false, Position = 2)]
 136        [switch]$GetErrorRecord = $true,
 137        [Parameter(Mandatory = $false, Position = 3)]
 138        [switch]$GetErrorInvocation = $true,
 139        [Parameter(Mandatory = $false, Position = 4)]
 140        [switch]$GetErrorException = $true,
 141        [Parameter(Mandatory = $false, Position = 5)]
 142        [switch]$GetErrorInnerException = $true
 143    )
 144
 145    Begin {
 146        ## If function was called without specifying an error record, then choose the latest error that occurred
 147        If (-not $ErrorRecord) {
 148            If ($global:Error.Count -eq 0) {
 149                #Write-Warning -Message "The `$Error collection is empty"
 150                Return
 151            }
 152            Else {
 153                [array]$ErrorRecord = $global:Error[0]
 154            }
 155        }
 156
 157        ## Allows selecting and filtering the properties on the error object if they exist
 158        [scriptblock]$SelectProperty = {
 159            Param (
 160                [Parameter(Mandatory = $true)]
 161                [ValidateNotNullorEmpty()]
 162                $InputObject,
 163                [Parameter(Mandatory = $true)]
 164                [ValidateNotNullorEmpty()]
 165                [string[]]$Property
 166            )
 167
 168            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name'
 169            ForEach ($Prop in $Property) {
 170                If ($Prop -eq '*') {
 171                    [string[]]$PropertySelection = $ObjectProperty
 172                    Break
 173                }
 174                ElseIf ($ObjectProperty -contains $Prop) {
 175                    [string[]]$PropertySelection += $Prop
 176                }
 177            }
 178            Write-Output -InputObject $PropertySelection
 179        }
 180
 181        #  Initialize variables to avoid error if 'Set-StrictMode' is set
 182        $LogErrorRecordMsg = $null
 183        $LogErrorInvocationMsg = $null
 184        $LogErrorExceptionMsg = $null
 185        $LogErrorMessageTmp = $null
 186        $LogInnerMessage = $null
 187    }
 188    Process {
 189        If (-not $ErrorRecord) { Return }
 190        ForEach ($ErrRecord in $ErrorRecord) {
 191            ## Capture Error Record
 192            If ($GetErrorRecord) {
 193                [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property
 194                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
 195            }
 196
 197            ## Error Invocation Information
 198            If ($GetErrorInvocation) {
 199                If ($ErrRecord.InvocationInfo) {
 200                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
 201                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
 202                }
 203            }
 204
 205            ## Capture Error Exception
 206            If ($GetErrorException) {
 207                If ($ErrRecord.Exception) {
 208                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property
 209                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
 210                }
 211            }
 212
 213            ## Display properties in the correct order
 214            If ($Property -eq '*') {
 215                #  If all properties were chosen for display, then arrange them in the order the error object displays them by default.
 216                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 217                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 218                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 219            }
 220            Else {
 221                #  Display selected properties in our custom order
 222                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 223                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 224                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 225            }
 226
 227            If ($LogErrorMessageTmp) {
 228                $LogErrorMessage = 'Error Record:'
 229                $LogErrorMessage += "`n-------------"
 230                $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String
 231                $LogErrorMessage += $LogErrorMsg
 232            }
 233
 234            ## Capture Error Inner Exception(s)
 235            If ($GetErrorInnerException) {
 236                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) {
 237                    $LogInnerMessage = 'Error Inner Exception(s):'
 238                    $LogInnerMessage += "`n-------------------------"
 239
 240                    $ErrorInnerException = $ErrRecord.Exception.InnerException
 241                    $Count = 0
 242
 243                    While ($ErrorInnerException) {
 244                        [string]$InnerExceptionSeperator = '~' * 40
 245
 246                        [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property
 247                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String
 248
 249                        If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator }
 250                        $LogInnerMessage += $LogErrorInnerExceptionMsg
 251
 252                        $Count++
 253                        $ErrorInnerException = $ErrorInnerException.InnerException
 254                    }
 255                }
 256            }
 257
 258            If ($LogErrorMessage) { $Output = $LogErrorMessage }
 259            If ($LogInnerMessage) { $Output += $LogInnerMessage }
 260
 261            Write-Output -InputObject $Output
 262
 263            If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' }
 264            If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' }
 265            If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' }
 266            If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' }
 267        }
 268    }
 269    End {
 270    }
 271}
 272#endregion
 273
 274#region Function Write-Log
 275Function Write-Log {
 276<#
 277.SYNOPSIS
 278    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format.
 279.DESCRIPTION
 280    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format and optionally display in the console.
 281.PARAMETER Message
 282    The message to write to the log file or output to the console.
 283.PARAMETER Severity
 284    Defines message type. When writing to console or CMTrace.exe log format, it allows highlighting of message type.
 285    Options: 1 = Information (default), 2 = Warning (highlighted in yellow), 3 = Error (highlighted in red)
 286.PARAMETER Source
 287    The source of the message being logged. Also used as the event log source.
 288.PARAMETER ScriptSection
 289    The heading for the portion of the script that is being executed. Default is: $script:installPhase.
 290.PARAMETER LogType
 291    Choose whether to write a CMTrace.exe compatible log file or a Legacy text log file.
 292.PARAMETER LoggingOptions
 293    Choose where to log 'Console', 'File', 'EventLog' or 'None'. You can choose multiple options.
 294.PARAMETER LogFileDirectory
 295    Set the directory where the log file will be saved.
 296.PARAMETER LogFileName
 297    Set the name of the log file.
 298.PARAMETER MaxLogFileSizeMB
 299    Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
 300.PARAMETER LogName
 301    Set the name of the event log.
 302.PARAMETER EventID
 303    Set the event id for the event log entry.
 304.PARAMETER WriteHost
 305    Write the log message to the console.
 306.PARAMETER ContinueOnError
 307    Suppress writing log message to console on failure to write message to log file. Default is: $true.
 308.PARAMETER PassThru
 309    Return the message that was passed to the function
 310.PARAMETER VerboseMessage
 311    Specifies that the message is a debug message. Verbose messages only get logged if -LogDebugMessage is set to $true.
 312.PARAMETER DebugMessage
 313    Specifies that the message is a debug message. Debug messages only get logged if -LogDebugMessage is set to $true.
 314.PARAMETER LogDebugMessage
 315    Debug messages only get logged if this parameter is set to $true in the config XML file.
 316.EXAMPLE
 317    Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace'
 318.EXAMPLE
 319    Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy'
 320.NOTES
 321    Slightly modified version of the PSADT logging cmdlet. I did not write the original cmdlet, please do not credit me for it.
 322.LINK
 323    https://psappdeploytoolkit.com
 324#>
 325    [CmdletBinding()]
 326    Param (
 327        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 328        [AllowEmptyCollection()]
 329        [Alias('Text')]
 330        [string[]]$Message,
 331        [Parameter(Mandatory = $false, Position = 1)]
 332        [ValidateRange(1, 3)]
 333        [int16]$Severity = 1,
 334        [Parameter(Mandatory = $false, Position = 2)]
 335        [ValidateNotNullorEmpty()]
 336        [string]$Source = $script:LogSource,
 337        [Parameter(Mandatory = $false, Position = 3)]
 338        [ValidateNotNullorEmpty()]
 339        [string]$ScriptSection = $script:RunPhase,
 340        [Parameter(Mandatory = $false, Position = 4)]
 341        [ValidateSet('CMTrace', 'Legacy')]
 342        [string]$LogType = 'CMTrace',
 343        [Parameter(Mandatory = $false, Position = 5)]
 344        [ValidateSet('Host', 'File', 'EventLog', 'None')]
 345        [string[]]$LoggingOptions = $script:LoggingOptions,
 346        [Parameter(Mandatory = $false, Position = 6)]
 347        [ValidateNotNullorEmpty()]
 348        [string]$LogFileDirectory = $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)),
 349        [Parameter(Mandatory = $false, Position = 7)]
 350        [ValidateNotNullorEmpty()]
 351        [string]$LogFileName = $($script:LogSource + '.log'),
 352        [Parameter(Mandatory = $false, Position = 8)]
 353        [ValidateNotNullorEmpty()]
 354        [int]$MaxLogFileSizeMB = '4',
 355        [Parameter(Mandatory = $false, Position = 9)]
 356        [ValidateNotNullorEmpty()]
 357        [string]$LogName = $script:LogName,
 358        [Parameter(Mandatory = $false, Position = 10)]
 359        [ValidateNotNullorEmpty()]
 360        [int32]$EventID = 1,
 361        [Parameter(Mandatory = $false, Position = 11)]
 362        [ValidateNotNullorEmpty()]
 363        [boolean]$ContinueOnError = $false,
 364        [Parameter(Mandatory = $false, Position = 12)]
 365        [switch]$PassThru = $false,
 366        [Parameter(Mandatory = $false, Position = 13)]
 367        [switch]$VerboseMessage = $false,
 368        [Parameter(Mandatory = $false, Position = 14)]
 369        [switch]$DebugMessage = $false,
 370        [Parameter(Mandatory = $false, Position = 15)]
 371        [boolean]$LogDebugMessage = $script:LogDebugMessages
 372    )
 373
 374    Begin {
 375        ## Get the name of this function
 376        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
 377
 378        ## Logging Variables
 379        #  Log file date/time
 380        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
 381        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
 382        If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes }
 383        [string]$LogTimePlusBias = $LogTime + '-' + $script:LogTimeZoneBias
 384        #  Initialize variables
 385        [boolean]$WriteHost = $false
 386        [boolean]$WriteFile = $false
 387        [boolean]$WriteEvent = $false
 388        [boolean]$DisableLogging = $false
 389        [boolean]$ExitLoggingFunction = $false
 390        If (('Host' -in $LoggingOptions) -and (-not ($VerboseMessage -or $DebugMessage))) { $WriteHost = $true }
 391        If ('File' -in $LoggingOptions) { $WriteFile = $true }
 392        If ('EventLog' -in $LoggingOptions) { $WriteEvent = $true }
 393        If ('None' -in $LoggingOptions) { $DisableLogging = $true }
 394        #  Check if the script section is defined
 395        [boolean]$ScriptSectionDefined = [boolean](-not [string]::IsNullOrEmpty($ScriptSection))
 396        #  Check if the source is defined
 397        [boolean]$SourceDefined = [boolean](-not [string]::IsNullOrEmpty($Source))
 398        #  Check if the event log and event source exit
 399        [boolean]$LogNameNotExists = (-not [System.Diagnostics.EventLog]::Exists($LogName))
 400        [boolean]$LogSourceNotExists = (-not [System.Diagnostics.EventLog]::SourceExists($Source))
 401        #  Check for overlapping log names
 402        [string[]]$OverLappingLogName = Get-EventLog -List | Where-Object -Property 'Log' -Like $($LogName.Substring(0,8) + '*') | Select-Object -ExpandProperty 'Log'
 403        If (-not [string]::IsNullOrEmpty($ScriptSection)) {
 404            Write-Warning -Message "Overlapping log names:`n$($OverLappingLogName | Out-String)"
 405            Write-Warning -Message 'Change the name of your log or use Remove-EventLog to remove the log(s) above!'
 406        }
 407
 408        ## Create script block for generating CMTrace.exe compatible log entry
 409        [scriptblock]$CMTraceLogString = {
 410            Param (
 411                [string]$lMessage,
 412                [string]$lSource,
 413                [int16]$lSeverity
 414            )
 415            "<![LOG[$lMessage]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$lSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$lSeverity`" " + "thread=`"$PID`" " + "file=`"$Source`">"
 416        }
 417
 418        ## Create script block for writing log entry to the console
 419        [scriptblock]$WriteLogLineToHost = {
 420            Param (
 421                [string]$lTextLogLine,
 422                [int16]$lSeverity
 423            )
 424            If ($WriteHost) {
 425                #  Only output using color options if running in a host which supports colors.
 426                If ($Host.UI.RawUI.ForegroundColor) {
 427                    Switch ($lSeverity) {
 428                        3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' }
 429                        2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' }
 430                        1 { Write-Host -Object $lTextLogLine }
 431                    }
 432                }
 433                #  If executing "powershell.exe -File <filename>.ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log.
 434                Else {
 435                    Write-Output -InputObject $lTextLogLine
 436                }
 437            }
 438        }
 439
 440        ## Create script block for writing log entry to the console as verbose or debug message
 441        [scriptblock]$WriteLogLineToHostAdvanced = {
 442            Param (
 443                [string]$lTextLogLine
 444            )
 445            #  Only output using color options if running in a host which supports colors.
 446            If ($Host.UI.RawUI.ForegroundColor) {
 447                If ($VerboseMessage) {
 448                    Write-Verbose -Message $lTextLogLine
 449                }
 450                Else {
 451                    Write-Debug -Message $lTextLogLine
 452                }
 453            }
 454            #  If executing "powershell.exe -File <filename>.ps1 > log.txt", then all the Write-Host calls are converted to Write-Output calls so that they are included in the text log.
 455            Else {
 456                Write-Output -InputObject $lTextLogLine
 457            }
 458        }
 459
 460        ## Create script block for event writing log entry
 461        [scriptblock]$WriteToEventLog = {
 462            If ($WriteEvent) {
 463                $EventType = Switch ($Severity) {
 464                    3 { 'Error' }
 465                    2 { 'Warning' }
 466                    1 { 'Information' }
 467                }
 468
 469                If ($LogNameNotExists -and (-not $LogSourceNotExists)) {
 470                    Try {
 471                        #  Delete event source if the log does not exist
 472                        $null = [System.Diagnostics.EventLog]::DeleteEventSource($Source)
 473                        $LogSourceNotExists = $true
 474                    }
 475                    Catch {
 476                        [boolean]$ExitLoggingFunction = $true
 477                        #  If error deleting event source, write message to console
 478                        If (-not $ContinueOnError) {
 479                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log source [$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 480                        }
 481                    }
 482                }
 483                If ($LogNameNotExists -or $LogSourceNotExists) {
 484                    Try {
 485                        #  Create event log
 486                        $null = New-EventLog -LogName $LogName -Source $Source -ErrorAction 'Stop'
 487                    }
 488                    Catch {
 489                        [boolean]$ExitLoggingFunction = $true
 490                        #  If error creating event log, write message to console
 491                        If (-not $ContinueOnError) {
 492                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 493                        }
 494                    }
 495                }
 496                Try {
 497                    #  Write to event log
 498                    Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -EntryType $EventType -Category '0' -Message $ConsoleLogLine -ErrorAction 'Stop'
 499                }
 500                Catch {
 501                    [boolean]$ExitLoggingFunction = $true
 502                    #  If error creating directory, write message to console
 503                    If (-not $ContinueOnError) {
 504                        Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to write to event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 505                    }
 506                }
 507            }
 508        }
 509
 510        ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file
 511        If (($DebugMessage -or $VerboseMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return }
 512        ## Exit function if logging to file is disabled and logging to console host is disabled
 513        If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return }
 514        ## Exit Begin block if logging is disabled
 515        If ($DisableLogging) { Return }
 516
 517        ## Create the directory where the log file will be saved
 518        If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) {
 519            Try {
 520                $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop'
 521            }
 522            Catch {
 523                [boolean]$ExitLoggingFunction = $true
 524                #  If error creating directory, write message to console
 525                If (-not $ContinueOnError) {
 526                    Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 527                }
 528                Return
 529            }
 530        }
 531
 532        ## Assemble the fully qualified path to the log file
 533        [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName
 534    }
 535    Process {
 536
 537        ForEach ($Msg in $Message) {
 538            ## If the message is not $null or empty, create the log entry for the different logging methods
 539            [string]$CMTraceMsg = ''
 540            [string]$ConsoleLogLine = ''
 541            [string]$LegacyTextLogLine = ''
 542            If ($Msg) {
 543                #  Create the CMTrace log message
 544                If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" }
 545
 546                #  Create a Console and Legacy "text" log entry
 547                [string]$LegacyMsg = "[$LogDate $LogTime]"
 548                If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" }
 549                If ($Source) {
 550                    [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg"
 551                    Switch ($Severity) {
 552                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" }
 553                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" }
 554                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" }
 555                    }
 556                }
 557                Else {
 558                    [string]$ConsoleLogLine = "$LegacyMsg :: $Msg"
 559                    Switch ($Severity) {
 560                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" }
 561                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" }
 562                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" }
 563                    }
 564                }
 565            }
 566
 567            ## Execute script block to write the log entry to the console as verbose or debug message
 568            & $WriteLogLineToHostAdvanced -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 569
 570            ## Exit function if logging is disabled
 571            If ($ExitLoggingFunction) { Return }
 572
 573            ## Execute script block to create the CMTrace.exe compatible log entry
 574            [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $lSeverity
 575
 576            ## Choose which log type to write to file
 577            If ($LogType -ieq 'CMTrace') {
 578                [string]$LogLine = $CMTraceLogLine
 579            }
 580            Else {
 581                [string]$LogLine = $LegacyTextLogLine
 582            }
 583
 584            ## Write the log entry to the log file and event log if logging is not currently disabled
 585            If (-not $DisableLogging) {
 586                If ($WriteFile) {
 587                    ## Write to file log
 588                    Try {
 589                        $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop'
 590                    }
 591                    Catch {
 592                        If (-not $ContinueOnError) {
 593                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 594                        }
 595                    }
 596                }
 597                If ($WriteEvent) {
 598                    ## Write to event log
 599                    Try {
 600                        & $WriteToEventLog -lMessage $ConsoleLogLine -lName $LogName -lSource $Source -lSeverity $Severity
 601                    }
 602                    Catch {
 603                        If (-not $ContinueOnError) {
 604                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 605                        }
 606                    }
 607                }
 608            }
 609
 610            ## Execute script block to write the log entry to the console if $WriteHost is $true and $LogLogDebugMessage is not $true
 611            & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 612        }
 613    }
 614    End {
 615        ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0
 616        Try {
 617            If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) {
 618                [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop'
 619                [decimal]$LogFileSizeMB = $LogFile.Length / 1MB
 620                If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) {
 621                    ## Change the file extension to "lo_"
 622                    [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_')
 623                    [hashtable]$ArchiveLogParams = @{ ScriptSection = $ScriptSection; Source = ${CmdletName}; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; WriteHost = $WriteHost; ContinueOnError = $ContinueOnError; PassThru = $false }
 624
 625                    ## Log message about archiving the log file
 626                    $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]."
 627                    Write-Log -Message $ArchiveLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}
 628
 629                    ## Archive existing log file from <filename>.log to <filename>.lo_. Overwrites any existing <filename>.lo_ file. This is the same method SCCM uses for log files.
 630                    Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop'
 631
 632                    ## Start new log file and Log message about archiving the old log file
 633                    $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached."
 634                    Write-Log -Message $NewLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}
 635                }
 636            }
 637        }
 638        Catch {
 639            ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size
 640        }
 641        Finally {
 642            If ($PassThru) { Write-Output -InputObject $Message }
 643        }
 644    }
 645}
 646#endregion
 647
 648#region Format-Bytes
 649Function Format-Bytes {
 650<#
 651.SYNOPSIS
 652    Formats a number of bytes in the corresponding sizes.
 653.DESCRIPTION
 654    Formats a number of bytes in the corresponding sizes depending on the size ('KB','MB','GB','TB','PB').
 655.PARAMETER Bytes
 656    Specifies bytes to format.
 657.EXAMPLE
 658    Format-Bytes -Bytes 12344567890
 659.INPUTS
 660    None.
 661.OUTPUTS
 662    None.
 663.NOTES
 664    Created by Ioan Popovici.
 665    v1.0.0 - 2021-09-01
 666
 667    This is an private function should tipically not be called directly.
 668    Credit to Anthony Howell.
 669.LINK
 670    https://theposhwolf.com/howtos/Format-Bytes/
 671.LINK
 672    https://MEM.Zone
 673.LINK
 674    https://MEM.Zone/GIT
 675.LINK
 676    https://MEM.Zone/ISSUES
 677.COMPONENT
 678    Powershell
 679.FUNCTIONALITY
 680    Format Bytes
 681#>
 682    Param (
 683        [Parameter(ValueFromPipeline = $true)]
 684        [ValidateNotNullOrEmpty()]
 685        [float]$Bytes
 686    )
 687    Begin {
 688        [string]$Output = $null
 689        [boolean]$Negative = $false
 690        $Sizes = 'KB','MB','GB','TB','PB'
 691    }
 692    Process {
 693        Try {
 694            If ($Bytes -le 0) {
 695                $Bytes = -$Bytes
 696                [boolean]$Negative = $true
 697            }
 698            For ($Counter = 0; $Counter -lt $Sizes.Count; $Counter++) {
 699                If ($Bytes -lt "1$($Sizes[$Counter])") {
 700                    If ($Counter -eq 0) {
 701                    $Number = $Bytes
 702                    $Sizes = 'B'
 703                    }
 704                    Else {
 705                        $Number = $Bytes / "1$($Sizes[$Counter-1])"
 706                        $Number = '{0:N2}' -f $Number
 707                        $Sizes = $Sizes[$Counter-1]
 708                    }
 709                }
 710            }
 711        }
 712        Catch {
 713            $Output = "Format Failed for Bytes ($Bytes! Error: $($_.Exception.Message)"
 714            Write-Log -Message $Output -EventID 2 -Severity 3
 715        }
 716        Finally {
 717            If ($Negative) { $Number = -$Number }
 718            $Output = '{0} {1}' -f $Number, $Sizes
 719            Write-Output -InputObject $Output
 720        }
 721    }
 722    End{
 723    }
 724}
 725#endregion
 726
 727#region Function Show-Progress
 728Function Show-Progress {
 729<#
 730.SYNOPSIS
 731    Displays progress info.
 732.DESCRIPTION
 733    Displays progress info and maximizes code reuse by automatically calculating the progress steps.
 734.PARAMETER Actity
 735    Specifies the progress activity. Default: 'Running Cleanup Please Wait...'.
 736.PARAMETER Status
 737    Specifies the progress status.
 738.PARAMETER CurrentOperation
 739    Specifies the current operation.
 740.PARAMETER Step
 741    Specifies the progress step. Default: $Script:Step ++.
 742.PARAMETER ID
 743    Specifies the progress bar id.
 744.PARAMETER Delay
 745    Specifies the progress delay in milliseconds. Default: 100.
 746.PARAMETER Loop
 747    Specifies if the call comes from a loop.
 748.EXAMPLE
 749    Show-Progress -Activity 'Running Install Please Wait' -Status 'Uploading Report' -Step ($Step++) -Delay 200
 750.EXAMPLE
 751    Show-Progress -Status "Downloading [$File.Name] --> [$($RSDataSource.Name)]" -Loop
 752.INPUTS
 753    None.
 754.OUTPUTS
 755    None.
 756.NOTES
 757    Created by Ioan Popovici.
 758    v1.0.0 - 2021-01-01
 759
 760    This is a private function and should typically not be called directly.
 761    Credit to Adam Bertram.
 762
 763    ## !! IMPORTANT !! ##
 764    #  You need to tokenize the scripts steps at the beginning of the script for Show-Progress to work:
 765
 766    ## Get script path and name
 767    [string]$ScriptPath = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)
 768    [string]$ScriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Definition)
 769    [string]$ScriptFullName = Join-Path -Path $ScriptPath -ChildPath $ScriptName
 770    #  Get progress steps
 771    $ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
 772    $ForEachSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Keyword' -and $_.Content -eq 'ForEach' }).Count)
 773    #  Set progress steps
 774    $Script:Steps = $ProgressSteps - $ForEachSteps
 775    $Script:Step = 0
 776.LINK
 777    https://adamtheautomator.com/building-progress-bar-powershell-scripts/
 778.LINK
 779    https://MEM.Zone
 780.LINK
 781    https://MEM.Zone/GIT
 782.LINK
 783    https://MEM.Zone/ISSUES
 784.COMPONENT
 785    Powershell
 786.FUNCTIONALITY
 787    Show Progress
 788#>
 789    [CmdletBinding()]
 790    Param (
 791        [Parameter(Mandatory=$false,Position=0)]
 792        [ValidateNotNullorEmpty()]
 793        [Alias('act')]
 794        [string]$Activity = 'Running Cleanup Please Wait...',
 795        [Parameter(Mandatory=$true,Position=1)]
 796        [ValidateNotNullorEmpty()]
 797        [Alias('sta')]
 798        [string]$Status,
 799        [Parameter(Mandatory=$false,Position=2)]
 800        [ValidateNotNullorEmpty()]
 801        [Alias('cro')]
 802        [string]$CurrentOperation,
 803        [Parameter(Mandatory=$false,Position=3)]
 804        [ValidateNotNullorEmpty()]
 805        [Alias('pid')]
 806        [int]$ID = 0,
 807        [Parameter(Mandatory=$false,Position=4)]
 808        [ValidateNotNullorEmpty()]
 809        [Alias('ste')]
 810        [int]$Step = $Script:Step ++,
 811        [Parameter(Mandatory=$false,Position=5)]
 812        [ValidateNotNullorEmpty()]
 813        [Alias('del')]
 814        [string]$Delay = 100,
 815        [Parameter(Mandatory=$false,Position=5)]
 816        [ValidateNotNullorEmpty()]
 817        [Alias('lp')]
 818        [switch]$Loop
 819    )
 820    Begin {
 821        If ($Loop) { $Script:Steps ++ }
 822        $PercentComplete = $($($Step / $Steps) * 100)
 823    }
 824    Process {
 825        Try {
 826            ##  Show progress
 827            Write-Progress -Activity $Activity -Status $Status -CurrentOperation $CurrentOperation -ID $ID -PercentComplete $PercentComplete
 828            Start-Sleep -Milliseconds $Delay
 829        }
 830        Catch {
 831            Throw (New-Object System.Exception("Could not Show progress status [$Status]! $($_.Exception.Message)", $_.Exception))
 832        }
 833    }
 834}
 835#endregion
 836
 837#region Function Start-WindowsCleanup
 838Function Start-WindowsCleanup {
 839<#
 840.SYNOPSIS
 841    Performs a Windows cleanup.
 842.DESCRIPTION
 843    Performs a Windows cleanup by removing volume caches, update backups, updates and CCM caches.
 844.PARAMETER CleanupOptions
 845    Supported options:
 846        "comCacheRepair"   # Component Cache Repair
 847        "comCacheCleanup"  # Component Cache Cleanup
 848        "volCacheCleanup"  # Volume Cache Cleanup
 849        "volShadowCleanup" # Volume Shadow Copy Cleanup
 850        "updCacheCleanup"  # Update Cache Cleanup
 851        "ccmCacheCleanup"  # CCM Cache Cleanup
 852        "Recommended"      # Performs some or all of the above-mentioned cleanup operations in a specific order depending on the operating system.
 853        "All"              # Performs all the above-mentioned cleanup operations.
 854    If set to "Recommended", the cleanup will be done in the recommended order.
 855    Default is: "Recommended".
 856.EXAMPLE
 857    Start-WindowsCleanup.ps1 -CleanupOptions "comCacheRepair", "comCacheCleanup", "updCacheCleanup", "volCacheCleanup", "ccmCacheCleanup"
 858.EXAMPLE
 859    Start-WindowsCleanup.ps1 -CleanupOptions "Recommended"
 860.EXAMPLE
 861    Start-WindowsCleanup.ps1 -CleanupOptions "All"
 862.INPUTS
 863    None.
 864.OUTPUTS
 865    None.
 866.NOTES
 867    Created by Ioan Popovici
 868    This is an internal script function and should typically not be called directly.
 869.LINK
 870    https://MEM.Zone
 871.LINK
 872    https://MEM.Zone/GIT
 873.LINK
 874    https://MEM.Zone/ISSUES
 875#>
 876    [CmdletBinding()]
 877    Param (
 878        [Parameter(Mandatory = $false)]
 879        [ValidateSet('comCacheRepair','comCacheCleanup','volCacheCleanup','volShadowCleanup','updCacheCleanup','ccmCacheCleanup','Recommended','All')]
 880        [Alias('Options')]
 881        [string[]]$CleanupOptions = 'Recommended',
 882        [switch]$OutputJson
 883    )
 884
 885    Begin {
 886        Try {
 887
 888            ## Variable  declaration
 889            [boolean]$SkipCleanup = $false
 890            [string]$StartWindowsCleanup = $null
 891
 892            ## Get Machine Operating System
 893            [string]$RegistryExPattern = '(Windows\ (?:7|8\.1|8|10|11|Server\ (?:2008\ R2|2012\ R2|2012|2016|2019|2022)))'
 894            [string]$MachineOS = (Get-WmiObject -Class 'Win32_OperatingSystem' | Select-Object -ExpandProperty 'Caption' | Select-String -AllMatches -Pattern $RegistryExPattern | Select-Object -ExpandProperty 'Matches').Value
 895
 896            ## Get volume info before cleanup
 897            $VolumeInfo = Get-Volume | Where-Object { $null -ne $PSItem.DriveLetter -and $PSItem.DriveType -eq 'Fixed' } | Select-Object -Property 'DriveLetter','SizeRemaining','Size'
 898        }
 899        Catch {}
 900
 901        ## Perform different cleanup actions depending on the detected Operating System, the action order is intentional
 902        Switch ($CleanupOptions) {
 903            'Recommended' {
 904                If ($MachineOS) {
 905                    Switch ($MachineOS) {
 906                        'Windows 7' {
 907                            $CleanupOptions = @('volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 908                            Break;
 909                        }
 910                        'Windows 8' {
 911                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 912                            Break;
 913                        }
 914                        'Windows 8.1' {
 915                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 916                            Break;
 917                        }
 918                        'Windows 10' {
 919                            $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup')
 920                            Break;
 921                        }
 922                        'Windows 11' {
 923                            $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup')
 924                            Break;
 925                        }
 926                        'Windows Server 2008 R2' {
 927                            $CleanupOptions = @('volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 928                            Break;
 929                        }
 930                        'Windows Server 2012' {
 931                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 932                            Break;
 933                        }
 934                        'Windows Server 2012 R2' {
 935                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
 936                            Break;
 937                        }
 938                        'Windows Server 2016' {
 939                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
 940                            Break;
 941                        }
 942                        'Windows Server 2019' {
 943                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
 944                            Break;
 945                        }
 946                        'Windows Server 2022' {
 947                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
 948                            Break;
 949                        }
 950                        Default {
 951                            $StartWindowsCleanup = 'Unknown Operating System, Skipping Cleanup!'
 952                            $SkipCleanup = $true
 953                        }
 954                    }
 955                Write-Verbose -Message 'Recommended Cleanup Selected!'
 956                }
 957                Else {
 958                    $StartWindowsCleanup = 'Unknown Operating System, Skipping Cleanup!'
 959                    $SkipCleanup = $true
 960                }
 961            }
 962            'All' { $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup') }
 963        }
 964    }
 965    Process {
 966        Try {
 967
 968            ## Write variables for verbose output
 969            Write-Verbose -Message "$MachineOS Detected. Starting Cleanup..."
 970            Write-Verbose -Message "Cleanup Options: $CleanupOptions"
 971
 972            ## Perform Cleanup Actions if $SkipCleanup is not true
 973            If (-not $SkipCleanup) {
 974                ForEach ($CleanupOption in $CleanupOptions) {
 975                    Switch ($CleanupOption) {
 976                        'comCacheRepair' {
 977
 978                            ## Start Component Cache Repair
 979                            Show-Progress -Status 'Running Component Cache Repair. This Can Take a While...' -Loop
 980                            Start-Process -FilePath 'DISM.exe' -ArgumentList '/Online /Cleanup-Image /RestoreHealth' -WindowStyle 'Hidden'
 981                            While (-not (Get-Process -Name 'TiWorker' -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }
 982                            Get-Process -Name 'TiWorker' | ForEach-Object { $PSItem.PriorityClass='High' }
 983                            Wait-Process -Name 'DISM'
 984                        }
 985                        'comCacheCleanup' {
 986
 987                            ## Start Component Cache Cleanup
 988                            Show-Progress -Status 'Running Component Cache Cleanup. This Can Take a While...' -Loop
 989                            Start-Process -FilePath 'DISM.exe' -ArgumentList '/Online /Cleanup-Image /StartComponentCleanup /ResetBase' -WindowStyle 'Hidden'
 990                            While (-not (Get-Process -Name 'TiWorker' -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }
 991                            Get-Process -Name 'TiWorker' | ForEach-Object { $PSItem.PriorityClass='High' }
 992                            Wait-Process -Name 'DISM'
 993                        }
 994                        'volCacheCleanup' {
 995
 996                            ## Start Volume Cache Cleanup
 997                            Show-Progress -Status 'Running Volume Cache Cleanup...'
 998
 999                            ## Get Volume Caches registry paths
1000                            [string]$RegistryVolumeCachesRootPath = 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
1001                            [string[]]$RegistryVolumeCachesPaths = Get-ChildItem -Path $RegistryVolumeCachesRootPath | Select-Object -ExpandProperty 'Name'
1002
1003                            ## CleanMgr cleanup settings
1004                            [string]$RegistrySageSet = '5432'
1005                            [string]$RegistryName = 'StateFlags' + $RegistrySageSet
1006                            [string]$RegistryValue = '00000002'
1007                            [string]$RegistryType = 'DWORD'
1008
1009                            ## Add registry entries required by CleanMgr
1010                            ForEach ($RegistryVolumeCachesPath in $RegistryVolumeCachesPaths) {
1011                                Show-Progress -Activity 'Running Volume Cache Cleanup...' -Status "Adding $RegistryName to $RegistryVolumeCachesPath" -Loop
1012                                $null = New-ItemProperty -Path Registry::$RegistryVolumeCachesPath -Name $RegistryName -Value $RegistryValue -PropertyType $RegistryType -Force
1013                            }
1014
1015                            ## If the machine is running Windows Server 2008 R2, copy the files required by CleanMgr and wait for the action to complete
1016                            If ($MachineOS -eq 'Windows Server 2008 R2') {
1017
1018                                ## Copy CleanMgr.exe and CleanMgr.exe.mui
1019                                Show-Progress -Activity 'Running Volume Cache Cleanup...' -Status "Copying CleanMgr.exe from $env:SystemRoot\winsxs\..." -Loop
1020                                $null = Copy-Item -Path "$env:SystemRoot\winsxs\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\cleanmgr.exe" -Destination "$env:SystemRoot\System32\" -Force
1021                                $null = Copy-Item -Path "$env:SystemRoot\winsxs\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\cleanmgr.exe.mui" -Destination "$env:SystemRoot\System32\en-US\" -Force
1022                            }
1023
1024                            ## Start Volume Cache Cleanup
1025                            Show-Progress -Status 'Running Volume Cache Cleanup. This May Take a While...' -Loop
1026                            Start-Process -FilePath 'CleanMgr.exe' -ArgumentList "/sagerun:$RegistrySageSet" -WindowStyle 'Hidden' -Wait
1027                        }
1028                        'volShadowCleanup' {
1029
1030                            ## Start Volume Cache Cleanup
1031                            Show-Progress -Status 'Running Volume Shadow Cleanup...' -Loop
1032                            Start-Process -FilePath 'vssadmin.exe' -ArgumentList 'Delete Shadows /All /Force' -WindowStyle 'Hidden' -Wait
1033                        }
1034                        'updCacheCleanup' {
1035
1036                            ## Start Update Cache Cleanup
1037                            Show-Progress -Status 'Running Windows Update Cache Cleanup...' -Loop
1038                            $null = Stop-Service -Name 'wuauserv' -Force -ErrorAction 'SilentlyContinue'
1039                            $null = Remove-Item -Path "$env:SystemRoot\SoftwareDistribution\" -Recurse -Force
1040                            $null = Start-Service -Name 'wuauserv' -ErrorAction 'SilentlyContinue'
1041                        }
1042                        'ccmCacheCleanup' {
1043
1044                            ## Start CCM Cache Cleanup
1045                            Show-Progress -Status 'Running CCM Cache Cleanup...' -Loop
1046
1047                            ## Initialize the CCM resource manager com object. New-Object does not respect $ErrorActionPreference = 'SilenlyContinue' hence the Try/Catch.
1048                            [__comobject]$CCMComObject = Try { New-Object -ComObject 'UIResource.UIResourceMgr' } Catch { $null }
1049
1050                            ## If the CCM client is installed, run the CCM cache cleanup
1051                            If ($null -ne $CCMComObject) {
1052
1053                                ## Get ccm cache path
1054                                [string]$DiskCachePath = $($CCMComObject.GetCacheInfo()).Location
1055
1056                                ## Get the CacheElementIDs to delete
1057                                $CacheItems = $CCMComObject.GetCacheInfo().GetCacheElements()
1058
1059                                ## Remove CCM cache items
1060                                ForEach ($CacheItem in $CacheItems) {
1061                                    Show-Progress -Activity 'Running CCM Cache Cleanup...' -Status "Removing $CacheItem.Location" -Loop
1062                                    $null = $CCMComObject.GetCacheInfo().DeleteCacheElement([string]$($CacheItem.CacheElementID))
1063                                }
1064
1065                                ## Remove orphaned cache items
1066                                Show-Progress -Activity 'Running CCM Cache Cleanup...' -Status "Removing 'orphaned' CCM cache items" -Loop
1067                                $null = Remove-Item -Path $(Join-Path -Path $DiskCachePath -ChildPath '\*') -Recurse -Force
1068                            }
1069                            Else { Write-Warning -Message 'CCM Client is not installed! Skipping CCM Cache Cleanup...' }
1070                        }
1071                        Default { $Output = "$CleanupOption is Not a Valid Cleanup Option!"; Break }
1072                    }
1073                }
1074
1075                ## Calculate the total freed up space and add it to the $VolumeInfo object
1076                ForEach ($Volume in $VolumeInfo) {
1077                    $CleanedSpace = (Get-Volume -DriveLetter $Volume.DriveLetter).SizeRemaining - $Volume.SizeRemaining | Format-Bytes
1078                    $Volume | Add-Member -MemberType 'NoteProperty' -Name 'ReclaimedSpace' -Value $CleanedSpace -ErrorAction 'SilentlyContinue'
1079                }
1080
1081                ## Format output
1082                $Output = $VolumeInfo | Select-Object -Property 'DriveLetter',
1083                    @{ Name = 'Size'         ; Expression = {Format-Bytes -Bytes $PSItem.Size} },
1084                    @{ Name = 'FreeSpace'; Expression = {Format-Bytes -Bytes $PSItem.SizeRemaining} },
1085                    ReclaimedSpace
1086
1087                ## Warn that a reboot might be needed
1088                Write-Warning -Message "SxS processing only occurs on system startup. Negative 'Reclaimed' values on repeaded runs are normal, you need to reboot." -Verbose
1089
1090                ## Write to the event log
1091                [string]$EventLogEntry = "Cleanup Completed for $env:COMPUTERNAME ($MachineOS)!`n$($Output | Out-String)"
1092                Write-Log -Message $EventLogEntry
1093            }
1094        }
1095        Catch {
1096            $Output = "Cleanup Failed for $env:COMPUTERNAME ($MachineOS)! Error: $($_.Exception.Message)"
1097            Write-Log -Message $Output -EventID 2 -Severity 3
1098        }
1099        Finally {
1100            Write-Output -InputObject $Output
1101        }
1102    }
1103    End {
1104    }
1105}
1106#endregion
1107
1108#endregion
1109##*=============================================
1110##* END FUNCTION LISTINGS
1111##*=============================================
1112
1113##*=============================================
1114##* SCRIPT BODY
1115##*=============================================
1116#region ScriptBody
1117
1118Try {
1119    $WindowsCleanup = Start-WindowsCleanup -CleanupOptions $CleanupOptions
1120}
1121Catch {
1122    $WindowsCleanup = "Cleanup for $env:COMPUTERNAME ($MachineOS)! Error: $($_.Exception.Message)"
1123}
1124Finally {
1125    Write-Output -InputObject $WindowsCleanup
1126}
1127
1128#endregion
1129##*=============================================
1130##* END SCRIPT BODY
1131##*=============================================

SHARE

article card image dark article card image light

Published by · Jun 17, 2023 tools · 1 mins read

Introducing: Windows Update Database Reinitialization Tool

Proactively repair corrupted Windows Update Database with Powershell and Configuration Manager ...

See More
article card image dark article card image light

Published by · Jun 18, 2023 tools · 4 mins read

Introducing: Configuration Manager Client Cache Cleanup Tool

Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines ...

See More