SMBpulse.psm1

#requires -version 5.1
<#
.SYNOPSIS
    SMBpulse - Messung und Auswertung des SMB-Durchsatzes auf Windows-Fileservern.

.DESCRIPTION
    SMBpulse ist ein PowerShell-Modul und enthaelt vier Cmdlets:

      Start-SmbPulse
        Erstellt und startet eine Langzeitmessung als Data Collector Set
        "SMB_Pulse" (Standard: 7 Tage, 15-Sekunden-Intervall, stuendliche
        BLG-Segmente).

      Watch-SmbPulse
        Zeigt den aktuellen SMB-Durchsatz direkt aus den Performance Countern
        an - mit farbcodierter Auslastungsanzeige fuer einen 1-Gbit/s-Link.

      Get-SmbPulseReport
        Wertet alle bereits abgeschlossenen BLG-Segmente aus und exportiert
        die Zusammenfassung sowie die groessten Einzelspitzen als CSV.

      Uninstall-SmbPulse
        Stoppt und entfernt Collector und Messdaten wieder.

    Voraussetzungen:
      - Windows PowerShell 5.1
      - Start-SmbPulse und Uninstall-SmbPulse erfordern eine Administratorsitzung

    Fruehere Versionen des Monitoring-Kits (Publisher-Name "SMB-Throughput-Monitor")
    mit dem Aelierungen Collector "SMB_Throughput_Monitor", Log "SMB_Throughput-Monitor"
    oder Collector "SMB_7Tage"/"SMB-Messung" werden automatisch erkannt und beim
    Start des neuen Collectors aufgeraeumt bzw. vom Uninstaller mitentfernt.

.NOTES
    Autor: Manuel Berfelde
#>


$ErrorActionPreference = 'Stop'

$Script:CollectorName    = 'SMB_Pulse'
$Script:LegacyCollectors = @(
    'SMB_Throughput_Monitor',
    'SMB_7Tage'
)

$Script:LogDirs = @(
    'C:\SMBpulse',
    'C:\PerfLogs\SMB-Throughput-Monitor',
    'C:\PerfLogs\SMB-Messung'
)

$Script:OutputDir  = $Script:LogDirs[0]
$Script:SummaryCsv = Join-Path $Script:OutputDir 'SMBpulse-Report.csv'
$Script:PeaksCsv   = Join-Path $Script:OutputDir 'SMBpulse-Peaks.csv'

# ----------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ----------------------------------------------------------------------------

function Test-Administrator {
    $identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Assert-Administrator {
    if (-not (Test-Administrator)) {
        throw 'Dieses Cmdlet muss in einer PowerShell als Administrator ausgefuehrt werden.'
    }
}

function Get-SmbCounterSet {
    param([Object]$AllSets)

    if (-not $AllSets) {
        $AllSets = @(Get-Counter -ListSet *)
    }

    $set = @($AllSets) |
        Where-Object {
            $_.CounterSetName -eq 'SMB-Serverfreigaben' -or
            $_.CounterSetName -eq 'SMB Server Shares' -or
            $_.CounterSetName -match '^SMB.*Server.*(Share|Freig)'
        } |
        Select-Object -First 1

    if (-not $set) {
        throw 'Der Performance-Counter-Satz fuer SMB-Serverfreigaben wurde nicht gefunden.'
    }

    return $set
}

function Get-SmbCounterKind {
    param([Parameter(Mandatory = $true)][string]$Path)

    if ($Path -match '\\(Datenbytes/Sek\.|Data Bytes/sec)$') {
        return 'Gesamt'
    }

    if ($Path -match '\\(Gelesene Bytes/s|Read Bytes/sec)$') {
        return 'Lesen'
    }

    if ($Path -match '\\(Geschriebene Bytes/Sek\.|Write Bytes/sec)$') {
        return 'Schreiben'
    }

    return $null
}

function Get-ThroughputColor {
    param([Parameter(Mandatory = $true)][double]$MbitSec)

    if ($MbitSec -ge 900) {
        return 'Red'
    }

    if ($MbitSec -ge 700) {
        return 'Yellow'
    }

    return 'Green'
}

function Write-MonitorBanner {
    param(
        [Parameter(Mandatory = $true)][string]$Title,
        [string]$Subtitle,
        [string]$Color = 'Cyan'
    )

    $width  = 62
    $top    = '+' + ('=' * ($width - 2)) + '+'
    $empty  = '|' + (' ' * ($width - 2)) + '|'

    $pad  = [Math]::Max(0, [int](($width - 2 - $Title.Length) / 2))
    $line = '|' + (' ' * $pad) + $Title + (' ' * ($width - 2 - $pad - $Title.Length)) + '|'

    Write-Host ''
    Write-Host $top   -ForegroundColor $Color
    Write-Host $empty -ForegroundColor $Color
    Write-Host $line  -ForegroundColor $Color

    if ($Subtitle) {
        Write-Host $empty -ForegroundColor $Color
        $pad  = [Math]::Max(0, [int](($width - 2 - $Subtitle.Length) / 2))
        $line = '|' + (' ' * $pad) + $Subtitle + (' ' * ($width - 2 - $pad - $Subtitle.Length)) + '|'
        Write-Host $line -ForegroundColor Gray
    }

    Write-Host $empty -ForegroundColor $Color
    Write-Host $top   -ForegroundColor $Color
    Write-Host ''
}

function Write-SectionTitle {
    param([Parameter(Mandatory = $true)][string]$Title)

    Write-Host ''
    Write-Host ('--- ' + $Title + ' ---') -ForegroundColor Cyan
}

function Write-InfoBox {
    param(
        [string[]]$Lines,
        [string]$Title = 'Ueberblick',
        [string]$Color = 'Cyan'
    )

    if (-not $Lines) {
        return
    }

    $width = 76
    $top   = '+' + ('=' * ($width - 2)) + '+'
    $empty = '|' + (' ' * ($width - 2)) + '|'

    Write-Host ''
    Write-Host $top -ForegroundColor $Color

    $pad  = [Math]::Max(0, [int](($width - 2 - $Title.Length) / 2))
    $line = '|' + (' ' * $pad) + $Title + (' ' * ($width - 2 - $pad - $Title.Length)) + '|'
    Write-Host $line -ForegroundColor $Color

    Write-Host $empty -ForegroundColor $Color

    foreach ($line in $Lines) {
        $text = $line

        if ($text -match ':') {
            $index = $text.IndexOf(':')
            $label = $text.Substring(0, $index).Trim()
            $value = $text.Substring($index + 1).Trim()
            $text  = (' {0,-28}: {1}' -f $label, $value)
        }
        else {
            $text = ' ' + $text
        }

        if ($text.Length -gt ($width - 3)) {
            $text = $text.Substring(0, $width - 3)
        }

        $pad = $width - 2 - $text.Length
        Write-Host ('|' + $text + (' ' * $pad) + '|')
    }

    Write-Host $empty -ForegroundColor $Color
    Write-Host $top   -ForegroundColor $Color
    Write-Host ''
}

function Write-Ok {
    param([string]$Text)
    Write-Host $Text -ForegroundColor Green
}

function Write-Warn {
    param([string]$Text)
    Write-Host $Text -ForegroundColor Yellow
}

function Write-Err {
    param([string]$Text)
    Write-Host $Text -ForegroundColor Red
}

function Write-Muted {
    param([string]$Text)
    Write-Host $Text -ForegroundColor DarkGray
}

function Convert-ToLogmanDuration {
    param([int]$TotalSeconds)

    $hours   = [Math]::Floor($TotalSeconds / 3600)
    $minutes = [Math]::Floor(($TotalSeconds % 3600) / 60)
    $seconds = $TotalSeconds % 60

    return ('{0:00}:{1:00}:{2:00}' -f $hours, $minutes, $seconds)
}

function Invoke-Logman {
    param(
        [Parameter(Mandatory = $true)][string[]]$ArgumentList,
        [switch]$Quiet
    )

    $logman = Join-Path $env:SystemRoot 'System32\logman.exe'
    if (Test-Path (Join-Path $env:SystemRoot 'Sysnative\logman.exe')) {
        $logman = Join-Path $env:SystemRoot 'Sysnative\logman.exe'
    }

    # logman startet gelegentlich nicht, wenn das aktuelle Arbeitsverzeichnis
    # fuer den Child-Prozess ungueltig ist ("Der Verzeichnisname ist ungueltig").
    # Deshalb immer aus einem garantiert gueltigen Verzeichnis heraus starten.
    Push-Location $env:SystemRoot
    try {
        if ($Quiet) {
            & $logman @ArgumentList 2>$null | Out-Null
        }
        else {
            & $logman @ArgumentList
        }
        return $LASTEXITCODE
    }
    finally {
        Pop-Location
    }
}

function Stop-AndDeleteCollector {
    param([string]$Name)

    if ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -ne 0) {
        return
    }

    $null = Invoke-Logman -ArgumentList @('stop', $Name) -Quiet
    Start-Sleep -Milliseconds 500
    $null = Invoke-Logman -ArgumentList @('delete', $Name) -Quiet

    # logman/PLA gibt die aktive BLG-Datei ggf. leicht verzoegert frei.
    for ($i = 0; $i -lt 20; $i++) {
        if ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -ne 0) {
            break
        }
        Start-Sleep -Milliseconds 500
    }

    Write-Ok "Vorhandenen Datensammler '$Name' entfernt."
}

function Find-SmbTotalCounter {
    param(
        [string[]]$Patterns,
        [string[]]$Paths
    )

    foreach ($pattern in $Patterns) {
        $match = $Paths |
            Where-Object {
                $_ -match '\(_Total\)\\' -and $_ -match $pattern
            } |
            Select-Object -First 1

        if ($match) {
            return $match
        }
    }

    return $null
}

# ----------------------------------------------------------------------------
# Start-SmbPulse
# ----------------------------------------------------------------------------

function Start-SmbPulse {
    <#
    .SYNOPSIS
        Erstellt und startet eine Langzeitmessung des SMB- und Netzwerkdurchsatzes.

    .DESCRIPTION
        Erstellt den Data Collector Set "SMB_Pulse" ueber logman. Storage:
          - 7 Tage Laufzeit
          - 15 Sekunden Abtastintervall
          - stuendliche BLG-Segmente

        Die stuendlichen Segmente erlauben bereits waehrend der Messwoche eine
        Auswertung abgeschlossener BLG-Dateien.

        Eventuell vorhandene Collector frueherer Versionen ("SMB_Throughput_Monitor",
        "SMB_7Tage") werden gestoppt und entfernt. Alte Messdaten bleiben erhalten.

    .PARAMETER Days
        Laufzeit der Messung in Tagen (1-30, Standard 7).

    .PARAMETER SampleSeconds
        Abstand zwischen zwei Messwerten in Sekunden (1-3600, Standard 15).

    .PARAMETER SegmentMinutes
        Groesse eines BLG-Zeitsegments in Minuten (5-1440, Standard 60).

    .EXAMPLE
        Start-SmbPulse

    .EXAMPLE
        Start-SmbPulse -Days 14 -SampleSeconds 10 -SegmentMinutes 30
    #>


    [CmdletBinding()]
    param(
        [ValidateRange(1,30)]
        [int]$Days = 7,

        [ValidateRange(1,3600)]
        [int]$SampleSeconds = 15,

        [ValidateRange(5,1440)]
        [int]$SegmentMinutes = 60
    )

    Assert-Administrator

    $CollectorName = $Script:CollectorName
    $OutputDir     = $Script:OutputDir

    Write-MonitorBanner -Title 'SMBpulse' -Subtitle 'Langzeitmessung von SMB- und Netzwerkdurchsatz'

    # Alte bzw. bereits vorhandene Collector sauber entfernen.
    Write-SectionTitle 'Bestehende Collector entfernen'
    foreach ($name in @($Script:LegacyCollectors + $CollectorName)) {
        Stop-AndDeleteCollector -Name $name
    }

    New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null

    Write-SectionTitle 'Performance Counter ermitteln'

    $AllSets = Get-Counter -ListSet *

    $SmbSet = Get-SmbCounterSet -AllSets $AllSets

    $NetSet = @($AllSets) |
        Where-Object {
            $_.CounterSetName -eq 'Netzwerkschnittstelle' -or
            $_.CounterSetName -eq 'Network Interface'
        } |
        Select-Object -First 1

    if (-not $NetSet) {
        throw 'Der Performance-Counter-Satz fuer Netzwerkschnittstellen wurde nicht gefunden.'
    }

    $SmbPaths = @($SmbSet.PathsWithInstances)
    $NetPaths = @($NetSet.PathsWithInstances)

    $SmbData = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @(
        '\\Datenbytes/Sek\.$',
        '\\Data Bytes/sec$'
    )

    $SmbRead = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @(
        '\\Gelesene Bytes/s$',
        '\\Read Bytes/sec$'
    )

    $SmbWrite = Find-SmbTotalCounter -Paths $SmbPaths -Patterns @(
        '\\Geschriebene Bytes/Sek\.$',
        '\\Write Bytes/sec$'
    )

    $SmbCounters = @($SmbData, $SmbRead, $SmbWrite) | Where-Object { $_ }

    if ($SmbCounters.Count -lt 3) {
        Write-Host ''
        Write-Warn 'Gefundene SMB-Counter:'
        $SmbPaths |
            Where-Object { $_ -match '\(_Total\)\\' } |
            ForEach-Object { Write-Host " $_" }

        throw 'Nicht alle benoetigten SMB-Durchsatzzaehler wurden gefunden.'
    }

    $NetCounters = @(
        $NetPaths |
            Where-Object {
                $_ -match '\\(Gesamtanzahl Bytes/s|Bytes Total/sec|Bytes gesendet/s|Bytes Sent/sec|Empfangene Bytes/s|Bytes Received/sec)$'
            }
    )

    if ($NetCounters.Count -eq 0) {
        throw 'Keine Netzwerk-Byte/s-Counter wurden gefunden.'
    }

    $Counters = @($SmbCounters | Sort-Object -Unique) + @($NetCounters | Sort-Object -Unique)

    Write-SectionTitle 'Aufzuzeichnende Counter'
    $Counters | ForEach-Object { Write-Host " $_" }

    $SampleInterval  = Convert-ToLogmanDuration -TotalSeconds $SampleSeconds
    $RunDuration     = Convert-ToLogmanDuration -TotalSeconds ($Days * 24 * 3600)
    $SegmentDuration = Convert-ToLogmanDuration -TotalSeconds ($SegmentMinutes * 60)

    $Arguments = @(
        'create', 'counter', $CollectorName,
        '-f', 'bin',
        '-o', "$OutputDir\SMB",
        '-si', $SampleInterval,
        '-rf', $RunDuration,
        '-cnf', $SegmentDuration,
        '-v', 'mmddhhmm',
        '-c'
    )
    $Arguments += $Counters

    Write-SectionTitle 'Datensammler erstellen'
    $createExit = Invoke-Logman -ArgumentList $Arguments
    if ($createExit -ne 0) {
        throw "logman create ist mit Exitcode $createExit fehlgeschlagen."
    }

    Write-SectionTitle 'Messung starten'
    $startExit = Invoke-Logman -ArgumentList @('start', $CollectorName)
    if ($startExit -ne 0) {
        throw "logman start ist mit Exitcode $startExit fehlgeschlagen."
    }

    Write-Ok 'Messung laeuft.'
    Write-InfoBox -Title 'Ueberblick' -Lines @(
        "Collector: $CollectorName",
        "Dauer: $Days Tag(e)",
        "Intervall: $SampleSeconds Sekunden",
        "BLG-Segment: $SegmentMinutes Minuten",
        "Ausgabe: $OutputDir"
    )

    Write-Muted 'Status pruefen:'
    Write-Muted " logman query $CollectorName"
    Write-Muted ''
    Write-Muted 'Live-Monitor:'
    Write-Muted ' Watch-SmbPulse'
    Write-Muted ''
    Write-Muted 'Auswertung:'
    Write-Muted ' Get-SmbPulseReport'
    Write-Host ''
}

# ----------------------------------------------------------------------------
# Watch-SmbPulse
# ----------------------------------------------------------------------------

function Watch-SmbPulse {
    <#
    .SYNOPSIS
        Live-Anzeige des aktuellen SMB-Durchsatzes.

    .DESCRIPTION
        Zeigt den aktuellen SMB-Durchsatz direkt aus den Windows Performance Countern.
        Es wird keine BLG-Datei benoetigt und der laufende Langzeit-Collector wird
        nicht beeinflusst.

        Standardmaessig werden angezeigt:
          - SMB Gesamt
          - SMB Lesen
          - SMB Schreiben

        Mit -PerShare werden zusaetzlich die einzelnen SMB-Freigaben angezeigt.

        Die Farben kodieren die Auslastung eines 1-Gbit/s-Links:
          - gruen: unter 700 Mbit/s
          - gelb: 700 - 900 Mbit/s
          - rot: ueber 900 Mbit/s

    .PARAMETER IntervalSeconds
        Aktualisierungsintervall in Sekunden (1-60, Standard 2).

    .PARAMETER PerShare
        Zeigt zusaetzlich die einzelnen SMB-Freigaben statt nur _Total.

    .EXAMPLE
        Watch-SmbPulse

    .EXAMPLE
        Watch-SmbPulse -IntervalSeconds 1 -PerShare
    #>


    [CmdletBinding()]
    param(
        [ValidateRange(1,60)]
        [int]$IntervalSeconds = 2,

        [switch]$PerShare
    )

    $SmbSet   = Get-SmbCounterSet
    $AllPaths = @($SmbSet.PathsWithInstances)

    if ($PerShare) {
        $CounterPaths = @(
            $AllPaths |
                Where-Object {
                    (Get-SmbCounterKind -Path $_) -ne $null
                } |
                Sort-Object -Unique
        )
    }
    else {
        $CounterPaths = @(
            $AllPaths |
                Where-Object {
                    $_ -match '\(_Total\)\\' -and
                    (Get-SmbCounterKind -Path $_) -ne $null
                } |
                Sort-Object -Unique
        )
    }

    if ($CounterPaths.Count -eq 0) {
        throw 'Keine passenden SMB-Durchsatzzaehler wurden gefunden.'
    }

    Write-MonitorBanner -Title 'SMBpulse Live Monitor' -Subtitle 'Aktuelle Durchsatzwerte aller Freigaben'

    Write-Host "Intervall: $IntervalSeconds Sekunde(n)"
    Write-Host 'Beenden: STRG+C'
    Write-Host ''

    while ($true) {
        try {
            $sample = Get-Counter -Counter $CounterPaths -ErrorAction Stop

            $rows = foreach ($counter in $sample.CounterSamples) {
                $kind = Get-SmbCounterKind -Path ([string]$counter.Path)
                if (-not $kind) {
                    continue
                }

                $value = [double]$counter.CookedValue
                if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) {
                    continue
                }

                $instance = [string]$counter.InstanceName

                if (-not $PerShare -or $instance -eq '_Total') {
                    $displayInstance = 'Alle Freigaben'
                }
                else {
                    $displayInstance = $instance
                }

                [PSCustomObject]@{
                    Freigabe = $displayInstance
                    Typ      = $kind
                    MBPS     = $value / 1MB
                    MbitSec  = ($value * 8) / 1000000
                }
            }

            $sorted = @(
                $rows |
                    Sort-Object Freigabe, @{
                        Expression = {
                            switch ($_.Typ) {
                                'Gesamt'    { 1 }
                                'Lesen'     { 2 }
                                'Schreiben' { 3 }
                                default     { 9 }
                            }
                        }
                    }
            )

            Clear-Host

            Write-Host 'SMBpulse Live Monitor' -ForegroundColor Cyan
            Write-Host ('=' * 76) -ForegroundColor Cyan
            Write-Host ("Zeit: {0}" -f (Get-Date -Format 'dd.MM.yyyy HH:mm:ss'))
            Write-Host ("Intervall: {0} Sekunde(n)" -f $IntervalSeconds)
            Write-Host 'Beenden: STRG+C'
            Write-Host ''
            Write-Host (" {0,-22}{1,-10}{2,12}{3,12}" -f 'Freigabe', 'Typ', 'MB/s', 'Mbit/s') -ForegroundColor Cyan

            if ($sorted.Count -eq 0) {
                Write-Host ' (momentan keine verwertbaren Messwerte)' -ForegroundColor Yellow
            }
            else {
                foreach ($row in $sorted) {
                    $color = Get-ThroughputColor -MbitSec $row.MbitSec

                    Write-Host (" {0,-22}" -f $row.Freigabe) -NoNewline
                    Write-Host ("{0,-10}" -f $row.Typ) -NoNewline -ForegroundColor DarkCyan
                    Write-Host ("{0,12}" -f ('{0:N2}' -f $row.MBPS)) -NoNewline -ForegroundColor $color
                    Write-Host ("{0,12}" -f ('{0:N2}' -f $row.MbitSec)) -ForegroundColor $color
                }
            }

            Write-Host ''
            Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Green
            Write-Host '< 700 Mbit/s' -NoNewline -ForegroundColor DarkGray
            Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Yellow
            Write-Host '700-900 Mbit/s' -NoNewline -ForegroundColor DarkGray
            Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Red
            Write-Host '> 900 Mbit/s (1-Gbit-Link)' -ForegroundColor DarkGray

            Start-Sleep -Seconds $IntervalSeconds
        }
        catch {
            Clear-Host

            Write-Host 'SMBpulse Live Monitor' -ForegroundColor Cyan
            Write-Host ('=' * 76) -ForegroundColor Cyan
            Write-Warn "Fehler beim Lesen der SMB-Counter: $($_.Exception.Message)"
            Write-Host ''

            Start-Sleep -Seconds $IntervalSeconds
        }
    }
}

# ----------------------------------------------------------------------------
# Get-SmbPulseReport
# ----------------------------------------------------------------------------

function Get-SmbPulseReport {
    <#
    .SYNOPSIS
        Wertet die BLG-Dateien von SMBpulse aus.

    .DESCRIPTION
        Liest alle bereits abgeschlossenen BLG-Segmente aus dem aktuellen sowie aus den
        vorherigen Messpfaden ein. Eine momentan noch aktive/unvollstaendige BLG-Datei
        wird automatisch uebersprungen.

        Ausgabe:
          - Konsolenuebersicht mit Durchschnitt, P95 und Maximum
          - SMBpulse-Report.csv
          - SMBpulse-Peaks.csv mit den groessten Einzelspitzen

    .PARAMETER TopPeaks
        Anzahl der groessten Einzelspitzen in der Peaks-CSV (1-500, Standard 50).

    .EXAMPLE
        Get-SmbPulseReport

    .EXAMPLE
        Get-SmbPulseReport -TopPeaks 100
    #>


    [CmdletBinding()]
    param(
        [ValidateRange(1,500)]
        [int]$TopPeaks = 50
    )

    $OutputDir = $Script:OutputDir
    $SummaryCsv = $Script:SummaryCsv
    $PeaksCsv   = $Script:PeaksCsv

    function Get-Percentile {
        param(
            [double[]]$Values,
            [ValidateRange(0,100)]
            [double]$Percentile
        )

        if (-not $Values -or $Values.Count -eq 0) {
            return 0
        }

        $sorted = @($Values | Sort-Object)
        $index  = [Math]::Ceiling(($Percentile / 100) * $sorted.Count) - 1

        if ($index -lt 0) {
            $index = 0
        }

        return [double]$sorted[$index]
    }

    function Get-CounterInfo {
        param($Counter)

        $path     = [string]$Counter.Path
        $instance = [string]$Counter.InstanceName

        if ($path -match 'SMB-Serverfreigaben|SMB Server Shares') {
            $kind = Get-SmbCounterKind -Path $path

            if (-not $kind) {
                return $null
            }

            $group = switch ($kind) {
                'Gesamt'    { 'SMB Gesamt' }
                'Lesen'     { 'SMB Lesen' }
                'Schreiben' { 'SMB Schreiben' }
            }

            return @{ Group = $group; Type = 'SMB'; Instance = '_Total' }
        }

        if ($path -match 'Netzwerkschnittstelle|Network Interface') {
            if ($path -match '\\(Gesamtanzahl Bytes/s|Bytes Total/sec)$') {
                return @{ Group = "Netzwerk Gesamt - $instance"; Type = 'Netzwerk'; Instance = $instance }
            }

            if ($path -match '\\(Empfangene Bytes/s|Bytes Received/sec)$') {
                return @{ Group = "Netzwerk Empfang - $instance"; Type = 'Netzwerk'; Instance = $instance }
            }

            if ($path -match '\\(Bytes gesendet/s|Bytes Sent/sec)$') {
                return @{ Group = "Netzwerk Senden - $instance"; Type = 'Netzwerk'; Instance = $instance }
            }
        }

        return $null
    }

    Write-MonitorBanner -Title 'SMBpulse Report' -Subtitle 'Auswertung der abgeschlossenen BLG-Segmente'

    New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null

    $files = @()

    foreach ($dir in $Script:LogDirs) {
        if (Test-Path $dir) {
            $files += Get-ChildItem -Path $dir -Filter '*.blg' -File -ErrorAction SilentlyContinue
        }
    }

    $files = @($files | Sort-Object FullName -Unique)

    if ($files.Count -eq 0) {
        Write-Warn 'Es wurden noch keine BLG-Dateien gefunden.'
        return
    }

    Write-Muted "Gefundene BLG-Dateien: $($files.Count)"
    Write-Host ''

    $samples       = New-Object System.Collections.Generic.List[object]
    $readableFiles = 0
    $skippedFiles  = 0

    foreach ($file in ($files | Sort-Object LastWriteTime)) {
        Write-Host -NoNewline "Pruefe $($file.FullName) ... "

        try {
            $data  = @(Import-Counter -Path $file.FullName -ErrorAction Stop)
            $added = 0

            foreach ($sampleSet in $data) {
                foreach ($counter in $sampleSet.CounterSamples) {
                    $info = Get-CounterInfo -Counter $counter

                    if (-not $info) {
                        continue
                    }

                    $value = [double]$counter.CookedValue

                    if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) {
                        continue
                    }

                    $samples.Add([PSCustomObject]@{
                        Timestamp = $sampleSet.Timestamp
                        Group     = $info.Group
                        Type      = $info.Type
                        Instance  = $info.Instance
                        BytesSec  = $value
                        MBsec     = [Math]::Round($value / 1MB, 3)
                        MbitSec   = [Math]::Round(($value * 8) / 1000000, 3)
                        Source    = $file.Name
                    })

                    $added++
                }
            }

            if ($added -gt 0) {
                Write-Ok "OK ($added Samples)"
                $readableFiles++
            }
            else {
                Write-Warn 'keine relevanten Daten'
            }
        }
        catch {
            Write-Warn 'noch aktiv/unlesbar - uebersprungen'
            $skippedFiles++
        }
    }

    if ($samples.Count -eq 0) {
        Write-Host ''
        Write-Warn 'Noch keine abgeschlossene BLG-Datei mit auswertbaren Daten vorhanden.'
        Write-Warn 'Bei der Standardkonfiguration entsteht nach etwa einer Stunde das erste abgeschlossene Segment.'
        return
    }

    $results = foreach ($group in ($samples | Group-Object Group)) {
        $values = @($group.Group | ForEach-Object { [double]$_.BytesSec })

        $avg = [double](($values | Measure-Object -Average).Average)
        $max = [double](($values | Measure-Object -Maximum).Maximum)
        $p95 = [double](Get-Percentile -Values $values -Percentile 95)

        $first = ($group.Group | Sort-Object Timestamp | Select-Object -First 1).Timestamp
        $last  = ($group.Group | Sort-Object Timestamp | Select-Object -Last 1).Timestamp

        [PSCustomObject]@{
            Counter               = $group.Name
            Samples               = $values.Count
            Von                   = $first
            Bis                   = $last
            'Durchschnitt MB/s'   = [Math]::Round($avg / 1MB, 2)
            'P95 MB/s'            = [Math]::Round($p95 / 1MB, 2)
            'Maximum MB/s'        = [Math]::Round($max / 1MB, 2)
            'Durchschnitt Mbit/s' = [Math]::Round(($avg * 8) / 1000000, 2)
            'P95 Mbit/s'          = [Math]::Round(($p95 * 8) / 1000000, 2)
            'Maximum Mbit/s'      = [Math]::Round(($max * 8) / 1000000, 2)
        }
    }

    $results = @($results | Sort-Object Counter)

    Write-Host ''
    Write-Ok "Lesbare BLG-Dateien: $readableFiles"
    Write-Warn "Uebersprungene BLG-Dateien: $skippedFiles"

    Write-SectionTitle 'Zusammenfassung'

    foreach ($result in $results) {
        Write-Host ''
        Write-Host $result.Counter -ForegroundColor DarkCyan
        Write-Muted (" {0} Messwerte ({1} - {2})" -f $result.Samples, $result.Von.ToString('dd.MM.yyyy HH:mm'), $result.Bis.ToString('dd.MM.yyyy HH:mm'))

        Write-Host (" Durchschnitt: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'Durchschnitt MB/s', $result.'Durchschnitt Mbit/s') -ForegroundColor Green
        Write-Host (" P95: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'P95 MB/s', $result.'P95 Mbit/s') -ForegroundColor Yellow
        Write-Host (" Maximum: {0,9:N2} MB/s {1,9:N2} Mbit/s" -f $result.'Maximum MB/s', $result.'Maximum Mbit/s') -ForegroundColor Red
    }

    $results |
        Export-Csv -Path $SummaryCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8

    $peaks = @(
        $samples |
            Sort-Object BytesSec -Descending |
            Select-Object -First $TopPeaks Timestamp, Group, Instance, MBsec, MbitSec, Source
    )

    $peaks |
        Export-Csv -Path $PeaksCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8

    Write-SectionTitle 'Ergebnisdateien'
    Write-Ok " $SummaryCsv"
    Write-Ok " $PeaksCsv"
    Write-Host ''

    return $results
}

# ----------------------------------------------------------------------------
# Uninstall-SmbPulse
# ----------------------------------------------------------------------------

function Uninstall-SmbPulse {
    <#
    .SYNOPSIS
        Entfernt alle systemseitigen Aenderungen von SMBpulse.

    .DESCRIPTION
        Stoppt und loescht:
          - SMB_Pulse
          - den alten Collector SMB_Throughput_Monitor
          - den alten Collector SMB_7Tage

        Standardmaessig werden ausserdem die von SMBpulse verwendeten
        Messdaten-Verzeichnisse geloescht. Vor dem Loeschen der Messdaten fragt das
        Cmdlet nach; mit -Force wird die Rueckfrage uebersprungen, mit -KeepLogs
        werden die Messdaten grundsaetzlich behalten.

        Windows Performance Logs and Alerts (PLA) kann die zuletzt beschriebene BLG-Datei
        nach "logman stop/delete" noch einige Sekunden geoeffnet halten. Das Cmdlet
        wartet deshalb gezielt auf die Dateifreigabe und wiederholt die Loeschung.

        Der Dienst "Performance Logs & Alerts" wird bewusst NICHT global beendet,
        damit andere Performance-Collector-Sets auf dem Server nicht gestoert werden.

    .PARAMETER KeepLogs
        Behaelt die Messdaten, stoppt und entfernt aber die Collector-Sets.

    .PARAMETER Force
        Ueberspringt die Rueckfrage vor dem Loeschen der Messdaten.

    .PARAMETER WaitSeconds
        Maximale Wartezeit auf die Freigabe geoeffneter BLG-/CSV-Dateien (5-120, Standard 30).

    .EXAMPLE
        Uninstall-SmbPulse

    .EXAMPLE
        Uninstall-SmbPulse -Force

    .EXAMPLE
        Uninstall-SmbPulse -KeepLogs -WaitSeconds 60
    #>


    [CmdletBinding()]
    param(
        [switch]$KeepLogs,

        [switch]$Force,

        [ValidateRange(5,120)]
        [int]$WaitSeconds = 30
    )

    Assert-Administrator

    $Collectors = $Script:LegacyCollectors + @($Script:CollectorName)
    $LogDirs    = $Script:LogDirs

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

        return ((Invoke-Logman -ArgumentList @('query', $Name) -Quiet) -eq 0)
    }

    function Stop-AndRemoveCollector {
        param([string]$Name)

        if (-not (Test-CollectorExists -Name $Name)) {
            Write-Muted "Datensammler '$Name' ist nicht vorhanden."
            return
        }

        Write-Host "Stoppe Datensammler '$Name' ..."
        $null = Invoke-Logman -ArgumentList @('stop', $Name) -Quiet

        # PLA arbeitet beim Schliessen der aktiven BLG teilweise asynchron.
        Start-Sleep -Seconds 1

        Write-Host "Loesche Datensammler '$Name' ..."
        $null = Invoke-Logman -ArgumentList @('delete', $Name) -Quiet

        for ($i = 0; $i -lt $WaitSeconds; $i++) {
            if (-not (Test-CollectorExists -Name $Name)) {
                Write-Ok ' entfernt'
                return
            }

            Start-Sleep -Seconds 1
        }

        throw "Der Datensammler '$Name' konnte innerhalb von $WaitSeconds Sekunden nicht vollstaendig entfernt werden."
    }

    function Remove-DirectoryWithRetry {
        param(
            [string]$Path,
            [int]$TimeoutSeconds
        )

        if (-not (Test-Path -LiteralPath $Path)) {
            Write-Muted "$Path ist nicht vorhanden."
            return
        }

        Write-Host "Loesche $Path ..."

        $lastError = $null

        for ($i = 0; $i -lt $TimeoutSeconds; $i++) {
            try {
                Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop

                if (-not (Test-Path -LiteralPath $Path)) {
                    Write-Ok ' entfernt'
                    return
                }
            }
            catch [System.IO.IOException] {
                $lastError = $_
            }
            catch [System.UnauthorizedAccessException] {
                $lastError = $_
            }

            if ($i -eq 0) {
                Write-Warn ' Datei noch durch Windows/PLA geoeffnet - warte auf Freigabe ...'
            }

            Start-Sleep -Seconds 1
        }

        # Noch einmal anzeigen, welche Dateien ggf. uebrig sind.
        $remaining = @(
            Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty FullName
        )

        Write-Host ''
        Write-Err "Das Verzeichnis konnte nach $TimeoutSeconds Sekunden noch nicht vollstaendig geloescht werden."

        if ($remaining.Count -gt 0) {
            Write-Warn 'Noch vorhandene Dateien:'
            $remaining | ForEach-Object { Write-Host " $_" }
        }

        if ($lastError) {
            throw $lastError
        }

        throw "Loeschen von '$Path' fehlgeschlagen."
    }

    Write-MonitorBanner -Title 'SMBpulse' -Subtitle 'Deinstallation'

    $removeLogs = -not $KeepLogs

    $planLines = @(
        ('Datensammler: ' + ($Collectors -join ', ')),
        ('Messpfad: ' + ($LogDirs -join ', '))
    )

    if ($KeepLogs) {
        $planLines += 'Messdaten: bleiben erhalten (-KeepLogs)'
    }

    Write-InfoBox -Title 'Durchgefuehrte Aenderungen' -Lines $planLines

    if ($removeLogs -and -not $Force) {
        Write-Warn 'Die Messdaten-Verzeichnisse werden vollstaendig geloescht.'
        $answer = Read-Host 'Fortfahren [J/N]'

        if ($answer -notmatch '^(j|ja|y|yes)$') {
            $removeLogs = $false
            Write-Warn 'Messdaten werden behalten.'
        }
    }

    Write-SectionTitle 'Datensammler entfernen'

    foreach ($collector in $Collectors) {
        Stop-AndRemoveCollector -Name $collector
    }

    if ($removeLogs) {
        Write-SectionTitle 'Messdaten loeschen'
        Write-Host 'Warte kurz auf die Freigabe eventuell noch geoeffneter BLG-Dateien ...'
        Start-Sleep -Seconds 2

        foreach ($dir in $LogDirs) {
            Remove-DirectoryWithRetry -Path $dir -TimeoutSeconds $WaitSeconds
        }
    }
    else {
        Write-SectionTitle 'Messdaten behalten'
        Write-Warn 'Die Messdaten werden nicht geloescht.'
    }

    Write-Host ''
    Write-Ok 'Deinstallation abgeschlossen.'
    Write-Host ''
}

Export-ModuleMember -Function 'Start-SmbPulse', 'Watch-SmbPulse', 'Get-SmbPulseReport', 'Uninstall-SmbPulse'