2
\$\begingroup\$

The script tests a server rendering PDFs. The server is running as a Windows service.

The script does the following:

  1. Prepare the test
    1. configure server
    2. export script variables to xml
    3. copy input
  2. Run the test with 4 paralel jobs
  3. Gather logs and clean up after the test

Step 1.2. is needed bacause the tests might be stopped with a BlueScreen and the server dies. So after it restarts I run the scipt with -clean switch to clean after the most recent interupted test. The clean option reads the XML file saved in step 1.2. so it does not have to query the OS/Server for all properites.

What do you think about intercepting Ctrl+C to enable a gracefull exit?

What do you think about using a hastable for global script variables - $ScriptVariables = @{}?

What do you think about exporting $ScriptVariables to XML and then reading it when using clean option?

What do you think about overriding Write-Output option?

I was tired of always running the script and piping its output to Tee-Object so I've overridden Write-Output

Any other suggestions are welcome too!

param(
    [alias("clean")][switch] $CleanOnly = $false,
    [alias("input","in")] $DirectoryWithInputs,
    [alias("iterations","iter")] $IterationsToDo = 5,
    $WebRequestTimeoutSec = 3600, # 60m * 60s = 1h
)

function Write-Output($message) {
    $timeStamp = $(Get-Date -f "dd'/'MM'/'yyyy HH:mm:ss fff")
    Write-Host "$($timeStamp): $message"
    Add-Content $ScriptVariables.LogFile "$($timeStamp): $message"
}

function Wait-ForResponse($uri) {
    $response = $null
    Write-Output "Giving max 100 seconds for $uri to response"
    foreach ($i in (1..20)) {
        $dots = ('.'*$i)
        Write-Output "Checking availability of $uri $dots"
        try {
            $response = Invoke-WebRequest -Uri $uri -Method POST -TimeoutSec 5 -UseBasicParsing
            if ($response.StatusCode -eq 200) {
                Write-Output "Server has responded with OK..."
                return
            }
        }
        catch [Exception] {}
        Start-Sleep -s 5
    }
    Assert ($response.StatusCode -eq 200) "$uri is not responding"
}

function Get-WinEventLogs($from, $to, $name) {
    $filter = @{}
    $filter.Add("StartTime", $from)
    $filter.Add("EndTime", $to)
    $filter.Add("LogName", @($name))
    Get-WinEvent -FilterHashtable $filter `
    | % { "{0:dd'/'MM'/'yyyy HH:mm:ss fff} {1} {2} {3} {4}" -f `
        $_.TimeCreated, `
        $_.levelDisplayName, `
        $_.Id, `
        $_.ProviderName, `
        $_.Message }
}

function CleanAfterTestAndGetLogs() {
    Write-Output "Stoping and removing all jobs..."
    Get-Job | Stop-Job
    Get-Job | Remove-Job

    Write-Output "Cleaning $($ScriptVariables.PDFMakerDocumentsPool)..."
    Remove-Item "$($ScriptVariables.PDFMakerDocumentsPool)\*" -recurse

    Write-Output "Copying logs..."
    Copy-Item "$($ScriptVariables.PDFMakerLogsDirectory)\*" $ScriptVariables.OutputDirectory -verbose

    Write-Output "Copying self..."
    Copy-Item $PSCommandPath $ScriptVariables.OutputDirectory

    Write-Output "Stopping PDFMaker..."
    Stop-Service $ScriptVariables.PDFMakerServiceName

    Write-Output "Resetting PDFMaker logs date format..."
    Reset-PDFMakerLogFormat $ScriptVariables.Log4jFilePath

    Wait-ForOSToReleasePorts
    Write-Output "Starting PDFMaker..."
    Start-Service $ScriptVariables.PDFMakerServiceName

    Write-Output "Getting Windows event logs..."
    $scriptEndTime = Get-Date
    Get-WinEventLogs $ScriptVariables.ScriptStartTime $ScriptEndTime "Application" `
    | Out-File "$($ScriptVariables.OutputDirectory)\winEventLogs_Application.log"
    Get-WinEventLogs $ScriptVariables.ScriptStartTime $ScriptEndTime "Setup" `
    | Out-File "$($ScriptVariables.OutputDirectory)\winEventLogs_Setup.log"
    Get-WinEventLogs $ScriptVariables.ScriptStartTime $ScriptEndTime "System" `
    | Out-File "$($ScriptVariables.OutputDirectory)\winEventLogs_System.log"

    Write-Output "Creating combined log..."
    $logEntires = @()
    Get-ChildItem `
    "$($ScriptVariables.OutputDirectory)\powershell*", `
    "$($ScriptVariables.OutputDirectory)\PDFMaker Server Log*", `
    "$($ScriptVariables.OutputDirectory)\winEventLogs*" `
    | % { $logEntires += Parse-LogFile $_.FullName }
    [System.Linq.Enumerable]::OrderBy($logEntires, [Func[object,DateTime]]{ param($x) $x.Date }) `
    | % { "{0:dd'/'MM'/'yyyy HH:mm:ss fff} {1,-51} {2}" -f $_.Date, $_.FileName, $_.Message } `
    | Add-content "$($ScriptVariables.OutputDirectory)\combined.log"

    Write-Output "Remove `"ongoing_`" prefix from output folder name"
    $NewOutputDirectory = $ScriptVariables.OutputDirectory.Replace("ongoing_", "")
    $NewLogFile = $ScriptVariables.LogFile.Replace("ongoing_", "")
    Move-Item $ScriptVariables.OutputDirectory $NewOutputDirectory
    $ScriptVariables.OutputDirectory = $NewOutputDirectory
    $ScriptVariables.LogFile = $NewLogFile

    Write-Output "Total time taken to run test:"
    ($scriptEndTime - $ScriptVariables.ScriptStartTime) | Out-String | % { Write-Output $_ }

    Write-Output "The test is done, will exit now..."
    [console]::TreatControlCAsInput = $false
}

function Set-PDFMakerLogFormat($file) {
    $content = Get-Content -Raw $file
    $content = $content.Replace("HH:mm:ss}", "HH:mm:ss SSS}")
    Set-Content -Path $file $content
}

function Reset-PDFMakerLogFormat($file) {
    $content = Get-Content -Raw $file
    $content = $content.Replace("HH:mm:ss SSS}", "HH:mm:ss}")
    Set-Content -Path $file $content
}

function Wait-ForOSToReleasePorts() {
    # https://technet.microsoft.com/en-us/library/cc938217.aspx?f=255&MSPPError=-2147217396
    $timeToWait = 240 + 10
    $timeRemaining = $timeToWait
    Write-Output "Waiting $timeToWait seconds for OS to release all closed ports..."
    foreach ($i in (1..($timeToWait/10))) {
        Write-Output "Remaining time: $timeRemaining s"
        $timeRemaining -= 10
        Start-Sleep -seconds 10
    }
}

function Parse-LogFile($path) {
    $file = Get-ChildItem $path
    Write-Output "Paring $path..."
    $lines = Get-Content $path
    $table = @()
    foreach($line in $lines) {
        $regex = [regex]"^(\d{1,2}/\d{1,2}/\d{4}\s\d{1,2}:\d{1,2}:\d{1,2}\s\d{0,3})(.*)"
        $match = $regex.Match($line)
        if($match.Success) {
            $dateString = $match.Groups[1].Value
            [String[]] $formats = "dd'/'MM'/'yyyy HH:mm:ss fff", "dd'/'MM'/'yyyy HH:mm:ss "
            $date = [datetime]::ParseExact($dateString, $formats, $null, [System.Globalization.DateTimeStyles]::None)
            $message = $match.Groups[2].Value
            $entry = New-Object -TypeName PSObject
            $entry | Add-Member -MemberType NoteProperty -Name Date -Value $date
            $entry | Add-Member -MemberType NoteProperty -Name FileName -Value $file.Name
            $entry | Add-Member -MemberType NoteProperty -Name Message -Value $message
            $table += $entry
        }
        elseif($table.Length -gt 0) {
            $table[$table.Length - 1].Message += "`n" + $line
        }
    }
    $table
}

$PredefinedInputNameToPath = @{}
$PredefinedInputNameToPath.200 = "\\some_share\load_tests_input\2017-02-28_200_reports"
$PredefinedInputNameToPath.50 = "\\some_share\load_tests_input\2017-02-28_50_reports"
$PredefinedInputNameToPath.large = "\\some_share\load_tests_input\2017-02-28_only_large_reports"
$PredefinedInputNameToPath.fast = "\\some_share\load_tests_input\2017-02-28_only_small_reports"
$PredefinedInputNameToPath.test = "\\some_share\load_tests_input\_2_small_test_reports"

$ScriptVariables = @{}

[console]::TreatControlCAsInput = $true

if (![Environment]::Is64BitProcess) {
    throw "This script must be run in x64 mode"
    Exit
}

if ($CleanOnly) {
    $outputDirectoryOfInteruptedTest = Get-ChildItem "ongoing_*"
    $ScriptVariables = Import-CliXml ($outputDirectoryOfInteruptedTest.FullName + "\scriptVariables.xml")
    $ScriptVariables.LogFile = $ScriptVariables.LogFile.Replace("powershell.log", "powershell_clean.log")
    CleanAfterTestAndGetLogs
    Exit
}

$ScriptVariables.ScriptStartTime = Get-Date
$ScriptVariables.LogFile = ".\powershell.log"

Write-Output "---------------------------------------------------------------"

$PDFMakerService = Get-Service | where { $_.Name -like "PDFMaker*" }
$ScriptVariables.PDFMakerServiceName = $PDFMakerService.Name
if($PDFMakerService.Status -ne "Running") {
    Write-Output "Starting $($ScriptVariables.PDFMakerServiceName)..."
    Wait-ForOSToReleasePorts
    Start-Service $ScriptVariables.PDFMakerServiceName
}
Wait-ForResponse "http://localhost:8080/"

if($PredefinedInputNameToPath.ContainsKey($DirectoryWithInputs)) {
    $ScriptVariables.InputPath = $PredefinedInputNameToPath[$DirectoryWithInputs]
    $ScriptVariables.InputName = $DirectoryWithInputs
}
else {
    $ScriptVariables.InputPath = $DirectoryWithInputs
    $ScriptVariables.InputName = "custom"
}

$ScriptVariables.PDFMakerInstallationDirectory = $ServiceNameToInstallationPath[$ScriptVariables.PDFMakerServiceName]
$ScriptVariables.Log4jFilePath = $ScriptVariables.PDFMakerInstallationDirectory + "\conf\log4j2.xml"
$ScriptVariables.PDFMakerLogsDirectory = $ScriptVariables.PDFMakerInstallationDirectory + "\log"
$ScriptVariables.OutputDirectoryName = "ongoing_{0}_{1}_{2,3}" -f `
    $env:ComputerName, `
    $ScriptVariables.ScriptStartTime.ToString("yyyy.MM.dd_HH-mm-ss"), `
    $ScriptVariables.InputName, `
    | % { $_.Replace(" ", "_") }
$ScriptVariables.OutputDirectory = (New-Item -itemtype directory $ScriptVariables.OutputDirectoryName).FullName
$ScriptVariables.PDFOutputDirectory = $ScriptVariables.OutputDirectory + "\pdfs"

Write-Output "Move powershell.log file to actual output folder"
Move-Item $ScriptVariables.LogFile ($ScriptVariables.OutputDirectory + "\" + $ScriptVariables.LogFile)
$ScriptVariables.LogFile = $ScriptVariables.OutputDirectory + "\" + $ScriptVariables.LogFile

Write-Output "Export script variables for later use"
$ScriptVariables | Export-CliXml ($ScriptVariables.OutputDirectory + "\scriptVariables.xml")

Write-Output "Script parameters and global varibles:"
Get-Variable -scope script | Out-String | % { Write-Output $_ }
$ScriptVariables.GetEnumerator() | % { Write-Output "$($_.Key) = $($_.Value)" }

Write-Output "Stopping $($ScriptVariables.PDFMakerServiceName)..."
Stop-Service $ScriptVariables.PDFMakerServiceName

Write-Output "Setting PDFMaker logs date format..."
Set-PDFMakerLogFormat $ScriptVariables.Log4jFilePath

Write-Output "Cleaning $($ScriptVariables.PDFMakerLogsDirectory)..."
Remove-Item ($ScriptVariables.PDFMakerLogsDirectory + "\*")

Write-Output "Cleaning $($ScriptVariables.PDFMakerDocumentsPool)..."
Remove-Item ($ScriptVariables.PDFMakerDocumentsPool + "\*") -recurse

Write-Output "Copying input to $($ScriptVariables.PDFMakerDocumentsPool)..."
Copy-Item "$($ScriptVariables.InputPath)\*" $ScriptVariables.PDFMakerDocumentsPool -recurse -verbose 4>&1 | %{ Write-Host "." -nonewline }
Write-Host #newline

Write-Output "Starting $($ScriptVariables.PDFMakerServiceName)"
Wait-ForOSToReleasePorts
Start-Service $ScriptVariables.PDFMakerServiceName
Wait-ForResponse "http://localhost:8080/"

$scriptBlock = {
    param(
        $id,
        $IterationsToDo,
        $PDFOutputDirectory,
        $PDFMakerDocumentsPool,
        $LogFile,
        $WebRequestTimeoutSec
    )

    function Write-Output($message) {
        $timeStamp = $(Get-Date -f "dd'/'MM'/'yyyy HH:mm:ss fff")
        Write-Host "$($timeStamp): $message"
        Add-Content "$($LogFile)job#$id" "$($timeStamp): $message"
    }

    function Render-PDF($directoryName, $outputFile, $retiresLeft = 3) {
        $url = # some url sent to the server requesting a pdf rendering from $directoryName
        try {
            Invoke-WebRequest $url -OutFile $outputFile -timeoutSec $WebRequestTimeoutSec
            Write-Output "job#$($id)_iter$($CurrentIteration): Done - pdf saved to: $outputFile"
        }
        catch {
            $errorText = $_ | Out-String
            if($retiresLeft -gt 0 -and $errorText.Contains("some string indicating that a retry might accualy render the pdf")) {
                Write-Output "job#$($id)_iter$($CurrentIteration): Something went wrong, retrying, there are still $retiresLeft"
                Render-PDF $directoryName $outputFile ($retiresLeft - 1)
            }
            else {
                $errorFile = $outputFile.replace(".pdf", ".txt")
                Out-File $errorFile -inputObject $_
                Write-Output "job#$($id)_iter$($CurrentIteration): Something went wrong - error saved"
            }
        }
    }

    Write-Output "This is job#$id"
    $CurrentIteration = 0
    while($IterationsToDo - $CurrentIteration -gt 0) {
        Write-Output "job#$($id): Iteration $CurrentIteration..."
        $iterationOutputDirectory = (New-Item -itemtype directory "$PDFOutputDirectory\job$($id)_iter$($CurrentIteration.ToString("D3"))").FullName
        Get-ChildItem "$PDFMakerDocumentsPool" -Directory `
        | sort-object name `
        | foreach {
            Write-Output "job#$($id)_iter$($CurrentIteration): Rendering $($_.Name)..."
            Render-PDF $_.Name "$iterationOutputDirectory\$($_.Name).pdf"
        }
        $CurrentIteration += 1
    }
}

foreach ($i in (1..4)) {
    start-job -scriptblock $scriptblock -argumentlist `
    $i, `
    $IterationsToDo, `
    $ScriptVariables.PDFOutputDirectory, `
    $ScriptVariables.PDFMakerDocumentsPool, `
    $ScriptVariables.LogFile, `
    $WebRequestTimeoutSec | Out-Null
}

$anyJobIsRunning = $true
$UserPressedCtrlC = $false
while($anyJobIsRunning -and -not($UserPressedCtrlC)) {
    if ([console]::KeyAvailable) {
        $key = [system.console]::readkey($true)
        if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
            $UserPressedCtrlC = $true
        }
    }
    else {
        Start-Sleep -s 3
    }
    Get-Job | Receive-Job
    $anyJobIsRunning = $false
    Get-Job | % {
        $anyJobIsRunning = $anyJobIsRunning -or ($_.State -eq "Running")
    }
}

CleanAfterTestAndGetLogs
$ScriptVariables.OutputDirectory
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.