Dark mode

Dark mode

There are 0 results matching

article card image dark article card image light

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

Introducing: macOS JAMF Offboarding Tool

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

See More
article card image dark article card image light

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

Introducing: Microsoft Cloud License Automation Tool - Part 1

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

See More
article card image dark article card image light

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

Introducing: Configuration Manager Set Implicit Uninstall Flag Tool

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

See More
article card image dark article card image light

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

Configuration Manager Next Maintenance Window SQL Function

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

See More
article card image dark article card image light

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

Introducing: Windows User Rights Assignment Tool - Part 3

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

See More
article card image dark article card image light

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

Introducing: Windows User Rights Assignment Tool - Part 2

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

See More
article card image dark article card image light

Published by · May 22, 2024 tools · 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 / help zone

MEM.Zone Blog Publishing Documentation

Published by Popovici Ioan · Sep 9, 1980 · 5 mins read
article card image dark article card image light

Quick Summary

Start publishing on MEM.Zone with the help of the templates in the content/help folder. They are preconfigured with the required front matter and the basic structure of the post.

Notes

Content needs to be written using the markdown syntax.
Most but, not all markdown features are supported.
Please follow the guidelines to ensure your post is published correctly.

Prerequisites

  • Github account and client
  • Local MEM.Zone-Site git repository
  • SSH key for signing commits
  • node.js
  • Hugo
  • Visual Studio Code

Recomendations

  • Use Visual Studio Code as your editor.
  • Remember that this blog only supports two levels of headings since one is taken up by the Quick Summary.
  • Format your post into sections using the ## and ### headings.
  • Add --- before each ## section to separate them.

Configuration

git clone [email protected]:MEM-Zone/MEM.Zone-Site.git /path/to/MEM.Zone-Site
cd /path/to/MEM.Zone-Site
brew install hugo, node
npm -i
npm start

Front Matter Parameters

The front matter contains the parameters used to enable and configure the features of the site and must respect the YAML syntax.

---
title: "Introducing: Configuration Manager Client Cache Cleanup Tool"
date: 2023-06-18
type: "posts"
coverImage: "MEMZONE-Article-Title.svg"
shortDescription: "Cleaning the ConfigMgr Client Cache the Right Way with PowerShell"
description: "Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines"
keywords: "ConfigMgr, SCCM, MEM, MEMCM, Configuration Manager, WMI, SDK, CCM Cache Cleanup"
authorName: "Popovici Ioan"
authorSocial: "https://MEM.Zone/Ioan"
categories:
  - "tools"
  - "configmgr"
  - "powershell"
social:
  - facebook: false
  - twitter: true
  - linkedIn: true
  - email: true
toc: true
draft: false
twitterCard:
  card: "player"
  player: "https://www.youtube.com/embed/3Ni-DZVA_40"
  playerWidth: 960
  playerHeight: 720
---

title

Specifies the post title which needs to be concise, short and easily understandable.

title: "Introducing: Configuration Manager Client Cache Cleanup Tool"

title: "Configuration Manager detailed, filterable Port Documentation"

title: "Configuration Manager Console Extension to show Device Collection Membership with Console Builder"
Notes

The first three words of the title will be emphasized, so try to make your title at least five words long, otherwise the title will not look good.

date

Specifies the date of publication and must be in the ISO 8601 format.

date: 2023-06-18

type

Specifies the type of the post and must be always set to “posts”.

type: "posts"

coverImage

Specify the post cover image in svg format, which are always located in the static/covers folder.

Notes

Always follow the -MEMZONE-Article-Title- naming standard for the cover images.
New images will be created on demand using the post title, a minimum of 5 need to be requested.

coverImage: "MEMZONE-Article-Title.svg"

shortDescription

Specifies the description used for Google indexing and needs to be concise, short and easily understandable.

Notes

Needs to differ from the title and description.

shortDescription: "Extend the ConfigMgr Console to Show Device Collection Membership"

shortDescription: "Cleaning the ConfigMgr Client Cache the Right Way with PowerShell"

shortDescription: "Detection Method using the ConfigMgr Application Version"

description

Specifies the description used for the blog article list page, needs to be concise, a bit longer than de shortDscription and easily understandable.

Notes

Needs to differ from the title and shortDescription.

description: "Use the Configuration Manager Console Builder, to add Collection Membership View to the Device Node"

description: "Cleaning the Configuration Manager Client Cache the Right Way with PowerShell and Configuration Baselines"

description: "Replace hardcoded application version in scripts, with the Configuration Manager Application Version"
Notes

The first three words of the title will be emphasized, so try to make your title at least five words long, otherwise the title will not look good

keywords

Specifies the keywords used for Google indexing.

keywords: "ConfigMgr, SCCM, MEM, MEMCM, Configuration Manager, Console Builder, Console Extension, Collection Membership"

authorName

Specifies the author’s name.

authorName: "Popovici Ioan"

authorSocial

Specifies the author’s social/webpage link.

authorSocial: "https://MEM.Zone/Ioan"

categories

Specifies the categories used for the post.

categories:
  - "configmgr"
  - "tools"
  - "powershell"
  - "windows"
Notes

For New categories

  • Add folder and the _index.md configuration file inside it in the content folder.
  • Open configuration files
    • config.prduction.yml
    • config.staging.yml
    • config.yml
      • Update the navigation property with the new category.
      • Choose the order of the categories as you want it to show in the menu.

Notes

Place your post in the first category specified here which must also be the main category.

social

Specifies the social links enabled for the post.

social:
  - facebook: false
  - twitter: true
  - linkedIn: true
  - email: true

toc

Specifies if the Table of Contents is enabled or disabled.

toc: true

draft

Specifies if the post is a draft or not. If the post is a draft it will not be published.

draft: true

twitterCard

Specifies the Twitter Card parameters if there is a need to specify another card type than the default summary_large_image.

  • card
    Specifies the card type.
  • image
    Specifies the card image link.
  • url
    Specifies the card url.
  • player
    Specifies the card player link.
  • playerWidth
    Specifies the card player width.
  • playerHeight
    Specifies the card player height.
twitterCard:
  card: "player"
  player: "https://www.youtube.com/embed/3Ni-DZVA_40"
  playerWidth: 960
  playerHeight: 720
Notes

The twitterCard parameters are optional and can be removed if not needed.
If you don’t know what you are doing do not use this feature.


Special Characters

  • \
    Escape character for characters like _, *, #, <>, etc.
  • {{<* shortcode *>}}
    Escape a shortcode, so it will not be executed.
  • ---
    Horizontal line separator, to be used between ## sections.

Summary

The Quick Summary heading is used to summarize the post and must be complete, descriptive but concise.

# 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.

## Links

- [Release history](https://MEMZ.one/Invoke-CCMCacheCleanup-CHANGELOG)
- [Bugs and feature requests](https://MEM.Zone/ISSUES)
Notes

  • #Quick Summary heading must be the first heading in the post and it can’t be changed.
  • ##Links heading must be the second heading in the post inside of the #Quick Summary and it can’t be changed.
  • <!--more--> tag is mandatory after the summary.


Write Notes

Three types of notes can be used in a post using the notes shortcode.


{{< notes >}}
Default note
{{< /notes >}}
Notes

Default note

  • warning
{{< notes warning >}}
Warning note
{{< /notes >}}
Notes

Warning note

  • critical
{{< notes critical >}}
Critical note
{{< /notes >}}
Notes

Critical note


Highlight Text

In the standard markdown syntax, _highlight_ specifies italic text, here it was changed to highlight text.

_Some highlighted text_

Some highlighted text

Notes

You still have the option to use the other markdown syntax for italic text *Italic Text*.


Embed Inline Code

Inline code can be embedded using the markdown ```code``` syntax, or using the hugo <code> shortcode.

Markdown syntax

```powershell
Write-Host "Hello World!"
```
Write-Host "Hello World!"
Notes

Does not support line numbers or show more/less functionality, and should be used for code snipplets of less than 10 lines long.

Hugo shortcode

  • <language>
    Specifies the code language for chroma syntax highlighter
  • hideLineNumbers
    Specifies to hide the line numbers
{{< code powershell >}}
Write-Host "Hello World! 1"
Write-Host "Hello World! 2"
{{< /code >}}
1Write-Host "Hello World! 1"
2Write-Host "Hello World! 2"
{{< code powershell hideLineNumbers >}}
Write-Host "Hello World!"
{{< /code >}}
Write-Host "Hello World!"

Embed GitHub Code

GitHub raw links can be embedded using the github shortcode with the option to hide the line numbers by specifying hideLineNumbers after the link.

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

##*=============================================
##* VARIABLE DECLARATION
##*=============================================
#region VariableDeclaration

## Set script requirements
#Requires -Version 3.0

## Get script parameters
Param (
    [Parameter(Mandatory = $false)]
    [ValidateNotNullorEmpty()]
    [string[]]$CleanupOptions = "Recommended"
)

## Get script path and name
[string]$ScriptName     = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Definition)
[string]$ScriptFullName = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Definition)

## Get Show-Progress steps
$ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
$ForEachSteps  = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Keyword' -and $_.Content -eq 'ForEach' }).Count)

## Set Show-Progress steps
$Script:Steps = $ProgressSteps - $ForEachSteps
$Script:Step  = 1

## Set script global variables
$script:LoggingOptions   = 'EventLog'
$script:LogName          = 'Endpoint Management'
$script:LogSource        = $ScriptName
$script:LogDebugMessages = $false
$script:LogFileDirectory = If ($LogPath) { Join-Path -Path $LogPath -ChildPath $script:LogName } Else { $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)) }


## Initialize variables
[string]$StartWindowsCleanup = $null

#endregion
##*=============================================
##* END VARIABLE DECLARATION
##*=============================================

##*=============================================
##* FUNCTION LISTINGS
##*=============================================
#region FunctionListings

#region Function Resolve-Error
Function Resolve-Error {
<#
.SYNOPSIS
    Enumerate error record details.
.DESCRIPTION
    Enumerate an error record, or a collection of error record, properties. By default, the details for the last error will be enumerated.
.PARAMETER ErrorRecord
    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.
.PARAMETER Property
    The list of properties to display from the error record. Use "*" to display all properties.
    Default list of error properties is: Message, FullyQualifiedErrorId, ScriptStackTrace, PositionMessage, InnerException
.PARAMETER GetErrorRecord
    Get error record details as represented by $_.
.PARAMETER GetErrorInvocation
    Get error record invocation information as represented by $_.InvocationInfo.
.PARAMETER GetErrorException
    Get error record exception details as represented by $_.Exception.
.PARAMETER GetErrorInnerException
    Get error record inner exception details as represented by $_.Exception.InnerException. Will retrieve all inner exceptions if there is more than one.
.EXAMPLE
    Resolve-Error
.EXAMPLE
    Resolve-Error -Property *
.EXAMPLE
    Resolve-Error -Property InnerException
.EXAMPLE
    Resolve-Error -GetErrorInvocation:$false
.NOTES
    Unmodified version of the PADT error resolving cmdlet. I did not write the original cmdlet, please do not credit me for it!
.LINK
    https://psappdeploytoolkit.com
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyCollection()]
        [array]$ErrorRecord,
        [Parameter(Mandatory = $false, Position = 1)]
        [ValidateNotNullorEmpty()]
        [string[]]$Property = ('Message', 'InnerException', 'FullyQualifiedErrorId', 'ScriptStackTrace', 'PositionMessage'),
        [Parameter(Mandatory = $false, Position = 2)]
        [switch]$GetErrorRecord = $true,
        [Parameter(Mandatory = $false, Position = 3)]
        [switch]$GetErrorInvocation = $true,
        [Parameter(Mandatory = $false, Position = 4)]
        [switch]$GetErrorException = $true,
        [Parameter(Mandatory = $false, Position = 5)]
        [switch]$GetErrorInnerException = $true
    )

    Begin {
        ## If function was called without specifying an error record, then choose the latest error that occurred
        If (-not $ErrorRecord) {
            If ($global:Error.Count -eq 0) {
                #Write-Warning -Message "The `$Error collection is empty"
                Return
            }
            Else {
                [array]$ErrorRecord = $global:Error[0]
            }
        }

        ## Allows selecting and filtering the properties on the error object if they exist
        [scriptblock]$SelectProperty = {
            Param (
                [Parameter(Mandatory = $true)]
                [ValidateNotNullorEmpty()]
                $InputObject,
                [Parameter(Mandatory = $true)]
                [ValidateNotNullorEmpty()]
                [string[]]$Property
            )

            [string[]]$ObjectProperty = $InputObject | Get-Member -MemberType '*Property' | Select-Object -ExpandProperty 'Name'
            ForEach ($Prop in $Property) {
                If ($Prop -eq '*') {
                    [string[]]$PropertySelection = $ObjectProperty
                    Break
                }
                ElseIf ($ObjectProperty -contains $Prop) {
                    [string[]]$PropertySelection += $Prop
                }
            }
            Write-Output -InputObject $PropertySelection
        }

        #  Initialize variables to avoid error if 'Set-StrictMode' is set
        $LogErrorRecordMsg = $null
        $LogErrorInvocationMsg = $null
        $LogErrorExceptionMsg = $null
        $LogErrorMessageTmp = $null
        $LogInnerMessage = $null
    }
    Process {
        If (-not $ErrorRecord) { Return }
        ForEach ($ErrRecord in $ErrorRecord) {
            ## Capture Error Record
            If ($GetErrorRecord) {
                [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord -Property $Property
                $LogErrorRecordMsg = $ErrRecord | Select-Object -Property $SelectedProperties
            }

            ## Error Invocation Information
            If ($GetErrorInvocation) {
                If ($ErrRecord.InvocationInfo) {
                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.InvocationInfo -Property $Property
                    $LogErrorInvocationMsg = $ErrRecord.InvocationInfo | Select-Object -Property $SelectedProperties
                }
            }

            ## Capture Error Exception
            If ($GetErrorException) {
                If ($ErrRecord.Exception) {
                    [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrRecord.Exception -Property $Property
                    $LogErrorExceptionMsg = $ErrRecord.Exception | Select-Object -Property $SelectedProperties
                }
            }

            ## Display properties in the correct order
            If ($Property -eq '*') {
                #  If all properties were chosen for display, then arrange them in the order the error object displays them by default.
                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
            }
            Else {
                #  Display selected properties in our custom order
                If ($LogErrorExceptionMsg) { [array]$LogErrorMessageTmp += $LogErrorExceptionMsg }
                If ($LogErrorRecordMsg) { [array]$LogErrorMessageTmp += $LogErrorRecordMsg }
                If ($LogErrorInvocationMsg) { [array]$LogErrorMessageTmp += $LogErrorInvocationMsg }
            }

            If ($LogErrorMessageTmp) {
                $LogErrorMessage = 'Error Record:'
                $LogErrorMessage += "`n-------------"
                $LogErrorMsg = $LogErrorMessageTmp | Format-List | Out-String
                $LogErrorMessage += $LogErrorMsg
            }

            ## Capture Error Inner Exception(s)
            If ($GetErrorInnerException) {
                If ($ErrRecord.Exception -and $ErrRecord.Exception.InnerException) {
                    $LogInnerMessage = 'Error Inner Exception(s):'
                    $LogInnerMessage += "`n-------------------------"

                    $ErrorInnerException = $ErrRecord.Exception.InnerException
                    $Count = 0

                    While ($ErrorInnerException) {
                        [string]$InnerExceptionSeperator = '~' * 40

                        [string[]]$SelectedProperties = & $SelectProperty -InputObject $ErrorInnerException -Property $Property
                        $LogErrorInnerExceptionMsg = $ErrorInnerException | Select-Object -Property $SelectedProperties | Format-List | Out-String

                        If ($Count -gt 0) { $LogInnerMessage += $InnerExceptionSeperator }
                        $LogInnerMessage += $LogErrorInnerExceptionMsg

                        $Count++
                        $ErrorInnerException = $ErrorInnerException.InnerException
                    }
                }
            }

            If ($LogErrorMessage) { $Output = $LogErrorMessage }
            If ($LogInnerMessage) { $Output += $LogInnerMessage }

            Write-Output -InputObject $Output

            If (Test-Path -LiteralPath 'variable:Output') { Clear-Variable -Name 'Output' }
            If (Test-Path -LiteralPath 'variable:LogErrorMessage') { Clear-Variable -Name 'LogErrorMessage' }
            If (Test-Path -LiteralPath 'variable:LogInnerMessage') { Clear-Variable -Name 'LogInnerMessage' }
            If (Test-Path -LiteralPath 'variable:LogErrorMessageTmp') { Clear-Variable -Name 'LogErrorMessageTmp' }
        }
    }
    End {
    }
}
#endregion

#region Function Write-Log
Function Write-Log {
<#
.SYNOPSIS
    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format.
.DESCRIPTION
    Write messages to a log file in CMTrace.exe compatible format or Legacy text file format and optionally display in the console.
.PARAMETER Message
    The message to write to the log file or output to the console.
.PARAMETER Severity
    Defines message type. When writing to console or CMTrace.exe log format, it allows highlighting of message type.
    Options: 1 = Information (default), 2 = Warning (highlighted in yellow), 3 = Error (highlighted in red)
.PARAMETER Source
    The source of the message being logged. Also used as the event log source.
.PARAMETER ScriptSection
    The heading for the portion of the script that is being executed. Default is: $script:installPhase.
.PARAMETER LogType
    Choose whether to write a CMTrace.exe compatible log file or a Legacy text log file.
.PARAMETER LoggingOptions
    Choose where to log 'Console', 'File', 'EventLog' or 'None'. You can choose multiple options.
.PARAMETER LogFileDirectory
    Set the directory where the log file will be saved.
.PARAMETER LogFileName
    Set the name of the log file.
.PARAMETER MaxLogFileSizeMB
    Maximum file size limit for log file in megabytes (MB). Default is 10 MB.
.PARAMETER LogName
    Set the name of the event log.
.PARAMETER EventID
    Set the event id for the event log entry.
.PARAMETER WriteHost
    Write the log message to the console.
.PARAMETER ContinueOnError
    Suppress writing log message to console on failure to write message to log file. Default is: $true.
.PARAMETER PassThru
    Return the message that was passed to the function
.PARAMETER VerboseMessage
    Specifies that the message is a debug message. Verbose messages only get logged if -LogDebugMessage is set to $true.
.PARAMETER DebugMessage
    Specifies that the message is a debug message. Debug messages only get logged if -LogDebugMessage is set to $true.
.PARAMETER LogDebugMessage
    Debug messages only get logged if this parameter is set to $true in the config XML file.
.EXAMPLE
    Write-Log -Message "Installing patch MS15-031" -Source 'Add-Patch' -LogType 'CMTrace'
.EXAMPLE
    Write-Log -Message "Script is running on Windows 8" -Source 'Test-ValidOS' -LogType 'Legacy'
.NOTES
    Slightly modified version of the PSADT logging cmdlet. I did not write the original cmdlet, please do not credit me for it.
.LINK
    https://psappdeploytoolkit.com
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyCollection()]
        [Alias('Text')]
        [string[]]$Message,
        [Parameter(Mandatory = $false, Position = 1)]
        [ValidateRange(1, 3)]
        [int16]$Severity = 1,
        [Parameter(Mandatory = $false, Position = 2)]
        [ValidateNotNullorEmpty()]
        [string]$Source = $script:LogSource,
        [Parameter(Mandatory = $false, Position = 3)]
        [ValidateNotNullorEmpty()]
        [string]$ScriptSection = $script:RunPhase,
        [Parameter(Mandatory = $false, Position = 4)]
        [ValidateSet('CMTrace', 'Legacy')]
        [string]$LogType = 'CMTrace',
        [Parameter(Mandatory = $false, Position = 5)]
        [ValidateSet('Host', 'File', 'EventLog', 'None')]
        [string[]]$LoggingOptions = $script:LoggingOptions,
        [Parameter(Mandatory = $false, Position = 6)]
        [ValidateNotNullorEmpty()]
        [string]$LogFileDirectory = $(Join-Path -Path $Env:WinDir -ChildPath $('\Logs\' + $script:LogName)),
        [Parameter(Mandatory = $false, Position = 7)]
        [ValidateNotNullorEmpty()]
        [string]$LogFileName = $($script:LogSource + '.log'),
        [Parameter(Mandatory = $false, Position = 8)]
        [ValidateNotNullorEmpty()]
        [int]$MaxLogFileSizeMB = '4',
        [Parameter(Mandatory = $false, Position = 9)]
        [ValidateNotNullorEmpty()]
        [string]$LogName = $script:LogName,
        [Parameter(Mandatory = $false, Position = 10)]
        [ValidateNotNullorEmpty()]
        [int32]$EventID = 1,
        [Parameter(Mandatory = $false, Position = 11)]
        [ValidateNotNullorEmpty()]
        [boolean]$ContinueOnError = $false,
        [Parameter(Mandatory = $false, Position = 12)]
        [switch]$PassThru = $false,
        [Parameter(Mandatory = $false, Position = 13)]
        [switch]$VerboseMessage = $false,
        [Parameter(Mandatory = $false, Position = 14)]
        [switch]$DebugMessage = $false,
        [Parameter(Mandatory = $false, Position = 15)]
        [boolean]$LogDebugMessage = $script:LogDebugMessages
    )

    Begin {
        ## Get the name of this function
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name

        ## Logging Variables
        #  Log file date/time
        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
        If (-not (Test-Path -LiteralPath 'variable:LogTimeZoneBias')) { [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes }
        [string]$LogTimePlusBias = $LogTime + '-' + $script:LogTimeZoneBias
        #  Initialize variables
        [boolean]$WriteHost = $false
        [boolean]$WriteFile = $false
        [boolean]$WriteEvent = $false
        [boolean]$DisableLogging = $false
        [boolean]$ExitLoggingFunction = $false
        If (('Host' -in $LoggingOptions) -and (-not ($VerboseMessage -or $DebugMessage))) { $WriteHost = $true }
        If ('File' -in $LoggingOptions) { $WriteFile = $true }
        If ('EventLog' -in $LoggingOptions) { $WriteEvent = $true }
        If ('None' -in $LoggingOptions) { $DisableLogging = $true }
        #  Check if the script section is defined
        [boolean]$ScriptSectionDefined = [boolean](-not [string]::IsNullOrEmpty($ScriptSection))
        #  Check if the source is defined
        [boolean]$SourceDefined = [boolean](-not [string]::IsNullOrEmpty($Source))
        #  Check if the event log and event source exit
        [boolean]$LogNameNotExists = (-not [System.Diagnostics.EventLog]::Exists($LogName))
        [boolean]$LogSourceNotExists = (-not [System.Diagnostics.EventLog]::SourceExists($Source))
        #  Check for overlapping log names
        [string[]]$OverLappingLogName = Get-EventLog -List | Where-Object -Property 'Log' -Like $($LogName.Substring(0,8) + '*') | Select-Object -ExpandProperty 'Log'
        If (-not [string]::IsNullOrEmpty($ScriptSection)) {
            Write-Warning -Message "Overlapping log names:`n$($OverLappingLogName | Out-String)"
            Write-Warning -Message 'Change the name of your log or use Remove-EventLog to remove the log(s) above!'
        }

        ## Create script block for generating CMTrace.exe compatible log entry
        [scriptblock]$CMTraceLogString = {
            Param (
                [string]$lMessage,
                [string]$lSource,
                [int16]$lSeverity
            )
            "<![LOG[$lMessage]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$lSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$lSeverity`" " + "thread=`"$PID`" " + "file=`"$Source`">"
        }

        ## Create script block for writing log entry to the console
        [scriptblock]$WriteLogLineToHost = {
            Param (
                [string]$lTextLogLine,
                [int16]$lSeverity
            )
            If ($WriteHost) {
                #  Only output using color options if running in a host which supports colors.
                If ($Host.UI.RawUI.ForegroundColor) {
                    Switch ($lSeverity) {
                        3 { Write-Host -Object $lTextLogLine -ForegroundColor 'Red' -BackgroundColor 'Black' }
                        2 { Write-Host -Object $lTextLogLine -ForegroundColor 'Yellow' -BackgroundColor 'Black' }
                        1 { Write-Host -Object $lTextLogLine }
                    }
                }
                #  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.
                Else {
                    Write-Output -InputObject $lTextLogLine
                }
            }
        }

        ## Create script block for writing log entry to the console as verbose or debug message
        [scriptblock]$WriteLogLineToHostAdvanced = {
            Param (
                [string]$lTextLogLine
            )
            #  Only output using color options if running in a host which supports colors.
            If ($Host.UI.RawUI.ForegroundColor) {
                If ($VerboseMessage) {
                    Write-Verbose -Message $lTextLogLine
                }
                Else {
                    Write-Debug -Message $lTextLogLine
                }
            }
            #  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.
            Else {
                Write-Output -InputObject $lTextLogLine
            }
        }

        ## Create script block for event writing log entry
        [scriptblock]$WriteToEventLog = {
            If ($WriteEvent) {
                $EventType = Switch ($Severity) {
                    3 { 'Error' }
                    2 { 'Warning' }
                    1 { 'Information' }
                }

                If ($LogNameNotExists -and (-not $LogSourceNotExists)) {
                    Try {
                        #  Delete event source if the log does not exist
                        $null = [System.Diagnostics.EventLog]::DeleteEventSource($Source)
                        $LogSourceNotExists = $true
                    }
                    Catch {
                        [boolean]$ExitLoggingFunction = $true
                        #  If error deleting event source, write message to console
                        If (-not $ContinueOnError) {
                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log source [$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                        }
                    }
                }
                If ($LogNameNotExists -or $LogSourceNotExists) {
                    Try {
                        #  Create event log
                        $null = New-EventLog -LogName $LogName -Source $Source -ErrorAction 'Stop'
                    }
                    Catch {
                        [boolean]$ExitLoggingFunction = $true
                        #  If error creating event log, write message to console
                        If (-not $ContinueOnError) {
                            Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                        }
                    }
                }
                Try {
                    #  Write to event log
                    Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -EntryType $EventType -Category '0' -Message $ConsoleLogLine -ErrorAction 'Stop'
                }
                Catch {
                    [boolean]$ExitLoggingFunction = $true
                    #  If error creating directory, write message to console
                    If (-not $ContinueOnError) {
                        Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to write to event log [$LogName`:$Source]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                    }
                }
            }
        }

        ## Exit function if it is a debug message and logging debug messages is not enabled in the config XML file
        If (($DebugMessage -or $VerboseMessage) -and (-not $LogDebugMessage)) { [boolean]$ExitLoggingFunction = $true; Return }
        ## Exit function if logging to file is disabled and logging to console host is disabled
        If (($DisableLogging) -and (-not $WriteHost)) { [boolean]$ExitLoggingFunction = $true; Return }
        ## Exit Begin block if logging is disabled
        If ($DisableLogging) { Return }

        ## Create the directory where the log file will be saved
        If (-not (Test-Path -LiteralPath $LogFileDirectory -PathType 'Container')) {
            Try {
                $null = New-Item -Path $LogFileDirectory -Type 'Directory' -Force -ErrorAction 'Stop'
            }
            Catch {
                [boolean]$ExitLoggingFunction = $true
                #  If error creating directory, write message to console
                If (-not $ContinueOnError) {
                    Write-Host -Object "[$LogDate $LogTime] [${CmdletName}] $ScriptSection :: Failed to create the log directory [$LogFileDirectory]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                }
                Return
            }
        }

        ## Assemble the fully qualified path to the log file
        [string]$LogFilePath = Join-Path -Path $LogFileDirectory -ChildPath $LogFileName
    }
    Process {

        ForEach ($Msg in $Message) {
            ## If the message is not $null or empty, create the log entry for the different logging methods
            [string]$CMTraceMsg = ''
            [string]$ConsoleLogLine = ''
            [string]$LegacyTextLogLine = ''
            If ($Msg) {
                #  Create the CMTrace log message
                If ($ScriptSectionDefined) { [string]$CMTraceMsg = "[$ScriptSection] :: $Msg" }

                #  Create a Console and Legacy "text" log entry
                [string]$LegacyMsg = "[$LogDate $LogTime]"
                If ($ScriptSectionDefined) { [string]$LegacyMsg += " [$ScriptSection]" }
                If ($Source) {
                    [string]$ConsoleLogLine = "$LegacyMsg [$Source] :: $Msg"
                    Switch ($Severity) {
                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Error] :: $Msg" }
                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Warning] :: $Msg" }
                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [$Source] [Info] :: $Msg" }
                    }
                }
                Else {
                    [string]$ConsoleLogLine = "$LegacyMsg :: $Msg"
                    Switch ($Severity) {
                        3 { [string]$LegacyTextLogLine = "$LegacyMsg [Error] :: $Msg" }
                        2 { [string]$LegacyTextLogLine = "$LegacyMsg [Warning] :: $Msg" }
                        1 { [string]$LegacyTextLogLine = "$LegacyMsg [Info] :: $Msg" }
                    }
                }
            }

            ## Execute script block to write the log entry to the console as verbose or debug message
            & $WriteLogLineToHostAdvanced -lTextLogLine $ConsoleLogLine -lSeverity $Severity

            ## Exit function if logging is disabled
            If ($ExitLoggingFunction) { Return }

            ## Execute script block to create the CMTrace.exe compatible log entry
            [string]$CMTraceLogLine = & $CMTraceLogString -lMessage $CMTraceMsg -lSource $Source -lSeverity $lSeverity

            ## Choose which log type to write to file
            If ($LogType -ieq 'CMTrace') {
                [string]$LogLine = $CMTraceLogLine
            }
            Else {
                [string]$LogLine = $LegacyTextLogLine
            }

            ## Write the log entry to the log file and event log if logging is not currently disabled
            If (-not $DisableLogging) {
                If ($WriteFile) {
                    ## Write to file log
                    Try {
                        $LogLine | Out-File -FilePath $LogFilePath -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop'
                    }
                    Catch {
                        If (-not $ContinueOnError) {
                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                        }
                    }
                }
                If ($WriteEvent) {
                    ## Write to event log
                    Try {
                        & $WriteToEventLog -lMessage $ConsoleLogLine -lName $LogName -lSource $Source -lSeverity $Severity
                    }
                    Catch {
                        If (-not $ContinueOnError) {
                            Write-Host -Object "[$LogDate $LogTime] [$ScriptSection] [${CmdletName}] :: Failed to write message [$Msg] to the log file [$LogFilePath]. `n$(Resolve-Error)" -ForegroundColor 'Red'
                        }
                    }
                }
            }

            ## Execute script block to write the log entry to the console if $WriteHost is $true and $LogLogDebugMessage is not $true
            & $WriteLogLineToHost -lTextLogLine $ConsoleLogLine -lSeverity $Severity
        }
    }
    End {
        ## Archive log file if size is greater than $MaxLogFileSizeMB and $MaxLogFileSizeMB > 0
        Try {
            If ((-not $ExitLoggingFunction) -and (-not $DisableLogging)) {
                [IO.FileInfo]$LogFile = Get-ChildItem -LiteralPath $LogFilePath -ErrorAction 'Stop'
                [decimal]$LogFileSizeMB = $LogFile.Length / 1MB
                If (($LogFileSizeMB -gt $MaxLogFileSizeMB) -and ($MaxLogFileSizeMB -gt 0)) {
                    ## Change the file extension to "lo_"
                    [string]$ArchivedOutLogFile = [IO.Path]::ChangeExtension($LogFilePath, 'lo_')
                    [hashtable]$ArchiveLogParams = @{ ScriptSection = $ScriptSection; Source = ${CmdletName}; Severity = 2; LogFileDirectory = $LogFileDirectory; LogFileName = $LogFileName; LogType = $LogType; MaxLogFileSizeMB = 0; WriteHost = $WriteHost; ContinueOnError = $ContinueOnError; PassThru = $false }

                    ## Log message about archiving the log file
                    $ArchiveLogMessage = "Maximum log file size [$MaxLogFileSizeMB MB] reached. Rename log file to [$ArchivedOutLogFile]."
                    Write-Log -Message $ArchiveLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}

                    ## 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.
                    Move-Item -LiteralPath $LogFilePath -Destination $ArchivedOutLogFile -Force -ErrorAction 'Stop'

                    ## Start new log file and Log message about archiving the old log file
                    $NewLogMessage = "Previous log file was renamed to [$ArchivedOutLogFile] because maximum log file size of [$MaxLogFileSizeMB MB] was reached."
                    Write-Log -Message $NewLogMessage @ArchiveLogParams -ScriptSection ${CmdletName}
                }
            }
        }
        Catch {
            ## If renaming of file fails, script will continue writing to log file even if size goes over the max file size
        }
        Finally {
            If ($PassThru) { Write-Output -InputObject $Message }
        }
    }
}
#endregion

#region Format-Bytes
Function Format-Bytes {
<#
.SYNOPSIS
    Formats a number of bytes in the corresponding sizes.
.DESCRIPTION
    Formats a number of bytes in the corresponding sizes depending on the size ('KB','MB','GB','TB','PB').
.PARAMETER Bytes
    Specifies bytes to format.
.EXAMPLE
    Format-Bytes -Bytes 12344567890
.INPUTS
    None.
.OUTPUTS
    None.
.NOTES
    Created by Ioan Popovici.
    v1.0.0 - 2021-09-01

    This is an private function should tipically not be called directly.
    Credit to Anthony Howell.
.LINK
    https://theposhwolf.com/howtos/Format-Bytes/
.LINK
    https://MEM.Zone
.LINK
    https://MEM.Zone/GIT
.LINK
    https://MEM.Zone/ISSUES
.COMPONENT
    Powershell
.FUNCTIONALITY
    Format Bytes
#>
    Param (
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [float]$Bytes
    )
    Begin {
        [string]$Output = $null
        [boolean]$Negative = $false
        $Sizes = 'KB','MB','GB','TB','PB'
    }
    Process {
        Try {
            If ($Bytes -le 0) {
                $Bytes = -$Bytes
                [boolean]$Negative = $true
            }
            For ($Counter = 0; $Counter -lt $Sizes.Count; $Counter++) {
                If ($Bytes -lt "1$($Sizes[$Counter])") {
                    If ($Counter -eq 0) {
                    $Number = $Bytes
                    $Sizes = 'B'
                    }
                    Else {
                        $Number = $Bytes / "1$($Sizes[$Counter-1])"
                        $Number = '{0:N2}' -f $Number
                        $Sizes = $Sizes[$Counter-1]
                    }
                }
            }
        }
        Catch {
            $Output = "Format Failed for Bytes ($Bytes! Error: $($_.Exception.Message)"
            Write-Log -Message $Output -EventID 2 -Severity 3
        }
        Finally {
            If ($Negative) { $Number = -$Number }
            $Output = '{0} {1}' -f $Number, $Sizes
            Write-Output -InputObject $Output
        }
    }
    End{
    }
}
#endregion

#region Function Show-Progress
Function Show-Progress {
<#
.SYNOPSIS
    Displays progress info.
.DESCRIPTION
    Displays progress info and maximizes code reuse by automatically calculating the progress steps.
.PARAMETER Actity
    Specifies the progress activity. Default: 'Running Cleanup Please Wait...'.
.PARAMETER Status
    Specifies the progress status.
.PARAMETER CurrentOperation
    Specifies the current operation.
.PARAMETER Step
    Specifies the progress step. Default: $Script:Step ++.
.PARAMETER ID
    Specifies the progress bar id.
.PARAMETER Delay
    Specifies the progress delay in milliseconds. Default: 100.
.PARAMETER Loop
    Specifies if the call comes from a loop.
.EXAMPLE
    Show-Progress -Activity 'Running Install Please Wait' -Status 'Uploading Report' -Step ($Step++) -Delay 200
.EXAMPLE
    Show-Progress -Status "Downloading [$File.Name] --> [$($RSDataSource.Name)]" -Loop
.INPUTS
    None.
.OUTPUTS
    None.
.NOTES
    Created by Ioan Popovici.
    v1.0.0 - 2021-01-01

    This is a private function and should typically not be called directly.
    Credit to Adam Bertram.

    ## !! IMPORTANT !! ##
    #  You need to tokenize the scripts steps at the beginning of the script for Show-Progress to work:

    ## Get script path and name
    [string]$ScriptPath = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition)
    [string]$ScriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Definition)
    [string]$ScriptFullName = Join-Path -Path $ScriptPath -ChildPath $ScriptName
    #  Get progress steps
    $ProgressSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Command' -and $_.Content -eq 'Show-Progress' }).Count)
    $ForEachSteps = $(([System.Management.Automation.PsParser]::Tokenize($(Get-Content -Path $ScriptFullName), [ref]$null) | Where-Object { $_.Type -eq 'Keyword' -and $_.Content -eq 'ForEach' }).Count)
    #  Set progress steps
    $Script:Steps = $ProgressSteps - $ForEachSteps
    $Script:Step = 0
.LINK
    https://adamtheautomator.com/building-progress-bar-powershell-scripts/
.LINK
    https://MEM.Zone
.LINK
    https://MEM.Zone/GIT
.LINK
    https://MEM.Zone/ISSUES
.COMPONENT
    Powershell
.FUNCTIONALITY
    Show Progress
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$false,Position=0)]
        [ValidateNotNullorEmpty()]
        [Alias('act')]
        [string]$Activity = 'Running Cleanup Please Wait...',
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateNotNullorEmpty()]
        [Alias('sta')]
        [string]$Status,
        [Parameter(Mandatory=$false,Position=2)]
        [ValidateNotNullorEmpty()]
        [Alias('cro')]
        [string]$CurrentOperation,
        [Parameter(Mandatory=$false,Position=3)]
        [ValidateNotNullorEmpty()]
        [Alias('pid')]
        [int]$ID = 0,
        [Parameter(Mandatory=$false,Position=4)]
        [ValidateNotNullorEmpty()]
        [Alias('ste')]
        [int]$Step = $Script:Step ++,
        [Parameter(Mandatory=$false,Position=5)]
        [ValidateNotNullorEmpty()]
        [Alias('del')]
        [string]$Delay = 100,
        [Parameter(Mandatory=$false,Position=5)]
        [ValidateNotNullorEmpty()]
        [Alias('lp')]
        [switch]$Loop
    )
    Begin {
        If ($Loop) { $Script:Steps ++ }
        $PercentComplete = $($($Step / $Steps) * 100)
    }
    Process {
        Try {
            ##  Show progress
            Write-Progress -Activity $Activity -Status $Status -CurrentOperation $CurrentOperation -ID $ID -PercentComplete $PercentComplete
            Start-Sleep -Milliseconds $Delay
        }
        Catch {
            Throw (New-Object System.Exception("Could not Show progress status [$Status]! $($_.Exception.Message)", $_.Exception))
        }
    }
}
#endregion

#region Function Start-WindowsCleanup
Function Start-WindowsCleanup {
<#
.SYNOPSIS
    Performs a Windows cleanup.
.DESCRIPTION
    Performs a Windows cleanup by removing volume caches, update backups, updates and CCM caches.
.PARAMETER CleanupOptions
    Supported options:
        "comCacheRepair"   # Component Cache Repair
        "comCacheCleanup"  # Component Cache Cleanup
        "volCacheCleanup"  # Volume Cache Cleanup
        "volShadowCleanup" # Volume Shadow Copy Cleanup
        "updCacheCleanup"  # Update Cache Cleanup
        "ccmCacheCleanup"  # CCM Cache Cleanup
        "Recommended"      # Performs some or all of the above-mentioned cleanup operations in a specific order depending on the operating system.
        "All"              # Performs all the above-mentioned cleanup operations.
    If set to "Recommended", the cleanup will be done in the recommended order.
    Default is: "Recommended".
.EXAMPLE
    Start-WindowsCleanup.ps1 -CleanupOptions "comCacheRepair", "comCacheCleanup", "updCacheCleanup", "volCacheCleanup", "ccmCacheCleanup"
.EXAMPLE
    Start-WindowsCleanup.ps1 -CleanupOptions "Recommended"
.EXAMPLE
    Start-WindowsCleanup.ps1 -CleanupOptions "All"
.INPUTS
    None.
.OUTPUTS
    None.
.NOTES
    Created by Ioan Popovici
    This is an internal script function and should typically not be called directly.
.LINK
    https://MEM.Zone
.LINK
    https://MEM.Zone/GIT
.LINK
    https://MEM.Zone/ISSUES
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [ValidateSet('comCacheRepair','comCacheCleanup','volCacheCleanup','volShadowCleanup','updCacheCleanup','ccmCacheCleanup','Recommended','All')]
        [Alias('Options')]
        [string[]]$CleanupOptions = 'Recommended',
        [switch]$OutputJson
    )

    Begin {
        Try {

            ## Variable  declaration
            [boolean]$SkipCleanup = $false
            [string]$StartWindowsCleanup = $null

            ## Get Machine Operating System
            [string]$RegistryExPattern = '(Windows\ (?:7|8\.1|8|10|11|Server\ (?:2008\ R2|2012\ R2|2012|2016|2019|2022)))'
            [string]$MachineOS = (Get-WmiObject -Class 'Win32_OperatingSystem' | Select-Object -ExpandProperty 'Caption' | Select-String -AllMatches -Pattern $RegistryExPattern | Select-Object -ExpandProperty 'Matches').Value

            ## Get volume info before cleanup
            $VolumeInfo = Get-Volume | Where-Object { $null -ne $PSItem.DriveLetter -and $PSItem.DriveType -eq 'Fixed' } | Select-Object -Property 'DriveLetter','SizeRemaining','Size'
        }
        Catch {}

        ## Perform different cleanup actions depending on the detected Operating System, the action order is intentional
        Switch ($CleanupOptions) {
            'Recommended' {
                If ($MachineOS) {
                    Switch ($MachineOS) {
                        'Windows 7' {
                            $CleanupOptions = @('volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows 8' {
                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows 8.1' {
                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows 10' {
                            $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows 11' {
                            $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2008 R2' {
                            $CleanupOptions = @('volCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2012' {
                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2012 R2' {
                            $CleanupOptions = @('comCacheRepair', 'comCacheCleanup', 'updCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2016' {
                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2019' {
                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        'Windows Server 2022' {
                            $CleanupOptions = @('updCacheCleanup', 'comCacheCleanup', 'ccmCacheCleanup')
                            Break;
                        }
                        Default {
                            $StartWindowsCleanup = 'Unknown Operating System, Skipping Cleanup!'
                            $SkipCleanup = $true
                        }
                    }
                Write-Verbose -Message 'Recommended Cleanup Selected!'
                }
                Else {
                    $StartWindowsCleanup = 'Unknown Operating System, Skipping Cleanup!'
                    $SkipCleanup = $true
                }
            }
            'All' { $CleanupOptions = @('comCacheRepair', 'volCacheCleanup', 'updCacheCleanup', 'comCacheCleanup', 'volShadowCleanup', 'ccmCacheCleanup') }
        }
    }
    Process {
        Try {

            ## Write variables for verbose output
            Write-Verbose -Message "$MachineOS Detected. Starting Cleanup..."
            Write-Verbose -Message "Cleanup Options: $CleanupOptions"

            ## Perform Cleanup Actions if $SkipCleanup is not true
            If (-not $SkipCleanup) {
                ForEach ($CleanupOption in $CleanupOptions) {
                    Switch ($CleanupOption) {
                        'comCacheRepair' {

                            ## Start Component Cache Repair
                            Show-Progress -Status 'Running Component Cache Repair. This Can Take a While...' -Loop
                            Start-Process -FilePath 'DISM.exe' -ArgumentList '/Online /Cleanup-Image /RestoreHealth' -WindowStyle 'Hidden'
                            While (-not (Get-Process -Name 'TiWorker' -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }
                            Get-Process -Name 'TiWorker' | ForEach-Object { $PSItem.PriorityClass='High' }
                            Wait-Process -Name 'DISM'
                        }
                        'comCacheCleanup' {

                            ## Start Component Cache Cleanup
                            Show-Progress -Status 'Running Component Cache Cleanup. This Can Take a While...' -Loop
                            Start-Process -FilePath 'DISM.exe' -ArgumentList '/Online /Cleanup-Image /StartComponentCleanup /ResetBase' -WindowStyle 'Hidden'
                            While (-not (Get-Process -Name 'TiWorker' -ErrorAction SilentlyContinue)) { Start-Sleep -Seconds 5 }
                            Get-Process -Name 'TiWorker' | ForEach-Object { $PSItem.PriorityClass='High' }
                            Wait-Process -Name 'DISM'
                        }
                        'volCacheCleanup' {

                            ## Start Volume Cache Cleanup
                            Show-Progress -Status 'Running Volume Cache Cleanup...'

                            ## Get Volume Caches registry paths
                            [string]$RegistryVolumeCachesRootPath = 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
                            [string[]]$RegistryVolumeCachesPaths = Get-ChildItem -Path $RegistryVolumeCachesRootPath | Select-Object -ExpandProperty 'Name'

                            ## CleanMgr cleanup settings
                            [string]$RegistrySageSet = '5432'
                            [string]$RegistryName = 'StateFlags' + $RegistrySageSet
                            [string]$RegistryValue = '00000002'
                            [string]$RegistryType = 'DWORD'

                            ## Add registry entries required by CleanMgr
                            ForEach ($RegistryVolumeCachesPath in $RegistryVolumeCachesPaths) {
                                Show-Progress -Activity 'Running Volume Cache Cleanup...' -Status "Adding $RegistryName to $RegistryVolumeCachesPath" -Loop
                                $null = New-ItemProperty -Path Registry::$RegistryVolumeCachesPath -Name $RegistryName -Value $RegistryValue -PropertyType $RegistryType -Force
                            }

                            ## If the machine is running Windows Server 2008 R2, copy the files required by CleanMgr and wait for the action to complete
                            If ($MachineOS -eq 'Windows Server 2008 R2') {

                                ## Copy CleanMgr.exe and CleanMgr.exe.mui
                                Show-Progress -Activity 'Running Volume Cache Cleanup...' -Status "Copying CleanMgr.exe from $env:SystemRoot\winsxs\..." -Loop
                                $null = Copy-Item -Path "$env:SystemRoot\winsxs\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\cleanmgr.exe" -Destination "$env:SystemRoot\System32\" -Force
                                $null = Copy-Item -Path "$env:SystemRoot\winsxs\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\cleanmgr.exe.mui" -Destination "$env:SystemRoot\System32\en-US\" -Force
                            }

                            ## Start Volume Cache Cleanup
                            Show-Progress -Status 'Running Volume Cache Cleanup. This May Take a While...' -Loop
                            Start-Process -FilePath 'CleanMgr.exe' -ArgumentList "/sagerun:$RegistrySageSet" -WindowStyle 'Hidden' -Wait
                        }
                        'volShadowCleanup' {

                            ## Start Volume Cache Cleanup
                            Show-Progress -Status 'Running Volume Shadow Cleanup...' -Loop
                            Start-Process -FilePath 'vssadmin.exe' -ArgumentList 'Delete Shadows /All /Force' -WindowStyle 'Hidden' -Wait
                        }
                        'updCacheCleanup' {

                            ## Start Update Cache Cleanup
                            Show-Progress -Status 'Running Windows Update Cache Cleanup...' -Loop
                            $null = Stop-Service -Name 'wuauserv' -Force -ErrorAction 'SilentlyContinue'
                            $null = Remove-Item -Path "$env:SystemRoot\SoftwareDistribution\" -Recurse -Force
                            $null = Start-Service -Name 'wuauserv' -ErrorAction 'SilentlyContinue'
                        }
                        'ccmCacheCleanup' {

                            ## Start CCM Cache Cleanup
                            Show-Progress -Status 'Running CCM Cache Cleanup...' -Loop

                            ## Initialize the CCM resource manager com object. New-Object does not respect $ErrorActionPreference = 'SilenlyContinue' hence the Try/Catch.
                            [__comobject]$CCMComObject = Try { New-Object -ComObject 'UIResource.UIResourceMgr' } Catch { $null }

                            ## If the CCM client is installed, run the CCM cache cleanup
                            If ($null -ne $CCMComObject) {

                                ## Get ccm cache path
                                [string]$DiskCachePath = $($CCMComObject.GetCacheInfo()).Location

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

                                ## Remove CCM cache items
                                ForEach ($CacheItem in $CacheItems) {
                                    Show-Progress -Activity 'Running CCM Cache Cleanup...' -Status "Removing $CacheItem.Location" -Loop
                                    $null = $CCMComObject.GetCacheInfo().DeleteCacheElement([string]$($CacheItem.CacheElementID))
                                }

                                ## Remove orphaned cache items
                                Show-Progress -Activity 'Running CCM Cache Cleanup...' -Status "Removing 'orphaned' CCM cache items" -Loop
                                $null = Remove-Item -Path $(Join-Path -Path $DiskCachePath -ChildPath '\*') -Recurse -Force
                            }
                            Else { Write-Warning -Message 'CCM Client is not installed! Skipping CCM Cache Cleanup...' }
                        }
                        Default { $Output = "$CleanupOption is Not a Valid Cleanup Option!"; Break }
                    }
                }

                ## Calculate the total freed up space and add it to the $VolumeInfo object
                ForEach ($Volume in $VolumeInfo) {
                    $CleanedSpace = (Get-Volume -DriveLetter $Volume.DriveLetter).SizeRemaining - $Volume.SizeRemaining | Format-Bytes
                    $Volume | Add-Member -MemberType 'NoteProperty' -Name 'ReclaimedSpace' -Value $CleanedSpace -ErrorAction 'SilentlyContinue'
                }

                ## Format output
                $Output = $VolumeInfo | Select-Object -Property 'DriveLetter',
                    @{ Name = 'Size'         ; Expression = {Format-Bytes -Bytes $PSItem.Size} },
                    @{ Name = 'FreeSpace'; Expression = {Format-Bytes -Bytes $PSItem.SizeRemaining} },
                    ReclaimedSpace

                ## Warn that a reboot might be needed
                Write-Warning -Message "SxS processing only occurs on system startup. Negative 'Reclaimed' values on repeaded runs are normal, you need to reboot." -Verbose

                ## Write to the event log
                [string]$EventLogEntry = "Cleanup Completed for $env:COMPUTERNAME ($MachineOS)!`n$($Output | Out-String)"
                Write-Log -Message $EventLogEntry
            }
        }
        Catch {
            $Output = "Cleanup Failed for $env:COMPUTERNAME ($MachineOS)! Error: $($_.Exception.Message)"
            Write-Log -Message $Output -EventID 2 -Severity 3
        }
        Finally {
            Write-Output -InputObject $Output
        }
    }
    End {
    }
}
#endregion

#endregion
##*=============================================
##* END FUNCTION LISTINGS
##*=============================================

##*=============================================
##* SCRIPT BODY
##*=============================================
#region ScriptBody

Try {
    $WindowsCleanup = Start-WindowsCleanup -CleanupOptions $CleanupOptions
}
Catch {
    $WindowsCleanup = "Cleanup for $env:COMPUTERNAME ($MachineOS)! Error: $($_.Exception.Message)"
}
Finally {
    Write-Output -InputObject $WindowsCleanup
}

#endregion
##*=============================================
##* END SCRIPT BODY
##*=============================================
Notes

When specifying hideLineNumbers make sure you add a space before and after the statement.

Notes

Only the first 10 lines of code will be shown with the option to expand the rest.
The code will be automatically updated using the link on each new site build.


Embed Image or GIF

Images can be embedded using the img shortcode with the option to specify optional parameters.

  • src
    Specifies the image link
  • alt
    Specifies the alternative image text
  • width
    Specifies the image width
  • height
    Specifies the image height
  • align
    Specifies the image alignment (left, center, right)
  • caption
    Specifies the image caption
{{< img src="/tools/windows-cache-cleanup-tool/powershell-windows-cleanup-in-progress.gif" alt="powershell-windows-cleanup-in-progress.gif" caption="Cleanup in Progress" >}}
article card image powershell-windows-cleanup-in-progress.gif
Cleanup in Progress

Embed YouTube Video

YouTube videos can be embedded using the youtube shortcode.

  • id
    Specifies the YouTube video slug.
  • title
    Specifies the YouTube video title.
  • width
    Specifies the YouTube video width, optional.
  • height
    Specifies the YouTube video height, optional.
  • align
    Specifies the YouTube video alignment (left, center, right), optional.
{{< youtube "3Ni-DZVA_40" >}}

article card image fun-window-jump.gif
Great Read!

SHARE

article card image dark article card image light

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

Introducing: macOS JAMF Offboarding Tool

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

See More
article card image dark article card image light

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

Introducing: Microsoft Cloud License Automation Tool - Part 1

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

See More