Dark mode

Dark mode

There are 0 results matching

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 · 1 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 · 3 mins read

Introducing: Configuration Manager SQL Products Reporting

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

See More
article card image dark article card image light

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

Application Detection Method using the Configuration Manager Application Version

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

See More
article card image dark article card image light

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

Introducing: Certificate Management Toolkit

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

See More
article card image dark article card image light

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

Configuration Manager Device Boundary and Network Information Report

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

See More
article card image dark article card image light

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

MEM.Zone Blog Publishing Documentation

Publishing Documentation for MEM.Zone ...

See More

We couldn’t find anything related to

“SCCM”

BLOG / tools zone

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

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