Private/Culture.ps1

#Requires -Version 5.1

function Get-SMBeatUtcNow {
    [DateTime]::UtcNow
}

function ConvertTo-SMBeatUtcString {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]$DateTime
    )

    $utc = $DateTime
    if ($utc.Kind -eq [DateTimeKind]::Local) {
        $utc = $utc.ToUniversalTime()
    }
    elseif ($utc.Kind -eq [DateTimeKind]::Unspecified) {
        $utc = [datetime]::SpecifyKind($utc, [DateTimeKind]::Utc)
    }

    $utc.ToString('yyyy-MM-ddTHH:mm:ssZ', $script:SMBeatInvariant)
}

function ConvertFrom-SMBeatUtcString {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    [datetime]::Parse(
        $Value,
        $script:SMBeatInvariant,
        [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal
    )
}

function Get-SMBeatRecordUtc {
    param(
        $Record
    )

    if ($null -eq $Record) {
        return [datetime]::MinValue
    }

    if ($Record.PSObject.Properties['TsUtc'] -and $null -ne $Record.TsUtc) {
        $utc = [datetime]$Record.TsUtc
        if ($utc.Kind -eq [DateTimeKind]::Utc) {
            return $utc
        }
        return [datetime]::SpecifyKind($utc, [DateTimeKind]::Utc)
    }

    if ($Record.PSObject.Properties['ts'] -and $Record.ts) {
        return ConvertFrom-SMBeatUtcString -Value ([string]$Record.ts)
    }

    [datetime]::MinValue
}

function ConvertTo-SMBeatJson {
    param(
        [Parameter(Mandatory = $true)]
        $InputObject
    )

    ConvertTo-Json -InputObject $InputObject -Depth 8 -Compress:$false
}

function ConvertFrom-SMBeatJson {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Json
    )

    ConvertFrom-Json -InputObject $Json
}

function Test-SMBeatSharingViolation {
    param($Exception)

    $cur = $Exception
    $n = 0
    while ($null -ne $cur -and $n -lt 6) {
        if ($cur -is [System.IO.IOException]) {
            $code = [int]($cur.HResult -band 0xFFFF)
            if ($code -eq 32 -or $code -eq 33) {
                return $true
            }
        }
        $cur = $cur.InnerException
        $n++
    }

    $false
}

function Invoke-SMBeatIoRetry {
    param(
        [Parameter(Mandatory = $true)]
        [scriptblock]$Action,
        [int]$Tries = 8
    )

    $n = 0
    while ($true) {
        try {
            return (& $Action)
        }
        catch {
            $n++
            if ($n -ge $Tries -or -not (Test-SMBeatSharingViolation -Exception $_.Exception)) {
                throw
            }
            Start-Sleep -Milliseconds (50 * $n)
        }
    }
}

function Write-SMBeatTextFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    $dir = Split-Path -Parent $Path
    if (-not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }

    Invoke-SMBeatIoRetry -Action {
        $fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
        try {
            $bytes = $script:SMBeatUtf8NoBom.GetBytes($Text)
            $fs.Write($bytes, 0, $bytes.Length)
        }
        finally {
            $fs.Dispose()
        }
    }
}

function Add-SMBeatTextFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    $dir = Split-Path -Parent $Path
    if (-not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }

    Invoke-SMBeatIoRetry -Action {
        $fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
        try {
            $bytes = $script:SMBeatUtf8NoBom.GetBytes($Text)
            $fs.Write($bytes, 0, $bytes.Length)
        }
        finally {
            $fs.Dispose()
        }
    }
}

function Read-SMBeatTextFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return $null
    }

    Invoke-SMBeatIoRetry -Action {
        $fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
        try {
            $sr = New-Object System.IO.StreamReader($fs, $script:SMBeatUtf8NoBom, $true)
            try {
                $sr.ReadToEnd()
            }
            finally {
                $sr.Dispose()
            }
        }
        finally {
        }
    }
}

function New-SMBeatSharedStreamReader {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    Invoke-SMBeatIoRetry -Action {
        $fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
        New-Object System.IO.StreamReader($fs, $script:SMBeatUtf8NoBom, $true)
    }
}

function Get-SMBeatLanguage {
    param(
        [string]$Language
    )

    if (-not [string]::IsNullOrWhiteSpace($Language)) {
        $normalized = $Language.Trim().ToLowerInvariant()
        if ($normalized -eq 'de' -or $normalized.StartsWith('de-')) {
            return 'de'
        }
        return 'en'
    }

    $ui = [string]$PSUICulture
    if ($ui.StartsWith('de', [System.StringComparison]::OrdinalIgnoreCase)) {
        return 'de'
    }
    return 'en'
}

function Resolve-SMBeatTimeZone {
    param(
        [object]$TimeZone
    )

    if ($null -eq $TimeZone -or ($TimeZone -is [string] -and [string]::IsNullOrWhiteSpace([string]$TimeZone))) {
        return [TimeZoneInfo]::Local
    }

    if ($TimeZone -is [TimeZoneInfo]) {
        return $TimeZone
    }

    return [TimeZoneInfo]::FindSystemTimeZoneById([string]$TimeZone)
}

function Convert-SMBeatUtcToTimeZone {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]$Utc,
        [Parameter(Mandatory = $true)]
        [TimeZoneInfo]$TimeZone
    )

    $asUtc = $Utc
    if ($asUtc.Kind -eq [DateTimeKind]::Local) {
        $asUtc = $asUtc.ToUniversalTime()
    }
    else {
        $asUtc = [datetime]::SpecifyKind($asUtc, [DateTimeKind]::Utc)
    }

    [TimeZoneInfo]::ConvertTimeFromUtc($asUtc, $TimeZone)
}

function Convert-SMBeatTimeZoneToUtc {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]$LocalTime,
        [Parameter(Mandatory = $true)]
        [TimeZoneInfo]$TimeZone
    )

    if ($LocalTime.Kind -eq [DateTimeKind]::Utc) {
        return $LocalTime
    }

    $unspec = [datetime]::SpecifyKind($LocalTime, [DateTimeKind]::Unspecified)
    [TimeZoneInfo]::ConvertTimeToUtc($unspec, $TimeZone)
}

function Get-SMBeatByteDelta {
    param(
        [Parameter(Mandatory = $true)]
        [uint64]$Current,
        [uint64]$Previous
    )

    if ($Current -ge $Previous) {
        return [int64]($Current - $Previous)
    }

    # Counter reset (reboot / instance recycle). Treat current raw as the new total for this interval.
    return [int64]$Current
}

function Get-SMBeatBucketStart {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]$LocalTime,
        [Parameter(Mandatory = $true)]
        [ValidateSet('Hour', 'Day', 'Week')]
        [string]$Granularity
    )

    $kind = $LocalTime.Kind

    switch ($Granularity) {
        'Hour' {
            return New-Object datetime $LocalTime.Year, $LocalTime.Month, $LocalTime.Day, $LocalTime.Hour, 0, 0, $kind
        }
        'Day' {
            return New-Object datetime $LocalTime.Year, $LocalTime.Month, $LocalTime.Day, 0, 0, 0, $kind
        }
        'Week' {
            $date = New-Object datetime $LocalTime.Year, $LocalTime.Month, $LocalTime.Day, 0, 0, 0, $kind
            $offset = ([int]$date.DayOfWeek + 6) % 7
            return $date.AddDays(-1 * $offset)
        }
    }
}

function Get-SMBeatBucketEnd {
    param(
        [Parameter(Mandatory = $true)]
        [datetime]$BucketStart,
        [Parameter(Mandatory = $true)]
        [ValidateSet('Hour', 'Day', 'Week')]
        [string]$Granularity
    )

    switch ($Granularity) {
        'Hour' { return $BucketStart.AddHours(1) }
        'Day'  { return $BucketStart.AddDays(1) }
        'Week' { return $BucketStart.AddDays(7) }
    }
}

function ConvertTo-SMBeatGB {
    param(
        [int64]$Bytes
    )

    [math]::Round(([double]$Bytes / 1000000000.0), 3)
}

function ConvertTo-SMBeatGiB {
    param(
        [int64]$Bytes
    )

    ConvertTo-SMBeatGB -Bytes $Bytes
}

function Get-SMBeatInstanceLeafName {
    param(
        [string]$Name
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $Name
    }

    $idx = $Name.LastIndexOf('\')
    if ($idx -ge 0 -and $idx -lt ($Name.Length - 1)) {
        return $Name.Substring($idx + 1)
    }

    return $Name
}

function Test-SMBeatAdminShareName {
    param(
        [string]$Name
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $false
    }

    $leaf = Get-SMBeatInstanceLeafName -Name $Name

    if ($leaf -eq 'IPC$' -or $leaf -eq 'ADMIN$' -or $leaf -eq 'print$') {
        return $true
    }

    if ($leaf.Length -eq 2 -and $leaf.EndsWith('$') -and $leaf[0] -match '[A-Za-z]') {
        return $true
    }

    return $false
}

function Test-SMBeatSkipInstance {
    param(
        [string]$Name,
        [string]$Kind,
        [switch]$IncludeAdminShares
    )

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $true
    }

    if ($Name -eq '_Total' -or $Name -like '*_Total*') {
        return $true
    }

    $leaf = Get-SMBeatInstanceLeafName -Name $Name

    if (($Kind -eq 'share' -or $Kind -eq 'smbperf') -and $leaf -eq 'IPC$') {
        return $true
    }

    if (($Kind -eq 'share' -or $Kind -eq 'smbperf') -and -not $IncludeAdminShares -and (Test-SMBeatAdminShareName -Name $Name)) {
        return $true
    }

    return $false
}