Dark mode

Dark mode

There are 0 results matching

article card image dark article card image light

Published by · May 5, 2025 tools · 6 mins read

Introducing: Windows Bulk Uninstall Tool

Bulk Uninstalling Applications with PowerShell with PowerShell and Configuration Manager or Intune ...

See More
article card image dark article card image light

Published by · Jun 25, 2024 tools · 2 mins read

Introducing: macOS JAMF Offboarding Tool

Offboarding macOS Devices from JAMF in Bulk using the JAMF API with a bash script ...

See More
article card image dark article card image light

Published by · Jun 25, 2024 tools · 2 mins read

Introducing: Microsoft Cloud License Automation Tool - Part 1

Automating Microsoft Cloud License Assignment and Reporting with PowerShell and Slack for Enterprise Mobility and Security E3 ...

See More
article card image dark article card image light

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

Introducing: Configuration Manager Set Implicit Uninstall Flag Tool

Setting Configuration Manager ConfigMgr Implicit Uninstall Flag with PowerShell for Required Application Deployments ...

See More
article card image dark article card image light

Published by · Jun 11, 2024 configmgr · 2 mins read

Configuration Manager Next Maintenance Window SQL Function

Get Next Configuration Manager Maintenance Window from a Schedule Token with Offset Days using an SQL Function. ...

See More
article card image dark article card image light

Published by · Jun 3, 2024 tools · 2 mins read

Introducing: Windows User Rights Assignment Tool - Part 3

Add, Remove, or Replace Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · May 28, 2024 tools · 2 mins read

Introducing: Windows User Rights Assignment Tool - Part 2

Get and Report Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · May 22, 2024 tools · 2 mins read

Introducing: Windows User Rights Assignment Tool - Part 1

Get Windows Rights Assignment with our PowerShell Tool. ...

See More
article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune Linux Onboarding Tool

Onboard Ubuntu Linux devices to Microsoft Intune using a bash script. Installs prerequisites and starts the user-driven enrollment. ...

See More
article card image dark article card image light

Published by · Apr 11, 2024 tools · 2 mins read

Introducing: Intune macOS Onboarding Tool

Onboard macOS devices to Microsoft Intune using a bash script that initiates the process. Optionally, the script converts mobile accounts, resets the FileVault key, and removes ...

See More
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 · 4 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: Configuration Manager Client Cache Cleanup Tool

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

Quick Summary

Configuration Manager cache self-cleanup doesn’t work as you might expect. It only deletes the cache if there is no space for a new item and even then it uses an eligibility method to determine what it can delete.
It then starts to purge the last items in chronological order but only until it has enough space for the new item.

To properly remove the cache you need to use the Configuration Manager Client GUI or SDK since direct removal of the cache folders is not supported.

The following PowerShell approach removes only unused cache items and can be either run directly or added to a Configuration Baseline.

Recommendations

For indiscriminately deleting all cache just use this PowerShell code.

## Initialize the CCM resource manager com object
[__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'

## Get the CacheElementIDs to delete
$CacheInfo = $CCMComObject.GetCacheInfo().GetCacheElements()

## Remove cache items
ForEach ($CacheItem in $CacheInfo) {
    $null = $CCMComObject.GetCacheInfo().DeleteCacheElement([string]$($CacheItem.CacheElementID))
}

Prerequisites


Built-in Cleanup Explained

Unreferenced Items

An item is unreferenced if it does not have an active ExecutionRequest.

The TombStoneDuration and MaxCacheDuration properties of the CacheElement Class object are used to manage cache item deletion. If the client does not have enough cache space for a new item then eligible unreferenced items are deleted.

An unreferenced item is eligible for deletion if the time specified in its ICacheElement::LastReferenceTime property is longer than the time specified in ICacheElement::TombStoneDuration. Deletion of eligible unreferenced cache items continues until enough space is made available in the cache for the new item.

If cache space is still required after every eligible unreferenced item has been deleted then eligible referenced items are deleted.

Referenced Items

An item is referenced if it has an active ExecutionRequest.

A referenced item is eligible for deletion if the time specified in its ICacheInfo::LastReferenceTime property is longer than the time specified ICacheInfo::MaxCacheDuration. Deletion of eligible referenced cache items continues until enough space is made available in the cache for the new item.

Notes

  • Items are deleted only when space is required.
  • Items are deleted in ascending time.
  • Unreferenced items are deleted first.
  • Cache content is deleted after the timeout period.
  • When no user is logged on, the cache is deleted only after the timeout period. If the cache is full any new deployment will fail until the timeout period is reached.


Script Cleanup Explained

A slightly modified logic as described above is used for determining deletion eligibility with the added possibility of selectively cleaning cache items, using the CacheType and CleanupType parameters.

Cache Types

  • All
    Selects all cache types.
  • Application
    Selects only cached applications.
  • Package
    Selects only cached packages.
  • Update
    Selects only cached updates.
  • Orphaned
    Selects only orphaned cache items.

Cleanup Types

  • All
    Process tombstoned and referenced items alike regardless of their eligibility.
  • Automatic
    Process tombstoned and referenced items depending on the FreeDiskSpaceThreshold value.
  • ListOnly
    Only lists items with resolved properties without further processing.
  • Tombstoned
    Process only tombstoned items.
  • Referenced
    Process only referenced items.
Notes

Only Tombstoned and Referenced parameters can be used together.


Parameters

CacheType

This parameter specifies which cache to clean.

  • All
  • Application
  • Package
  • Update
  • Orphaned

Defaults to All.

CleanupType

This parameter specifies the cleanup type to perform.

  • All
  • Automatic
  • ListOnly
  • Tombstoned
  • Referenced

Defaults to Automatic.

FreeDiskSpaceThreshold

Free disk space threshold percentage after which the cache is cleaned.
When the Automatic cleanup type is selected, cleanup will proceed anyway, and the value will be used in conjunction with it.

Defaults to 100.

SkipSuperPeer

Terminates script if the client is a super-peer when using peer cache.

DeletePinned

Deletes cache even if it’s pinned for applications and packages.

LoggingOptions

  • Host
    Prints output to the console.
  • File
    Writes output to a file.
  • EventLog
    Writes output to the event log.
  • None
    Suppresses all output.

Defaults to Host, File, EventLog

LogName

Sets log folder name and event log name.

Defaults to Configuration Manager.

LogSource

Sets log file name and event source name.

Defaults to Invoke-CCMCacheCleanup.

LogDebugMessages

Writes debug messages following the LoggingOptions parameter.

Notes

In a configuration baseline, use a discovery script and omit Host in the LoggingOptions parameter to suppress all output.


Preview

article card image powershell-cleanup
Cleanup In Progress
article card image file-cleanup-log
Cleanup File Log
article card image eventlog-cleanup-log
Cleanup Event Logs

Code

   1<#
   2.SYNOPSIS
   3    Cleans the configuration manager client cache.
   4.DESCRIPTION
   5    Cleans the configuration manager client cache of all unneeded with the option to delete pinned content.
   6.PARAMETER CacheType
   7    Specifies Cache Type to clean. ('All', 'Application', 'Package', 'Update', 'Orphaned').
   8    Supports multiple values. Default is: 'All'.
   9    If it's set to 'All' all cache will be processed and other specified types will be ignored.
  10.PARAMETER CleanupType
  11    Specifies Cleanup Type to clean. ('All', 'Automatic', 'ListOnly', 'Tombstoned', 'Referenced').
  12    Supports only one value. Default is: 'Automatic'.
  13    A 'Referenced' item is eligible for deletion if the time specified in its 'LastReferenceTime' property is longer than the time specified 'MaxCacheDuration'.
  14    A 'Unreferenced' item is eligible for deletion if the time specified in its 'LastReferenceTime' property is longer than the time specified in 'TombStoneDuration'.
  15
  16    Available Cleanup Options:
  17        - 'All'
  18            Tombstoned and Referenced cache will be deleted.
  19            The 'EligibleForDeletion' convention is NOT respected.
  20            Not recommended but still safe to use, cache will be re-downloaded when needed.
  21        - 'Automatic'
  22            'Tombstoned' and 'Referenced' will be selected depending on 'FreeDiskSpaceThreshold' parameter.
  23            If under the threshold only 'Tombstoned' cache items will be deleted.
  24            If over the threshold, both 'Tombstoned' and 'Referenced' cache items will be deleted.
  25            The 'EligibleForDeletion' convention is still respected.
  26        - 'Tombstoned'
  27            Only 'Tombstoned' cache items will be deleted.
  28            The 'EligibleForDeletion' convention is still respected.
  29        - 'Referenced'
  30            Only 'Referenced' cache items will be deleted.
  31            The 'EligibleForDeletion' convention is still respected.
  32            Not recommended but still safe to use, cache will be re-downloaded when needed.
  33    Note that 'SkipSuperPeer' and 'DeletePinned' switches are always respected in all cleanup types.
  34.PARAMETER FreeDiskSpaceThreshold
  35    Specifies the free disk space threshold percentage after which the cache is cleaned. Default is: '100'.
  36    If it's set to '100', Free Space Threshold Percentage is ignored.
  37.PARAMETER SkipSuperPeer
  38    This switch specifies to skip cleaning if the client is a super-peer (Peer Cache). Default is: $false.
  39.PARAMETER DeletePinned
  40    This switch specifies to remove cache even if it's pinned (Applications and Packages). Default is: $false.
  41.PARAMETER LoggingOptions
  42    Specifies logging options: ('Host', 'File', 'EventLog', 'None'). Default is: ('Host', 'File', 'EventLog').
  43.PARAMETER LogName
  44    Specifies log folder name and event log name. Default is: 'Configuration Manager'.
  45.PARAMETER LogSource
  46    Specifies log file name and event source name. Default is: 'Invoke-CCMCacheCleanup'.
  47.PARAMETER LogDebugMessages
  48    This switch specifies to log debug messages. Default is: $false.
  49.EXAMPLE
  50    Invoke-CCMCacheCleanup -CacheType 'All' -CleanupType 'Automatic' -DeletePinned
  51    .EXAMPLE
  52    Invoke-CCMCacheCleanup -CacheType 'Orphaned' -CleanupType 'ListOnly' -DeletePinned
  53.EXAMPLE
  54    Invoke-CCMCacheCleanup -CacheType "Application, Package, Update, Orphaned" -CleanupType 'Tombstoned' -FreeDiskSpaceThreshold '100' -SkipSuperPeer -DeletePinned
  55.INPUTS
  56    None.
  57.OUTPUTS
  58    System.Management.Automation.PSObject
  59.NOTES
  60    Created by Ioan Popovici
  61.LINK
  62    https://MEMZ.one/Invoke-CCMCacheCleanup
  63.LINK
  64    https://MEMZ.one/Invoke-CCMCacheCleanup-CHANGELOG
  65.LINK
  66    https://MEMZ.one/Invoke-CCMCacheCleanup-GIT
  67.LINK
  68    https://MEM.Zone/ISSUES
  69.COMPONENT
  70    CM Client
  71.FUNCTIONALITY
  72    Clean CM Client Cache
  73#>
  74
  75##*=============================================
  76##* VARIABLE DECLARATION
  77##*=============================================
  78#region VariableDeclaration
  79
  80## Set script requirements
  81#Requires -Version 3.0
  82
  83## Get script parameters
  84[CmdletBinding()]
  85Param (
  86    [Parameter(Mandatory = $false, Position = 0)]
  87    [ValidateSet('All', 'Application', 'Package', 'Update', 'Orphaned')]
  88    [Alias('Type')]
  89    [string[]]$CacheType = 'All',
  90    [Parameter(Mandatory = $false, Position = 1)]
  91    [ValidateSet('All', 'Automatic', 'ListOnly', 'Tombstoned', 'Referenced')]
  92    [Alias('Action')]
  93    [string]$CleanupType = 'Automatic',
  94    [Parameter(Mandatory = $false, Position = 2)]
  95    [ValidateNotNullorEmpty()]
  96    [Alias('FreeSpace')]
  97    [int16]$FreeDiskSpaceThreshold = 100,
  98    [Parameter(Mandatory = $false, Position = 3)]
  99    [switch]$SkipSuperPeer,
 100    [Parameter(Mandatory = $false, Position = 4)]
 101    [switch]$DeletePinned,
 102    [Parameter(Mandatory = $false, Position = 5)]
 103    [ValidateSet('Host', 'File', 'EventLog', 'None')]
 104    [Alias('Logging')]
 105    [string[]]$LoggingOptions = @('File', 'EventLog'),
 106    [Parameter(Mandatory = $false, Position = 6)]
 107    [string]$LogName = 'Configuration Manager',
 108    [Parameter(Mandatory = $false, Position = 7)]
 109    [string]$LogSource = 'Invoke-CCMCacheCleanup',
 110    [Parameter(Mandatory = $false, Position = 8)]
 111    [switch]$LogDebugMessages = $false
 112)
 113
 114
 115## Get script path, name and configuration file path
 116[string]$ScriptName       = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Definition)
 117[string]$ScriptFullName   = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Definition)
 118
 119## Get Show-Progress steps
 120$ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
 121#  Set progress steps
 122$Script:Steps = $ProgressSteps
 123$Script:Step = 0
 124$Script:DefaultSteps = $Script:Steps
 125$Script:CurrentStep = 0
 126
 127## Set script global variables
 128$script:LoggingOptions   = $LoggingOptions
 129$script:LogName          = $LogName
 130$script:LogSource        = $ScriptName
 131$script:LogDebugMessages = $false
 132$script:LogFileDirectory = If ($LogPath) { Join-Path -Path $LogPath -ChildPath $script:LogName } Else { $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)) }
 133
 134## Initialize result variable
 135[pscustomobject]$Output = @()
 136
 137#  Initialize ShouldRun with true. It will be checked in the script body
 138[boolean]$ShouldRun = $true
 139
 140#endregion
 141##*=============================================
 142##* END VARIABLE DECLARATION
 143##*=============================================
 144
 145##*=============================================
 146##* FUNCTION LISTINGS
 147##*=============================================
 148#region FunctionListings
 149
 150#region Function Resolve-Error
 151Function Resolve-Error {
 152<#
 153.SYNOPSIS
 154    Enumerate error record details.
 155.DESCRIPTION
 156    Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated.
 157.PARAMETER ErrorRecord
 158    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.
 159.PARAMETER Property
 160    The list of properties to display from the error record. Use "*" to display all properties.
 161    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
 162.PARAMETER GetErrorRecord
 163    Get error record details as represented by $_.
 164.PARAMETER GetErrorInvocation
 165    Get error record invocation information as represented by $_.InvocationInfo.
 166.PARAMETER GetErrorException
 167    Get error record exception details as represented by $_.Exception.
 168.PARAMETER GetErrorInnerException
 169    Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one.
 170.EXAMPLE
 171    Resolve-Error
 172.EXAMPLE
 173    Resolve-Error -Property *
 174.EXAMPLE
 175    Resolve-Error -Property InnerException
 176.EXAMPLE
 177    Resolve-Error -GetErrorInvocation:$false
 178.NOTES
 179    Unmodified version of the PADT error resolving cmdlet. I did not write the original cmdlet, please do not credit me for it!
 180.LINK
 181    https://psappdeploytoolkit.com
 182#>
 183    [CmdletBinding()]
 184    Param (
 185        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 186        [AllowEmptyCollection()]
 187        [array]$ErrorRecord,
 188        [Parameter(Mandatory = $false, Position = 1)]
 189        [ValidateNotNullorEmpty()]
 190        [string[]]$Property = ('Message', 'InnerException', 'FullyQualifiedErrorId', 'ScriptStackTrace', 'PositionMessage'),
 191        [Parameter(Mandatory = $false, Position = 2)]
 192        [switch]$GetErrorRecord = $true,
 193        [Parameter(Mandatory = $false, Position = 3)]
 194        [switch]$GetErrorInvocation = $true,
 195        [Parameter(Mandatory = $false, Position = 4)]
 196        [switch]$GetErrorException = $true,
 197        [Parameter(Mandatory = $false, Position = 5)]
 198        [switch]$GetErrorInnerException = $true
 199    )
 200
 201    Begin {
 202        ## If function was called without specifying an error record, then choose the latest error that occurred
 203        If (-not $ErrorRecord) {
 204            If ($global:Error.Count -eq 0) {
 205                #Write-Warning -Message "The `$Error collection is empty"
 206                Return
 207            }
 208            Else {
 209                [array]$ErrorRecord = $global:Error[0]
 210            }
 211        }
 212
 213        ## Allows selecting and filtering the properties on the error object if they exist
 214        [scriptblock]$SelectProperty = {
 215            Param (
 216                [Parameter(Mandatory = $true)]
 217                [ValidateNotNullorEmpty()]
 218                $InputObject,
 219                [Parameter(Mandatory = $true)]
 220                [ValidateNotNullorEmpty()]
 221                [string[]]$Property
 222            )
 223
 224            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name'
 225            ForEach ($Prop in $Property) {
 226                If ($Prop -eq '*') {
 227                    [string[]]$PropertySelection = $ObjectProperty
 228                    Break
 229                }
 230                ElseIf ($ObjectProperty -contains $Prop) {
 231                    [string[]]$PropertySelection += $Prop
 232                }
 233            }
 234            Write-Output -InputObject $PropertySelection
 235        }
 236
 237        #  Initialize variables to avoid error if 'Set-StrictMode' is set
 238        $LogErrorRecordMsg = $null
 239        $LogErrorInvocationMsg = $null
 240        $LogErrorExceptionMsg = $null
 241        $LogErrorMessageTmp = $null
 242        $LogInnerMessage = $null
 243    }
 244    Process {
 245        If (-not $ErrorRecord) { Return }
 246        ForEach ($ErrRecord in $ErrorRecord) {
 247            ## Capture Error Record
 248            If ($GetErrorRecord) {
 249                [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property
 250                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
 251            }
 252
 253            ## Error Invocation Information
 254            If ($GetErrorInvocation) {
 255                If ($ErrRecord.InvocationInfo) {
 256                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
 257                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
 258                }
 259            }
 260
 261            ## Capture Error Exception
 262            If ($GetErrorException) {
 263                If ($ErrRecord.Exception) {
 264                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property
 265                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
 266                }
 267            }
 268
 269            ## Display properties in the correct order
 270            If ($Property -eq '*') {
 271                #  If all properties were chosen for display, then arrange them in the order the error object displays them by default.
 272                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 273                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 274                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 275            }
 276            Else {
 277                #  Display selected properties in our custom order
 278                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
 279                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
 280                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
 281            }
 282
 283            If ($LogErrorMessageTmp) {
 284                $LogErrorMessage = 'Error Record:'
 285                $LogErrorMessage += "`n-------------"
 286                $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String
 287                $LogErrorMessage += $LogErrorMsg
 288            }
 289
 290            ## Capture Error Inner Exception(s)
 291            If ($GetErrorInnerException) {
 292                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) {
 293                    $LogInnerMessage = 'Error Inner Exception(s):'
 294                    $LogInnerMessage += "`n-------------------------"
 295
 296                    $ErrorInnerException = $ErrRecord.Exception.InnerException
 297                    $Count = 0
 298
 299                    While ($ErrorInnerException) {
 300                        [string]$InnerExceptionSeperator = '~' * 40
 301
 302                        [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property
 303                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String
 304
 305                        If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator }
 306                        $LogInnerMessage += $LogErrorInnerExceptionMsg
 307
 308                        $Count++
 309                        $ErrorInnerException = $ErrorInnerException.InnerException
 310                    }
 311                }
 312            }
 313
 314            If ($LogErrorMessage) { $Output = $LogErrorMessage }
 315            If ($LogInnerMessage) { $Output += $LogInnerMessage }
 316
 317            Write-Output -InputObject $Output
 318
 319            If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' }
 320            If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' }
 321            If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' }
 322            If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' }
 323        }
 324    }
 325    End {
 326    }
 327}
 328#endregion
 329
 330#region Function Write-Log
 331Function Write-Log {
 332<#
 333.SYNOPSIS
 334    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format.
 335.DESCRIPTION
 336    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format and optionally display in the console.
 337.PARAMETER Message
 338    The message to write to the log file or output to the console.
 339.PARAMETER Severity
 340    Defines message type. When writing to console or CMTrace.exe log format, it allows highlighting of message type.
 341    Options: 1 = Information (default), 2 = Warning (highlighted in yellow), 3 = Error (highlighted in red)
 342.PARAMETER Source
 343    The source of the message being logged. Also used as the event log source.
 344.PARAMETER ScriptSection
 345    The heading for the portion of the script that is being executed. Default is: $script:installPhase.
 346.PARAMETER LogType
 347    Choose whether to write a CMTrace.exe compatible log file or a Legacy text log file.
 348.PARAMETER LoggingOptions
 349    Choose where to log 'Console', 'File', 'EventLog' or 'None'. You can choose multiple options.
 350.PARAMETER LogFileDirectory
 351    Set the directory where the log file will be saved.
 352.PARAMETER LogFileName
 353    Set the name of the log file.
 354.PARAMETER MaxLogFileSizeMB
 355    Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
 356.PARAMETER LogName
 357    Set the name of the event log.
 358.PARAMETER EventID
 359    Set the event id for the event log entry.
 360.PARAMETER WriteHost
 361    Write the log message to the console.
 362.PARAMETER ContinueOnError
 363    Suppress writing log message to console on failure to write message to log file. Default is: $true.
 364.PARAMETER PassThru
 365    Return the message that was passed to the function
 366.PARAMETER VerboseMessage
 367    Specifies that the message is a debug message. Verbose messages only get logged if -LogDebugMessage is set to $true.
 368.PARAMETER DebugMessage
 369    Specifies that the message is a debug message. Debug messages only get logged if -LogDebugMessage is set to $true.
 370.PARAMETER LogDebugMessage
 371    Debug messages only get logged if this parameter is set to $true in the config XML file.
 372.EXAMPLE
 373    Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace'
 374.EXAMPLE
 375    Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy'
 376.NOTES
 377    Slightly modified version of the PSADT logging cmdlet. I did not write the original cmdlet, please do not credit me for it.
 378.LINK
 379    https://psappdeploytoolkit.com
 380#>
 381    [CmdletBinding()]
 382    Param (
 383        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
 384        [AllowEmptyCollection()]
 385        [Alias('Text')]
 386        [string[]]$Message,
 387        [Parameter(Mandatory = $false, Position = 1)]
 388        [ValidateRange(1, 3)]
 389        [int16]$Severity = 1,
 390        [Parameter(Mandatory = $false, Position = 2)]
 391        [ValidateNotNullorEmpty()]
 392        [string]$Source = $script:LogSource,
 393        [Parameter(Mandatory = $false, Position = 3)]
 394        [ValidateNotNullorEmpty()]
 395        [string]$ScriptSection = $script:RunPhase,
 396        [Parameter(Mandatory = $false, Position = 4)]
 397        [ValidateSet('CMTrace', 'Legacy')]
 398        [string]$LogType = 'CMTrace',
 399        [Parameter(Mandatory = $false, Position = 5)]
 400        [ValidateSet('Host', 'File', 'EventLog', 'None')]
 401        [string[]]$LoggingOptions = $script:LoggingOptions,
 402        [Parameter(Mandatory = $false, Position = 6)]
 403        [ValidateNotNullorEmpty()]
 404        [string]$LogFileDirectory = $script:LogFileDirectory,
 405        [Parameter(Mandatory = $false, Position = 7)]
 406        [ValidateNotNullorEmpty()]
 407        [string]$LogFileName = $($script:LogSource + '.log'),
 408        [Parameter(Mandatory = $false, Position = 8)]
 409        [ValidateNotNullorEmpty()]
 410        [int]$MaxLogFileSizeMB = '4',
 411        [Parameter(Mandatory = $false, Position = 9)]
 412        [ValidateNotNullorEmpty()]
 413        [string]$LogName = $script:LogName,
 414        [Parameter(Mandatory = $false, Position = 10)]
 415        [ValidateNotNullorEmpty()]
 416        [int32]$EventID = 1,
 417        [Parameter(Mandatory = $false, Position = 11)]
 418        [ValidateNotNullorEmpty()]
 419        [boolean]$ContinueOnError = $false,
 420        [Parameter(Mandatory = $false, Position = 12)]
 421        [switch]$PassThru = $false,
 422        [Parameter(Mandatory = $false, Position = 13)]
 423        [switch]$VerboseMessage = $false,
 424        [Parameter(Mandatory = $false, Position = 14)]
 425        [switch]$DebugMessage = $false,
 426        [Parameter(Mandatory = $false, Position = 15)]
 427        [boolean]$LogDebugMessage = $script:LogDebugMessages
 428    )
 429
 430    Begin {
 431        ## Get the name of this function
 432        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
 433
 434        ## Logging Variables
 435        #  Log file date/time
 436        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
 437        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
 438        If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes }
 439        [string]$LogTimePlusBias = $LogTime + '-' + $script:LogTimeZoneBias
 440        #  Initialize variables
 441        [boolean]$WriteHost = $false
 442        [boolean]$WriteFile = $false
 443        [boolean]$WriteEvent = $false
 444        [boolean]$DisableLogging = $false
 445        [boolean]$ExitLoggingFunction = $false
 446        If (('Host' -in $LoggingOptions) -and (-not ($VerboseMessage -or $DebugMessage))) { $WriteHost = $true }
 447        If ('File' -in $LoggingOptions) { $WriteFile = $true }
 448        If ('EventLog' -in $LoggingOptions) { $WriteEvent = $true }
 449        If ('None' -in $LoggingOptions) { $DisableLogging = $true }
 450        #  Check if the script section is defined
 451        [boolean]$ScriptSectionDefined = [boolean](-not [string]::IsNullOrEmpty($ScriptSection))
 452        #  Check if the source is defined
 453        [boolean]$SourceDefined = [boolean](-not [string]::IsNullOrEmpty($Source))
 454        #  Check if the event log and event source exit
 455        [boolean]$LogNameNotExists = (-not [System.Diagnostics.EventLog]::Exists($LogName))
 456        [boolean]$LogSourceNotExists = (-not [System.Diagnostics.EventLog]::SourceExists($Source))
 457
 458        ## Create script block for generating CMTrace.exe compatible log entry
 459        [scriptblock]$CMTraceLogString = {
 460            Param (
 461                [string]$lMessage,
 462                [string]$lSource,
 463                [int16]$lSeverity
 464            )
 465            "<![LOG[$lMessage]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$lSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$lSeverity`" " + "thread=`"$PID`" " + "file=`"$Source`">"
 466        }
 467
 468        ## Create script block for writing log entry to the console
 469        [scriptblock]$WriteLogLineToHost = {
 470            Param (
 471                [string]$lTextLogLine,
 472                [int16]$lSeverity
 473            )
 474            If ($WriteHost) {
 475                #  Only output using color options if running in a host which supports colors.
 476                If ($Host.UI.RawUI.ForegroundColor) {
 477                    Switch ($lSeverity) {
 478                        3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' }
 479                        2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' }
 480                        1 { Write-Host -Object $lTextLogLine }
 481                    }
 482                }
 483                #  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.
 484                Else {
 485                    Write-Output -InputObject $lTextLogLine
 486                }
 487            }
 488        }
 489
 490        ## Create script block for writing log entry to the console as verbose or debug message
 491        [scriptblock]$WriteLogLineToHostAdvanced = {
 492            Param (
 493                [string]$lTextLogLine
 494            )
 495            #  Only output using color options if running in a host which supports colors.
 496            If ($Host.UI.RawUI.ForegroundColor) {
 497                If ($VerboseMessage) {
 498                    Write-Verbose -Message $lTextLogLine
 499                }
 500                Else {
 501                    Write-Debug -Message $lTextLogLine
 502                }
 503            }
 504            #  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.
 505            Else {
 506                Write-Output -InputObject $lTextLogLine
 507            }
 508        }
 509
 510        ## Create script block for event writing log entry
 511        [scriptblock]$WriteToEventLog = {
 512            If ($WriteEvent) {
 513                $EventType = Switch ($Severity) {
 514                    3 { 'Error' }
 515                    2 { 'Warning' }
 516                    1 { 'Information' }
 517                }
 518
 519                If ($LogNameNotExists -and (-not $LogSourceNotExists)) {
 520                    Try {
 521                        #  Delete event source if the log does not exist
 522                        $null = [System.Diagnostics.EventLog]::DeleteEventSource($Source)
 523                        $LogSourceNotExists = $true
 524                    }
 525                    Catch {
 526                        [boolean]$ExitLoggingFunction = $true
 527                        #  If error deleting event source, write message to console
 528                        If (-not $ContinueOnError) {
 529                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log source [$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 530                        }
 531                    }
 532                }
 533                If ($LogNameNotExists -or $LogSourceNotExists) {
 534                    Try {
 535                        #  Create event log
 536                        $null = New-EventLog -LogName $LogName -Source $Source -ErrorAction 'Stop'
 537                    }
 538                    Catch {
 539                        [boolean]$ExitLoggingFunction = $true
 540                        #  If error creating event log, write message to console
 541                        If (-not $ContinueOnError) {
 542                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 543                        }
 544                    }
 545                }
 546                Try {
 547                    #  Write to event log
 548                    Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -EntryType $EventType -Category '0' -Message $ConsoleLogLine -ErrorAction 'Stop'
 549                }
 550                Catch {
 551                    [boolean]$ExitLoggingFunction = $true
 552                    #  If error creating directory, write message to console
 553                    If (-not $ContinueOnError) {
 554                        Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to write to event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 555                    }
 556                }
 557            }
 558        }
 559
 560        ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file
 561        If (($DebugMessage -or $VerboseMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return }
 562        ## Exit function if logging to file is disabled and logging to console host is disabled
 563        If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return }
 564        ## Exit Begin block if logging is disabled
 565        If ($DisableLogging) { Return }
 566
 567        ## Create the directory where the log file will be saved
 568        If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) {
 569            Try {
 570                $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop'
 571            }
 572            Catch {
 573                [boolean]$ExitLoggingFunction = $true
 574                #  If error creating directory, write message to console
 575                If (-not $ContinueOnError) {
 576                    Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 577                }
 578                Return
 579            }
 580        }
 581
 582        ## Assemble the fully qualified path to the log file
 583        [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName
 584    }
 585    Process {
 586
 587        ForEach ($Msg in $Message) {
 588            ## If the message is not $null or empty, create the log entry for the different logging methods
 589            [string]$CMTraceMsg = ''
 590            [string]$ConsoleLogLine = ''
 591            [string]$LegacyTextLogLine = ''
 592            If ($Msg) {
 593                #  Create the CMTrace log message
 594                If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" }
 595
 596                #  Create a Console and Legacy "text" log entry
 597                [string]$LegacyMsg = "[$LogDate $LogTime]"
 598                If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" }
 599                If ($Source) {
 600                    [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg"
 601                    Switch ($Severity) {
 602                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" }
 603                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" }
 604                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" }
 605                    }
 606                }
 607                Else {
 608                    [string]$ConsoleLogLine = "$LegacyMsg :: $Msg"
 609                    Switch ($Severity) {
 610                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" }
 611                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" }
 612                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" }
 613                    }
 614                }
 615            }
 616
 617            ## Execute script block to write the log entry to the console as verbose or debug message
 618            & $WriteLogLineToHostAdvanced -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 619
 620            ## Exit function if logging is disabled
 621            If ($ExitLoggingFunction) { Return }
 622
 623            ## Execute script block to create the CMTrace.exe compatible log entry
 624            [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $lSeverity
 625
 626            ## Choose which log type to write to file
 627            If ($LogType -ieq 'CMTrace') {
 628                [string]$LogLine = $CMTraceLogLine
 629            }
 630            Else {
 631                [string]$LogLine = $LegacyTextLogLine
 632            }
 633
 634            ## Write the log entry to the log file and event log if logging is not currently disabled
 635            If (-not $DisableLogging) {
 636                If ($WriteFile) {
 637                    ## Write to file log
 638                    Try {
 639                        $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop'
 640                    }
 641                    Catch {
 642                        If (-not $ContinueOnError) {
 643                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 644                        }
 645                    }
 646                }
 647                If ($WriteEvent) {
 648                    ## Write to event log
 649                    Try {
 650                        & $WriteToEventLog -lMessage $ConsoleLogLine -lName $LogName -lSource $Source -lSeverity $Severity
 651                    }
 652                    Catch {
 653                        If (-not $ContinueOnError) {
 654                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
 655                        }
 656                    }
 657                }
 658            }
 659
 660            ## Execute script block to write the log entry to the console if $WriteHost is $true and $LogLogDebugMessage is not $true
 661            & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity
 662        }
 663    }
 664    End {
 665        ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0
 666        Try {
 667            If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) {
 668                [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop'
 669                [decimal]$LogFileSizeMB = $LogFile.Length / 1MB
 670                If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) {
 671                    ## Change the file extension to "lo_"
 672                    [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_')
 673                    [hashtable]$ArchiveLogParams = @{ ScriptSection = $ScriptSection; Source = ${CmdletName}; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; WriteHost = $WriteHost; ContinueOnError = $ContinueOnError; PassThru = $false }
 674
 675                    ## Log message about archiving the log file
 676                    $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]."
 677                    Write-Log -Message $ArchiveLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}
 678
 679                    ## 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.
 680                    Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop'
 681
 682                    ## Start new log file and Log message about archiving the old log file
 683                    $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached."
 684                    Write-Log -Message $NewLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}
 685                }
 686            }
 687        }
 688        Catch {
 689            ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size
 690        }
 691        Finally {
 692            If ($PassThru) { Write-Output -InputObject $Message }
 693        }
 694    }
 695}
 696#endregion
 697
 698#region Function Show-Progress
 699Function Show-Progress {
 700<#
 701.SYNOPSIS
 702    Displays progress info.
 703.DESCRIPTION
 704    Displays progress info and maximizes code reuse by automatically calculating the progress steps.
 705.PARAMETER Activity
 706    Specifies the progress activity. Default: 'Cleaning Up Configuration Manager Client Cache, Please Wait...'.
 707.PARAMETER Status
 708    Specifies the progress status.
 709.PARAMETER CurrentOperation
 710    Specifies the current operation.
 711.PARAMETER Step
 712    Specifies the progress step. Default: $Script:Step ++.
 713.PARAMETER Steps
 714    Specifies the progress steps. Default: $Script:Steps ++.
 715.PARAMETER ID
 716    Specifies the progress bar id.
 717.PARAMETER Delay
 718    Specifies the progress delay in milliseconds. Default: 0.
 719.PARAMETER Loop
 720    Specifies if the call comes from a loop.
 721.EXAMPLE
 722    Show-Progress -Activity 'Cleaning Up Configuration Manager Client Cache, Please Wait...' -Status 'Cleaning WMI' -Step ($Step++) -Delay 200
 723.INPUTS
 724    None.
 725.OUTPUTS
 726    None.
 727.NOTES
 728    Created by Ioan Popovici.
 729    v2.0.0 - 2021-01-01
 730
 731    This is an private function should typically not be called directly.
 732    Credit to Adam Bertram.
 733
 734    ## !! IMPORTANT !! ##
 735    #  You need to tokenize the scripts steps at the beginning of the script in order for Show-Progress to work:
 736
 737    ## Get script path and name
 738    [string]$ScriptPath = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)
 739    [string]$ScriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Definition)
 740    [string]$ScriptFullName = Join-Path -Path $ScriptPath -ChildPath $ScriptName
 741    #  Get progress steps
 742    $ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $PSItem.Type -eq 'Command' -and $PSItem.Content -eq 'Show-Progress' }).Count)
 743    $ForEachSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $PSItem.Type -eq 'Keyword' -and $PSItem.Content -eq 'ForEach' }).Count)
 744    #  Set progress steps
 745    $Script:Steps = $ProgressSteps - $ForEachSteps
 746    $Script:Step = 0
 747.LINK
 748    https://adamtheautomator.com/building-progress-bar-powershell-scripts/
 749.LINK
 750    https://MEM.Zone
 751.LINK
 752    https://MEM.Zone/GIT
 753.LINK
 754    https://MEM.Zone/ISSUES
 755.COMPONENT
 756    Powershell
 757.FUNCTIONALITY
 758    Show Progress
 759#>
 760    [CmdletBinding()]
 761    Param (
 762        [Parameter(Mandatory=$false,Position=0)]
 763        [ValidateNotNullorEmpty()]
 764        [Alias('act')]
 765        [string]$Activity = 'Cleaning Up Configuration Manager Client Cache, Please Wait...',
 766        [Parameter(Mandatory=$true,Position=1)]
 767        [ValidateNotNullorEmpty()]
 768        [Alias('sta')]
 769        [string]$Status,
 770        [Parameter(Mandatory=$false,Position=2)]
 771        [ValidateNotNullorEmpty()]
 772        [Alias('cro')]
 773        [string]$CurrentOperation,
 774        [Parameter(Mandatory=$false,Position=3)]
 775        [ValidateNotNullorEmpty()]
 776        [Alias('pid')]
 777        [int]$ID = 0,
 778        [Parameter(Mandatory=$false,Position=4)]
 779        [ValidateNotNullorEmpty()]
 780        [Alias('ste')]
 781        [int]$Step = $Script:Step ++,
 782        [Parameter(Mandatory=$false,Position=5)]
 783        [ValidateNotNullorEmpty()]
 784        [Alias('sts')]
 785        [int]$Steps = $Script:Steps,
 786        [Parameter(Mandatory=$false,Position=6)]
 787        [ValidateNotNullorEmpty()]
 788        [Alias('del')]
 789        [string]$Delay = 0,
 790        [Parameter(Mandatory=$false,Position=7)]
 791        [ValidateNotNullorEmpty()]
 792        [Alias('lp')]
 793        [switch]$Loop
 794    )
 795    Begin {
 796
 797
 798    }
 799    Process {
 800        Try {
 801            If ($Step -eq 0) {
 802                $Step ++
 803                $Script:Step ++
 804                $Steps ++
 805                $Script:Steps ++
 806            }
 807            If ($Steps -eq 0) {
 808                $Steps ++
 809                $Script:Steps ++
 810            }
 811
 812            [boolean]$Completed = $false
 813            [int]$PercentComplete = $($($Step / $Steps) * 100)
 814
 815            If ($PercentComplete -ge 100)  {
 816                $PercentComplete = 100
 817                $Completed = $true
 818                $Script:CurrentStep ++
 819                $Script:Step = $Script:CurrentStep
 820                $Script:Steps = $Script:DefaultSteps
 821            }
 822
 823            ## Debug information
 824            Write-Verbose -Message "Percent Step: $Step"
 825            Write-Verbose -Message "Percent Steps: $Steps"
 826            Write-Verbose -Message "Percent Complete: $PercentComplete"
 827            Write-Verbose -Message "Completed: $Completed"
 828
 829            ##  Show progress
 830            Write-Progress -Activity $Activity -Status $Status -CurrentOperation $CurrentOperation -ID $ID -PercentComplete $PercentComplete -Completed:$Completed
 831            If ($Delay -ne 0) { Start-Sleep -Milliseconds $Delay }
 832        }
 833        Catch {
 834            Throw (New-Object System.Exception("Could not Show progress status [$Status]! $($PSItem.Exception.Message)", $PSItem.Exception))
 835        }
 836    }
 837}
 838#endregion
 839
 840#region Format-Bytes
 841Function Format-Bytes {
 842<#
 843.SYNOPSIS
 844    Formats a number of bytes in the corresponding sizes.
 845.DESCRIPTION
 846    Formats a number of bytes bytes in the corresponding sizes depending or the size ('KB','MB','GB','TB','PB').
 847.PARAMETER Bytes
 848    Specifies bytes to format.
 849.EXAMPLE
 850    Format-Bytes -Bytes 12344567890
 851.INPUTS
 852    System.Single.
 853.OUTPUTS
 854    System.String.
 855.NOTES
 856    Created by Ioan Popovici.
 857    v1.0.0 - 2021-09-01
 858
 859    This is an private function should typically not be called directly.
 860    Credit to Anthony Howell.
 861.LINK
 862    https://theposhwolf.com/howtos/Format-Bytes/
 863.LINK
 864    https://MEM.Zone
 865.LINK
 866    https://MEM.Zone/GIT
 867.LINK
 868    https://MEM.Zone/ISSUES
 869.COMPONENT
 870    Powershell
 871.FUNCTIONALITY
 872    Format Bytes
 873#>
 874    Param (
 875        [Parameter(ValueFromPipeline = $true)]
 876        [ValidateNotNullOrEmpty()]
 877        [float]$Bytes
 878    )
 879    Begin {
 880        [string]$Output = $null
 881        [boolean]$Negative = $false
 882        $Sizes = 'KB','MB','GB','TB','PB'
 883    }
 884    Process {
 885        Try {
 886            If ($Bytes -le 0) {
 887                $Bytes = -$Bytes
 888                [boolean]$Negative = $true
 889            }
 890            For ($Counter = 0; $Counter -lt $Sizes.Count; $Counter++) {
 891                If ($Bytes -lt "1$($Sizes[$Counter])") {
 892                    If ($Counter -eq 0) {
 893                    $Number = $Bytes
 894                    $Sizes = 'B'
 895                    }
 896                    Else {
 897                        $Number = $Bytes / "1$($Sizes[$Counter-1])"
 898                        $Number = '{0:N2}' -f $Number
 899                        $Sizes = $Sizes[$Counter-1]
 900                    }
 901                }
 902            }
 903        }
 904        Catch {
 905            $Output = "Format Failed for Bytes ($Bytes! Error: $($_.Exception.Message)"
 906            Write-Log -Message $Output -EventID 2 -Severity 3
 907        }
 908        Finally {
 909            If ($Negative) { $Number = -$Number }
 910            $Output = '{0} {1}' -f $Number, $Sizes
 911            Write-Output -InputObject $Output
 912        }
 913    }
 914    End{
 915    }
 916}
 917#endregion
 918
 919#region Function Get-CCMApplicationInfo
 920Function Get-CCMApplicationInfo {
 921<#
 922.SYNOPSIS
 923    Lists ccm cached application information.
 924.DESCRIPTION
 925    Lists ccm cached application information.
 926.PARAMETER ContentID
 927    Specify cache ContentID, optional.
 928.EXAMPLE
 929    Get-CCMApplicationInfo
 930.INPUTS
 931    None.
 932.OUTPUTS
 933    None.
 934    System.Management.Automation.PSObject.
 935.NOTES
 936    This is an internal script function and should typically not be called directly.
 937.LINK
 938    https://MEM.Zone
 939.LINK
 940    https://MEM.Zone/GIT
 941.LINK
 942    https://MEM.Zone/ISSUES
 943.COMPONENT
 944    CM Client Cache
 945.FUNCTIONALITY
 946    Get cached application name
 947#>
 948    [CmdletBinding()]
 949    Param (
 950        [Parameter(Mandatory = $false, Position = 0)]
 951        [ValidateNotNullorEmpty()]
 952        [string]$ContentID = $null
 953    )
 954    Begin {
 955        Try {
 956
 957            ## Get the name of this function and write verbose header
 958            [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
 959            #  Write verbose header
 960            Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${CmdletName}
 961
 962            ## Get ccm application list
 963            $Applications = Get-CimInstance -Namespace 'Root\ccm\ClientSDK' -ClassName 'CCM_Application' -Verbose:$false -ErrorAction 'SilentlyContinue'
 964
 965            ## Initialize output object
 966            [psobject]$Output = @()
 967        }
 968        Catch {
 969
 970            ## Return custom error
 971            $Message       = [string]"Error getting cached applications.`n{0}. `n!! TEST !!`n{1}" -f $($PSItem.Exception.Message), $(Resolve-Error)
 972            $Exception     = [Exception]::new($Message)
 973            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
 974            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $Applications)
 975            #  Write to log
 976            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
 977            #  Throw terminating error
 978            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
 979        }
 980    }
 981    Process {
 982        Try {
 983
 984            ## Get cached application info
 985            $Output = ForEach ($Application in $Applications) {
 986
 987                ## Show progress bar
 988                Show-Progress -Status "Getting info for [Application] --> [$($Application.FullName)]" -Steps $Applications.Count
 989
 990                ## Get application deployment types
 991                $ApplicationDTs = ($Application | Get-CimInstance -Verbose:$false -ErrorAction 'SilentlyContinue').AppDTs
 992
 993                ## Get application content ID
 994                ForEach ($DeploymentType in $ApplicationDTs) {
 995
 996                    ## Get allowed actions (each action can have a different content id)
 997                    ForEach ($ActionType in $DeploymentType.AllowedActions) {
 998
 999                        #  Assemble Invoke-Method arguments
1000                        $Arguments = [hashtable]@{
1001                            'AppDeliveryTypeID' = [string]$($DeploymentType.ID)
1002                            'Revision'          = [uint32]$($DeploymentType.Revision)
1003                            'ActionType'        = [string]$($ActionType)
1004                        }
1005                        #  Get app content ID via GetContentInfo wmi method
1006                        $AppContentID = (Invoke-CimMethod -Namespace 'Root\ccm\cimodels' -ClassName 'CCM_AppDeliveryType' -MethodName 'GetContentInfo' -Arguments $Arguments -Verbose:$false).ContentID
1007                        [psobject]@{
1008                            'Name'              = $Application.FullName
1009                            'ContentID'         = $AppContentID
1010                            'AppDeliveryTypeID' = $DeploymentType.ID
1011                            'InstallState'      = $Application.InstallState
1012                        }
1013                    }
1014                }
1015            }
1016            $Output = $Output | Sort-Object | Select-Object -Unique
1017        }
1018        Catch {
1019
1020            ## Return custom error
1021            $Message       = [string]"Error getting cached application {0}.`n{1}. `n!! TEST !!`n{2}" -f $($Application.Name), $($PSItem.Exception.Message), $(Resolve-Error)
1022            $Exception     = [Exception]::new($Message)
1023            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1024            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $Application)
1025            #  Write to log
1026            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1027            #  Throw terminating error
1028            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1029        }
1030        Finally {
1031            If (-not [string]::IsNullOrWhiteSpace($ContentID)) { $Output = $Output | Where-Object -Property 'ContentID' -eq $ContentID }
1032            Write-Output -InputObject $Output
1033        }
1034    }
1035    End {
1036
1037        ## Write verbose footer
1038        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${CmdletName}
1039    }
1040}
1041#endregion
1042
1043#region Function Get-CCMOrphanedCache
1044Function Get-CCMOrphanedCache {
1045<#
1046.SYNOPSIS
1047    Lists ccm orphaned cache items.
1048.DESCRIPTION
1049    Lists configuration manager client disk cache items that are not found in WMI and vice versa.
1050.EXAMPLE
1051    Get-CCMOrphanedCache
1052.INPUTS
1053    None.
1054.OUTPUTS
1055    None.
1056    System.Management.Automation.PSCustomObject.
1057.NOTES
1058    This is an internal script function and should typically not be called directly.
1059.LINK
1060    https://MEM.Zone
1061.LINK
1062    https://MEM.Zone/GIT
1063.LINK
1064    https://MEM.Zone/ISSUES
1065.COMPONENT
1066    CM Client Cache
1067.FUNCTIONALITY
1068    Get orphaned cached items
1069#>
1070
1071    [CmdletBinding()]
1072    Param ()
1073    Begin {
1074        Try {
1075
1076            ## Get the name of this function and write verbose header
1077            [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
1078
1079            #  Write verbose header
1080            Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${CmdletName}
1081
1082            ## Initialize the CCM resource manager com object
1083            [__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'
1084
1085            ## Get ccm cache info
1086            $CacheInfo = $CCMComObject.GetCacheInfo()
1087
1088            ## Get ccm disk cache info
1089            [string]$DiskCachePath = $CacheInfo.Location
1090            [psobject]$DiskCacheInfo = Get-ChildItem -LiteralPath $DiskCachePath | Select-Object -Property 'FullName'
1091
1092            ## Get ccm wmi cache info
1093            $WmiCacheInfo = $CacheInfo.GetCacheElements()
1094
1095            ## Get ccm wmi cache paths
1096            [string[]]$WmiCachePaths = $WmiCacheInfo | Select-Object -ExpandProperty 'Location'
1097
1098            ## Create a file system object
1099            $FileSystemObject = New-Object -ComObject 'Scripting.FileSystemObject'
1100
1101            ## Initialize output object
1102            [pscustomobject]$Output = $null
1103        }
1104        Catch {
1105
1106            ## Return custom error
1107            $Message       = [string]"Error getting orphaned cache items `n{0}" -f $(Resolve-Error)
1108            $Exception     = [Exception]::new($Message)
1109            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1110            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $WmiCacheInfo)
1111            #  Write to log
1112            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1113            #  Throw terminating error
1114            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1115        }
1116    }
1117    Process {
1118        Try {
1119
1120            [scriptblock]$GetCCMOrphanedCache = {
1121                ## Process disk cache items
1122                ForEach ($CacheElement in $DiskCacheInfo) {
1123                    $CacheElementPath = $($CacheElement.FullName)
1124                    $CacheElementSize = $FileSystemObject.GetFolder($CacheElementPath).Size
1125
1126                    ## Show progress bar
1127                    Show-Progress -Status "Searching Disk for Orphaned CCMCache --> [$CacheElementPath]" -Steps $DiskCacheInfo.Count
1128
1129                    ## Include if disk cache path is not present in wmi
1130                    If ($CacheElementPath -notin $WmiCachePaths) {
1131                        #  Assemble output object
1132                        [pscustomobject]@{
1133                            'CacheType'           = 'Orphaned'
1134                            'Name'                = 'Orphaned Disk Cache'
1135                            'Tombstoned'          = $true
1136                            'EligibleForDeletion' = $true
1137                            'ContentID'           = 'N/A'
1138                            'Location'            = $CacheElementPath
1139                            'ContentVersion'      = '0'
1140                            'LastReferenceTime'   = $CacheInfo.MaxCacheDuration + 1
1141                            'ReferenceCount'      = '0'
1142                            'ContentSize'         = $CacheElementSize
1143                            'CacheElementID'      = 'N/A'
1144                            'Status'              = 'Cached'
1145                        }
1146                    }
1147                }
1148
1149                ## Process wmi cache items
1150                ForEach ($CacheElement in $WmiCacheInfo) {
1151
1152                    ## Show progress bar
1153                    Show-Progress -Status "Searching WMI for Orphaned CCMCache --> [$($CacheElement.CacheElementID)]" -Steps $WmiCacheInfo.Count
1154
1155                    ## Include if wmi cache path is not present on disk
1156                    If ($CacheElement.Location -notin $DiskCacheInfo.FullName) {
1157                        #  Assemble output object props
1158                        [pscustomobject]@{
1159                            'CacheType'           = 'Orphaned'
1160                            'Name'                = 'Orphaned WMI Cache'
1161                            'Tombstoned'          = $true
1162                            'EligibleForDeletion' = $true
1163                            'ContentID'           = $CacheElement.ContentID
1164                            'Location'            = $CacheElement.Location
1165                            'ContentVersion'      = $CacheElement.ContentVersion
1166                            'LastReferenceTime'   = $CacheInfo.MaxCacheDuration + 1
1167                            'ReferenceCount'      = '0'
1168                            'ContentSize'         = $CacheElement.ContentSize
1169                            'CacheElementID'      = $CacheElement.CacheElementID
1170                            'Status'              = 'Cached'
1171                        }
1172                    }
1173                }
1174            }
1175            $Output = $GetCCMOrphanedCache.Invoke()
1176        }
1177        Catch {
1178
1179            ## Return custom error
1180            If ( [string]::IsNullOrWhiteSpace($CacheElementPath) ) { $CacheElementPath = $CacheElement.Location }
1181            $Message       = [string]"Error getting orphaned cache item '{0}'`n{1}" -f $CacheElementPath, $(Resolve-Error)
1182            $Exception     = [Exception]::new($Message)
1183            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1184            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheElement)
1185            #  Write to log
1186            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1187            #  Throw terminating error
1188            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1189        }
1190        Finally {
1191            Write-Output -InputObject $Output
1192        }
1193    }
1194    End {
1195
1196        ## Write verbose footer
1197        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${CmdletName}
1198    }
1199}
1200#endregion
1201
1202#region Function Get-CCMCacheInfo
1203Function Get-CCMCacheInfo {
1204<#
1205.SYNOPSIS
1206    Gets the ccm cache information.
1207.DESCRIPTION
1208    Gets the ccm cache information like cache type, status and delete flag.
1209.PARAMETER CacheType
1210    Specifies Cache Type to process. ('All', 'Application', 'Package', 'Update', 'Orphaned'). Default is: 'All'.
1211    If it's set to 'All' all cache will be processed.
1212.EXAMPLE
1213    Get-CCMCacheInfo -CacheType 'Application'
1214.INPUTS
1215    None.
1216.OUTPUTS
1217    None.
1218    System.Management.Automation.PSObject.
1219.NOTES
1220    This is an internal script function and should typically not be called directly.
1221.LINK
1222    https://MEM.Zone
1223.LINK
1224    https://MEM.Zone/GIT
1225.LINK
1226    https://MEM.Zone/ISSUES
1227.COMPONENT
1228    CM Client Cache
1229.FUNCTIONALITY
1230    Get ccm cache info
1231#>
1232    [CmdletBinding()]
1233    Param (
1234        [Parameter(Mandatory = $false, Position = 0)]
1235        [ValidateSet('All', 'Application', 'Package', 'Update', 'Orphaned')]
1236        [Alias('Type')]
1237        [string[]]$CacheType = 'All'
1238    )
1239    Begin {
1240        Try {
1241
1242            ## Get the name of this function and write verbose header
1243            [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
1244            #  Write verbose header
1245            Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${CmdletName}
1246
1247            ## Initialize the CCM resource manager com object
1248            [__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'
1249
1250            ## Get ccm cache info
1251            $CacheInfo = $CCMComObject.GetCacheInfo()
1252
1253            ## Get ccm cache info
1254            $CachedElements = $CacheInfo.GetCacheElements()
1255
1256            ## Get ccm cached application info
1257            $ApplicationInfo = Get-CCMApplicationInfo
1258
1259            ## Get ccm cached package info
1260            $PackageInfo = Get-CimInstance -Namespace 'Root\ccm\ClientSDK' -ClassName 'CCM_Program' -ErrorAction 'SilentlyContinue' -Verbose:$false
1261
1262            ## Get ccm update list
1263            $UpdateInfo = Get-CimInstance -Namespace 'Root\ccm\SoftwareUpdates\UpdatesStore' -ClassName 'CCM_UpdateStatus' -ErrorAction 'SilentlyContinue' -Verbose:$false
1264
1265            ## CurrentTime
1266            $Now = [datetime]::Now
1267
1268            ## Initialize output object
1269            [psobject]$Output = @()
1270        }
1271        Catch {
1272
1273            ## Return custom error
1274            $Message       = [string]"Error getting cached elements`n{0}" -f $(Resolve-Error)
1275            $Exception     = [Exception]::new($Message)
1276            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1277            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheInfo)
1278            #  Write to log
1279            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1280            #  Throw terminating error
1281            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1282        }
1283    }
1284    Process {
1285        Try {
1286
1287            ## Filter cache elements by Cache Type
1288            $CachedElements = Switch ($CacheType) {
1289                'All' {
1290                    $CachedElements
1291                    Get-CCMOrphanedCache
1292                    Break
1293                }
1294                'Application' {
1295                    $CachedElements | Where-Object -Property 'ContentID' -match '^Content'
1296                }
1297                'Package' {
1298                    $CachedElements | Where-Object -Property 'ContentID' -match '^\w{8}$'
1299                }
1300                'Update' {
1301                    $CachedElements | Where-Object -Property 'ContentID' -match '^[\dA-F]{8}-(?:[\dA-F]{4}-){3}[\dA-F]{12}$'
1302                }
1303                'Orphaned' {
1304                    Get-CCMOrphanedCache
1305                }
1306            }
1307
1308            ## Sort by CacheType
1309            $CachedElements = $CachedElements | Sort-Object -Property 'CacheType'
1310
1311            ## Get cached element info
1312            ForEach ($CachedElement in $CachedElements) {
1313
1314                ## Debug info
1315                Write-Log -Message "CurrentCachedElement: `n $($CachedElement | Out-String)" -DebugMessage -ScriptSection ${CmdletName}
1316
1317                ## Get the cache info for the element using the ContentID
1318                Switch -Regex ($CachedElement.ContentID) {
1319                    '^Content' {
1320                        $ResolvedCacheType = 'Application'
1321                        $Name      = $($ApplicationInfo | Where-Object -Property 'ContentID' -eq $CachedElement.ContentID).FullName
1322                        Break
1323                    }
1324                    '^\w{8}$' {
1325                        $ResolvedCacheType = 'Package'
1326                        $Name      = $($PackageInfo | Where-Object -Property 'PackageID' -eq $CachedElement.ContentID).FullName
1327                        Break
1328                    }
1329                    '^[\dA-F]{8}-(?:[\dA-F]{4}-){3}[\dA-F]{12}$'   {
1330                        $ResolvedCacheType = 'Update'
1331                        $Name      = $($UpdateInfo | Where-Object -Property 'UniqueID' -eq $CachedElement.ContentID).Title
1332                        Break
1333                    }
1334                    Default {
1335
1336                        ## Return custom error
1337                        $Message       = [string]"Invalid cache type '{0}'`n{1}" -f $($CacheType), $(Resolve-Error)
1338                        $Exception     = [Exception]::new($Message)
1339                        $ExceptionType = [Management.Automation.ErrorCategory]::NotImplemented
1340                        $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheType)
1341                        #  Write to log
1342                        Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1343                        #  Throw terminating error
1344                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1345                    }
1346                }
1347
1348                ## Only write the info to the result object for non-orphaned cache. Orphaned cache already has this info populated.
1349                If ($CachedElement.CacheType -ne 'Orphaned') {
1350                    ## An unreferenced item is eligible for deletion if the time specified in its LastReferenceTime property is longer than the time specified in TombStoneDuration
1351                    If ($CachedElement.ReferenceCount -eq 0) {
1352                        $TombStoned          = If ($Now - $CachedElement.LastReferenceTime -ge $CacheInfo.TombStoneDuration) { $true } Else { $false }
1353                        $EligibleForDeletion = $TombStoned
1354                        $Status              = 'Cached'
1355                    }
1356
1357                    ## A referenced item is eligible for deletion if the time specified in its LastReferenceTime property is longer than the time specified MaxCacheDuration
1358                    Else {
1359                        $TombStoned          = $false
1360                        $EligibleForDeletion = If ($Now - $CachedElement.LastReferenceTime -ge $CacheInfo.MaxCacheDuration) { $true } Else { $false }
1361                        $Status              = 'Cached'
1362                    }
1363
1364                    ## Add new object properties
1365                    If ([string]::IsNullOrWhiteSpace($Name)) { $Name = 'N/A' }
1366                    $CachedElement | Add-Member -MemberType 'NoteProperty' -Name 'CacheType' -Value $ResolvedCacheType -ErrorAction 'SilentlyContinue'
1367                    $CachedElement | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name -ErrorAction 'SilentlyContinue'
1368                    $CachedElement | Add-Member -MemberType 'NoteProperty' -Name 'TombStoned' -Value $TombStoned -ErrorAction 'SilentlyContinue'
1369                    $CachedElement | Add-Member -MemberType 'NoteProperty' -Name 'EligibleForDeletion' -Value $EligibleForDeletion -ErrorAction 'SilentlyContinue'
1370                    $CachedElement | Add-Member -MemberType 'NoteProperty' -Name 'Status' -Value $Status -ErrorAction 'SilentlyContinue'
1371                }
1372
1373                ## Show progress bar
1374                Show-Progress -Status "Getting info for [$($CachedElement.CacheType)] --> [$($CachedElement.CacheElementId)]" -Steps $($CachedElements.ContentID).Count
1375
1376                ## Set Output
1377                $Output = $CachedElements
1378            }
1379        }
1380        Catch {
1381
1382            ## Return custom error
1383            $Message       = [string]"Error getting cached element '{0}'`n{1}" -f $($CachedElement.ContentID), $(Resolve-Error)
1384            $Exception     = [Exception]::new($Message)
1385            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1386            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CachedElement)
1387            #  Write to log
1388            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1389            #  Throw terminating error
1390            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1391        }
1392        Finally {
1393            Write-Output -InputObject $Output
1394        }
1395    }
1396    End {
1397
1398        ## Write verbose footer
1399        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${CmdletName}
1400    }
1401}
1402#endregion
1403
1404#region Function Remove-CCMCacheElement
1405Function Remove-CCMCacheElement {
1406<#
1407.SYNOPSIS
1408    Deletes a ccm cache element.
1409.DESCRIPTION
1410    Deletes a ccm cache element by CacheElement.
1411.PARAMETER CacheElement
1412    Specifies the cache element CacheElement to process.
1413.PARAMETER DeletePinned
1414    Specifies to remove cache even if it's pinned.
1415.EXAMPLE
1416    Remove-CCMCacheElement -CacheElement $CacheElement -DeletePinned
1417.INPUTS
1418    System.Management.Automation.PSObject.
1419    System.Management.Automation.PSCustomObject.
1420.OUTPUTS
1421    System.Management.Automation.PSObject.
1422.NOTES
1423    This is an internal script function and should typically not be called directly.
1424.LINK
1425    https://MEM.Zone
1426.LINK
1427    https://MEM.Zone/GIT
1428.LINK
1429    https://MEM.Zone/ISSUES
1430.COMPONENT
1431    CM Client Cache
1432.FUNCTIONALITY
1433    Removes a ccm cache element.
1434#>
1435    [CmdletBinding()]
1436    Param (
1437        [Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0)]
1438        [Alias('CacheItem')]
1439        [psobject]$CacheElement,
1440        [Parameter(Mandatory = $false, Position = 1)]
1441        [switch]$DeletePinned
1442    )
1443
1444    Begin {
1445        Try {
1446
1447            ## Get the name of this function and write verbose header
1448            [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
1449
1450            #  Write verbose header
1451            Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${CmdletName}
1452
1453            ## Initialize the CCM resource manager com object
1454            [__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'
1455
1456            ## Initialize output object
1457            [psobject]$Output = $null
1458        }
1459        Catch {
1460
1461            ## Return custom error
1462            $Message       = [string]"Error getting ccm cache`n{0}" -f $(Resolve-Error)
1463            $Exception     = [Exception]::new($Message)
1464            $ExceptionType = [Management.Automation.ErrorCategory]::ObjectNotFound
1465            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheInfo)
1466            #  Write to log
1467            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1468            #  Throw terminating error
1469            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1470        }
1471    }
1472    Process {
1473        Try {
1474            If ($CacheElement.CacheElementID -eq 'N/A') {
1475                Try {
1476                    $null = Remove-Item -LiteralPath $CacheElement.Location -Recurse -Force -ErrorAction 'Stop'
1477                    $CacheElement.Status = 'Deleted'
1478                }
1479                Catch { $CacheElement.Status = "Delete Error" }
1480            }
1481            Else {
1482
1483                ## Delete cache Element
1484                $null = $CCMComObject.GetCacheInfo().DeleteCacheElementEx([string]$($CacheElement.CacheElementID), [bool]$DeletePinned)
1485                $CacheElement.Status = 'Deleted'
1486
1487                ## This is a hack making the script slower to check if the cache element is pinned.
1488                #  'PersistInCache' value is no longer in use and there is no documentation about the 'DeploymentFlags'
1489                If ($CacheElement.CacheType -in @('Application', 'Package')) {
1490
1491                    ## Check if the CacheElement has been deleted
1492                    $CacheInfo = ($CCMComObject.GetCacheInfo().GetCacheElements()) | Where-Object { $PSItem.CacheElementID -eq $CacheElement.CacheElementID }
1493                    #  If cache item still exists perform additional checks.
1494                    If ($CacheInfo.CacheElementID.Count -eq 1) {
1495                        If ($DeletePinned) { $CacheElement.Status = 'Delete Error' }
1496                        #  If cache item still exists and DeletePinned is not specified set the Status to 'Pinned'
1497                        Else { $CacheElement.Status = 'Pinned' }
1498                    }
1499                }
1500            }
1501            $Output = $CacheElement
1502        }
1503        Catch {
1504
1505            ## Return custom error
1506            $Message       = [string]"Error deleting cache item '{0}'`n{1}" -f $($CacheElement.CacheElementID), $(Resolve-Error)
1507            $Exception     = [Exception]::new($Message)
1508            $ExceptionType = [Management.Automation.ErrorCategory]::OperationStopped
1509            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheElement)
1510            #  Write to log
1511            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1512            #  Throw terminating error
1513            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1514        }
1515        Finally {
1516            Write-Output -InputObject $Output
1517        }
1518    }
1519    End {
1520
1521        ## Write verbose footer
1522        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${CmdletName}
1523    }
1524}
1525#endregion
1526
1527#region Function Invoke-CCMCacheCleanup
1528Function Invoke-CCMCacheCleanup {
1529<#
1530.SYNOPSIS
1531    Cleans the configuration manager client cache.
1532.DESCRIPTION
1533    Cleans the configuration manager client cache according to the specified parameters.
1534.PARAMETER CacheType
1535    Specifies Cache Type to clean. ('All', 'Application', 'Package', 'Update', 'Orphaned'). Default is: 'All'.
1536    If it's set to 'All' all cache will be processed.
1537.PARAMETER CleanupType
1538    Specifies Cleanup Type to clean. ('All', 'Automatic', 'ListOnly', 'Tombstoned', 'Referenced'). Default is: 'Automatic'.
1539    If 'All', 'Automatic' or 'ListOnly' is selected the other options will be ignored.
1540    An 'Referenced' item is eligible for deletion if the time specified in its 'LastReferenceTime' property is longer than the time specified 'MaxCacheDuration'.
1541    An 'Unreferenced' item is eligible for deletion if the time specified in its 'LastReferenceTime' property is longer than the time specified in 'TombStoneDuration'.
1542
1543    Available Cleanup Options:
1544        - 'All'
1545            Tombstoned and Referenced cache will be deleted, 'SkipSuperPeer' and 'DeletePinned' switches will still be respected.
1546            The 'EligibleForDeletion' convention is NOT respected.
1547            Not recommended but still safe to use, cache will be redownloaded when needed
1548        - 'Automatic'
1549            'Tombstoned' and 'Referenced' will be selected depending on 'FreeDiskSpaceThreshold' parameter.
1550            If under threshold only 'Tombstoned' cache items will be deleted.
1551            If over threshold, both 'Tombstoned' and 'Referenced' cache items will be deleted.
1552            The 'EligibleForDeletion' convention is still respected.
1553        - 'Tombstoned'
1554            Only 'Tombstoned' cache items will be deleted.
1555            The 'EligibleForDeletion' convention is still respected.
1556        - 'Referenced'
1557            Only 'Referenced' cache items will be deleted.
1558            The 'EligibleForDeletion' convention is still respected.
1559            Not recommended but still safe to use, cache will be redownloaded when needed
1560.PARAMETER DeletePinned
1561    This switch specifies to remove cache even if it's pinned (Applications and Packages). Default is: $false.
1562.EXAMPLE
1563    Invoke-CCMCacheCleanup -CacheType "Application, Package, Update, Orphaned" -CleanupType "Tombstoned, Referenced" -DeletePinned
1564.INPUTS
1565    None.
1566.OUTPUTS
1567    System.Management.Automation.PSObject.
1568.NOTES
1569    Created by Ioan Popovici
1570.LINK
1571    https://MEM.Zone
1572.LINK
1573    https://MEM.Zone/GIT
1574.LINK
1575    https://MEM.Zone/ISSUES
1576.COMPONENT
1577    CM Client
1578.FUNCTIONALITY
1579    Clean CM Client Cache
1580#>
1581    [CmdletBinding()]
1582    Param (
1583        [Parameter(Mandatory = $false, Position = 0)]
1584        [ValidateSet('All', 'Application', 'Package', 'Update', 'Orphaned')]
1585        [Alias('Type')]
1586        [string[]]$CacheType = 'All',
1587        [Parameter(Mandatory = $false, Position = 1)]
1588        [ValidateSet('All', 'Automatic', 'ListOnly', 'Tombstoned', 'Referenced')]
1589        [Alias('Action')]
1590        [string[]]$CleanupType = 'Automatic',
1591        [Parameter(Mandatory = $false, Position = 2)]
1592        [ValidateNotNullorEmpty()]
1593        [Alias('FreeSpace')]
1594        [int16]$FreeDiskSpaceThreshold = 100,
1595        [Parameter(Mandatory = $false, Position = 3)]
1596        [switch]$SkipSuperPeer,
1597        [Parameter(Mandatory = $false, Position = 4)]
1598        [switch]$DeletePinned
1599    )
1600
1601    Begin {
1602
1603        ## Get the name of this function and write verbose header
1604        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
1605
1606        #  Write verbose header
1607        Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${CmdletName}
1608
1609        ## Initialize output object
1610        [psobject]$Output = $null
1611    }
1612    Process {
1613        Try {
1614
1615            ## Get cache elements info according to selected options
1616            [psobject]$CacheElements = Switch ($CacheType) {
1617                'All' {
1618                    Get-CCMCacheInfo -CacheType 'All' -ErrorAction 'Stop'
1619                    Break
1620                }
1621                'Application' {
1622                    Get-CCMCacheInfo -CacheType 'Application' -ErrorAction 'Stop'
1623                }
1624                'Package' {
1625                    Get-CCMCacheInfo -CacheType 'Package' -ErrorAction 'Stop'
1626                }
1627                'Update' {
1628                    Get-CCMCacheInfo -CacheType 'Update' -ErrorAction 'Stop'
1629                }
1630                'Orphaned' {
1631                    Get-CCMCacheInfo -CacheType 'Orphaned' -ErrorAction 'Stop'
1632                }
1633            }
1634
1635            ## Remove null objects from array (should not be needed)
1636            $CacheElements = $CacheElements | Where-Object { $null -ne $PSItem }
1637
1638            ## Set Script Block
1639            [scriptblock]$CleanupCacheSB = {
1640                Show-Progress -Status "[$PSitem] Cache Deletion for [$($CacheElement.CacheType)] --> [$($CacheElement.CacheElementID)]" -Steps ($CacheElements.ContentID).Count
1641                Remove-CCMCacheElement -CacheElement $CacheElement -DeletePinned:$DeletePinned
1642            }
1643
1644            ## Process cache elements
1645            $Output = ForEach ($CacheElement in $CacheElements) {
1646                If ($CacheElement.EligibleForDeletion -or $CleanupType -contains 'All' -or $CleanupType -contains 'ListOnly') {
1647                    Switch ($CleanupType) {
1648                        'All' {
1649                            $CleanupCacheSB.Invoke()
1650                            Break
1651                        }
1652                        'Automatic' {
1653                            If ($DriveFreeSpacePercentage -gt $FreeDiskSpaceThreshold) {
1654                                If ($CacheElement.TombStoned) {
1655                                    $CleanupCacheSB.Invoke()
1656                                }
1657                            }
1658                            Else {
1659                                $CleanupCacheSB.Invoke()
1660                            }
1661                            Break
1662                        }
1663                        'ListOnly' {
1664                            $CacheElement
1665                            Break
1666                        }
1667                        'TombStoned' {
1668                            If ($CacheElement.TombStoned) {
1669                                $CleanupCacheSB.Invoke()
1670                            }
1671                            Break
1672                        }
1673                        'Referenced' {
1674                            If ($CacheElement.ReferenceCount -gt 0) {
1675                                $CleanupCacheSB.Invoke()
1676                            }
1677                            Break
1678                        }
1679                        Default {
1680
1681                            ## Return custom error
1682                            $Message       = [string]"Invalid cache type '{0}'`n{1}" -f $($CacheElement.CacheType), $(Resolve-Error)
1683                            $Exception     = [Exception]::new($Message)
1684                            $ExceptionType = [Management.Automation.ErrorCategory]::OperationStopped
1685                            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheElement)
1686                            #  Write to log
1687                            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1688                            #  Throw terminating error
1689                            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1690                        }
1691                    }
1692                }
1693            }
1694        }
1695        Catch {
1696
1697            ## Return custom error
1698            $Message       = [string]"Error processing cache for removal '{0}'`n{1}" -f $($CacheElement.CacheElementID), $(Resolve-Error)
1699            $Exception     = [Exception]::new($Message)
1700            $ExceptionType = [Management.Automation.ErrorCategory]::OperationStopped
1701            $ErrorRecord   = [System.Management.Automation.ErrorRecord]::new($Exception, $PSItem.FullyQualifiedErrorId, $ExceptionType, $CacheElement)
1702            #  Write to log
1703            Write-Log -Message $Message -Severity '3' -ScriptSection ${CmdletName}
1704            #  Throw terminating error
1705            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
1706        }
1707        Finally {
1708            Write-Output -InputObject $Output
1709        }
1710    }
1711    End {
1712
1713        ## Write verbose footer
1714        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${CmdletName}
1715    }
1716}
1717#endregion
1718
1719#endregion
1720##*=============================================
1721##* END FUNCTION LISTINGS
1722##*=============================================
1723
1724##*=============================================
1725##* SCRIPT BODY
1726##*=============================================
1727#region ScriptBody
1728
1729Try {
1730
1731    ## Set the script section
1732    [string]${ScriptSection} = 'Main:Initialization'
1733
1734    ## Write Start verbose message
1735    Write-Log -Message 'Start' -VerboseMessage -ScriptSection ${ScriptSection}
1736
1737    ## Initialize the CCM resource manager com object
1738    [__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'
1739
1740    ## Get cache drive free space percentage
1741    #  Get ccm cache drive location
1742    [string]$CacheDrive = $($CCMComObject.GetCacheInfo()).Location | Split-Path -Qualifier
1743    #  Get cache drive info
1744    $CacheDriveInfo = Get-CimInstance -ClassName 'Win32_LogicalDisk' -Filter "DeviceID='$CacheDrive'" -Verbose:$false
1745    #  Get cache drive size in GB
1746    [int16]$DriveSize = $($CacheDriveInfo.Size) / 1GB
1747    #  Get cache drive free space in GB
1748    [int16]$DriveFreeSpace = $($CacheDriveInfo.FreeSpace) / 1GB
1749    #  Calculate percentage
1750    [int16]$DriveFreeSpacePercentage = ($DriveFreeSpace * 100 / $DriveSize)
1751
1752    ## Get super peer status
1753    $CanBeSuperPeer = [boolean]$(Get-CimInstance -Namespace 'root\ccm\Policy\Machine\ActualConfig' -ClassName 'CCM_SuperPeerClientConfig' -Verbose:$false -ErrorAction 'SilentlyContinue').CanBeSuperPeer
1754
1755    ## Set run condition.
1756    #  The script will exit if:
1757    #      * Disk free space greater than 'FreeDiskSpaceThreshold' and 'CleanupTpe' is not 'Automatic'.
1758    #      * CanBeSuperPeer is true and 'SkipSuperPeer' is not specified.
1759    #  Note that 'Automatic' cleanup will be allowed even the first condition is not met as it's using a different logic depending on the 'FreeDiskSpaceThreshold' parameter.
1760    If (($DriveFreeSpacePercentage -gt $FreeDiskSpaceThreshold -and $CleanupType -ne 'Automatic') -or ($CanBeSuperPeer -eq $true -and $SkipSuperPeer)) { $ShouldRun = $false }
1761
1762    ## Check run condition and stop execution if $ShouldRun is not $true
1763    If ($ShouldRun) {
1764        Write-Log -Message 'Should Run test passed' -VerboseMessage -ScriptSection ${ScriptSection}
1765    }
1766    Else {
1767        Write-Log -Message "Should Run test failed. Check 'FreeDiskSpaceThreshold' and 'SkipSuperPeer' parameters." -Severity '3' -ScriptSection ${ScriptSection}
1768        Write-Log -Message "FreeSpace/Threshold [$DriveFreeSpacePercentage`/$FreeDiskSpaceThreshold] | IsSuperPeer/SkipSuperPeer [$CanBeSuperPeer`/$SkipSuperPeer]" -VerboseMessage -ScriptSection ${ScriptSection}
1769        Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${ScriptSection}
1770
1771        ## Stop execution
1772        Exit
1773    }
1774    Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${ScriptSection}
1775}
1776Catch {
1777    Write-Log -Message "Script initialization failed. `n$(Resolve-Error)" -Severity '3' -ScriptSection ${ScriptSection}
1778    Throw "Script initialization failed. $($PSItem.Exception.Message)"
1779}
1780Try {
1781
1782    ## Set the script section
1783    [string]${ScriptSection} = 'Main:CacheCleanup'
1784
1785    ## Write debug action
1786    Write-Log -Message "Cleanup Actions [$CleanupType] on [$CacheType]" -DebugMessage -ScriptSection ${ScriptSection}
1787
1788    ## Perform cleanup action
1789    $Output = Invoke-CCMCacheCleanup -CacheType $CacheType -CleanupType $CleanupType -DeletePinned:$DeletePinned
1790}
1791Catch {
1792    Write-Log -Message "Could not perform cleanup action. `n$(Resolve-Error)" -Severity '3' -ScriptSection ${ScriptSection}
1793    Throw "Could not perform cleanup action. `n$($PSItem.Exception.Message)"
1794}
1795Finally {
1796
1797    ## Set the script section
1798    [string]${ScriptSection} = 'Main:Output'
1799
1800    ## Calculate total deleted size
1801    [int16]$TotalDeleted = $($Output | Where-Object { $PSItem.Status -eq 'Deleted' }).Count
1802    $TotalDeletedSize = ($Output | Where-Object { $PSItem.Status -eq 'Deleted' } | Measure-Object -Property 'ContentSize' -Sum | Select-Object -ExpandProperty 'Sum') * 1000 | Format-Bytes
1803    If (-not $TotalDeletedSize) { $TotalDeletedSize = 0 }
1804
1805    ## Assemble output output
1806    $Output = $($Output | Format-List -Property 'CacheType', 'Name', 'Location', 'ContentSize', 'CacheElementID', 'Status' | Out-String) + "Total Found  : $($Output.Count) `nTotal Deleted: $TotalDeleted [$TotalDeletedSize]"
1807
1808    ## Write output to log, event log and console and status
1809    Write-Log -Message $Output -ScriptSection ${ScriptSection} -PassThru
1810
1811    ## Write verbose stop
1812    Write-Log -Message 'Stop' -VerboseMessage -ScriptSection ${ScriptSection}
1813}

👍 Liked this? Share it with your team and consider subscribing and following—it helps us keep the good stuff coming. 😎

SHARE

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