SDP-functions.ps1 #5

  • //
  • guest/
  • perforce_software/
  • sdp/
  • main/
  • Server/
  • Windows/
  • p4/
  • common/
  • bin/
  • SDP-functions.ps1
  • View
  • Commits
  • Open Download .zip Download (22 KB)
# ============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE
# ----------------------------------------------------------------------------

<#
    .Synopsis
        Various SDP related shared functions
        
    .Description
        Functions intended to be called by other scripts.
        This script is not intended to be run directly.
        
#>

Set-StrictMode -Version 2.0

Function Parse-SDPConfigFile([string]$scriptpath) {
    $SDPConfigFile = Split-Path -parent $scriptpath | Join-Path -childpath "..\..\config\sdp_config.ini"
    $SDPConfigFile = Resolve-Path $SDPConfigFile
    $ini = Parse-Inifile($SDPConfigFile)

    $section = "${SDPInstance}:${env:computername}".tolower()
    if ($ini[$section] -eq $null) {
        throw @"
Section '$section' not found in ${SDPConfigFile} - did you specify a valid SDPInstance parameter
(before the ':') or are you on the wrong machine (hostname - after the ':')?
"@
    }
    
    try {
        $global:sdp_global_root = $ini[$section]["sdp_global_root"]
        $global:sdp_serverid = $ini[$section]["sdp_serverid"]
        $global:sdp_p4serviceuser = $ini[$section]["sdp_p4serviceuser"]
        $global:sdp_p4superuser = $ini[$section]["sdp_p4superuser"]
        $global:admin_pass_filename = $ini[$section]["admin_pass_filename"]
        $global:email_pass_filename = $ini[$section]["email_pass_filename"]
        $global:mailfrom = $ini[$section]["mailfrom"]
        $global:maillist = $ini[$section]["maillist"]
        $global:mailhost = $ini[$section]["mailhost"]
        $global:mailhostport = $ini[$section]["mailhostport"]
        $global:python = $ini[$section]["python"]
        $global:remote_depotdata_root = $ini[$section]["remote_depotdata_root"]
        $global:keepckps = 0
        $global:keeplogs = 0
        $result = [int32]::TryParse($ini[$section]["keepckps"], [ref]$global:keepckps)
        $result = [int32]::TryParse($ini[$section]["keeplogs"], [ref]$global:keeplogs)
        $global:limit_one_daily_checkpoint = $false
        $value = $ini[$section]["limit_one_daily_checkpoint"]
        if ($value -match "true|1|yes|y") {
            $global:limit_one_daily_checkpoint = $true
        }
        $global:remote_sdp_instance = $ini[$section]["remote_sdp_instance"]
        $global:p4target = $ini[$section]["p4target"]
    }
    catch {
        $message = $_.Exception.GetBaseException().Message 
        Write-Error $message 
        throw "Error parsing ini file: section ${section}"
    }

    $global:SDP_INSTANCE_HOME = -join($global:SDP_GLOBAL_ROOT, "\p4\", $SDPInstance)
    $global:SDP_INSTANCE_BIN_DIR = -join($global:SDP_INSTANCE_HOME, "\bin")
    $global:P4exe =  -join($global:SDP_INSTANCE_BIN_DIR, "\p4.exe")
    $global:P4Dexe =  -join($global:SDP_INSTANCE_BIN_DIR, "\p4d.exe")
    Ensure-PathExists $global:p4exe
    Ensure-PathExists $global:p4dexe
    $global:SDP_INSTANCE_SSL_DIR = -join($global:SDP_INSTANCE_HOME, "\ssl")
    $global:P4CONFIG = "p4config.txt"
    $global:P4ROOT = -join($global:SDP_INSTANCE_HOME, "\root")
    $global:P4LOG = -join($global:SDP_INSTANCE_HOME, "\logs\", $SDPINSTANCE, ".log")
    $global:P4JOURNAL = -join($global:SDP_INSTANCE_HOME, "\logs\journal")
    $global:P4USER = $global:SDP_P4SUPERUSER
    $env:P4TICKETS = -join($global:SDP_INSTANCE_HOME, "\P4Tickets.txt")
    $env:P4TRUST = -join($global:SDP_INSTANCE_HOME, "\P4Trust.txt")
    $env:P4ENVIRO = -join($global:SDP_INSTANCE_HOME, "\P4Enviro.txt")
    $global:SDP_INSTANCE_P4SERVICE_NAME = -join("p4_", $SdpInstance)
    $global:P4PORT = Get-P4PORT
    $global:SCRIPTS_DIR = -join($global:SDP_GLOBAL_ROOT, "\p4\common\bin")
    $global:LOGS_DIR = -join($global:SDP_INSTANCE_HOME, "\logs")
    $global:OFFLINE_DB_DIR = -join($global:SDP_INSTANCE_HOME, "\offline_db")
    $global:CHECKPOINTS_DIR = -join($global:SDP_INSTANCE_HOME, "\checkpoints")
    $global:REMOTE_CHECKPOINTS_DIR = -join($global:REMOTE_DEPOTDATA_ROOT, "\p4\", $global:REMOTE_SDP_INSTANCE, "\checkpoints")
    $global:ADMIN_PASS_FILE = -join($global:SCRIPTS_DIR, "\", $global:ADMIN_PASS_FILENAME)
    $global:EMAIL_PASS_FILE = -join($global:SCRIPTS_DIR, "\", $global:EMAIL_PASS_FILENAME)
    $Global:EDGESERVER = $false
    $serverid_file = -join($global:P4ROOT, "\server.id")
    $global:SERVERID = get-content $serverid_file
    # PATH=%SDP_INSTANCE_BIN_DIR%;%SCRIPTS_DIR%;%PATH%
    Ensure-PathExists $global:SDP_INSTANCE_BIN_DIR
    Ensure-PathExists $global:P4ROOT
    Ensure-PathExists $global:LOGS_DIR
    Ensure-PathExists $global:OFFLINE_DB_DIR
    Ensure-PathExists $global:CHECKPOINTS_DIR
    Ensure-PathExists $global:ADMIN_PASS_FILE
}

Function Set-vars {
    $dbfile = "$global:P4ROOT\db.server"
    if (!(Test-Path -path $dbfile)) {
        return
    }
    # SERVERVAL from the server record bitwise anded with 4096 indicates an edge server.
    $serverval = & $p4dexe -r $global:P4ROOT -jd - db.server | select-string "@$global:SERVERID@" | %{$_.line.split()[8]}
    [int]$serverint = [convert]::toint32($serverval, 10)
    if ($serverval -band 4096) {
        $global:EDGESERVER = $true
    }
}

Function Get-P4PORT {
    $regkey = "HKLM:\SYSTEM\CurrentControlSet\services\${global:SDP_INSTANCE_P4SERVICE_NAME}\parameters"
    (get-itemproperty $regkey).P4PORT
}

Function Get-Date-Time () {
    Get-Date -format "yyyy\/MM\/dd HH\:mm\:ss"
}

Function Ensure-PathExists([string]$path) {
    if (!(Test-Path -path $path)) {
        throw "Error - required path $path doesn't exist!"
    }
}

Function Create-LogFile () {
    if (!$global:LogFileName) {
        throw "ERROR - Global:LogFileName not set"
    }
    $global:LogFile = Join-Path $global:LOGS_DIR $global:LogFileName
    Rotate-CurrentLogFile
    write-debug "Using logfile: $global:logfile"
    write-output "Starting ${global:ScriptTask}" | set-content -path $global:Logfile
}

Function Append-To-File([string]$message, [string]$filename) {
    $OFS = $null
    if (test-path variable:global:OFS) {
        $OFS = $global:OFS
    }
    $global:OFS = "`r`n"
    "$message" | add-content -path $filename
    if ($OFS) {
        $global:OFS = $OFS
    }
}

Function Log([string]$message) {
    # Logs output to file and to console
    $datetime = get-date-time
    write-host "$datetime $message"
    Append-To-File "$datetime $message" $global:Logfile
}

Function LogException([exception]$exception) {
    $datetime = get-date-time
    $message = $exception.message
    write-error -message "$datetime $message" -Exception $exception
    write-output "$datetime $message" | add-content -path $global:Logfile
}

Function Parse-IniFile ($file) {
    $ini = @{}

    # Create a default section if none exist in the file. Like a java prop file.
    $section = "NO_SECTION"
    $ini[$section] = @{}

    switch -regex -file $file {
        "^\[(.+)\]$" {
            $section = $matches[1].Trim().ToLower()
            $ini[$section] = @{}
        }
        "^\s*([^#].+?)\s*=\s*(.*)" {
            $name,$value = $matches[1..2]
            # skip comments that start with semicolon:
            if (!($name.StartsWith(";"))) {
                $ini[$section][$name] = $value.Trim()
            }
        }
    }
    $ini
}

Function Invoke-P4Login () {
    Log "Logging in to P4 Instance"
    $cmd = "$global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER login -a < $global:ADMIN_PASS_FILE >> $global:LogFile 2>&1"
    Log $cmd
    cmd /c $cmd
    if ($lastexitcode -ne 0) {
        throw "ERROR - failed to login!"
    }
}

Function Run-ReplicaStatus ([string]$statuslog) {
    Log "Getting replica status"
    $result = & $global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER pull -lj 2>&1
    Log $result
    if ((Test-Path $statuslog -pathtype leaf)) {
        remove-item $statuslog
    }
    Append-To-File "$result" $statuslog
    if ($lastexitcode -ne 0) {
        Log "WARNING - pull command exited with error code"
    }
}

Function Check-OfflineDBExists () {
    Log "Checking offline DB exists"
    $path = Join-Path $global:OFFLINE_DB_DIR "db.counters"
    Log "checking existence of $path"
    if (!(Test-Path $path -pathtype leaf)) {
        throw "ERROR - offline_db doesn't exist!"
    }
}

Function Ensure-CheckpointNotRunning () {
    Log "Ensuring there is no other checkpoint script running"
    $checkFile = Join-Path $global:CHECKPOINTS_DIR "ckp_running.txt"
    Log "checking existence of $checkFile"
    if (Test-Path $checkFile -pathtype leaf) {
        throw "Error - a checkpoint operation is already running!"
    }
    write-output "Checkpoint running" | set-content -path $checkFile
}

Function Signal-CheckpointComplete () {
    $checkFile = Join-Path $global:CHECKPOINTS_DIR "ckp_running.txt"
    remove-item $checkFile
}

Function Get-CurrentJournalCounter () {
    Log "Getting current live journal counter"
    $cmd = "$global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER counter journal"
    run-cmd-with-check $cmd "Error - failed to get live journal counter!"
    [int]$global:JOURNAL_NUM = $result
    [int]$global:CHECKPOINT_NUM = $global:JOURNAL_NUM + 1        
    Log "Live journal/checkpoint counters ${global:JOURNAL_NUM}/${global:CHECKPOINT_NUM}"
}

Function Get-JournalCounter ([string]$rootdir) {
    $cmd = "$global:P4Dexe -r $rootdir -jd - db.counters"
    Log $cmd
    $rawjournal = Invoke-Expression $cmd | select-string "@journal@" | %{$_.line.split()[4]}
    Log $rawjournal
    $journal = [regex]::match($rawjournal, '^@([0-9]+)@$').Groups[1].Value
    if ($lastexitcode -ne 0) {
        throw "Error - failed to get $rootdir journal counter!"
    }
    return [int]$journal
}

Function Get-OfflineJournalCounter () {
    Log "Getting offline journal counter"
    $journal = Get-JournalCounter $global:OFFLINE_DB_DIR
    Log "Offline journal counter $journal"
    [int]$global:Offline_journal_num = $journal
}

Function Rotate-File ([string]$Rotate_file) {
    if (Test-Path $Rotate_file -pathtype leaf) {
        $suffix = (Get-ChildItem $Rotate_file).lastwritetime | Get-Date -format "yyyyMMdd\-HHmmss"
        $newname = "$Rotate_file.$suffix"
        write-host "Rotating $Rotate_file to $newname"
        rename-item -path $Rotate_file -newname $newname
    }
}

Function Rotate-CurrentLogFile () {
    $Rotate_logname = $global:LOGFILE
    if (Test-Path $Rotate_logname -pathtype leaf) {
        $suffix = (Get-ChildItem $Rotate_logname).lastwritetime | Get-Date -format "yyyyMMdd\-HHmmss"
        $newname = "$Rotate_logname.$suffix"
        write-host "Rotating $Rotate_logname to $newname"
        rename-item -path $Rotate_logname -newname $newname
    }
}

Function Rotate-LogFile () {
    param([string]$Rotate_logname, [bool]$zip)
    if (Test-Path $Rotate_logname -pathtype leaf) {
        $newname = "$Rotate_logname.$global:logid"
        Log "Rotating $Rotate_logname to $newname"
        rename-item -path $Rotate_logname -newname "$newname"
        if ($zip) {
            $gzip = -join($global:SCRIPTS_DIR, "\gzip.exe")
            $cmd = "$gzip $newname"
            run-cmd-with-check $cmd "failed to gzip file"
        }
    }
}

Function Rotate-Logfiles () {
    Log "Rotating various logfiles"
    $datetime = Get-Date -format "yyyyMMdd\-HHmmss"
    $global:LOGID = "${global:JOURNAL_NUM}.$datetime"

    Rotate-LogFile "${global:sdp_serverid}.log" $true
    Rotate-LogFile "log" $true
    Rotate-LogFile "p4broker.log" $true
    Rotate-LogFile "audit.log" $true
    Rotate-LogFile "sync_replica.log" $false
}

Function Move-Files ([string]$source, [string]$target) {
    Get-ChildItem -Path $source |
        foreach-object {
            $f = $_.FullName
            Log "Moving $f to $target"
            move-item $f $target
        }
}

Function Copy-Files ([string]$source, [string]$target) {
    Get-ChildItem -Path $source |
        foreach-object {
            $f = $_.FullName
            Log "Copyig $f to $target"
            copy-item $f $target
        }
}

Function Remove-Files {
    # Remove older files while keeping a specified minimum number
    param([string]$remove_filename, [int]$keepnum)

    $file_path = -join($remove_filename, "*")
    $files = @(Get-ChildItem -Path $file_path | Sort-Object -Property LastWriteTime -Descending)
    if ($files) {
        for ($j = $keepnum; $j -lt $files.count; $j++) {
            $f = $files[$j].FullName
            Log("Removing: $f")
            remove-item $f -Force
        }
    }
}

Function Remove-OldLogs () {
    # Remove old Checkpoint Logs
    # Use KEEPCKPS rather than KEEPLOGS, so we keep the same number
    # of checkpoint logs as we keep checkpoints.
    if ($global:keepckps -eq 0) {
        Log "Skipping cleanup of old checkpoint logs because KEEPCKPS is set to 0."
    } else {
        log "Deleting old checkpoint logs.  Keeping latest $global:KEEPCKPS, per KEEPCKPS setting in sdp_config.ini."
        remove-files $global:P4LOG $global:KEEPCKPS
        remove-files "checkpoint.log" $global:KEEPCKPS
        remove-files "log" $global:KEEPCKPS
        remove-files "p4broker.log" $global:KEEPCKPS
        remove-files "audit.log" $global:KEEPCKPS
        remove-files "sync_replica.log" $global:KEEPCKPS
        remove-files "replica_status.log" $global:KEEPCKPS
        remove-files "p4verify.log" $global:KEEPCKPS
        remove-files "recreate_offline_db.log" $global:KEEPCKPS
        remove-files "upgrade.log" $global:KEEPCKPS
    }
}

Function Is-Replica () {
    # If a replica then add -t for transfer to args
    $target = & $global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER configure show 2>&1 | select-string "P4TARGET="
    Log $target
    if ($lastexitcode -ne 0) {
        return $false
    }
    if ($target -ne $null -and $target -ne "") {
        return $true
    }
    return $false
}

Function Truncate-Journal () {
    Log "Truncating live journal"
    $checkpoint_path = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME, ".ckp.", $global:Checkpoint_num, ".gz")
    if (Test-Path $checkpoint_path -pathtype leaf) {
        throw "ERROR - $checkpoint_path already exists, check the backup process"
    }
    if (!$global:EDGESERVER) {
        $journalprefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME)
        $journalpath = -join($journalprefix, ".jnl.", $global:JOURNAL_NUM)
        if (Test-Path $journalpath -pathtype leaf) {
            throw "ERROR - $journalpath already exists, check the backup process"
        }
        log "Truncating journal..."
        # 'p4d -jj' does a copy-then-delete, instead of a simple mv.
        # during 'p4d -jj' the perforce server will hang the responses to clients.
        $cmd = "$global:P4Dexe -r $global:P4ROOT -J $global:P4JOURNAL -jj $journalprefix"
        run-cmd-with-check $cmd "ERROR - attempting to truncate journal"
    }
}

Function Run-Checkpoint () {
    $checkpoint_prefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME)
    $cmd = "$global:P4DEXE -r $global:P4ROOT -J $global:P4JOURNAL -jc -Z $checkpoint_prefix"
    run-cmd-with-check $cmd "ERROR - attempting to checkpoint live database"
}

Function Replay-JournalsToOfflineDB () {
    Log "Applying all oustanding journal files to offline_db"
    for ($j = $global:Offline_journal_num; $j -le $global:JOURNAL_NUM; $j++) {
        $journalprefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME)
        $journalpath = -join($journalprefix, ".jnl.", $j)
        $cmd = "$global:P4DEXE -r $global:OFFLINE_DB_DIR -jr $journalpath"
        run-cmd-with-check $cmd "ERROR - attempting to replay journal"
    }
}

function Create-OfflineCheckpoint {
    Log "Creating offline checkpoint"
    $checkpoint_prefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME, ".ckp.")
    if ($global:limit_one_daily_checkpoint -ne 0) {
        $curr_date = Get-Date -format "yyyyMMdd"
        $checkpoint_path = -join($checkpoint_prefix, "*.gz")
        $files = @(Get-ChildItem -Path $checkpoint_path | Sort-Object -Property LastWriteTime -Descending)
        if (!$files -or $files.count -eq 0) {
           Log "LIMIT_ONE_DAILY_CHECKPOINT is set to true, but no checkpoint exists."
        } else {
            $ckp_date = $files[0].LastWriteTime | Get-Date -format "yyyyMMdd"
            if ($curr_date -eq $ckp_date) {
                Log "A checkpoint was already created today, and LIMIT_ONE_DAILY_CHECKPOINT is set to true."
                Log "Skipping offline checkpoint dump."
                return
            }
        }
    }
    $checkpoint_path = -join($checkpoint_prefix, $global:checkpoint_num, ".gz")
    $cmd = "$global:P4DEXE -r $global:OFFLINE_DB_DIR -jd -z $checkpoint_path"
    run-cmd-with-check $cmd "ERROR - attempting to create offline checkpoint"
}

Function Check-OfflineDBUsable () {
    $offline_db_usable = -join($global:OFFLINE_DB_DIR, "\offline_db_usable.txt")
    if (!(test-path $offline_db_usable -pathtype leaf)) {
        $msg = "Offline database not in a usable state. Check the backup process."
        Log $msg
        throw $msg
    }
}

Function Recreate-OfflineDBFiles () {
    Log "Recreating offline db files"
    $offline_db_usable = -join($global:OFFLINE_DB_DIR, "\offline_db_usable.txt")
    $checkpoint_prefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME, ".ckp.")
    $checkpoint_path = -join($checkpoint_prefix, "*.gz")
    $files = @(Get-ChildItem -Path $checkpoint_path | Sort-Object -Property LastWriteTime -Descending)
    if (!$files -or $files.count -eq 0) {
        $msg = "No checkpoints found - run live-checkpoint.ps1"
        Log $msg
        throw $msg
    }
    if (test-path $offline_db_usable -pathtype leaf) {
        remove-item $offline_db_usable
    }
    remove-files "${global:OFFLINE_DB_DIR}\db." 0
    
    Log "Recovering from latest checkpoint found"
    $checkpoint_path = $files[0]
    $cmd = "$global:P4DEXE -r $global:OFFLINE_DB_DIR -jr -z $checkpoint_path"
    run-cmd-with-check $cmd "ERROR - attempting to recover offline db files"
    "Offline db file restored successfully." | set-content -path $offline_db_usable
}

Function Remove-OldCheckpointsAndJournals () {
    if ($global:keepckps -eq 0) {
        Log "Skipping cleanup of old checkpoints because KEEPCKPS is set to 0."
    } else {
        log "Deleting obsolete checkpoints and journals.  Keeping latest $global:KEEPCKPS, per KEEPCKPS setting in sdp_config.ini."
        $checkpoint_prefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME, ".ckp.")
        $journal_prefix = -join($global:CHECKPOINTS_DIR, "\", $global:SDP_INSTANCE_P4SERVICE_NAME, ".jnl.")
        # We multiply KEEPCKP by 2 for the ckp files because of the md5 files.
        remove-files $checkpoint_prefix ($global:KEEPCKPS * 2)
        remove-files $journal_prefix ($global:KEEPCKPS)
    }
}

Function run-cmd ([string]$cmd) {
    Log $cmd
    $result = Invoke-Expression $cmd
    Log $result
}

Function run-cmd-with-check ([string]$cmd, [string]$errmsg) {
    Log $cmd
    $result = Invoke-Expression $cmd
    Log $result
    if ($lastexitcode -ne 0) {
        throw "$errmsg"
    }    
}

Function Log-DiskSpace () {
    log "Checking disk space..."
    $cmd = "$global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER diskspace"
    run-cmd "$cmd"
}

Function set-counter () {
    Invoke-P4Login
    $datetime = get-date-time
    $cmd = "$global:P4exe -p $global:P4PORT -u $global:SDP_P4SUPERUSER counter lastSDPCheckpoint ""$datetime"""
    run-cmd -cmd $cmd
}

Function Compare-JournalNumbers () {
    # Ensures offline and live db counters are the same - avoids using out-of-date offline_db
    $offlineJournal = Get-JournalCounter $global:OFFLINE_DB_DIR
    $liveJournal = Get-JournalCounter $global:P4ROOT
    if ($liveJournal -ne $offlineJournal) {
      log "$global:P4ROOT journal number is: $liveJournal"
      log "$global:OFFLINE_DB_DIR journal number is: $offlineJournal"
      throw "$global:P4ROOT and $global:OFFLINE_DB_DIR journal numbers do not match." 
    }
}

Function Move-OfflineDBToLive () {
    # Compare the Offline and Master journal numbers before switching to make sure they match.
    Compare-JournalNumbers
    log "Switching out db files..."
    $savedir = "${global:P4ROOT}\save"
    if (!(test-path -path $savedir -type container)) {
        New-Item $savedir -ItemType directory
    }
    remove-files "$savedir\db." 0
    move-files "${global:P4ROOT}\db.*" $savedir
    move-files "${global:offline_db_dir}\db.*" $global:P4ROOT
    remove-item "${global:offline_db_dir}\offline_db_usable.txt"
}

Function Test-IsAdmin {
    ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
}

Function Check-AdminPrivileges {
    if (!(Test-IsAdmin)) {
        throw "Please run this script with admin priviliges"
    }
}

Function Stop-LiveService () {
    Stop-Service $global:SDP_INSTANCE_P4SERVICE_NAME
}

Function Start-LiveService () {
    Start-Service $global:SDP_INSTANCE_P4SERVICE_NAME
}

Function send-email ([string]$subject, [string]$logfilename) {
    Log "Sending email with subject: $subject"
    if ($logfilename) {
        $contents = get-content $logfilename
    } else {
        $contents = get-content $global:logfile
    }
    $SMTPServer = $global:mailhost
    $SMTPPort = "25"
    if ($global:mailhostport -and $global:mailhostport -ne "") {
        $SMTPPort = $global:mailhostport
    }
    try {
        $email_password = ""
        if ((test-path $global:email_pass_file -pathtype leaf)) {
            Log "Found  $global:email_pass_file"
            $email_password = get-content $global:email_pass_file
        }
        $message = New-Object System.Net.Mail.MailMessage
        $message.subject = $subject
        $message.body = $contents -join "<br>`n"
        $message.isbodyhtml = $true
        $message.to.add($global:maillist)
        $message.from = $global:mailfrom
        $smtp = New-Object System.Net.Mail.SmtpClient($SMTPServer, $SMTPPort)
        if ($SMTPPort -ne "25") {
            $smtp.EnableSSL = $true
        }
        if ($email_password -ne "") {
            $smtp.Credentials = New-Object System.Net.NetworkCredential($global:mailfrom, $email_password)
        }
        $smtp.send($message)
    }
    Catch
    {
        $ErrorMessage = $_.Exception.Message
        Log "Failed to send email to ${global:mailhost}: ${ErrorMessage}"
    }
}
# Change User Description Committed
#15 30915 C. Thomas Tyler Released SDP 2024.1.30913 (2024/11/20).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#14 30388 C. Thomas Tyler Released SDP 2024.1.30385 (2024/06/11).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#13 28858 C. Thomas Tyler Released SDP 2022.1.28855 (2022/05/27).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#12 28240 C. Thomas Tyler Released SDP 2021.1.28238 (2021/11/12).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#11 27921 C. Thomas Tyler Released SDP 2020.1.27919 (2021/07/19).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#10 27761 C. Thomas Tyler Released SDP 2020.1.27759 (2021/05/07).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#9 27331 C. Thomas Tyler Released SDP 2020.1.27325 (2021/01/29).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#8 26161 C. Thomas Tyler Released SDP 2019.3.26159 (2019/11/06).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#7 25596 C. Thomas Tyler Released SDP 2019.2.25594 (2019/05/02).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#6 23006 C. Thomas Tyler Released SDP 2017.3.23003 (2017/10/19).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#5 22931 Robert Cowham Updated docs for Windows.
New rotate-log-files script.
Working upgrade.ps1
#4 22920 Robert Cowham Bring latest changes in from dev for Windows.
- fixed p4verify for replicas with -t
- created upgrade.ps1
#3 22760 Robert Cowham Bring in 2 new scripts:
recover-edge.ps1 - equivalent of unix recover_edge.sh
create-filtered-edge-checkpoint.ps1 - equivalent of unix edge_dump.sh

See scripts for documentation.
#2 20974 C. Thomas Tyler Released SDP 2016.2.20972 (2016/11/01).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
#1 20767 C. Thomas Tyler Released SDP 2016.2.20755 (2016/09/29).
Copy Up using 'p4 copy -r -b perforce_software-sdp-dev'.
//guest/perforce_software/sdp/dev/Server/Windows/p4/common/bin/SDP-functions.ps1
#16 20646 Robert Cowham Got it working as far daily_backup tests.
Refactor a few names.
Auto wrap .ps1 commands.
#15 20624 Robert Cowham Handle global:OFS properly if not set.
Useful for array output.
#14 20595 Robert Cowham New replica-status command
Add corresponding function
Improve end of line handling when logging
#13 20545 Robert Cowham Updated for sending emails via gmail accounts - known to work (if properly configured!)
#12 20537 Robert Cowham Update send-email to work with gmail accounts
#11 20296 Robert Cowham Avoid using write-output in log function so we don't have to be so careful with returns from get-journalCounter
#10 20293 Robert Cowham Implement recreate-live-from-offline_db.ps1 - replacement fro weekly b_backup.bat which no longer needs to be run
weekly.
#9 20204 Robert Cowham Fix checkpoint error - make sure we rotate correct journal.
#8 20186 Robert Cowham Fix typo.
Tidy up old server logs.
#7 20184 Robert Cowham Added live-checkpoint.ps1
Fixed problem where all log files and others being removed - not keeping required number.
#6 20175 Robert Cowham Set-strictmode
Remove warnings
Improve exception logging
#5 20150 Robert Cowham Refactored names to use PowerShell Verb-Noun convention
#4 20149 Robert Cowham Split rotation of current script log file from rotation of other log files.
#3 20148 Robert Cowham Added log info for functions
#2 20146 Robert Cowham Refactor email sending.
Output final message of success/failure.
#1 20142 Robert Cowham Initial versions of Powershell scripts