Obs/bin/ObsAgent/lib/Scripts/LogCollectionHelper.psm1

<##############################################################
 # #
 # Copyright (C) Microsoft Corporation. All rights reserved. #
 # #
 ##############################################################>


Import-Module $PSScriptRoot\GenericHelper.psm1 -Force -Verbose:$false

function Invoke-ScriptBlockWithRetries
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ScriptBlock] $ScriptBlock,

        [Parameter(Mandatory = $false)]
        [Object] $Argument,

        [Parameter(Mandatory = $true)]
        [int] $MaxTries,

        [Parameter(Mandatory = $false)]
        [int] $IntervalInSeconds = 30

    )

    $functionName = "$($MyInvocation.MyCommand.Name)"
    Trace-Progress "$functionName : Retrying max $MaxTries interval [$IntervalInSeconds] ScriptBlock = [$ScriptBlock]"

    $attempt = 1
    $success = $false
    do
    {
        Trace-Progress "$functionName : Starting attempt $attempt"
        try
        {
            $result = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Argument -ErrorAction Stop
            $success = $true
        }
        catch
        {
            $message = "Exception occurred while trying to execute scriptblock command:" + $_.Exception.ToString()
            Trace-Progress "$functionName : $message"

            if ($attempt -ge $MaxTries)
            {
                throw
            }

            Start-Sleep -Seconds $IntervalInSeconds
        }
        finally
        {
            Trace-Progress "$functionName : Completed attempt $attempt; Status = $success"
            $attempt++
        }
    }
    while (!$success)

    return $result
}

function Get-WindowsEventLog
{
    Param
    (
        [parameter(Mandatory=$true)]
        [string[]]
        $ComputerNames,

        [parameter(Mandatory=$false)]
        [HashTable]
        [ValidateNotNull()]
        $ComputerPSSessions,

        [parameter(Mandatory=$true)]
        [string[]]
        $LogPattern,

        [parameter(Mandatory=$false)]
        [DateTime]
        $EventsFromDate = (Get-Date).AddHours(-1),

        [parameter(Mandatory=$false)]
        [DateTime]
        $EventsToDate = (Get-Date),

        [Parameter(Mandatory=$false)]
        [REF]
        $ExcludedEndpoints,

        [parameter(Mandatory=$true)]
        [PSObject]
        $Roles,

        [parameter(Mandatory=$true)]
        [string]
        $CurrentRole,

        [parameter(Mandatory=$true)]
        [string]
        $DestPathWithRoleName,

        [parameter(Mandatory=$true)]
        [bool]
        $LocalMode
    )

    $functionName = "$($MyInvocation.MyCommand.Name)_$CurrentRole"

    # Get the time span in milliseconds
    function Get-TimeSpan($Date)
    {
        $timeSpan = New-TimeSpan -Start $Date -End (Get-Date)
        return [Math]::Round($timeSpan.TotalMilliseconds)
    }

    # Calculate number of milliseconds and prepare the WEvtUtil parameter to filter based on date/time
    $toSpan = Get-TimeSpan -Date $EventsToDate
    $fromSpan = Get-TimeSpan -Date $EventsFromDate

    $exportLogJobs = @()

    # Copy logs from remote machine to local machine
    foreach($computerName in $ComputerNames)
    {
        $session = $null
        $machineName = $computerName.Split('.')[0]
        Trace-Progress "$functionName : computername = [$computerName] machinename = [$machineName]"

        if (!$LocalMode)
        {
            if ($ComputerPSSessions)
            {
                Trace-Progress "$functionName :Checking if the session for $computerName session is valid in ComputerPSSessions array"
                if ($ComputerPSSessions.ContainsKey($computerName))
                {
                    $session = $ComputerPSSessions[$computerName]
                    if (($null -ne $session) -and ( ($session.State -ne "Opened") -or ($session.Availability -ne "Available") ) )
                    {
                        if($session) {
                            #if we had opened the session previously, close it before overwriting this variable with new session.
                            Remove-PSSession -Session $session -ErrorAction SilentlyContinue
                        }
                        Trace-Progress "$functionName :The session for $computerName went into state = [$($session.state)] , availabilty = [$($session.Availability)], ! Reinitializing!"
                        $session = Initialize-PSSession -ComputerPSSessions $ComputerPSSessions -ComputerFqdn $computerName -ExcludedEndpoints ([REF]$ExcludedEndpoints.Value)
                        if ($null -ne $session)
                        {
                            $ComputerPSSessions[$computerName] = $session
                        }
                    }
                }
                else
                {
                    Trace-Progress "$functionName :$computerName session not found in ComputerPSSessions[] array, unable to collect event logs "
                }
            }
            else
            {
                Trace-Progress "$functionName :Creating a PSSession to [$computerName] as ComputerPSSessions[] array is null"
                $session = New-PSSession -ComputerName $computerName -ErrorAction SilentlyContinue
                # $ComputerPsSessions are not provided and we are opening a new session for each computername, we need to close these before we leave.
            }
        }
        
        if ($LocalMode -or (($null -ne $session) -and ($session.State -eq "Opened") -and ($session.Availability -eq "Available")))
        {
            if ($LocalMode)
            {
                $logPath = "$($env:TEMP)WinEvents$CurrentRole\"
            }
            else
            {
                $logPath = Invoke-Command -Session $session {$tmp = "$($env:TEMP)WinEvents$using:CurrentRole\" ; $tmp = $tmp.ToLower().Replace("c:","\\$($env:ComputerName)\c$"); return $tmp}
            }
            
            Trace-Progress "$functionName :Log path computer = $logPath -- for $computerName "
            $initblock = [ScriptBlock]::Create("Import-Module -Name '$PSScriptRoot\LogCollectionHelper.psm1' -Force; Import-Module -Name '$PSScriptRoot\GenericHelper.psm1' -Force")
            # Collect logs on remote machine
            if ($LocalMode)
            {
                $exportLogJobs += Start-Job -ScriptBlock {
                    Collect-WindowsEventLogs -LogFolder $using:logPath -FromSpan $using:fromSpan -ToSpan $using:ToSpan -LogPattern $using:LogPattern
                } -InitializationScript $initblock -ErrorAction Continue
            }
            else
            {
                Invoke-Command -Session $session $initBlock
                $exportLogJobs += Invoke-Command -AsJob -Session $session {
                    Collect-WindowsEventLogs -LogFolder $using:logPath -FromSpan $using:fromSpan -ToSpan $using:ToSpan -LogPattern $using:LogPattern
                } -ErrorAction Continue
            }
        }
        else
        {
            if($null -eq $session) {
                Trace-Progress "$functionName :Could not establish a PS session with the computer [$computerName]." -Warning
            } else {
                Trace-Progress "$functionName :Session with the computer [$computerName] is stale - Session state = [$($session.State)], Session Availability =[$($session.Availability)]" -Warning
            }
        }
    }

    Trace-Progress "$functionName :Kicked off $($exportLogJobs.count) jobs to collect windows events"

    try
    {
        $ProgressPreference = "SilentlyContinue"
        $exportLogJobOutput = $exportLogJobs | Wait-job | Receive-Job

        Trace-Progress "$functionName :Finished waiting for jobs count = [$($exportLogJobs.Count)]"
        Write-Output $exportLogJobs # dont change to trace-progress
        Trace-Progress "$functionName :DestPathWithRoleName = [$DestPathWithRoleName]"

        foreach ($o in $exportLogJobOutput)
        {
            Trace-Progress "$functionName :Job retruned logpath = [$($o.logPath)] from computer = $($o.ComputerName)"
            if (-not [string]::IsNullOrEmpty($o.logPath))
            {
                Trace-Progress "$functionName :Copying from Source: $($o.logPath) to Destination: $DestPathWithRoleName"
                try
                {
                    $windowsEventFiles = Get-ChildItem -Path $o.logPath -File -Recurse
                }
                catch
                {
                    Trace-Progress -Message "$functionName :Failed to get files from $($o.logPath) on $($o.computerName). Error: $_" -Warning
                }

                if (($null -ne $windowsEventFiles ) -or ($windowsEventFiles.Count -gt 0))
                {
                    $destPath = Join-Path -Path $DestPathWithRoleName -ChildPath $o.ComputerName
                    try
                    {
                        Trace-Progress "$functionName :Creating new directory $destPath"
                        New-ASPath -Path $destPath -Type Directory
                        Copy-Item -Path $o.logPath -Destination $destPath -Force -Recurse
                    }
                    catch
                    {
                        Trace-Progress "$functionName :Failed to copy logs from $($o.logPath). Error: $_" -Warning
                    }

                    Remove-Item $o.logPath -Force -Recurse -ErrorAction SilentlyContinue
                }
            }
            else
            {
                Trace-Progress "$functionName :No logs copied as path on remote machine was empty. $($o.logPath)"
            }
        }
        Trace-Progress "$functionName :all evtx logs from all role vm's complete."
        $allEvtxCollectionSuccess = $true
    }
    finally
    {
        Trace-Progress "$functionName :In Finally block"

        if(!$allEvtxCollectionSuccess) {
            Trace-Progress "$functionName :Finally block- Unclean Exit detected, stopping all export jobs, if in progress"
            $exportLogJobs | Stop-Job
            $exportLogJobs | Receive-Job
        }

        Trace-Progress "$functionName :In Finally block, removing all job"
        $exportLogJobs | remove-job -ErrorAction SilentlyContinue
    }
}

function Collect-WindowsEventLogs
{
    Param
    (
    [parameter(Mandatory=$true)]
    [string]
    $LogFolder,

    [parameter(Mandatory=$true)]
    [double]
    $FromSpan,

    [parameter(Mandatory=$true)]
    [double]
    $ToSpan,

    [parameter(Mandatory=$true)]
    [string[]]
    $LogPattern
    )

    if (-not (Test-Path $logFolder))
    {
        $null = New-Item -ItemType Directory -Path $logFolder
    }

    $timestamps = @{}

    $qParameter = "*[System[TimeCreated[timediff(@SystemTime) <= $fromSpan] and TimeCreated[timediff(@SystemTime) >=$toSpan]]]"

    $reservedChannels = @("Microsoft.AzureStack.LCMController.EventSource/Admin")

    foreach ($lp in $logPattern)
    {
        $eventLogs = Get-WinEvent -ListLog $lp -Force -ErrorAction SilentlyContinue
        if (!$eventLogs.count)
        {
            $timestamps.$lp = @{}
        }
        else
        {
            $eventLogs | Foreach-Object {
                $fileSuffix = "Event_"+$_.LogName.Replace("/","-")+".EVTX"
                $logFile = $logFolder + $fileSuffix
                $locale = (Get-Culture).Name
                # Export log file using the WEvtUtil command-line tool
                # For Analytical and Debug log: disable => export => enable, as export cannot be performed over an enabled direct channel.
                $directChannel = $false
                $allLatestTimeCreated = $null
                if ($_.LogType -in @('Analytical','Debug'))
                {
                    if ($_.IsEnabled)
                    {
                        $directChannel = $true
                        # Disable Logs
                        WEvtUtil.exe sl /e:false $_.LogName
                    }
                }
                else
                {
                    # We cant collect latest time in O(1) for Analytical and Debug Log, so leave it as null
                    # Here are are collecting the latest time for Regular Logs only
                    $allLatestTimeCreated = Get-WinEvent -logname $_.LogName -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated"
                }
                # Export logs based on query to file with overwrite
                if ($reservedChannels -icontains $lp)
                {
                    WEvtUtil.exe epl $_.LogName $logFile /ow:true
                }
                else
                {
                    WEvtUtil.exe epl $_.LogName $logFile /q:$qParameter /ow:true
                }
                
                $allOldestTimeCreated = Get-WinEvent -logname $_.LogName -oldest -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated"
                if ($directChannel -eq $true)
                {
                    # Enable Logs
                    echo y | WEvtUtil.exe sl /e:true $_.LogName | out-null
                }
                # Archive logs (saves all locale specific information to allow reading of events without publisher)
                # WEvtUtil.exe al $logFile /l:$locale
        
                $copiedLatestTimeCreated = Get-WinEvent -path $logFile -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated"
                $copiedOldestTimeCreated = Get-WinEvent -path $logFile -oldest -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated"
        
                if ($allOldestTimeCreated -or $copiedOldestTimeCreated) { $timestamps.($_.LogName) = @{} }
                if ($allOldestTimeCreated) { $timestamps.($_.LogName).all = @{ oldestTimeCreated = $allOldestTimeCreated; latestTimeCreated = $allLatestTimeCreated } }
                if ($copiedOldestTimeCreated) { $timestamps.($_.LogName).copied = @{ oldestTimeCreated = $copiedOldestTimeCreated; latestTimeCreated = $copiedLatestTimeCreated } }
            }
        }
    }

    # Return the computerName and the logFolder
    @{
        ComputerName = $env:ComputerName
        VMName = $null
        logPath = $logFolder
        timestamps = $timestamps
    }
}

#
# For security reasons we strictly restrict the files to be included to certain extentions
#
function Get-FileLog
{
    Param
    (
        [parameter(Mandatory=$true, ParameterSetName='File')]
        [string[]]
        $ComputerNames,

        [parameter(Mandatory=$false, ParameterSetName='File')]
        [HashTable]
        [ValidateNotNull()]
        $ComputerPSSessions,

        [parameter(Mandatory=$true)]
        [string[]]
        $SourceLogFilePaths,

        [parameter(Mandatory=$false)]
        [DateTime]
        $FilesFromDate = (Get-Date).AddHours(-1),

        [parameter(Mandatory=$false)]
        [DateTime]
        $FilesToDate = (Get-Date),

        [parameter(Mandatory=$true, ParameterSetName='CSV')]
        [string]
        $CSVLogsFolderName,

        [parameter(Mandatory=$true)]
        [string]
        $Role,

        [Parameter(Mandatory=$false, ParameterSetName='File')]
        [REF]$ExcludedEndpoints,

        [parameter(Mandatory=$true)]
        [string]
        $DestPathWithRoleName,

        [parameter(Mandatory=$true)]
        [bool]
        $LocalMode,

        [parameter(Mandatory=$false)]
        [bool]
        $IsArcA = $false,

        [parameter(Mandatory=$true)]
        [bool]
        $SaveToPathSelected
    )

    Trace-EnteringMethod
    $functionName = "$($MyInvocation.MyCommand.Name)_$Role"
    $CSVLogsCopied = @()
    $ProgressPreference = "SilentlyContinue"

    foreach($logPath in $SourceLogFilePaths)
    {
        if ($logPath.Contains('$'))
        {
            # The path might contain environment variables, hence expanding it to actual path.
            # Example for valid environment variables: $env:WinDir, $env:SystemDrive, $env:ProgramData.
            # Avoid using environment variables that are different per user ex. $env:temp.
            $logPath = $ExecutionContext.InvokeCommand.ExpandString($logPath)
        }

        # Copy-Item -FromSession has a bug where it does not respect wild card over remote, as well as failing to copy some logs due to file locks.
        # Manually copying the files by mapping the drive.
        $logPathLeaf = Split-Path -Path $logPath -Leaf
        $logPathParent = Split-Path -Path $logPath -Parent

        if ($PsCmdlet.ParameterSetName -eq "CSV")
        {
            try
            {
                if ($logPath -notin $CSVLogsCopied)
                {
                    Trace-Progress "$functionName :Copying from $logPath"
                    $CSVLogDestRelativePath = $CSVLogsFolderName

                    $items = $null
                    if (Test-Path $logPath -ErrorAction SilentlyContinue)
                    {
                        Trace-Progress "$functionName : Copying csv logs from Source: $logPathParent to Destination: $CSVLogDestRelativePath"
                        $items = Get-FilteredChildItem -Path $logPath -FromDate $FilesFromDate -ToDate $FilesToDate -IsArcA $IsArcA
                        if (($null -ne $items.filteredItems) -and ($items.filteredItems.Count -gt 0))
                        {
                            Copy-FilteredChildItem -Items $items.filteredItems -Source $logPathParent -ChildFolder $CSVLogDestRelativePath -DestPathWithRoleName $DestPathWithRoleName
                        
                            # If we are sending the logs to an SMB Share, then we want all files compressed
                            if (-not $SaveToPathSelected)
                            {
                                Extract-CompressedFiles -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $CSVLogDestRelativePath
                            }
                        }
                        else
                        {
                            Trace-Progress "$functionName : Skipping Copy-FilteredChildItem and checking for cab files, as items.FilteredItems is null."
                        }
                        <#
                        if ($items.filesToSkipCompression.Count -gt 0)
                        {
                            Trace-Progress "$functionName : total files to skip compression = $($items.filesToSkipCompression.Count)"
                            Copy-FilteredChildItem -Items $items.filesToSkipCompression -Source $logPathParent -ChildFolder $CSVLogDestRelativePath -ZipPipeline $UncompressedPipeline
                        }
                        #>


                        $CSVLogsCopied += $logPath
                    }
                    else
                    {
                        Trace-Progress "$functionName :Folder $logPath does not exist. Logs from '$logPath' were not collected." -Warning
                    }
                }
            }
            catch
            {
                Trace-Progress "$functionName : Failed to copy CSV logs at log path : $logpath. Error : $_" -Error
            }
        }
        elseif ($PsCmdlet.ParameterSetName -eq "File")
        {
            # Copy logs from remote machine to local machine
            foreach($computerName in $ComputerNames)
            {
                try
                {
                    $session = $null
                    $machineName = $computerName.Split('.')[0]
                    $destRelativePath = $machineName

                    if (!$LocalMode)
                    {
                        if ($ComputerPSSessions)
                        {
                            if ($ComputerPSSessions.ContainsKey($computerName))
                            {
                                $session = $ComputerPSSessions[$computerName]
                                if (($null -ne $session) -and ($session.State -ne "Opened"))
                                {
                                    Trace-Progress "$functionName :The session for $computerName went into $($session.state) state! Reinitializing!"
                                    $session = Initialize-PSSession -ComputerPSSessions $ComputerPSSessions -ComputerFqdn $computerName -ExcludedEndpoints ([REF]$ExcludedEndpoints.Value)
                                    if ($null -ne $session)
                                    {
                                        $ComputerPSSessions[$computerName] = $session
                                    }
                                }
                            }
                        }
                        else
                        {
                            Trace-Progress "$functionName :Creating a PSSession to $computerName"
                            $session = New-PSSession -ComputerName $computerName -ErrorAction SilentlyContinue
                        }
                    }
                    
                    if (!$LocalMode -and (($null -eq $session) -or ($session.State -ne "Opened")))
                    {
                        Trace-progress -Message "$functionName :Could not establish a PS session with the computer. Logs were not copied from this computer." -Warning
                    }
                    else
                    {
                        Trace-Progress "$functionName : Copying from $logPath"

                        if (!$LocalMode)
                        {
                            $logPathRoot = ("\\$computerName\$($logPathParent -replace ':', '$')").TrimEnd('\')
                            $mappedDriveName = "Remote" + $machineName

                            $mappedDrive = Get-PSDrive $mappedDriveName -ErrorAction SilentlyContinue
                        
                            if ((-not $mappedDrive))
                            {
                                Trace-Progress "$functionName : Creating mapped drive : $mappedDriveName"
                                $mappedDrive = New-PSDrive -Name $mappedDriveName -PSProvider FileSystem -Root $logPathRoot -ErrorVariable DriveError -ErrorAction SilentlyContinue
                                if ($DriveError.count -gt 0)
                                {
                                    $err = $DriveError[0]
                                    $errorMessage = $err.Exception.Message
                                    Trace-Progress "$functionName : Error creating mapped drive : $errorMessage" -Warning
                                }
                            }
                            else
                            {
                                Trace-Progress "$functionName : Mapped drive for $mappedDriveName exists"
                            }
                        }

                        if ($LocalMode -or $mappedDrive)
                        {
                            if ($LocalMode)
                            {
                                $logPathRoot = $logPathParent.TrimEnd('\')
                                $newLogPath = $logPath
                            }
                            else
                            {
                                $newLogPath = $mappedDriveName + ':' + $logPathLeaf
                            }
                            
                            Trace-Progress "$functionName : newLogPath = [$newLogPath]"

                            $items = $null
                            if (Test-Path $newLogPath -ErrorAction Continue)
                            {
                                if ($LocalMode)
                                {
                                    Trace-Progress "$functionName :Copying file logs from Source: $newLogPath to Destination: $DestPathWithRoleName $destRelativePath"
                                }
                                else
                                {
                                    Trace-Progress "$functionName :Copying file logs from Source: $newLogPath (Remote is mapped drive for $logPathRoot) to Destination: $DestPathWithRoleName $destRelativePath"
                                }
                                
                                $items = Get-FilteredChildItem -Path $newLogPath -FromDate $FilesFromDate -ToDate $FilesToDate -IsArcA $IsArcA

                                if (($null -ne $items.filteredItems) -and ($items.filteredItems.Count -gt 0))
                                {
                                    Copy-FilteredChildItem -Items $items.filteredItems -Source $logPathRoot -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $destRelativePath -ComputerName $machineName
                                
                                     # If we are sending the logs to an SMB Share, then we want all files compressed
                                    if (-not $SaveToPathSelected)
                                    {
                                        Extract-CompressedFiles -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $CSVLogDestRelativePath
                                    }
                                    Trace-Progress "$functionName :Completed Copy-FilteredChildItems"
                                }
                                else
                                {
                                    Trace-Progress "$functionName : Skipping Copy-FilteredChildItem and checking for cab files, as items.FilteredItems is null."
                                }
                            }
                            else
                            {
                                Trace-Progress "$functionName :Folder $newLogPath does not exist on $computerName. Logs from '$logPath' were not collected." -Warning
                            }

                            if (!$LocalMode)
                            {
                                Trace-Progress "$functionName :Removing PS Drive $mappedDrive"
                                Remove-PSDrive $mappedDrive -Verbose
                            }
                        }
                    }
                }
                catch
                {
                    Trace-Progress "$functionName : Failure to collect logs for log path : $LogPath on computer : $ComputerName. Error: $_" -Error
                    if ($mappedDrive)
                    {
                        Remove-PSDrive $mappedDrive
                    }
                }
            }

            if (-not $ComputerPSSessions -and ($null -ne $session))
            {
                Remove-PSSession -Session $session -ErrorAction SilentlyContinue
            }
        }
        else{
            # We should never see this, if we see this means error in role xml.
            Trace-Progress "$functionName :Folder [$logPath] - [$logPathParent] is neither file log or CSV log check..." -Warning
        }
    }
}

#
# Gets files according to filtered extensions and date range.
# returns the child items based on the filtered criteria
#
function Get-FilteredChildItem
{
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$true)]
        [string]
        $Path,

        [Parameter(Mandatory=$true)]
        [DateTime]
        $FromDate,

        [Parameter(Mandatory=$true)]
        [DateTime]
        $ToDate,

        [parameter(Mandatory=$false)]
        [switch]
        $IncludeDumpFile,

        [parameter(Mandatory=$false)]
        [bool]
        $IsArcA = $false
    )
    Trace-EnteringMethod
    $functionName = $($MyInvocation.MyCommand.Name)
    $allowedFileExtensions = '*.txt','*.log','*.etl','*.out','*.xml','*.htm','*.html','*.mta','*.evtx','*.tsf','*.json','*.zip','*.csv','*.err','*.cab'
    # Note following file extensions are omitted - '*.blg', ,'*.trace', '*.bin'
    if ($isArcA)
    {
        $allowedFileExtensions = $allowedFileExtensions + "*.dtr", "*.bin", "*.trace"
    }
    if ($IncludeDumpFile)
    {
        $allowedFileExtensions = $allowedFileExtensions + "*.dmp"
    }
    $dateFilterExt = @('*.bin')
    $excludedFiles = @('*unattend.xml')
    #$skipCompressionFileExtensions = @('*.bin', '*.zip', '*.cab')
    $reservedFiles = @("CBS.log", "ActionList.xml", "DeviceInventory.xml")
    $reservedPaths = @("TestObservability", "$env:SystemDrive\CloudDeployment\Logs", "$env:SystemDrive\Observability\ECE", "$env:SystemDrive\Observability\ECEAgent",
        "MasLogs", "$env:windir\logs\DISM", "$env:SystemDrive\Observability\Download\UdiSessions\Scan-*\udiapi.log", "$env:SystemDrive\Observability\Download\UdiSessions\Download-*\udiapi.log",
        "$env:windir\Logs\MoSetup\udiapi-osget*.log", "$env:windir\Logs\MoSetup\UpdateAgent*.log" )
    #$reservedPattern = @('MonAgentHost', 'AzureStack_Validation')
    $filesToSkipCompression = @()

    Trace-Progress "$functionName :Path : $Path"

    try {
        if (Test-Path -Path $Path -PathType leaf)
        {
            Trace-Progress "$functionName testpath success - path is a leaf = $Path"
            $unfilteredItems = Get-ChildItem -Path $Path -Force -ErrorAction stop
        }
        else
        {
            Trace-Progress "$functionName testpath is not a leaf = $Path"
            $unfilteredItems = Get-ChildItem -Path $Path -Recurse -Force -ErrorAction stop
        }
        Trace-Progress "$functionName : Found $($unfilteredItems.Count) unfiltered items in $Path."

        # Apply special filtering for bin files based on date range as logs get added to these files incrementally, so we cannot depend on creation/modification date.
        if ($allowedFileExtensions | Where-Object {$dateFilterExt -Contains $_})
        {
            Trace-Progress "$functionName allowedFileExtentions $allowedFileExtensions - dateFilterExt = $dateFilterExt"
            $items1 = @()
            try
            {
                $childItems = Get-ChildItem -Path $Path -Include $dateFilterExt -Recurse -Force -ErrorAction stop
                Trace-Progress "$functionName childItems = $($childItems.count) , childItems = $($childItems -join ',')"
            }
            catch
            {
                Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, Powershell Exception: $_" -Error
            }
            $directories = $childItems | Group-Object Directory
            Trace-Progress "$functionName : Obtained $($directories.count) directories"

            foreach ($directory in $directories)
            {
                Trace-Progress "$functionName : Processing direcotry = $directory "
                $files = @($directory.Group | Sort-Object CreationTime,Name)
                if ($files.Count -le 2)
                {
                    $items1 += $files
                }
                elseif (($FromDate -le $ToDate) -and ($ToDate -ge $files[0].CreationTime))
                {
                    # Start from the first file modified after FromDate
                    $filesModifiedAfterFromDate = $files | Where-Object {$_.LastWriteTime -ge $FromDate}
                    # If there is less then 3 files which were modified after from date, just get the last 3 files to help investigation
                    if ($filesModifiedAfterFromDate.Count -gt 2)
                    {
                        $fromFile = $filesModifiedAfterFromDate[0]
                    }
                    else
                    {
                        # Get last 3 log files if there is no log written in specific time range
                        $fromFile = $files[0 - [math]::min($files.Count, 3)]
                    }

                    # End at the first file modifed after the ToDate.
                    $filesModifiedAfterToDate = $files | Where-Object {$_.LastWriteTime -ge $ToDate}
                    if ($filesModifiedAfterToDate.Count -gt 0)
                    {
                        $toFile = $filesModifiedAfterToDate[0]
                    }
                    else
                    {
                        $toFile = $files[-1]
                    }

                    $fromIndex = [array]::IndexOf($files, $fromFile)
                    $toIndex = [array]::IndexOf($files, $toFile)
                    if($fromIndex -ne '-1' -and $toIndex -ne '-1' -and $fromIndex -le $toIndex)
                    {
                        $items1 += $files[$fromIndex..$toIndex]
                    }
                }
            }

            <#
            # by disabling this, all the files will be in item1
            [System.Array]$tmp = @($items1 | ForEach-Object {$r=@()} {$t=$_; $skipCompressionFileExtensions | ForEach-Object {if ($t -like $_){$r+=$t}}} {$r})
            $filesToSkipCompression = $tmp
             
            Trace-Progress "$functionName : Zipping skipped for files with dateFilterExt are : $tmp"
            $items1 = $items1 | Where-Object { $_ -NotIn $filesToSkipCompression }
            #>

        }
    }catch {
        Trace-Progress "$functionName : Failed while parsing for bin files $_" -Error
        Trace-Progress -Message "$functionName : StackTrace : $($PSItem.ScriptStackTrace)" -Error
    }
    # Rest of files, ex.("*.etl","*.txt","*.log", ..etc) are filtered based on creation/modification date range, except reserved folders/files.
    # Adding try catch block because powershell throws .net terminating exception which is not ignored by powershell with “ErrorAction SilentlyContinue”
    try
    {
        $ext = $allowedFileExtensions | Where-Object { $_ -notin $dateFilterExt}

        $items2 = @()

        # Handles possible arrays of files/folders
        $pathItemResult = Get-Item $Path
        foreach ($pathItem in $pathItemResult)
        {
            if($pathItem -is [System.IO.DirectoryInfo])
            {
                # Get items recursively for folders
                $items2 += Get-ChildItem -Path $pathItem -Include $ext -Exclude $excludedFiles -Recurse -Force -ErrorVariable Item2Errors -ErrorAction Continue
            }
            elseif ($pathItem -is [System.IO.FileInfo])
            {
                # Get items non recursively for files
                $items2 += Get-ChildItem -Path $pathItem -Include $ext -Exclude $excludedFiles -Force -ErrorVariable Item2Errors -ErrorAction Continue
            }
            else
            {
                Trace-Progress "$functionName : Failed to handle '$pathItem' item type: $($pathItem.GetType().FullName)"
            }
        }
    }
    catch [UnauthorizedAccessException]
    {
        Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, .Net Exception: $_" -Error

        # This is a temporary workaround to handle the issue in Bug 4780610, where accessing (by Get-ChildItem above) some of the .blg files copied to our SF clusters'
        # diagnostic shares result in an AccessDenied error. Since we already have the unfiltered list of items, as a fallback, we will perform the filtering directly
        # against that list instead of relying on Get-ChildItem.
        $items2 = Get-ItemsByExtension -UnfilteredItems $unfilteredItems -Include $ext -Exclude $excludedFiles -ErrorAction SilentlyContinue
    }
    catch
    {
        Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, Powershell Exception: $_" -Error
    }
    Trace-Progress -Message "$functionName : item2 count = $($items2.count)"
    $items2 = $items2 | Where-Object {((($_.CreationTime -ge $FromDate) -or ($_.LastWriteTime -ge $FromDate)) -and $_.CreationTime -le $ToDate)}

    Trace-Progress -Message "FromDate $($FromDate.ToString()) ToDate = $($ToDate.ToString()) " 

    # since we use -ErrorAction continue, most errors will not hit the catch block. See if there were any errors in getting $items2
    # Note: Found a bug where Get-ChildItem did not finish getting items if a file is not found (likely because it was pruned or zipped). Solution is to use
    # -ErrorAction Continue instead of -ErrorAction Stop.
    foreach ($err in $Item2Errors)
    {
        $errorMessage = $err.Exception.Message
        if ($errorMessage -like "Could not find item *")
        {
            $fileNotFound = $errorMessage.Split()[-1].Trim('.')
            # if file not found is .etl or .blg, check if it was zipped
            if (($fileNotFound.endswith(".etl")) -or ($fileNotFound.endswith(".blg")))
            {
                $extension = $fileNotFound.Substring($fileNotFound.length - 3)
                $zippedFileName = $fileNotFound + ".zip"
                $srcFile = $null
                try {
                    # This is the new zip file that needs to be copied in lieu of original etl or blg
                    $srcFile = Get-Item -Path $zippedFileName -ErrorAction Stop
                }
                catch
                {
                    Trace-Progress -Message "$functionName : Failed to Fetch the zip file in the abscence of $($extension) [$zippedFileName]" -Error
                }
                if ($srcFile -ne $null)
                {
                    $items2 += $srcFile
                    Trace-Progress -Message "$functionName : Successfully added [$zippedFileName] to list of items to copy"
                }
            }
            else
            {
                if (($fileNotFound).EndsWith('.zip'))
                {
                    # assume the file was pruned, so make it a warning
                    Trace-Progress -Message "$functionName : $errorMessage" -warning
                }
                else
                {
                    Trace-Progress -Message "$functionName : $errorMessage" -Error
                }
            }
        }
        else
        {
            Trace-Progress -Message "$functionName : $errorMessage" -Error
        }
    }
     
    # Contents of reserved paths are always copied.
    # NOTE:
    # using $path.contains looks for substring which can be faulty, To match the full folder name should use EndsWith

    # e.g. of faulty comparision is when SDN matches folders 'SDN' and 'SDNDiagnostics' cause all files from both folders to be picked up.

    $items3 = @()
    $pathInReservedPaths = $false
    foreach ($reservedPath in $reservedPaths)
    {
        if ($path.ToLower().Trim("\").EndsWith($reservedPath.ToLower().Trim("\")))
        {
            $pathInReservedPaths = $true
            break
        }
    }
    if ($pathInReservedPaths)
    {
       Trace-Progress "Path $path is in reserved paths. Getting all files at this path, regardless of log collection time range."
       $items3 = Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { !$_.PSIsContainer }
    }

    # Reserved files are always copied.
    $items4 = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue | Where-Object {$_.Name -in $reservedFiles}

<#
    $items5 = @()
    if (($reservedPattern | ForEach-Object {$Path.Contains($_)}) -contains $true)
    {
        $items5 = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue
    }
#>

    # [System.Array]$tmp1 = @(@($items1) + @($items2) | ForEach-Object {$r=@()} {$t=$_; $skipCompressionFileExtensions | ForEach-Object {if ($t -like $_){$r+=$t}}} {$r})
    # $filesToSkipCompression += $tmp1
    
    Trace-Progress "$functionName : adding items1.count = $($items1.count) and items2.count = $($items2.count) and items3.count = $($items3.count) after applying time filter"
    $filteredItems = @($items1) + @($items2) + @($items3) + @($items4) | Sort-Object -Property FullName -Unique

    Trace-Progress "$functionName : Returning unfilteredItems ($($unfilteredItems.count)), filteredItems ($($filteredItems.count)), filesToSkipCompression ($($filesToSkipCompression.count))"
    return @{
        unfilteredItems = @($unfilteredItems)
        filteredItems = @($filteredItems)
        filesToSkipCompression = @($filesToSkipCompression)
    }
}

function Get-TimestampsHelper
{
    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true)]
    [AllowEmptyCollection()]
    [System.IO.FileSystemInfo[]]
    $Items
    )

    if ($Items.count -eq 0) { return @{} }

    $oldestCreationTime = $Items[0].CreationTimeUtc
    $latestCreationTime = $Items[0].CreationTimeUtc
    $oldestLastWriteTime = $Items[0].LastWriteTimeUtc
    $latestLastWriteTime = $Items[0].LastWriteTimeUtc

    foreach ($item in $Items)
    {
        if ($null -ne $item.CreationTimeUtc)
        {
            if ($null -eq $oldestCreationTime -or $oldestCreationTime -gt $item.CreationTimeUtc) { $oldestCreationTime = $item.CreationTimeUtc }
            if ($null -eq $latestCreationTime -or $latestCreationTime -lt $item.CreationTimeUtc) { $latestCreationTime = $item.CreationTimeUtc }
        }
        if ($null -ne $item.LastWriteTimeUtc)
        {
            if ($null -eq $oldestLastWriteTime -or $oldestLastWriteTime -gt $item.LastWriteTimeUtc) { $oldestLastWriteTime = $item.LastWriteTimeUtc }
            if ($null -eq $latestLastWriteTime -or $latestLastWriteTime -lt $item.LastWriteTimeUtc) { $latestLastWriteTime = $item.LastWriteTimeUtc }
        }
    }

    return @{
        oldestCreationTime = $oldestCreationTime
        latestCreationTime = $latestCreationTime
        oldestLastWriteTime = $oldestLastWriteTime
        latestLastWriteTime = $latestLastWriteTime
    }
}

function Get-Timestamps
{
    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true)]
    [AllowNull()]
    [AllowEmptyCollection()]
    [System.IO.FileSystemInfo[]]
    $all,
    [Parameter(Mandatory=$true)]
    [AllowNull()]
    [AllowEmptyCollection()]
    [System.IO.FileSystemInfo[]]
    $copied
    )

    $newDetails = @{}
    if ($all.count)
    {
        $allTimestamps = Get-TimestampsHelper $all
        $newDetails.all = $allTimestamps
        $newDetails.all.count = $all.count
    }

    if ($copied.count)
    {
        $copiedTimestamps = Get-TimestampsHelper $copied
        $newDetails.copied = $copiedTimestamps
        $newDetails.copied.count = $copied.count
    }

    return $newDetails
}

#
# Copy filtered files recursively by re-creating the folder structure at the destination to match the source.
#
function Copy-FilteredChildItem
{
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [Object[]]
        $Items,

        [Parameter(Mandatory=$true)]
        [string]
        $Source,

        [Parameter(Mandatory=$true)]
        [string]
        $ChildFolder,

        [Parameter(Mandatory=$true)]
        [string]
        $DestPathWithRoleName,

        [Parameter(Mandatory=$false)]
        [string]
        $ComputerName
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    # Handle paths with wildcard(s); set Source to deepest non-wildcard parent that resolves to a full directory.
    if ($Source -ne $env:SystemDrive)
    {
        $sourceItem = Get-Item $Source
        while ($sourceItem -isnot [System.IO.DirectoryInfo])
        {
            $Source = Split-Path $Source -Parent
            $sourceItem = Get-Item $Source
        }
    }
    else
    {
        # handle exception when path is $env:systemdrive, in that case get-item $source returns current path not c:/
        $Source += "\"
        $sourceItem = Get-Item $Source
    }

    # Catches cases where string path may not match the resolved file path, e.g.
    # 'C:\Users\ADMINI~1\AppData' (string) vs. C:\Users\Administrator (${item}.FullName)
    # Also resolves wildcard paths into valid expanded paths.
    $Source = $sourceItem.FullName.TrimEnd('\')

    Trace-Progress "$functionName DestPathWithRoleName = $DestPathWithRoleName"
    foreach ($item in $Items)
    {
        $Destination = Join-Path -Path $DestPathWithRoleName -ChildPath $ChildFolder

        $itemDir = $item.DirectoryName
        $itemName = $item.FullName

        if (($null -eq $itemDir) -or ($null -eq $itemName))
        {
            Trace-Progress "$functionName : Null directory or fullname found. Item $item, Directory $itemDir, ItemName $itemName" -Warning

            # Skip processing this item
            continue
        }

        $dir = $itemDir.Replace($Source, $Destination)
        $target = $itemName.Replace($Source, $Destination)

        if (!(Test-Path $dir -ErrorAction Continue))
        {
            Trace-Progress "$functionName Creating new directory: $dir"
            $null = New-Item $dir -Type Directory
        }

        if ($ComputerName)
        {
            if ($item.Extension -in @('.bin','.etl'))
            {
                $parent = Split-Path $target -Parent
                $leaf = Split-Path $target -Leaf
                $target = "$($parent)\$($computerName)_$($leaf)"
            }
        }

        if (!(Test-Path $target -ErrorAction Continue))
        {
            try
            {
                Trace-Progress -Message "$functionName : Copying item $($item.FullName) to [$target]"
                Copy-Item -Path $item.FullName -Destination $target -Force -ErrorAction Stop
            }
            catch [System.Management.Automation.ItemNotFoundException], [System.IO.FileNotFoundException]
            {
                $errorHResult =  "0x$('{0:x8}' -f $_.Exception.HResult)"

                # Prepare the error message but dont trace immediately
                $actualErrorMessage = "$functionName : Failed to copy [$($item.FullName)] to $target. HResult : $errorHResult. Error: $_"

                # Does the file that failed to copy end with .etl or .blg? if yes, maybe it just got converted to .zip, so attempt to copy zip instead.
                # If copy of that fails too, then trace the original error - $actualErrorMessage
                if(($item.FullName).EndsWith('.etl') -or ($item.FullName).EndsWith('.blg'))
                {
                    # This is best case attempt when etl or blg file just got converted to zip file.
                    $extension = ($item | select Extension).Extension
                    $zippedFileName = $item.FullName + ".zip"
                    $target = $target + ".zip"
                    $srcFile = $null
                    try {
                        # This is the new zip file that needs to be copied in lieu of original etl or blg
                        $srcFile = Get-Item -Path $zippedFileName -ErrorAction Stop
                    }
                    catch
                    {
                        Trace-Progress -Message $actualErrorMessage -Error
                        Trace-Progress -Message "Failed to Fetch the zip file in the abscence of $($extension) [$zippedFileName]"
                    }
                    if($srcFile)
                    {
                        try {
                            Copy-Item -Path $zippedFileName -Destination $target -Force -ErrorAction Ignore
                            Trace-Progress -Message "$functionName : attempting to copy ZIP file instead of $($extension) file succeeded [$zippedFileName] to [$target]"
                        }
                        catch
                        {
                            # this is not the original error, we found matching zip file and copying of that failed
                            # this is a best case effort, if this fails trace original error.
                            Trace-Progress -Message "$functionName : copying ZIP file instead of $($extension) file failed as well [$zippedFileName] to [$target]"

                            # also add the original error into the error list.
                            Trace-Progress -Message $actualErrorMessage -Error
                        }
                    }
                } else {
                    # the file that failed to copy is not an etl or blg, so we dont have an alternative to that.
                    if (($item.FullName).EndsWith('.zip'))
                    {
                        # assume the file was pruned, so make it a warning
                        Trace-Progress -Message $actualErrorMessage -warning
                    }
                    else
                    {
                        Trace-Progress -Message $actualErrorMessage -Error
                    }
                }
            }
            catch
            {
                $errorHResult =  "0x$('{0:x8}' -f $_.Exception.HResult)"
                Trace-Progress -Message "$functionName : Failed to copy $($item.FullName) to $target. HResult : $errorHResult. Error: $_" -Error

                # On failure, display the size of the directories in system drive
                if($target.StartsWith($env:systemdrive[0]))
                {
                    $sysDrive = Get-PSDrive $env:systemdrive[0]
                    Trace-Progress -Message "SystemDrive = $($sysDrive.Name), UsedSpace = $($($sysDrive.Used)/1GB), FreeSpace = $($($sysDrive.Free)/1GB) "
                }
                #if we are copying to a user specified destination folder, HRESULT will have error incase of diskfull, no need to print folder sizes
            }
        }
    }
    Trace-Progress "$functionName Complete.."
}

function Extract-CompressedFiles
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $DestPathWithRoleName,

        [Parameter(Mandatory=$false)]
        [string]
        $ChildFolder
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    Trace-Progress "$functionName : DestPathWithRoleName = $DestPathWithRoleName ChildFolder = $ChildFolder"
    $searchFolder = Join-Path -Path $DestPathWithRoleName -ChildPath $ChildFolder
    
    $count = 0    
    do
    {
        $zipFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "zip"
        $cabFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "cab"
        $tarFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "tar"
        
        $count += 1
        foreach ($zipFile in $zipFiles)
        {
            try
            {
                Extract-ZipFile -ZipFile $zipFile
            }
            catch
            {
                Trace-Progress "$functionName : ZIP file $($zipFile.FullName) exception during processing: $($_.Exception.ToString())"
                Trace-Progress "$functionName : ZIP file $($zipFile.FullName) exception during processing: $($_.Exception.Message)" -Error
            }
        }

        foreach ($cabFile in $cabFiles)
        {
            try
            {
                Extract-CabFile -CabFile $cabFile
            }
            catch
            {
                Trace-Progress "$functionName CAB file $($cabFile.FullName) exception during processing: $($_.Exception.ToString())"
                Trace-Progress "$functionName CAB file $($cabFile.FullName) exception during processing: $($_.Exception.Message)" -Error
            }
        }

        foreach ($tarFile in $tarFiles)
        {
            try
            {
                Extract-TarFile -TarFile $tarFile
            }
            catch
            {
                Trace-Progress "$functionName TAR file $($tarFile.FullName) exception during processing: $($_.Exception.ToString())"
                Trace-Progress "$functionName TAR file $($tarFile.FullName) exception during processing: $($_.Exception.Message)" -Error
            }
        }
    } 
    # Break after 10 round of while loop, since if compressed file cannot be deleted for some reason, loop would go on forever.
    while (($zipFiles.Count -gt 0 -or $cabFiles.Count -gt 0 -or $tarFiles.Count -gt 0) -and $count -lt 10)

    Trace-Progress "$functionName Complete.."
}

#
# Search for zip files in the input directory and extract the content in to <filename>_zip directory and delete the zip files.
#
function Extract-ZipFile
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [System.IO.FileInfo]
        $ZipFile
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    Trace-Progress "$functionName : Processing ZIP file $($zipFile.FullName)"
    $zipDirectoryPath = Join-Path -Path $zipFile.Directory -ChildPath ($zipFile.BaseName+"_ZIP")

    Trace-Progress "$functionName : Going to create extract folder $zipDirectoryPath for ZIP file $($zipFile.FullName)"
    Expand-Archive -LiteralPath $zipFile.FullName -DestinationPath $zipDirectoryPath

    $ZipFileOpened = [System.IO.Compression.ZipFile]::Open($zipFile.FullName,[System.IO.Compression.ZipArchiveMode]::Read)
    $internalFile = $ZipFileOpened.Entries | Where-Object {-not [string]::IsNullOrEmpty($_.Name)}
    $internalFileCount = $internalFile.Count
    $ZipFileOpened.Dispose()

    $extractedFiles = Get-ChildItem -Path $zipDirectoryPath -Filter "*.*" -File -Recurse
    Trace-Progress "$functionName : ZIP file $($zipFile.FullName) Internal File count $internalFileCount extracted file count $($extractedFiles.Count)"

    Remove-Item $zipFile.FullName -Force -Recurse  
}

#
# Search for cab files in the input directory and extract the content in to <filename>_cab directory and delete the cab files.
#
function Extract-CabFile
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [System.IO.FileInfo]
        $CabFile
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    Trace-Progress "$functionName Processing CAB file $($cabFile.FullName)"

    Add-Type -Path "$PSScriptRoot\..\Microsoft.Deployment.Compression.Cab.dll" -ErrorAction Ignore -Verbose:$false | Out-Null
    $cabObject = New-Object -TypeName "Microsoft.Deployment.Compression.Cab.CabInfo" -ArgumentList $cabFile.FullName
    $cabDirectoryPath = Join-Path -Path $cabFile.Directory -ChildPath ($cabFile.BaseName+"_CAB")

    Trace-Progress "$functionName Going to create extract folder $cabDirectoryPath for CAB file $($cabFile.FullName)"
    $temp = New-Item -Path $cabDirectoryPath -ItemType Directory

    $cabObject.Unpack($cabDirectoryPath)
    $internalFileCount = $cabObject.GetFiles().Count
    $extractedFiles = Get-ChildItem -Path $cabDirectoryPath -Filter "*.*" -File -Recurse
    Trace-Progress "$functionName CAB file $($cabFile.FullName) Internal File count $internalFileCount extracted file count $($extractedFiles.Count)"

    $cabObject.Delete()
}

#
# Search for tar files in the input directory and extract the content in to <filename>_tar directory and delete the tar files.
#
function Extract-TarFile
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [System.IO.FileInfo]
        $TarFile
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    Trace-Progress "$functionName Processing TAR file $($tarFile.FullName)"

    $tarFileBaseName = $tarFile.BaseName.Replace(".", "")
    $tarDirectoryPath = Join-Path -Path $tarFile.Directory -ChildPath ($tarFileBaseName+"_TAR")

    Trace-Progress "$functionName Going to create extract folder $tarDirectoryPath for TAR file $($tarFile.FullName)"
    $temp = New-Item -Path $tarDirectoryPath -ItemType Directory

    tar.exe -xf $tarFile.FullName --directory $tarDirectoryPath
    $internalFileCount = (tar tvf $tarFile.FullName).Count
    $extractedFiles = Get-ChildItem -Path $tarDirectoryPath -Recurse
    Trace-Progress "$functionName TAR file $($tarFile.FullName) Internal File count $internalFileCount (includes directories). Extracted file count $($extractedFiles.Count). (Includes directories)"

    $tarFile.Delete()
}

function Get-CompressedFiles
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $SearchFolder,

        [Parameter(Mandatory=$true)]
        [string]
        $CompressionType
    )
    
    $functionName = $($MyInvocation.MyCommand.Name)

    Trace-Progress "$functionName Going to search $CompressionType files under searchFolder = $searchFolder"
    $filter = "*." + $CompressionType
    if ($CompressionType -ieq "tar")
    {
        $filter += "*"
    }
    
    $compressedFiles = Get-ChildItem -Path $searchFolder -Filter $filter -File -Recurse -ErrorAction Ignore
    if ($CompressionType -ieq "zip")
    {
        $compressedFiles = $compressedFiles | Where-Object {-not $_.FullName.EndsWith("etl.zip")}
    }
    Trace-Progress "$functionName $CompressionType files count $($compressedFiles.Count) under searchFolder = $searchFolder"
    return $compressedFiles
}

#
# Creates a PowerShell Session if needed.
#
function Initialize-PSSession
{
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$false)]
        [HashTable]
        [ValidateNotNull()]
        $ComputerPSSessions,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerFqdn,

        [Parameter(Mandatory=$false)]
        [REF]$ExcludedEndpoints
    )

    $functionName = $($MyInvocation.MyCommand.Name)

    if ($ComputerPSSessions)
    {
        if ($ComputerPSSessions.ContainsKey($ComputerFqdn))
        {
            $session = $ComputerPSSessions[$ComputerFqdn]
            if (($null -ne $session) -and ($session.State -ne "Opened"))
            {
                Trace-Progress "$functionName : The session for $ComputerFqdn went into $($session.state) state! Reinitializing!"
            }
        }
    }

    if ($null -eq $session -or $session.State -ne "Opened")
    {
        # Client call for new PS session can hang forever when server side WSMan layer is not responding.To unblock log collection,
        # we are testing the PS session creation in different thread using start-job if the monitoring job doesn’t return the PS session object in 2 min we declare the server to be in a bad state.
        $scriptBlock = [ScriptBlock]::Create(${function:Test-PSSession})
        $psSessionObject = Invoke-ScriptBlockCommand -ScriptBlock $scriptBlock -ArgumentList $ComputerFqdn -TimeOutInSec 120

        # Validate if the server is connectable
        if (($null -eq $psSessionObject -or $psSessionObject.State -ne 'Opened') -or (!(Test-Connection -ComputerName $ComputerFqdn -Quiet)))
        {
            Trace-Progress -Message "$functionName : Computer $ComputerFqdn is unreachable, Could not establish a PS session earlier. Will not retry" -Error
            $ExcludedEndpoints.Value += $ComputerFqdn
            return $null
        }

        <# New-PSSessionOption paramter:
                .IdleTimeout : Determines how long the session stays open if the computer does not receive any communication. This includes the heartbeat signal
                               {It means if no operation is happening, session will be open as long as session connection is established and it will help us from create PS session timeout}
                .OperationTimeout - Determines the maximum time that any operation in the session can run.
                                {This prevent very large file like +25GB copy operation and help us from diskspace issue}
                .MaxConnectionRetryCount :Specifies the number of times that PowerShell attempts to make a connection to a target machine if the current attempt fails due to network issues.
        #>

        $sessionOptions = New-PSSessionOption -OperationTimeout ([timespan]"00:10:00").TotalMilliseconds -MaxConnectionRetryCount 1 -IdleTimeout 600000
        $session = New-PSSession -ComputerName $ComputerFqdn -SessionOption $sessionOptions -ErrorAction Continue
        if ($null -eq $session)
        {
            $ExcludedEndpoints.Value += $ComputerFqdn
            Trace-Progress -Message "$functionName : Could not establish a PS session with the computer $ComputerFqdn." -error
        }
    }

    return $session
}

<#
.SYNOPSIS
    This is the generic function to execute the command or function as script block in separate powershell thread using start-job and return the job output object.
#>

function Invoke-ScriptBlockCommand
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ScriptBlock]$ScriptBlock,

        [Parameter(Mandatory=$false)]
        $ArgumentList,

        [Parameter(Mandatory=$false)]
        [int]$TimeOutInSec = 120
    )

    $functionName = $($MyInvocation.MyCommand.Name)
    $jobOutput = $null

    # Start and get the monitoring job result
    try
    {
        $monitoringJob = Start-Job -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -Verbose
        $jobOutput = $monitoringJob | Wait-Job -Timeout $TimeOutInSec | Receive-Job
        $monitoringJob | Stop-Job
        $monitoringJob | Remove-Job
    }
    catch
    {
        Trace-Progress "$functionName : ScriptBlock - $ScriptBlock, failed with an error: $_ " -Error
    }

    return $jobOutput
}

function Test-PSSession
{
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$false)]
        [string]
        $ComputerFqdn,

        [Parameter(Mandatory=$false)]
        [PSCredential]
        $LocalAdminCredential
    )

    if($LocalAdminCredential)
    {
        $session = New-PSSession -ComputerName $ComputerFqdn -Credential $LocalAdminCredential -ErrorAction Continue
    }
    else
    {
        $session = New-PSSession -ComputerName $ComputerFqdn -ErrorAction Continue
    }

    return $session
}

<#
.SYNOPSIS
    Return items that match the provided filter conditions for file extensions to include/exclude.
#>

function Get-ItemsByExtension
{
    [CmdletBinding()]
    param(

        [Parameter(Mandatory=$true)]
        [Object[]]
        $UnfilteredItems,

        [Parameter(Mandatory=$true)]
        [string[]]
        $Include,

        [Parameter(Mandatory=$true)]
        [string[]]
        $Exclude
    )

    $filteredItems = New-Object System.Collections.Generic.List[System.Object]
    foreach ($unfilteredItem in $UnfilteredItems)
    {
        $excludedItem = $false
        foreach ($extensionToExclude in $Exclude)
        {
            if ($unfilteredItem -like $extensionToExclude)
            {
                $excludedItem = $true
            }
        }

        if (-not $excludedItem)
        {
            foreach ($extensionToInclude in $Include)
            {
                if ($unfilteredItem -like $extensionToInclude)
                {
                    $filteredItems.Add($unfilteredItem)
                }
            }
        }
    }

    return $filteredItems
}

function Get-ContainerStateLog
{
    param
    (
        [parameter(Mandatory=$false)]
        [DateTime]
        $FilesFromDate = (Get-Date).AddHours(-1),

        [parameter(Mandatory=$false)]
        [DateTime]
        $FilesToDate = (Get-Date),

        [parameter(Mandatory=$true)]
        [string]
        $Role,

        [parameter(Mandatory=$true)]
        [string]
        $DestPathWithRoleName
    )

    $containerStateLogDirPath = Join-Path -Path $DestPathWithRoleName -ChildPath "ContainerStateLogs"
    $containerStateErrorLogPath = Join-Path -Path $containerStateLogDirPath -ChildPath "ContainerStateCollectionErrors.txt"

    Trace-Progress -Message "Start container state log collection of $Role to $containerStateLogDirPath."

    New-Item $containerStateLogDirPath -ItemType Directory -Force | Out-Null

    # Collect HCS state
    $hcsStateLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HcsState.txt"
    Invoke-ExpressionWithTracing -Expression "hcsdiag list" -TraceFilePath $hcsStateLogFilePath

    # Collect HNS state
    $hnsNetworksLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_Networks.txt"
    $hnsEndpointsLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_Endpoints.txt"
    $hnsPolicyListLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_PolicyList.txt"
    
    $hnsNetworksCommands = @(
        "Get-HnsNetwork | select Name, Type, ActivityId, ID, @{Name='Subnets'; Expression={ `$_.Subnets | select AddressPrefix, GatewayAddress, ID }} | Out-String",
        "Get-HnsNetwork | ForEach-Object { Get-HnsNetwork -Id `$_.ID -Detailed } | ConvertTo-Json -Depth 20"
    )

    $hnsEndpointsCommands = @(
        "Get-HnsEndpoint | select ActivityId, ID, IpAddress, MacAddress, State | Format-Table | Out-String",
        "Get-HnsEndpoint | ConvertTo-Json -Depth 20"
    )

    foreach ($hnsNetworksCommand in $hnsNetworksCommands)
    {
        Invoke-ExpressionWithTracing -Expression $hnsNetworksCommand -TraceFilePath $hnsNetworksLogFilePath
    }

    foreach ($hnsEndpointsCommand in $hnsEndpointsCommands)
    {
        Invoke-ExpressionWithTracing -Expression $hnsEndpointsCommand -TraceFilePath $hnsEndpointsLogFilePath
    }

    Invoke-ExpressionWithTracing -Expression "Get-HnsPolicyList | ConvertTo-Json -Depth 20" -TraceFilePath $hnsPolicyListLogFilePath

    # Collect Docker engine state
    $dockerStateEngineLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "DockerState-Engine.txt"
    $dockerEngineStateCommands = @(
        "docker version",
        "docker info",
        "docker ps -sa",
        "docker images",
        "docker volume ls",
        "docker system df -v",
        "docker network ls"
    )

    foreach ($dockerEngineStateCommand in $dockerEngineStateCommands)
    {
        Invoke-ExpressionWithTracing -Expression $dockerEngineStateCommand -TraceFilePath $dockerStateEngineLogFilePath
    }

    $networkIds = docker network ls -q
    foreach ($networkId in $networkIds)
    {
        Invoke-ExpressionWithTracing -Expression "docker inspect $networkId" -TraceFilePath $dockerStateEngineLogFilePath
    }

    # Collect container specific diagnostics
    $allContainerIds = docker ps -aq
    [System.Collections.Generic.HashSet[string]]$runningContainerIds = docker ps -q

    # List of SF environment variables to include in the output. Other SF environment variable names starting with "Fabric" will be redacted.
    $sfEnvironmentVariablesToInclude = [System.Collections.Generic.HashSet[string]]@(
        "Fabric_ApplicationHostId",
        "Fabric_ApplicationHostType",
        "Fabric_ApplicationId",
        "Fabric_ApplicationName",
        "Fabric_CodePackageName",
        "Fabric_Endpoint_InstanceEndpoint",
        "Fabric_Endpoint_IPOrFQDN_InstanceEndpoint",
        "Fabric_Folder_App_Log",
        "Fabric_Folder_App_Temp",
        "Fabric_Folder_App_Work",
        "Fabric_Folder_Application",
        "Fabric_Folder_Application_OnHost",
        "Fabric_IsContainerHost",
        "Fabric_NodeId",
        "Fabric_NodeIPOrFQDN"
        "Fabric_NodeName"
        "Fabric_PartitionId",
        "Fabric_ServiceName",
        "Fabric_ServicePackageActivationId",
        "Fabric_ServicePackageName",
        "Fabric_ServicePackageVersionInstance",
        "Fabric_ContainerName",
        "FabricCodePath",
        "FabricLogRoot"
    )

    foreach ($containerId in $allContainerIds)
    {
        try
        {
            $dockerInspectOutput = docker inspect $containerId | ConvertFrom-Json

            for ($i = 0; $i -lt $dockerInspectOutput.Config.Env.Count; $i++)
            {
                $envVariablePair = $dockerInspectOutput.Config.Env[$i] -split '=', 2
                if ($envVariablePair.Length -eq 2)
                {
                    $envVariableName = $envVariablePair[0]

                    if ($envVariableName -ieq "AZS_DEPLOYMENT_APPLICATION_NAME")
                    {
                        $applicationName = $envVariablePair[1] -replace "/", "+"
                    }
                    elseif ($envVariableName -ieq "AZS_DEPLOYMENT_SERVICE_NAME")
                    {
                        $serviceName = $envVariablePair[1]
                    }

                    if ($envVariableName.StartsWith("Fabric") -and (-not $sfEnvironmentVariablesToInclude.Contains($envVariableName)))
                    {
                        $redactedEnvVariable = "$envVariableName=[redacted]"
                        $dockerInspectOutput.Config.Env[$i] = $redactedEnvVariable
                    }
                }
                else
                {
                    # Unable to parse environment variable string, so will redact it completely to be safe (i.e., by avoiding leaking sensitive information).
                    $dockerInspectOutput.Config.Env[$i] = "[redacted]"
                }
            }

            $containerStateLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "DockerState-${applicationName}_${serviceName}_${containerId}.txt"
            Add-Content $containerStateLogFilePath "docker inspect $containerId"
            Add-Content $containerStateLogFilePath $($dockerInspectOutput | ConvertTo-Json -Depth 10)
        }
        catch
        {
            Add-Content $containerStateErrorLogPath "Error while collecting docker inspect output of $containerId. ExceptionMessage: $($_.Exception.Message), ExceptionType: $($_.Exception.GetType().Name)"
        }

        # Collect running container specific diagnostics.
        if ($runningContainerIds.Contains($containerId))
        {
            $containerStateCommands = @(
                "docker top $containerId",
                "docker stats $containerId --no-stream"
            )

            foreach ($containerStateCommand in $containerStateCommands)
            {
                Invoke-ExpressionWithTracing -Expression $containerStateCommand -TraceFilePath $containerStateLogFilePath
            }
        }
    }

    Trace-Progress -Message "Finished container state log collection."
} 

function Invoke-ExpressionWithTracing
{   
    param
    (
        [parameter(Mandatory=$true)]
        [string] 
        $Expression, 

        [parameter(Mandatory=$true)]
        [string] 
        $TraceFilePath
    )

    try
    {
        Add-Content $TraceFilePath $Expression
        Invoke-Expression $Expression *>&1 | Add-Content -Path $TraceFilePath
        Add-Content $TraceFilePath "`n"
    }
    catch
    {
        Add-Content $containerStateErrorLogPath "Error executing '$Expression'. ExceptionMessage: $($_.Exception.Message), ExceptionType: $($_.Exception.GetType().Name)" 
    }
}

function Get-ServiceFabricLog
{
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$false)] 
        [string] 
        $LogPath = "$env:SystemDrive\ServiceFabricLogs",

        [parameter(Mandatory=$false)] 
        [DateTime] 
        $FromDate = (Get-Date).AddHours(-4),

        [parameter(Mandatory=$false)] 
        [DateTime] 
        $ToDate = (Get-Date)
    )

    $packagePath = Join-Path $env:SystemDrive -ChildPath "ServiceFabric\Tools\Microsoft.Azure.ServiceFabric.WindowsServer.SupportPackage.zip"
    # Expand SF Support Package.
    $toolsDir = Join-Path $env:SystemDrive -ChildPath "ServiceFabric\Tools"
    $collectorPath = Join-Path $toolsDir "StandaloneLogCollector.exe"

    if (-not (Test-Path($collectorPath)))
    {
        Trace-Progress "$functionName : Unzipping tool to:$collectorPath"
        Expand-Archive $packagePath -DestinationPath $toolsDir -Force
    }

    $timeStamp = $((Get-Date).ToString('yyyyMMddHHmmss'))
    $outputPath = "$env:SystemDrive\MASLogs\StandaloneLogCollector_StdOut_$timeStamp.txt"

    # Perform the log directory cleanup only in default path case to prevent security vulnerability
    if (Test-Path $LogPath) {
        Trace-Progress "$functionName : Removing existing ServiceFabric logs in:$LogPath"
        $null = Remove-Item -Path $LogPath -Recurse -Force
    }

    #Bugfix: StandaloneLogCollector.exe Fails With "Unable to load DLL 'FabricClient.dll'"
    if ( $env:Path -notlike "*C:\Program Files\Microsoft Service Fabric\bin\fabric\fabric.code*")
    {
        $env:Path = $env:Path + ";C:\Program Files\Microsoft Service Fabric\bin\fabric\fabric.code"
    }
        
    . $collectorPath -Output $LogPath -Mode Collect -StartUtcTime $FromDate.ToUniversalTime() -EndUtcTime $ToDate.ToUniversalTime() -IncludeLeaseLogs > $outputPath 2>&1

    Copy-Item -Path $outputPath -Destination $LogPath\ -Force -ErrorAction Continue

    $miscellaneousLogsZipPath =  Join-Path $LogPath "miscellaneousLogs.zip"
    $miscellaneousLogsPath =  Join-Path $LogPath "miscellaneousLogs" 
    Trace-Progress "$functionName : Unzipping $miscellaneousLogsZipPath"
    Expand-Archive -Path $miscellaneousLogsZipPath -DestinationPath $miscellaneousLogsPath -Force

    Trace-Progress "$functionName : Removing $miscellaneousLogsZipPath after unzipping"
    $null = Remove-Item -Path $miscellaneousLogsZipPath -Force

    # Clean up the duplicate EventLogs folder as it separately collected along with Event_Application, Event_System
    $miscellaneousEventLogsPath =  Join-Path $miscellaneousLogsPath "EventLogs" 
    Trace-Progress "$functionName : Removing $miscellaneousEventLogsPath after unzipping"
    $null = Remove-Item -Path $miscellaneousEventLogsPath -Force  -Recurse  
}

Function Get-RoleLogs
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]
        $argumentsObject,

        [Parameter(Mandatory=$true)]
        [String]
        $role
    )

    $OutputPath = $argumentsObject.OutputPath

    $FromDateG = $argumentsObject.FromDateG
    $ToDateG = $argumentsObject.ToDateG
    $FromDate = $argumentsObject.FromDate
    $ToDate = $argumentsObject.ToDate
    $roles = $argumentsObject.roles
    $domain = $argumentsObject.domain
    $destPath = $argumentsObject.destPath
    $roleNames = $argumentsObject.roleNames
    $nodeNames = $argumentsObject.nodeNames
    $filterByNode = $argumentsObject.FilterByNode
    $vmRoleNames = $argumentsObject.vmRoleNames
    $FilterByLogType = $argumentsObject.FilterByLogType
    $allClusterInfo = $argumentsObject.allClusterInfo
    $localMode = $argumentsObject.localMode
    $isArcA = $argumentsObject.isArcAEnv
    $saveToPathSelected = $argumentsObject.SaveToPathSelected

    $functionName = "$($MyInvocation.MyCommand.Name)_$role"
    $perfRoleStartDate = Get-Date
    $roleLogDetails = @{"role" = $role; "StartDate" = $perfRoleStartDate}

    try
    {
        Write-Output "`r"
        Trace-Progress "$functionName : Collecting logs for role: $role"

        #Trace-InvokingProcessStats -Role ($role+"_Start")

        if (!$localMode)
        {
            $endpointPSSessions = @{}
            $ExcludedEndpoints = @()
        }
        
        # TODOTODO Override the nodes with node names passed
        #$nodes = $roles[$role].Nodes

        $nodes = @()
        $currentRoleData = $roles[$role]
        if ("PhysicalMachines" -in $currentRoleData.Nodes) {
            $nodes += $nodeNames
        }
        if ("AllVms" -in $currentRoleData.Nodes -and $vmRoleNames["AllVms"].count -gt 0) {
            $nodes += $vmRoleNames["AllVms"]
        } elseif ($role -in $currentRoleData.Nodes -and $vmRoleNames[$role].count -gt 0) {
            $nodes += $vmRoleNames[$role]
        }
        
        Trace-Progress "$functionName : Nodes to collect logs from for role [$role] = [$($nodes -join ', ')]. Note that this is before applying node filter."

        <#
        Thave above elseif should resolve to following code, if there are more specialized roles get added
        and their rolename is not same as the defined in Get-InfraVMNames() we need to remove the above elseif and
        update below cases.
 
        elseif ("NC" -in $currentRoleData.Nodes) {
            $nodes += $vmRoleNames["NC"]
        } elseif ("SLB" -in $currentRoleData.Nodes) {
            $nodes += $vmRoleNames["SLB"]
        } elseif ("GWY" -in $currentRoleData.Nodes) {
            $nodes += $vmRoleNames["GWY"]
        }#>


        $logsTobeCollected = (($currentRoleData.FileLog.count -gt 0) -or ($currentRoleData.CSVLog.count ) -or
         ($currentRoleData.WindowsEventLog.count ) -or ($null -eq $currentRoleData.ScriptExecution))

        # $rolePublicInfoLogs -- This is the xml node will <Logs></Logs>

        if ($logsTobeCollected)
        {
            $roleLogDetails.logsAvailable = $true
            Trace-Progress "$functionName : Destination path : $OutputPath"
            $destinationFolderPath = Join-Path -Path $OutputPath -ChildPath $role

            # Iterate over each end-point and collect logs
            if ($localMode)
            {
                $node = $env:ComputerName
                $endpoint = if ($null -eq $domain) { $node } else { "$node.$domain" }
                $endpoints = @($endpoint) 
            }
            else
            {
                $endpoints = @()
            }
            
            if ($filterByNode) 
            {
                Trace-Progress "$functionName : Node filter list = $($filterByNode -join ', ')"
                $nodes = $nodes | Where-Object { $_ -in $filterByNode}
                Trace-Progress "$functionName : Node list after applying node filter = $($nodes -join ', ')"
            } else {
                Trace-Progress "$functionName : No node filter specified"
            }

            if (!$localMode)
            {
                foreach ($node in $nodes)
                {
                    $session = $null
                    $endpoint = $node + ".$domain"
                    Trace-Progress "$functionName : Creating a PSSession to $endpoint"

                    if ($ExcludedEndpoints -contains $endpoint)
                    {
                        Trace-Progress -Message "$functionName : Could not establish a PS session earlier with the computer $endpoint. Will not retry." -Error
                    }
                    else
                    {
                        $session = Initialize-PSSession -ComputerPSSessions $endpointPSSessions -ComputerFqdn $endpoint -ExcludedEndpoints ([REF]$ExcludedEndpoints)
                        if ($null -ne $session)
                        {
                            $endpointPSSessions[$endpoint] = $session
                            $endpoints += $endpoint
                        }
                    }
                }
                Trace-Progress "$functionName : nodescount = [$($nodes.count)] endpointPSSessions count = [ $($endpointPSSessions.Count)], endpoints Count = [$($endpoints.Count)]"
            }
            
            if ($role -in "ServiceFabric")
            {
                if ($isArcA -and $localMode)
                {
                    # the sf log collector only supports max path length of 35, so we collect logs in temp short path dir and then move it.
                    $SFLogTempPath = "$env:SystemDrive\MASLogs\ServiceFabricLogs"
                    Trace-Progress -Message "$functionName : ServiceFabric logs will be temporarily output to:$SFLogTempPath"
                    Get-ServiceFabricLog -LogPath $SFLogTempPath -FromDate $FromDate -ToDate $ToDate
                    Trace-Progress -Message "$functionName : Move ServiceFabric logs from $SFLogTempPath to $OutputPath"
                    Move-Item $SFLogTempPath $OutputPath
                }
            }

            # $endpoints is an empty array if there are no endpoints, it is not null.
            if($endpoints -gt 0)
            {
                # Collecting Windows event logs
                if ($FilterByLogType -contains 'WindowsEvent')
                {
                    if ($currentRoleData.WindowsEventLog)
                    {
                        $logPattern = $currentRoleData.WindowsEventLog
                        Trace-Progress -Message "$functionName : Collecting windows event logs with log patterns: $($logPattern -join ', '), with date range: from $FromDateG until $ToDateG, from machines $($endpoints -join ', ')"
                        
                        try 
                        {
                            if ($localMode)
                            {
                                Get-WindowsEventLog -ComputerNames $endpoints -LogPattern $logPattern -EventsFromDate $FromDate -EventsToDate $ToDate -Roles $roles -CurrentRole $role `
                                    -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode
                            }
                            else
                            {
                                 Get-WindowsEventLog -ComputerNames $endpoints -ComputerPSSessions $endpointPSSessions -LogPattern $logPattern -EventsFromDate $FromDate -EventsToDate $ToDate `
                                    -ExcludedEndpoints ([REF]$ExcludedEndpoints) -Roles $roles -CurrentRole $role -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode
                            }
                            
                            Trace-Progress "$functionName : Successfully dumped and copied all the windows event log from individual machines to $destinationFolderPath"
                        }
                        catch
                        {
                            Trace-Progress "$functionName : Failed during windows event log collection $($_.Exception.Message)" -Error
                        }
                    }
                }
                else
                {
                    Trace-Progress -Message "$functionName : Skipping WindowsEventLog collection."
                }

                # Collecting log files.
                if ($FilterByLogType -contains 'File')
                {
                    if ($currentRoleData.FileLog)
                    {
                        $sourceLogPaths = foreach($entry in $currentRoleData.FileLog)
                        {
                            $entry
                            Trace-Progress -Message "$functionName : Collecting files from '$($entry)'."
                        }
                        try
                        {
                            if ($localMode)
                            {
                                Get-FileLog -ComputerNames $endpoints -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role `
                                    -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected
                            }
                            else
                            {
                                Get-FileLog -ComputerNames $endpoints -ComputerPSSessions $endpointPSSessions -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate `
                                    -Role $role -ExcludedEndpoints ([REF]$ExcludedEndpoints) -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected
                            }
                            
                        }
                        catch 
                        {
                            Trace-Progress "$functionName : Failed during File log collection $($_.Exception.Message)" -Error
                        }
                    }
                }
                else
                {
                    Trace-Progress -Message "$functionName : Skipping FileLog collection."
                }

                # Collecting container state.
                if ($FilterByLogType -contains 'ContainerState')
                {
                    try
                    {
                        if ($isArcA -and $role -eq "MASLogs")
                        {
                            Get-ContainerStateLog -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role -DestPathWithRoleName $destinationFolderPath
                        }
                        else
                        {
                            Trace-Progress -Message "$functionName : Skipping ContainerState collection for non-ArcA MASLogs."
                        }
                    }
                    catch 
                    {
                        Trace-Progress "$functionName : Failed during ContainerState collection $($_.Exception.Message)" -Error
                    }
                }
                else
                {
                    Trace-Progress -Message "$functionName : Skipping ContainerState collection."
                }

                if (!$localMode)
                {
                    #Remove PSSessions
                    Trace-Progress -Message "$functionName : Role : $role, Removing PS Sessions."
                    foreach ($psSession in $endpointPSSessions.Values)
                    {
                        if ($null -ne $psSession)
                        {
                            Remove-PSSession -Session $psSession -ErrorAction SilentlyContinue
                        }
                    }      
                }        
            } 

            if ($FilterByLogType -contains 'CSV')
            {
                if ($currentRoleData.CSVLog)
                {
                    if ($localMode)
                    {
                        $isPrimaryNode = $false
                        # if LocalMode, each node is doing it's own log collection in parallel. Only want the primary node to collect CSV logs.
                        Trace-Progress -Message "$functionName : In Local Mode. Determining primary node, so that only primary node collects CSV logs"
                        try
                        {
                            $nodes = Get-ClusterNode | Where-Object {$_.State -ieq "Up" } | Sort-Object -Property Name
                            $primaryNode = $nodes[0].Name.ToLower()
                            $isPrimaryNode = $primaryNode -eq ($env:COMPUTERNAME).ToLower()
                            if ($isPrimaryNode)
                            {
                                Trace-Progress -message "$functionName : This is the primary node. This node will collect CSV logs."
                            }
                            else
                            {
                                Trace-Progress -message "$functionName : This is not the primary node. This node will not collect CSV logs."
                            }
                        }
                        catch
                        {
                            # If we can't get primary node, it is likely deployment failed before cluster creation. In this case,
                            # there would be no CSV Logs in cluster storage, as cluster storage is not available.
                            # Even if there were race conditions in copying over CSV Logs, it would not cause log collection to fail.
                            Trace-Progress -message "$functionName : Error getting primary node : $_ Will collect CSV logs on this node."
                            $isPrimaryNode = $true
                        }
                    }
                    if (!$localMode -or $isPrimaryNode)
                    {
                        $sourceLogPaths = foreach($entry in $currentRoleData.CSVLog)
                        {
                            $entry
                            Trace-Progress -Message "$functionName : Collecting CSV files from '$entry'."
                        }

                        try
                        {
                            Get-FileLog -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role -CSVLogsFolderName "CSVLogs" `
                                -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected
                        }
                        catch 
                        {
                            Trace-Progress "$functionName : Failed during CSV log collection $($_.Exception.Message)" -Error
                        }
                    }
                }
            }
            else
            {
                Trace-Progress -Message "$functionName : Skipping CSV Log collection."
            }

            if (($FilterByLogType -contains 'Script') -and ($null -ne $currentRoleData.ScriptExecution)) {
                Trace-Progress -Message "$functionName : $role defined ScriptExecution, call the script to collect log."
                # Get the PS1 file location.
                $ScriptInfo = $currentRoleData.ScriptExecution
                $scriptPath = $ScriptInfo.ScriptPath

                if ($false -eq [string]::IsNullOrEmpty($scriptInfo.NugetName)) {
                    # The script execution specified NugetName, try to get it from t The command exported by CloudCommon module.
                    $nugetRootPath = Get-ASArtifactPath -NugetName $scriptInfo.NugetName -ErrorAction Ignore
                    $scriptPath = Join-Path $nugetRootPath $ScriptInfo.ScriptPath
                }

                Trace-Progress -Message "$functionName : Got $role log collecting script location: $scriptPath "
                # check the script file exists, and the script only support PS1 file
                # We expect the script has 3 parameters:
                # [Parameter(Mandatory = $true, HelpMessage = "The current role of logCollection; in script, we pass in for log purpose.")]
                # [string] $Role,
                # [parameter(Mandatory = $true, HelpMessage = "The Destination path to save the logCollection output.")]
                # [string] $DestPathWithRoleName,
                # [Parameter(Mandatory = $true, HelpMessage = "augumentsObject contains log collection parameters")]
                # [PSCustomObject] $argumentsObject
                if ($true -eq (Test-Path -Path $scriptPath -PathType Leaf) -and (".ps1" -eq [System.IO.Path]::GetExtension($scriptPath)) ) {
                    $scriptParameter = @{
                        RoleName                  = $role
                        LogOutputPathWithRoleName = $destinationFolderPath
                        argumentsObject           = $argumentsObject
                    }

                    # Save the script transcript to destination folder.
                    $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($scriptPath)
                    $transcriptPath = "$destinationFolderPath\Script_Output_$scriptName.log"

                    $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
                    $outputContent = ""
                    try {
                        Trace-Progress -Message "$functionName : Calling script [$scriptPath] to save logs to $destinationFolderPath."
                        Start-Transcript -Path $transcriptPath -Verbose
                        # call the script. with target parameters.
                        # TODOTODO: The customized script may hang, we need to limit the execution time and space usage.
                        $output = & $scriptPath @scriptParameter    
                        $outputContent = $output | ConvertTo-Json
                    }
                    catch {
                        $exceptionContent = $_.Exception.ToString()
                        Trace-Progress -Message "$functionName : Failed to call script [$scriptPath] for $role. Exception [$exceptionContent]" -Warning
                    }
                    finally {
                        $stopWatch.stop()
                        Stop-Transcript
                        Add-Content -Path $transcriptPath -Value "Script Output: `n $outputContent"
                        Trace-Progress -Message "$functionName : Executing script [$scriptPath] for $role finished, used $($stopwatch.Elapsed.TotalSeconds) seconds."
                    }
                }
                else {
                    Trace-Progress -Message "$functionName : Didn't find script at $scriptPath for $role, or it is not a valid ps1 file, skipping script collection." -Warning
                }
            }
            else {
                Trace-Progress -Message "$functionName : No Script need to run for $role, Skipping script collection." -Warning
            }
        }
        else
        {
            $roleLogDetails.logsAvailable = $false
            Trace-Progress -Message "$functionName : No logs collected for this role as none is specified in input configuration file."
        }
        $normalTermination = $true
    }
    catch
    {
        Trace-Progress -Message "$functionName : Collecting logs failed with error: $_" -Error
        Trace-Progress -Message "$functionName : StackTrace : $($PSItem.ScriptStackTrace)" -Error
        $normalTermination = $true
    }
    finally
    {
        if (!$localMode)
        {
            Trace-Progress -Message "$functionName : Role: $role cleaningup endpointPSSessions, current opened sessions count = [$($endpointPSSessions.Values.Count)] "
            foreach ($psSession in $endpointPSSessions.Values)
            {
                if ($null -ne $psSession)
                {
                    Trace-Progress -Message "$functionName : Removing session = [$psSession] "
                    Remove-PSSession -Session $psSession -ErrorAction SilentlyContinue
                }
            }
        }
        
        if ($normalTermination -ne $true) {
            Trace-Progress -Message "$functionName : $role : unclean exit detected " -Error
            Trace-Progress -Message "$functionName : $role : Wait 30 seconds for child jobs to complete"
            Start-Sleep 30 
            # incase of an unclean exit, give time for sub jobs to complete before exiting parent job
            #[environment]::Exit(0)
            #$ZippingJobs.Values | Remove-Job -force
        }

        Write-ErrorsIfExist -Role $role
        #Trace-InvokingProcessStats -Role ($role+"_End")

        $roleLogCollectionTime = ((Get-Date) - $perfRoleStartDate).TotalMinutes.ToString("0.0##")
        Trace-Progress -Message "$functionName : Time taken to collect role $role is [$roleLogCollectionTime] Minutes"
    }
}

# This method prints the $global:errorList in the calling process (each role and resource provider collection runs as a separate Process)
# Ensure this is called almost at the end of the job/role collection
function Write-ErrorsIfExist
{
    Param
    (
        [parameter(Mandatory=$true)]
        [string] $Role
    )

    $functionName = $($MyInvocation.MyCommand.Name) + "_$Role"
    # this variable is created when any trace-progress with -error is invoked.
    # Each role runs in its own process, so we can clear the $error automatic variable as well.
    if (((Test-Path variable:global:errorList) -and $Global:errorList -ne "") -or $Error.Count -gt 0)
    {
        Trace-Progress -Message "$functionName : Total entries in Global error list = $($Global:errorList.count)"

        $errorMessage = "ErrorList: `n" + $Global:errorList + ($Error | Get-Unique | Out-String)
        Write-Host $errorMessage -ForegroundColor "Red"     #Dont change this to trace-progress
        Trace-Progress -Message $errorMessage
        $Error.Clear()
    } else
    {
        Trace-Progress -Message "$functionName : No Errors during role $Role"
    }
}
function Get-FreeSpace
{
    Param
    (
        [parameter(Mandatory=$true)]
        [string]$RelativePath
    )

    $destinationFolder = Get-Item -Path (Split-Path $RelativePath -Parent)
    $fsobuild = new-Object -comobject Scripting.FileSystemObject
    $destinationFolderObj =  $fsobuild.GetFolder($destinationFolder)
    $freeSpaceBytes = $destinationFolderObj.Drive.FreeSpace
    $freeSpaceKb = $freeSpaceBytes / 1024

    return $freeSpaceKb
}

Export-ModuleMember -Function Get-FreeSpace
Export-ModuleMember -Function Invoke-ScriptBlockWithRetries
Export-ModuleMember -Function Write-ErrorsIfExist
Export-ModuleMember -Function Get-RoleLogs
Export-ModuleMember -Function Get-WindowsEventLog
Export-ModuleMember -Function Collect-WindowsEventLogs
Export-ModuleMember -Function Get-FileLog
Export-ModuleMember -Function Get-FilteredChildItem
Export-ModuleMember -Function Copy-FilteredChildItem
Export-ModuleMember -Function Initialize-PSSession
Export-ModuleMember -Function Test-PSSession
Export-ModuleMember -Function Invoke-ScriptBlockCommand
# SIG # Begin signature block
# MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAL344ag4x4DX/i
# EOqcMUn6WYScoxPxrZyrLBSpw+z4nKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKBT1xuXN/QWVboDEQk5etnw
# wpU2WDnCfsugnSB5FcZ+MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAZxw+KKOcGrNC0SfN8IrlEtJ/UkjXfQdWjV2rIRmQZwjn1sXGHe/1+f1o
# J7aBFrb3dAd0ezNmuhhewJFLj+jJzrC3dhYu92k6MqExQRCyfkBvBPQxQ1qgsSRx
# le/WkLKVgXws8lR975SjwLpjq15+t/hwUuamFPRDihcY9DA+WcQDCTYR6F/cY/zN
# hemF1KQL4N/mkC5Ml2aw/QLp7Y5bb79gO5vWiC0wW6hUKwuEViJ+VszGSSSb3gTw
# Uf3DP/i7nh6AAdcg4xT55TBmwOLtH4GKMyhKXkiOqKgocEtghQcixtXlg8pnJ9Oj
# 4DY9R6Hl3fLffVbpKKYX3SLJR7xGRKGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC
# FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCBapgV/6fp7GE8TBYQV9vE/K3UQxsYjrkk/rpNCdMOE9gIGZjOt3LML
# GBMyMDI0MDUxNjE4NDQ0Ni43MDhaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OkZDNDEtNEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNzI1WhcNMjUwMTEwMTkwNzI1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGQzQxLTRC
# RDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVjtZhV+kFmb8cKQpg2mzis
# DlRI978Gb2amGvbAmCd04JVGeTe/QGzM8KbQrMDol7DC7jS03JkcrPsWi9WpVwsI
# ckRQ8AkX1idBG9HhyCspAavfuvz55khl7brPQx7H99UJbsE3wMmpmJasPWpgF05z
# ZlvpWQDULDcIYyl5lXI4HVZ5N6MSxWO8zwWr4r9xkMmUXs7ICxDJr5a39SSePAJR
# IyznaIc0WzZ6MFcTRzLLNyPBE4KrVv1LFd96FNxAzwnetSePg88EmRezr2T3HTFE
# lneJXyQYd6YQ7eCIc7yllWoY03CEg9ghorp9qUKcBUfFcS4XElf3GSERnlzJsK7s
# /ZGPU4daHT2jWGoYha2QCOmkgjOmBFCqQFFwFmsPrZj4eQszYxq4c4HqPnUu4hT4
# aqpvUZ3qIOXbdyU42pNL93cn0rPTTleOUsOQbgvlRdthFCBepxfb6nbsp3fcZaPB
# fTbtXVa8nLQuMCBqyfsebuqnbwj+lHQfqKpivpyd7KCWACoj78XUwYqy1HyYnStT
# me4T9vK6u2O/KThfROeJHiSg44ymFj+34IcFEhPogaKvNNsTVm4QbqphCyknrwBy
# qorBCLH6bllRtJMJwmu7GRdTQsIx2HMKqphEtpSm1z3ufASdPrgPhsQIRFkHZGui
# hL1Jjj4Lu3CbAmha0lOrAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQURIQOEdq+7Qds
# lptJiCRNpXgJ2gUwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAORURDGrVRTbnulf
# sg2cTsyyh7YXvhVU7NZMkITAQYsFEPVgvSviCylr5ap3ka76Yz0t/6lxuczI6w7t
# Xq8n4WxUUgcj5wAhnNorhnD8ljYqbck37fggYK3+wEwLhP1PGC5tvXK0xYomU1nU
# +lXOy9ZRnShI/HZdFrw2srgtsbWow9OMuADS5lg7okrXa2daCOGnxuaD1IO+65E7
# qv2O0W0sGj7AWdOjNdpexPrspL2KEcOMeJVmkk/O0ganhFzzHAnWjtNWneU11WQ6
# Bxv8OpN1fY9wzQoiycgvOOJM93od55EGeXxfF8bofLVlUE3zIikoSed+8s61NDP+
# x9RMya2mwK/Ys1xdvDlZTHndIKssfmu3vu/a+BFf2uIoycVTvBQpv/drRJD68eo4
# 01mkCRFkmy/+BmQlRrx2rapqAu5k0Nev+iUdBUKmX/iOaKZ75vuQg7hCiBA5xIm5
# ZIXDSlX47wwFar3/BgTwntMq9ra6QRAeS/o/uYWkmvqvE8Aq38QmKgTiBnWSS/uV
# PcaHEyArnyFh5G+qeCGmL44MfEnFEhxc3saPmXhe6MhSgCIGJUZDA7336nQD8fn4
# y6534Lel+LuT5F5bFt0mLwd+H5GxGzObZmm/c3pEWtHv1ug7dS/Dfrcd1sn2E4gk
# 4W1L1jdRBbK9xwkMmwY+CHZeMSvBMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpG
# QzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAFpuZafp0bnpJdIhfiB1d8pTohm+ggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOnwn2IwIhgPMjAyNDA1MTYyMzA2MTBaGA8yMDI0MDUxNzIzMDYxMFowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA6fCfYgIBADAHAgEAAgIYmzAHAgEAAgITVDAKAgUA
# 6fHw4gIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAISJNra7Ggw4B/8Tn4yp
# x+hXDg8/G8srkcJl7vwzRVbuS6W9fR0YCSMS6fkjFayeozoQe5FrKcY1U7cZw3XC
# UhSsQGfYXh1matPvCIPXbjvnphAuKz0TdujD/q+eE6Gqa++ey0goT0AYhcfDRwvx
# gxxBejaOpgYnpl40jU7s7YN9MYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJYIZIAWUDBAIB
# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx
# IgQgYTx/m7EnhKIQb7RmkryxKwUK4vKWVHds0K/a9ZqT0F8wgfoGCyqGSIb3DQEJ
# EAIvMYHqMIHnMIHkMIG9BCAriSpKEP0muMbBUETODoL4d5LU6I/bjucIZkOJCI9/
# /zCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB4pmZ
# lfHc4yDrAAEAAAHiMCIEILplb72oW4+73iQROSoFPO5HtCiiSLulx8XotCt2EMse
# MA0GCSqGSIb3DQEBCwUABIICAAajFu1RLbb5qf/Gz0lydB9miP4dFMz/LlkSSvV2
# cTuoG/cmUIHE5aS3aIvma//70yWGLWFrSeB3ILIcHH7esqQAJ/tGRVZw3RWr3uV+
# 9xMZSdlXNdDvqmn3xZzsX5z6l9jCEpTynsWJ3isoZAVqqnYPK4DBBqWsrqyVlgn9
# NxdbrKbxvXj1miZKnXpxuoxhBV1FCIo6lHaJKZ3UOqupaJHGynjG+maV82hXMMQf
# Ct2d70jJHx1Dk+xw9vX+yPahO7HqS5IqyaKWih8IkUUl0qGMRstzA46XOREmPaIu
# DO1vQgTrL5VoxLb3zoqVcsxtZkOIJXoWrjTkBqe+V0tqGCdGJopUSzSBwT4lB8Cc
# aVcnFAL2AmdXN+fKunWWX+2hE7ljUl2SpTInx4A1JfLaKqwHc2h+7lhstCAlAIbq
# H0/qQPchlmlKftg8Dsc/9DI5DBJb+e9+ZmbiTd7tn02HDv5TXSnG0E4/sC7vUwBH
# B8DLD6m6X8u4KaP42yGvBuAL62u0unz5SJp5cX3mopYo0t6okYb0Cn6zaQsKE/zR
# ndkvdyHt+fuGQlbgnpOOHgsE0Vku5JrqnF8M42lMbACJ9r8bzzY+ygW58Pm/Lm6W
# L+L3jgUgfKx280NO2Dz+W7Z1wCbLsZ7semtKwE3chkZZi0a0JQ3pGimYv0z3rt2Q
# t5YW
# SIG # End signature block