SPSWakeUP.ps1

<#PSScriptInfo
    .VERSION 3.0.3
 
    .GUID 1fc873b1-5854-46cb-8632-29cee879bb55
 
    .AUTHOR luigilink (Jean-Cyril DROUHIN)
 
    .COPYRIGHT
 
    .TAGS
    script powershell sharepoint warmup
 
    .LICENSEURI
    https://github.com/luigilink/SPSWakeUp/blob/main/LICENSE
 
    .PROJECTURI
    https://github.com/luigilink/SPSWakeUp
 
    .ICONURI
 
    .EXTERNALMODULEDEPENDENCIES
 
    .REQUIREDSCRIPTS
 
    .EXTERNALSCRIPTDEPENDENCIES
 
    .RELEASENOTES
 
    .PRIVATEDATA
#>


<#
    .SYNOPSIS
    SPSWakeUP script for SharePoint OnPremises
 
    .DESCRIPTION
    SPSWakeUp is a PowerShell script tool to warm up all site collection in your SharePoint environment.
    It's compatible with all supported versions for SharePoint (2016 to Subscription Edition).
    Use WebRequest object in multi-thread to download JS, CSS and Pictures files,
    Log script results in log file,
    Configure automatically prerequisites for a best warm-up,
 
    .PARAMETER Install
    Use the switch Install parameter if you want to add the warmup script in taskscheduler
    InstallAccount parameter need to be set
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Install -InstallAccount (Get-Credential)
 
    .PARAMETER InstallAccount
    Need parameter InstallAccount whent you use the switch Install parameter
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Install -InstallAccount (Get-Credential)
 
    .PARAMETER Uninstall
    Use the switch Uninstall parameter if you want to remove the warmup script from taskscheduler
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Uninstall
 
    .PARAMETER AllSites
    Use the boolean AllSites parameter if you want to warmup the SPWebs of each site collection
    and only warmup the root web of the site collection.
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -AllSites:$True
 
    .PARAMETER AdminSites
    Use the boolean AdminSites parameter if you want to warmup the Central Administration Site collection
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -AdminSites:$True
 
    .PARAMETER Transcript
    Use the boolean Transcript parameter if you want to start Transcrit PowerShell Feature.
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Transcript:$True
 
    .EXAMPLE
    SPSWakeUP.ps1 -Install -InstallAccount (Get-Credential)
    SPSWakeUP.ps1 -Uninstall
    SPSWakeUP.ps1 -AllSites:$True
    SPSWakeUP.ps1 -AdminSites:$True
    SPSWakeUP.ps1 -Transcript:$True
 
    .NOTES
    FileName: SPSWakeUP.ps1
    Authors: luigilink (Jean-Cyril DROUHIN)
                Nutsoft (Des Finkenzeller)
    Date: April 08, 2025
    Version: 3.0.3
    Licence: MIT License
 
    .LINK
    https://spjc.fr/
    https://github.com/luigilink/spswakeup
#>

param
(
    [Parameter(Position = 1)]
    [switch]
    $Install,

    [Parameter(Position = 2)]
    [System.Management.Automation.PSCredential]
    $InstallAccount,

    [Parameter(Position = 3)]
    [switch]
    $Uninstall,

    [Parameter(Position = 4)]
    [System.Boolean]
    $AllSites = $true,

    [Parameter(Position = 5)]
    [System.Boolean]
    $AdminSites = $true,

    [Parameter(Position = 6)]
    [System.Boolean]
    $Transcript = $false
)

Clear-Host
$Host.UI.RawUI.WindowTitle = "WarmUP script running on $env:COMPUTERNAME"

# Define variable
$spsWakeupVersion = '3.0.3'
$currentUser = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name
$scriptRootPath = Split-Path -parent $MyInvocation.MyCommand.Definition
$hostEntries = New-Object -TypeName System.Collections.Generic.List[string]
$hostsFile = "$env:windir\System32\drivers\etc\HOSTS"
$hostsFileCopy = $hostsFile + '.' + (Get-Date -UFormat "%y%m%d%H%M%S").ToString() + '.copy'

# Start Transcript parameter is equal to True
if ($Transcript) {
    $pathLogFile = Join-Path -Path $scriptRootPath -ChildPath ('SPSWakeUP_script_' + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log')
    Start-Transcript -Path $pathLogFile -IncludeInvocationHeader
}

# Check UserName and Password if Install parameter is used
if ($Install) {
    if ($null -eq $InstallAccount) {
        Write-Warning -Message ('SPSWakeUp: Install parameter is set. Please set also InstallAccount ' + `
                "parameter. `nSee https://github.com/luigilink/SPSWakeUp/wiki for details.")
        Break
    }
    else {
        $UserName = $InstallAccount.UserName
        $Password = $InstallAccount.GetNetworkCredential().Password
        $currentDomain = 'LDAP://' + ([ADSI]'').distinguishedName
        Write-Output "Checking Account `"$UserName`" ..."
        $dom = New-Object System.DirectoryServices.DirectoryEntry($currentDomain, $UserName, $Password)
        if ($null -eq $dom.Path) {
            Write-Warning -Message "Password Invalid for user:`"$UserName`""
            Break
        }
    }
}

#region logging and trap exception
# ===================================================================================
# Func: Write-LogException
# Desc: write Exception in powershell session and in error file
# ===================================================================================
function Write-LogException {
    [CmdletBinding()]
    [Alias()]
    [OutputType([String])]
    param
    (
        [Parameter(Mandatory = $true)]
        $Message
    )

    $pathErrLog = Join-Path -Path $scriptRootPath -ChildPath (((Get-Date).Ticks.ToString()) + '_errlog.xml')
    Write-Warning -Message $Message.Exception.Message
    Export-Clixml -Path $pathErrLog -InputObject $Message -Depth 3
}
# ===================================================================================
# Func: Clear-SPSLog
# Desc: Clean Log Files
# ===================================================================================
function Clear-SPSLog {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $path,

        [Parameter()]
        [System.UInt32]
        $days = 30
    )

    if (Test-Path $path) {
        # Get the current date
        $Now = Get-Date
        # Definie the extension of log files
        $Extension = '*.log'
        # Define LastWriteTime parameter based on $days
        $LastWrite = $Now.AddDays(-$days)
        # Get files based on lastwrite filter and specified folder
        $files = Get-Childitem -Path "$path\*.*" -Include $Extension | Where-Object -FilterScript {
            $_.LastWriteTime -le "$LastWrite"
        }

        if ($files) {
            Write-Output '--------------------------------------------------------------'
            Write-Output "Cleaning log files in $path ..."
            foreach ($file in $files) {
                if ($null -ne $file) {
                    Write-Output "Deleting file $file ..."
                    Remove-Item $file.FullName | Out-Null
                }
            }
        }
    }
}
#endregion

#region Installation in Task Scheduler
# ===================================================================================
# Func: Add-SPSTask
# Desc: Add SPSWakeUP Task in Task Scheduler
# ===================================================================================
function Add-SPSTask {
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path
    )

    $TrigSubscription =
    @"
<QueryList><Query Id="0" Path="System"><Select Path="System">*[System[Provider[@Name='Microsoft-Windows-IIS-IISReset'] and EventID=3201]]</Select></Query></QueryList>
"@

    $TaskDate = Get-Date -Format yyyy-MM-dd
    $TaskName = 'SPSWakeUP'
    $Hostname = $Env:computername

    # Connect to the local TaskScheduler Service
    $TaskSvc = New-Object -ComObject ('Schedule.service')
    $TaskSvc.Connect($Hostname)
    $TaskFolder = $TaskSvc.GetFolder('\')
    $TaskSPSWKP = $TaskFolder.GetTasks(0) | Where-Object -FilterScript {
        $_.Name -eq $TaskName
    }
    $TaskCmd = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
    $TaskCmdArg =
    @"
-Command Start-Process "$PSHOME\powershell.exe" -Verb RunAs -ArgumentList "'-ExecutionPolicy Bypass ""$path\SPSWakeUP.ps1""'"
"@


    if ($TaskSPSWKP) {
        Write-Warning -Message 'Shedule Task already exists - skipping.'
    }
    else {
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Adding SPSWakeUP script in Task Scheduler Service ...'

        # Get Credentials for Task Schedule
        $TaskAuthor = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name
        $TaskUser = $UserName
        $TaskUserPwd = $Password

        # Add a New Task Schedule
        $TaskSchd = $TaskSvc.NewTask(0)
        $TaskSchd.RegistrationInfo.Description = 'SPSWakeUp Task - Start at 6:00 daily'
        $TaskSchd.RegistrationInfo.Author = $TaskAuthor
        $TaskSchd.Principal.RunLevel = 1

        # Task Schedule - Modify Settings Section
        $TaskSettings = $TaskSchd.Settings
        $TaskSettings.AllowDemandStart = $true
        $TaskSettings.Enabled = $true
        $TaskSettings.Hidden = $false
        $TaskSettings.StartWhenAvailable = $true

        # Task Schedule - Trigger Section
        $TaskTriggers = $TaskSchd.Triggers

        # Add Trigger Type 2 OnSchedule Daily Start at 6:00 AM
        $TaskTrigger1 = $TaskTriggers.Create(2)
        $TaskTrigger1.StartBoundary = $TaskDate + 'T06:00:00'
        $TaskTrigger1.DaysInterval = 1
        $TaskTrigger1.Repetition.Duration = 'PT12H'
        $TaskTrigger1.Repetition.Interval = 'PT1H'
        $TaskTrigger1.Enabled = $true

        # Add Trigger Type 8 At StartUp Delay 10M
        $TaskTrigger2 = $TaskTriggers.Create(8)
        $TaskTrigger2.Delay = 'PT10M'
        $TaskTrigger2.Enabled = $true

        # Add Trigger Type 0 OnEvent IISReset
        $TaskTrigger3 = $TaskTriggers.Create(0)
        $TaskTrigger3.Delay = 'PT20S'
        $TaskTrigger3.Subscription = $TrigSubscription
        $TaskTrigger3.Enabled = $true

        $TaskAction = $TaskSchd.Actions.Create(0)
        $TaskAction.Path = $TaskCmd
        $TaskAction.Arguments = $TaskCmdArg
        try {
            $TaskFolder.RegisterTaskDefinition( $TaskName, $TaskSchd, 6, $TaskUser , $TaskUserPwd , 1)
            Write-Output 'Successfully added SPSWakeUP script in Task Scheduler Service'
        }
        catch {
            Write-LogException -Message $_
        }
    }
}
# ===================================================================================
# Func: Remove-SPSTask
# Desc: Remove SPSWakeUP Task from Task Scheduler
# ===================================================================================
function Remove-SPSTask {
    $TaskName = 'SPSWakeUP'
    $Hostname = $Env:computername

    # Connect to the local TaskScheduler Service
    $TaskSvc = New-Object -ComObject ('Schedule.service')
    $TaskSvc.Connect($Hostname)
    $TaskFolder = $TaskSvc.GetFolder('\')
    $TaskSPSWKP = $TaskFolder.GetTasks(0) | Where-Object -FilterScript {
        $_.Name -eq $TaskName
    }
    if ($null -eq $TaskSPSWKP) {
        Write-Warning -Message 'Shedule Task already removed - skipping.'
    }
    else {
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Removing SPSWakeUP script in Task Scheduler Service ...'
        try {
            $TaskFolder.DeleteTask($TaskName, $null)
            Write-Output 'Successfully removed SPSWakeUP script from Task Scheduler Service'
        }
        catch {
            Write-LogException -Message $_
        }
    }
}
#endregion

#region Load SharePoint Powershell Snapin for SharePoint Server
# ===================================================================================
# Name: Add-PSSharePoint
# Description: Load SharePoint Powershell Snapin
# ===================================================================================
function Add-PSSharePoint {
    if ($null -eq (Get-PSSnapin | Where-Object -FilterScript { $_.Name -eq 'Microsoft.SharePoint.PowerShell' })) {
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Loading SharePoint Powershell Snapin ...'
        Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction Stop | Out-Null
        Write-Output '--------------------------------------------------------------'
    }
}
# ===================================================================================
# Name: Get-SPSThrottleLimit
# Description: Get Number Of Throttle Limit
# ===================================================================================
function Get-SPSThrottleLimit {
    # Get Number Of Throttle Limit
    process {
        try {
            $cimInstanceProc = @(Get-CimInstance -ClassName Win32_Processor)
            $cimInstanceSocket = $cimInstanceProc.count
            $numLogicalCpu = $cimInstanceProc[0].NumberOfLogicalProcessors * $cimInstanceSocket
            $NumThrottle = @{ $true = 10; $false = 2 * $numLogicalCpu }[$numLogicalCpu -ge 8]
            return $NumThrottle
        }
        catch {
            Write-Warning -Message $_
        }
    }
}
#endregion

#region get all site collections and all web applications
# ===================================================================================
# Name: Get-SPSVersion
# Description: PowerShell script to display SharePoint products from the registry.
# ===================================================================================
function Get-SPSVersion {
    process {
        try {
            # location in registry to get info about installed software
            $regLoc = Get-ChildItem HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall
            # Get SharePoint Products and language packs
            $programs = $regLoc |  Where-Object -FilterScript {
                $_.PsPath -like '*\Office*'
            } | ForEach-Object -Process { Get-ItemProperty $_.PsPath }
            # output the info about Products and Language Packs
            $spsVersion = $programs | Where-Object -FilterScript {
                $_.DisplayName -like '*SharePoint Server*'
            }
            # Return SharePoint version
            $spsVersion.DisplayVersion
        }
        catch {
            Write-Warning -Message $_
        }
    }
}
# ===================================================================================
# Name: Add-SPSHostEntry
# Description: Add Web Application and HSNC Urls in hostEntries Variable
# ===================================================================================
function Add-SPSHostEntry {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $url
    )

    $url = $url -replace 'https://', ''
    $url = $url -replace 'http://', ''
    $hostNameEntry = $url.split('/')[0]
    [void]$hostEntries.Add($hostNameEntry)
}
# ===================================================================================
# Name: Get-SPSAdminUrl
# Description: Get All Url of Central Admin
# ===================================================================================
function Get-SPSAdminUrl {
    try {
        # Initialize ArrayList Object
        $tbCASitesURL = New-Object -TypeName System.Collections.ArrayList
        # Get Central Administration Url and add it in ArrayList Object
        $webAppADM = Get-SPWebApplication -IncludeCentralAdministration | Where-Object -FilterScript {
            $_.IsAdministrationWebApplication
        }
        [void]$tbCASitesURL.Add("$($webAppADM.Url)")
        # List of the most useful administration pages and Quick launch top links
        $urlsAdmin = @('Lists/HealthReports/AllItems.aspx', `
                '_admin/FarmServers.aspx', `
                '_admin/Server.aspx', `
                '_admin/WebApplicationList.aspx', `
                '_admin/ServiceApplications.aspx', `
                'applications.aspx', `
                'systemsettings.aspx', `
                'monitoring.aspx', `
                'backups.aspx', `
                'security.aspx', `
                'upgradeandmigration.aspx', `
                'apps.aspx', `
                'generalapplicationsettings.aspx')
        foreach ($urlAdmin in $urlsAdmin) {
            [void]$tbCASitesURL.Add("$($webAppADM.Url)$($urlAdmin)")
        }
        # Get Service Application Urls and then in ArrayList Object
        $sa = Get-SPServiceApplication
        $linkUrls = $sa | ForEach-Object { $_.ManageLink.Url } | Select-Object -Unique
        foreach ($linkUrl in $linkUrls) {
            $siteADMSA = $linkUrl.TrimStart('/')
            [void]$tbCASitesURL.Add("$($webAppADM.Url)$($siteADMSA)")
        }
        return $tbCASitesURL
    }
    catch {
        Write-LogException -Message $_
    }
}
# ===================================================================================
# Name: Get-SPSSitesUrl
# Description: Get All Site Collections Url
# ===================================================================================
function Get-SPSSitesUrl {
    try {
        # Initialize ArrayList Object
        $tbSitesURL = New-Object -TypeName System.Collections.ArrayList
        # Get Url of all site collection
        $webApps = Get-SPWebApplication -ErrorAction SilentlyContinue
        if ($null -ne $webApps) {
            foreach ($webApp in $webApps) {
                $sites = $webApp.sites
                foreach ($site in $sites) {
                    if ($AllSites) {
                        $webs = (Get-SPWeb -Site $site -Limit ALL -ErrorAction SilentlyContinue)
                        if ($null -ne $webs) {
                            foreach ($web in $webs) {
                                if ($web.Url -notmatch 'sitemaster-') {
                                    [void]$tbSitesURL.Add("$($web.Url)")
                                }
                            }
                        }
                    }
                    else {
                        if ($site.RootWeb.Url -notmatch 'sitemaster-') {
                            [void]$tbSitesURL.Add("$($site.RootWeb.Url)")
                        }
                    }
                    $site.Dispose()
                }
            }
        }
        # Add Topology.svc in ArrayList Object
        [void]$tbSitesURL.Add('http://localhost:32843/Topology/topology.svc')
        return $tbSitesURL
    }
    catch {
        Write-LogException -Message $_
    }
}
# ===================================================================================
# Name: Get-SPSWebAppUrl
# Description: Get All Web Applications and Host Named Site Collection Url
# ===================================================================================
function Get-SPSWebAppUrl {
    try {
        # Initialize ArrayList Object
        $webAppURL = New-Object -TypeName System.Collections.ArrayList
        # Get SPwebApplication Object
        $webApps = Get-SPWebApplication -ErrorAction SilentlyContinue
        if ($null -ne $webApps) {
            foreach ($webapp in $webApps) {
                [void]$webAppURL.Add($webapp.GetResponseUri('Default').AbsoluteUri)
                $spSrvIsInUri = Get-SPServer | Where-Object -FilterScript {
                    $webapp.GetResponseUri('Default').AbsoluteUri -match $_.Name
                }
                if ($null -eq $spSrvIsInUri) {
                    Add-SPSHostEntry -Url $webapp.GetResponseUri('Default').AbsoluteUri
                }
            }
            $sites = $webApps | ForEach-Object -Process {
                $_.sites
            }
            $HSNCs = $sites | Where-Object -FilterScript {
                $_.HostHeaderIsSiteName -eq $true
            }
            foreach ($HSNC in $HSNCs) {
                if ($HSNC.Url -notmatch 'sitemaster-') {
                    [void]$webAppURL.Add($HSNC.Url)
                    Add-SPSHostEntry -Url $HSNC.Url
                }
                $HSNC.Dispose()
            }
        }
        return $webAppURL
    }
    catch {
        Write-Warning -Message $_
    }
}
#endregion

#region Invoke webRequest
# ===================================================================================
# Name: Invoke-SPSWebRequest
# Description: Multi-Threading Request Url with System.Net.WebClient Object
# ===================================================================================
function Invoke-SPSWebRequest {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $Urls,

        [Parameter(Mandatory = $true)]
        $throttleLimit
    )

    $ScriptBlock =
    {
        param
        (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $Uri,

            [Parameter()]
            $Useragent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome,

            [Parameter()]
            $SessionWeb
        )

        Process {
            try {
                $startProcess = Get-Date
                if ($null -ne $sessionWeb) {
                    $webResponse = Invoke-WebRequest -Uri $uri `
                        -WebSession $sessionWeb `
                        -TimeoutSec 90 `
                        -UserAgent $useragent `
                        -UseBasicParsing
                }
                else {
                    $webResponse = Invoke-WebRequest -Uri $uri `
                        -UseDefaultCredentials `
                        -TimeoutSec 90 `
                        -UserAgent $useragent `
                        -UseBasicParsing
                }
                $timeExec = '{0:N2}' -f (((Get-Date) - $startProcess).TotalSeconds)
                $Response = "$([System.int32]$webResponse.StatusCode) - $($webResponse.StatusDescription)"
            }
            catch {
                $Response = $_.Exception.Message
            }
            finally {
                if ($webResponse) {
                    $webResponse.Close()
                    Remove-Variable webResponse
                }
            }
            $RunResult = New-Object PSObject
            $RunResult | Add-Member -MemberType NoteProperty -Name Url -Value $uri
            $RunResult | Add-Member -MemberType NoteProperty -Name 'Time(s)' -Value $TimeExec
            $RunResult | Add-Member -MemberType NoteProperty -Name Status -Value $Response
            $RunResult
        }
    }

    try {
        # Initialize variables and runpsace for Multi-Threading Request
        $psUserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
        $Jobs = @()
        $iss = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $Pool = [runspacefactory]::CreateRunspacePool(1, $throttleLimit, $iss, $Host)
        $Pool.Open()

        # Initialize WebSession from First SPWebApplication object
        $webApp = Get-SpWebApplication | Select-Object -first 1
        $authentUrl = ("$($webapp.GetResponseUri('Default').AbsoluteUri)" + '_windows/default.aspx?ReturnUrl=/_layouts/15/Authenticate.aspx?Source=%2f')
        Write-Output "Getting webSession by opening $($authentUrl) with Invoke-WebRequest"
        Invoke-WebRequest -Uri $authentUrl `
            -SessionVariable webSession `
            -UseDefaultCredentials `
            -UseBasicParsing `
            -TimeoutSec 90 `
            -UserAgent $psUserAgent
        foreach ($Url in $Urls) {
            $Job = [powershell]::Create().AddScript($ScriptBlock).AddParameter('Uri', $Url).AddParameter('UserAgent', $psUserAgent).AddParameter('SessionWeb', $webSession)
            $Job.RunspacePool = $Pool
            $Jobs += New-Object PSObject -Property @{
                Url    = $Url
                Pipe   = $Job
                Result = $Job.BeginInvoke()
            }
        }

        While ($Jobs.Result.IsCompleted -contains $false) {
            Start-Sleep -S 1
        }

        $Results = @()
        foreach ($Job in $Jobs) {
            $Results += $Job.Pipe.EndInvoke($Job.Result)
        }

    }
    catch {
        Write-Output 'An error occurred invoking multi-threading function'
        Write-LogException -Message $_
    }

    Finally {
        $Pool.Dispose()
    }
    $Results
}
#endregion

#region Configuration and permission
# ===================================================================================
# Func: Disable-LoopbackCheck
# Desc: This setting usually kicks out a 401 error when you try to navigate to sites
# that resolve to a loopback address e.g. 127.0.0.1
# ===================================================================================
function Disable-LoopbackCheck {
    $lsaPath = 'HKLM:\System\CurrentControlSet\Control\Lsa'
    $lsaPathValue = Get-ItemProperty -path $lsaPath
    if (-not ($lsaPathValue.DisableLoopbackCheck -eq '1')) {
        Write-Output 'Disabling Loopback Check...'
        New-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\Lsa -Name 'DisableLoopbackCheck' -value '1' -PropertyType dword -Force | Out-Null
    }
    else {
        Write-Output 'Loopback Check already Disabled - skipping.'
    }
}
# ====================================================================================
# Func: Clear-HostsFileCopy
# Desc: Clear previous HOSTS File copy
# ====================================================================================
function Clear-HostsFileCopy {
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $hostsFilePath,

        [Parameter()]
        [System.UInt32]
        $numberFiles = 10
    )

    $hostsFolderPath = Split-Path $hostsFilePath
    if (Test-Path $hostsFolderPath) {
        # Definie the extension of log files
        $extension = '*.copy'

        # Get files with .copy extension, sort them by name, from most recent to oldest and skip the first numberFiles variable
        $copyFiles = Get-ChildItem -Path "$hostsFolderPath\*.*" -Include $extension | Sort-Object -Descending -Property Name | Select-Object -Skip $numberFiles
        if ($copyFiles) {
            Write-Output '--------------------------------------------------------------'
            Write-Output "Cleaning backup HOSTS files in $hostsFolderPath ..."
            foreach ($copyFile in $copyFiles) {
                if ($null -ne $copyFile) {
                    Write-Output " * Deleting File $copyFile ..."
                    Remove-Item $copyFile.FullName | Out-Null
                }
            }
        }
    }
}
# ===================================================================================
# Func: Add-SPSUserPolicy
# Desc: Applies Read Access to the specified accounts for a web application
# ===================================================================================
function Add-SPSUserPolicy {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Urls,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $UserName
    )

    Write-Output '--------------------------------------------------------------'
    Write-Output "Add Read Access to $UserName for All Web Applications ..."
    foreach ($url in $Urls) {
        try {
            $webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup("$url")
            $displayName = 'SPSWakeUP Account'

            # If the web app is not Central Administration
            if ($webapp.IsAdministrationWebApplication -eq $false) {
                # If the web app is using Claims auth, change the user accounts to the proper syntax
                if ($webapp.UseClaimsAuthentication -eq $true) {
                    $user = (New-SPClaimsPrincipal -identity $UserName -identitytype 1).ToEncodedString()
                }
                else {
                    $user = $UserName
                }
                Write-Output "Checking Read access for $user account to $url..."
                [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webapp.Policies
                $policyExist = $policies | Where-Object -FilterScript {
                    $_.Displayname -eq 'SPSWakeUP Account'
                }

                if (-not ($policyExist)) {
                    Write-Output "Applying Read access for $user account to $url..."
                    [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add($user, $displayName)
                    $policyRole = $webApp.PolicyRoles.GetSpecialRole([Microsoft.SharePoint.Administration.SPPolicyRoleType]::FullRead)
                    if ($null -ne $policyRole) {
                        $policy.PolicyRoleBindings.Add($policyRole)
                    }
                    $webapp.Update()
                    Write-Output "Done Applying Read access for `"$user`" account to `"$url`""
                }
            }
        }
        catch {
            Write-LogException -Message $_
        }
    }
}
#endregion

#region Main
# ===================================================================================
#
# SPSWakeUP Script - MAIN Region
#
# ===================================================================================
$DateStarted = Get-date
$psVersion = ($host).Version.ToString()
$spsVersion = Get-SPSVersion
if ($PSVersionTable.PSVersion -gt [Version]'2.0' -and $spsVersion -lt 15) {
    powershell -Version 2 -File $MyInvocation.MyCommand.Definition
    exit
}

Write-Output '-------------------------------------'
Write-Output "| Automated Script - SPSWakeUp v$spsWakeupVersion"
Write-Output "| Started on : $DateStarted by $currentUser"
Write-Output "| PowerShell Version: $psVersion"
Write-Output "| SharePoint Version: $spsVersion"
Write-Output '-------------------------------------'

# Check Permission Level
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
    Write-Warning -Message 'You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!'
    Break
}
else {
    if ($Uninstall) {
        # Remove SPSWakeup script from scheduled Task
        Remove-SPSTask
    }
    elseif ($Install) {
        # Add SPSWakeup script in a new scheduled Task
        Add-SPSTask -Path $scriptRootPath

        # Load SharePoint Powershell Snapin
        Add-PSSharePoint

        # Get All Web Applications Urls
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Get URLs of All Web Applications ...'
        $getSPWebApps = Get-SPSWebAppUrl

        # Add read access for Warmup User account in User Policies settings
        Add-SPSUserPolicy -Urls $getSPWebApps -UserName $UserName
    }
    else {
        Write-Output "Setting power management plan to `"High Performance`"..."
        Start-Process -FilePath "$env:SystemRoot\system32\powercfg.exe" `
            -ArgumentList '/s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' `
            -NoNewWindow

        # Load SharePoint Powershell Snapin, Assembly and System.Web
        Add-PSSharePoint

        # From SharePoint 2016, check if MinRole equal to Search
        $currentSPServer = Get-SPServer | Where-Object -FilterScript { $_.Address -eq $env:COMPUTERNAME }
        if ($null -ne $currentSPServer -and (Get-SPFarm).buildversion.major -ge 16) {
            if ($currentSPServer.Role -eq 'Search') {
                Write-Warning -Message 'You run this script on server with Search MinRole'
                Write-Output 'Search MinRole is not supported in SPSWakeUp'
                Break
            }
        }

        # Invoke-WebRequest on Central Admin if AdminSites parameter equal to True
        if ($AdminSites) {
            $spCASvcInstance = $currentSPServer.ServiceInstances | Where-Object -FilterScript { $_.TypeName -eq 'Central Administration' }
            if ($spCASvcInstance.Status -eq 'Online') {
                Write-Output '--------------------------------------------------------------'
                Write-Output 'Opening All Central Admin Urls with Invoke-WebRequest, Please Wait...'
                $getSPADMSites = Get-SPSAdminUrl
                foreach ($spADMUrl in $getSPADMSites) {
                    try {
                        $startInvoke = Get-Date
                        $webResponse = Invoke-WebRequest -Uri $spADMUrl -UseDefaultCredentials -TimeoutSec 90 -UseBasicParsing
                        $TimeExec = '{0:N2}' -f (((Get-Date) - $startInvoke).TotalSeconds)
                        Write-Output '-----------------------------------'
                        Write-Output "| Url : $spADMUrl"
                        Write-Output "| Time : $TimeExec"
                        Write-Output "| Status : $($webResponse.StatusCode) - $($webResponse.StatusDescription)"
                    }
                    catch {
                        Write-LogException -Message $_
                    }
                }
            }
            else {
                Write-Warning -Message "No Central Admin Service Instance running on $env:COMPUTERNAME"
            }
        }

        # Get All Web Applications Urls, Host Named Site Collection and Site Collections
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Get URLs of All Web Applications ...'
        $getSPWebApps = Get-SPSWebAppUrl

        Write-Output '--------------------------------------------------------------'
        Write-Output 'Get URLs of All Site Collection ...'
        $getSPSites = Get-SPSSitesUrl
        if ($null -ne $getSPWebApps -and $null -ne $getSPSites) {
            if ($hostEntries) {
                # Disable LoopBack Check
                Disable-LoopbackCheck
                # Remove Duplicate Entries
                $hostEntries = $hostEntries | Get-Unique
                # Initialize variables
                $hostFileNeedsBackup = $true
                $hostIPV4Addr = '127.0.0.1'
                # Make backup copy of the Hosts file with today's date Add Web Application and Host Named Site Collection Urls in HOSTS system File
                Write-Output '--------------------------------------------------------------'
                Write-Output 'Add Urls of All Web Applications or HSNC in HOSTS File ...'
                foreach ($hostEntry in $hostEntries) {
                    $hostEntryIsPresent = Select-String -Path $hostsFile -Pattern $hostEntry
                    if ($null -eq $hostEntryIsPresent) {
                        if ($hostFileNeedsBackup) {
                            Write-Verbose -Message "Backing up $hostsFile file to: $hostsFileCopy"
                            Copy-Item -Path $hostsFile -Destination $hostsFileCopy -Force
                            $hostFileNeedsBackup = $false
                        }
                        # Remove http or https information to keep only HostName or FQDN
                        if ($hostEntry.Contains(':')) {
                            Write-Warning -Message "$hostEntry cannot be added in HOSTS File, only web applications with 80 or 443 port are added."
                        }
                        else {
                            Write-Output "Adding $($hostEntry) in HOSTS file"
                            Add-Content -Path $hostsFile -value "$hostIPV4Addr `t $hostEntry"
                        }
                    }
                }
            }
            # Request Url with Invoke-WebRequest CmdLet for All Urls
            Write-Output '--------------------------------------------------------------'
            Write-Output 'Opening All sites Urls with Invoke-WebRequest, Please Wait...'
            $InvokeResults = Invoke-SPSWebRequest -Urls $getSPSites -throttleLimit (Get-SPSThrottleLimit)
            # Show the results
            foreach ($InvokeResult in $InvokeResults) {
                if ($null -ne $InvokeResult.Url) {
                    Write-Output '-----------------------------------'
                    Write-Output "| Url : $($InvokeResult.Url)"
                    Write-Output "| Time : $($InvokeResult.'Time(s)') seconds"
                    Write-Output "| Status : $($InvokeResult.Status)"
                }
            }
        }

        $DateEnded = Get-Date
        $totalUrls = $getSPSites.Count
        $totalDuration = ($DateEnded - $DateStarted).TotalSeconds

        Write-Output '-------------------------------------'
        Write-Output '| Automated Script - SPSWakeUp'
        Write-Output "| Started on : $DateStarted"
        Write-Output "| Completed on : $DateEnded"
        Write-Output "| SPSWakeUp waked up $totalUrls urls in $totalDuration seconds"
        Write-Output '--------------------------------------------------------------'
        Write-Output '| REPORTING: Memory Usage for each worker process (W3WP.EXE)'
        Write-Output '| Process Creation Date | Memory | Application Pool Name'
        Write-Output '--------------------------------------------------------------'

        $w3wpProcess = Get-CimInstance Win32_Process -Filter "name = 'w3wp.exe'" | Select-Object WorkingSetSize, CommandLine, CreationDate | Sort-Object CommandLine
        foreach ($w3wpProc in $w3wpProcess) {
            $w3wpProcCmdLine = $w3wpProc.CommandLine.Replace('c:\windows\system32\inetsrv\w3wp.exe -ap "', '')
            $pos = $w3wpProcCmdLine.IndexOf('"')
            $appPoolName = $w3wpProcCmdLine.Substring(0, $pos)
            $w3wpMemoryUsage = [Math]::Round($w3wpProc.WorkingSetSize / 1MB)
            Write-Output "| $($w3wpProc.CreationDate) | $($w3wpMemoryUsage) MB | $($appPoolName)"
        }
        Write-Output '--------------------------------------------------------------'

        Trap { Continue }

        # Clean the copy files of system HOSTS folder
        Clear-HostsFileCopy -hostsFilePath $hostsFile
        # Clean the folder of log files
        Clear-SPSLog -path $scriptRootPath
        # Stop Transcript parameter is equal to True
        if ($Transcript) {
            $pathLogFile = Join-Path -Path $scriptRootPath -ChildPath ('SPSWakeUP_script_' + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log')
            Stop-Transcript
        }
    }
    Exit
}
#endregion