DSCResources/MSFT_xExchJetstress/MSFT_xExchJetstress.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCDscExamplesPresent", "")]
[CmdletBinding()]
param()

function Get-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Performance","Stress","DatabaseBackup","SoftRecovery")]
        [System.String]
        $Type,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressParams,

        [System.UInt32]
        $MaxWaitMinutes = 0,

        [System.UInt32]
        $MinAchievedIOPS = 0
    )

    #Load helper module
    Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0

    LogFunctionEntry -Parameters @{"JetstressPath" = $JetstressPath; "JetstressParams" = $JetstressParams} -VerbosePreference $VerbosePreference

    $returnValue = @{
        Type = $Type
        JetstressPath = $JetstressPath
        JetstressParams = $JetstressParams
        MaxWaitMinutes = $MaxWaitMinutes        
    }

    $returnValue
}

function Set-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")]
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Performance","Stress","DatabaseBackup","SoftRecovery")]
        [System.String]
        $Type,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressParams,

        [System.UInt32]
        $MaxWaitMinutes = 0,

        [System.UInt32]
        $MinAchievedIOPS = 0
    )

    #Load helper module
    Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0

    LogFunctionEntry -Parameters @{"JetstressPath" = $JetstressPath; "JetstressParams" = $JetstressParams} -VerbosePreference $VerbosePreference

    $jetstressRunning = IsJetstressRunning
    $jetstressSuccessful = JetstressTestSuccessful @PSBoundParameters

    if ($jetstressSuccessful -eq $false -and (Get-ChildItem -LiteralPath "$($JetstressPath)" | Where-Object {$null -ne $_.Name -like "$($Type)*.html"}))
    {
        throw "Jetstress was previously executed and resulted in a failed run. Clean up any $($Type)*.html files in the Jetstress install directory before trying to run this resource again."
    }

    if ($jetstressRunning -eq $false) #Jetstress isn't running. Kick it off
    {
        $initializingESE = $false

        #If the ESE counters haven't been registered for Perfmon, Jetstress is going to need to initialize ESE and restart the process.
        if ((Test-Path -LiteralPath "$($env:SystemRoot)\Inf\ESE\eseperf.ini") -eq $false)
        {
            $initializingESE = $true
        }

        #Create and start the Jetstress scheduled task
        StartJetstress @PSBoundParameters

        #Give an additional 60 seconds if ESE counters were just initialized.
        if ($initializingESE -eq $true)
        {
            Write-Verbose "Jetstress has never initialized performance counters for ESE. Waiting a full 60 seconds for this to occurr"
            
            Start-Sleep -Seconds 5

            for ($i = 55; $i -gt 0; $i--)
            {
                $jetstressRunning = IsJetstressRunning

                if ($jetstressRunning -eq $false)
                {
                    break
                }
                else
                {
                    Start-Sleep -Seconds 1
                }
            }

            #I've found that Jetstress doesn't always restart after loading ESE when running as local system in a scheduled task in the background
            #If Jetstress isn't running at this point, but the perf counters were registered, we probably need to reboot the server
            #If Jetstress isn't running and ESE is not registered, something failed.
            if ($jetstressRunning -eq $false)
            {
                if ((Test-Path -LiteralPath "$($env:SystemRoot)\Inf\ESE\eseperf.ini") -eq $true)
                {
                    Write-Verbose "ESE performance counters were registered. Need to reboot server."

                    $global:DSCMachineStatus = 1
                    return
                }
                else
                {
                    throw "Jetstress failed to register MSExchange Database performance counters"
                }
            }
            else
            {
                #Looks like Jetstress restarted itself successfully. Let's let it run.
            }
        }
        else
        {
            $jetstressRunning = IsJetstressRunning
        }

        #Wait up to a minute for Jetstress to start. If it hasn't started by then, something went wrong
        $checkMaxTime = [DateTime]::Now.AddMinutes(1)

        while ($jetstressRunning -eq $false -and $checkMaxTime -gt [DateTime]::Now)
        {
            $jetstressRunning = IsJetstressRunning

            if ($jetstressRunning -eq $false)
            {
                Start-Sleep -Seconds 1
            }
        }

        if ($jetstressRunning -eq $false)
        {
            throw "Waited 60 seconds after launching the Jetstress scheduled task, but failed to detect that JetstressCmd.exe is running"
        }
    }

    while ($jetstressRunning -eq $true)
    {
        Write-Verbose "Jetstress is still running at '$([DateTime]::Now)'."

        #Wait for 5 minutes before logging to the screen again, but actually check every 5 seconds whether Jetstress has completed.
        for ($i = 0; $i -lt 300 -and $jetstressRunning -eq $true; $i += 5)
        {
            $jetstressRunning = IsJetstressRunning

            if ($jetstressRunning -eq $true)
            {
                Start-Sleep -Seconds 5
            }
        }
    }

    #Check the final status on the Jetstress run
    if ($jetstressRunning -eq $false)
    {
        Write-Verbose "Jetstress testing finished at '$([DateTime]::Now)'."

        $overallTestSuccessful = JetstressTestSuccessful @PSBoundParameters

        if ($overallTestSuccessful -eq $false)
        {
            throw "Jetstress finished running, but the test did not complete successfully"
        }
        else
        {
            Write-Verbose "Jetstress finished, and the configured test passed"
        }
    }
    else
    {
        throw "Jetstress is still running after waiting $($MaxWaitMinutes) minutes"
    }
}


function Test-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Performance","Stress","DatabaseBackup","SoftRecovery")]
        [System.String]
        $Type,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressParams,

        [System.UInt32]
        $MaxWaitMinutes = 0,

        [System.UInt32]
        $MinAchievedIOPS = 0
    )

    #Load helper module
    Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0

    LogFunctionEntry -Parameters @{"JetstressPath" = $JetstressPath; "JetstressParams" = $JetstressParams} -VerbosePreference $VerbosePreference

    $jetstressRunning = IsJetstressRunning -MaximumWaitSeconds 1

    if ($jetstressRunning -eq $true)
    {
        return $false
    }
    else
    {
        $jetstressSuccessful = JetstressTestSuccessful @PSBoundParameters

        return $jetstressSuccessful
    }
}

#Checks whether the JetstressCmd.exe process is currently running
function IsJetstressRunning
{
    $process = Get-Process -Name JetstressCmd -ErrorAction SilentlyContinue

    return ($null -ne $process)
}

#Used to create a scheduled task which will initiate the Jetstress run
function StartJetstress
{
    [CmdletBinding()]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Performance","Stress","DatabaseBackup","SoftRecovery")]
        [System.String]
        $Type,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressParams,

        [System.UInt32]
        $MaxWaitMinutes = 0,

        [System.UInt32]
        $MinAchievedIOPS = 0
    )

    Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0

    $fullPath = Join-Path -Path "$($JetstressPath)" -ChildPath "JetstressCmd.exe"

    StartScheduledTask -Path "$($fullPath)" -Arguments "$($JetstressParams)" -WorkingDirectory "$($JetstressPath)" -TaskName "Jetstress" -MaxWaitMinutes $MaxWaitMinutes -VerbosePreference $VerbosePreference -TaskPriority 1
}

#Looks in the latest Type*.html file to determine whether the last Jetstress run passed
function JetstressTestSuccessful
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [parameter(Mandatory = $true)]
        [ValidateSet("Performance","Stress","DatabaseBackup","SoftRecovery")]
        [System.String]
        $Type,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

        [parameter(Mandatory = $true)]
        [System.String]
        $JetstressParams,

        [System.UInt32]
        $MaxWaitMinutes = 0,

        [System.UInt32]
        $MinAchievedIOPS = 0
    )

    $overallTestSuccessful = $false
    $achievedIOPSTarget = $false

    $outputFiles = Get-ChildItem -LiteralPath "$($JetstressPath)" | Where-Object {$_.Name -like "$($Type)*.html"}

    if ($null -ne $outputFiles -and $outputFiles.Count -ge 1)
    {
        $outputFiles = $outputFiles | Sort-Object -Property LastWriteTime -Descending

        $latest = $outputFiles[0]

        $content = Get-Content -LiteralPath "$($latest.FullName)"

        $foundOverallResults = $false
        $foundAchievedIOPS = $false

        for ($i = 0; $i -lt $content.Length -and ($foundOverallResults -eq $false -or $foundAchievedIOPS -eq $false); $i++)
        {
            if ($content[$i].Contains("<td class=`"grid_row_header`">Overall Test Result</td>") -or $content[$i].Contains("<td class=`"grid_row_header`">Achieved Transactional I/O per Second</td>"))
            {
                $resultStart = $content[$i + 1].IndexOf('>') + 1
                $resultEnd = $content[$i + 1].LastIndexOf('<')

                $result = $content[$i + 1].Substring($resultStart, $resultEnd - $resultStart)

                if ($content[$i].Contains("<td class=`"grid_row_header`">Overall Test Result</td>"))
                {
                    $foundOverallResults = $true

                    Write-Verbose "File $($latest.FullName)'' has an 'Overall Test Result' of '$($result)'"

                    if ($result -like "Pass")
                    {
                        $overallTestSuccessful = $true
                    }
                }
                else
                {
                    $foundAchievedIOPS = $true

                    if ([string]::IsNullOrEmpty($result) -eq $false)
                    {
                        Write-Verbose "File $($latest.FullName)'' has an 'Achieved Transactional I/O per Second' value of '$($result)'"

                        [Decimal]$decResult = [Decimal]::Parse($result)

                        if ($decResult -ge $MinAchievedIOPS)
                        {
                            $achievedIOPSTarget = $true
                        }
                    }
                    else
                    {
                        Write-Verbose "Value for 'Achieved Transactional I/O per Second' is empty"
                    }
                }
            }
        }

        if ($foundOverallResults -eq $false)
        {
            Write-Verbose "Unable to find 'Overall Test Result' in file '$($latest.FullName)'"
        }

        if ($foundAchievedIOPS -eq $false)
        {
            Write-Verbose "Unable to find 'Achieved Transactional I/O per Second' in file '$($latest.FullName)'"
        }
    }
    else
    {
        Write-Verbose "Unable to find any files matching '$($Type)*.html' in folder '$($JetstressPath)'"
    }

    return ($overallTestSuccessful -eq $true -and $achievedIOPSTarget -eq $true)
}

Export-ModuleMember -Function *-TargetResource