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 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 { $lines = @(& $logman @ArgumentList) foreach ($line in $lines) { Write-Host $line } } 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.' } $ClientSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Clientkontexte' -or $_.CounterSetName -eq 'SMB Client Contexts' } | Select-Object -First 1 $ShareSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Clientfreigaben' -or $_.CounterSetName -eq 'SMB Client Shares' } | Select-Object -First 1 $ClientPaths = @(Get-WildcardCounterPaths -CounterSet $ClientSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') $SharePaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') if ($ClientPaths.Count -eq 0) { Write-Warn 'Der Counter-Satz "SMB Client Contexts" wurde nicht gefunden - Client-Details werden nicht aufgezeichnet.' } if ($SharePaths.Count -eq 0) { Write-Warn 'Der Counter-Satz "SMB Client Shares" wurde nicht gefunden - Freigaben-Details werden nicht aufgezeichnet.' } $Counters = @($SmbCounters | Sort-Object -Unique) + @($NetCounters | Sort-Object -Unique) + @($ClientPaths | Sort-Object -Unique) + @($SharePaths | 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-, Netzwerk- und Client-Durchsatzes. .DESCRIPTION Zeigt den aktuellen Durchsatz direkt aus den Windows Performance Countern. Es wird keine BLG-Datei benoetigt und der laufende Langzeit-Collector wird nicht beeinflusst. Angezeigt werden: - SMB Gesamt / Lesen / Schreiben (optional alle Freigaben) - Netzwerk-Gesamtwert je NIC samt Auslastungsbalken relativ zur tatsaechlichen Link-Geschwindigkeit (via Get-NetAdapter) - Top-Clients und Top-Benutzer (derzeit aktivster Durchsatz) - Top-Freigaben Farben und Balken kodieren die Auslastung relativ zur jeweiligen Link-Geschwindigkeit: gruen unter 70 %, gelb 70-90 %, rot ueber 90 %. .PARAMETER IntervalSeconds Aktualisierungsintervall in Sekunden (1-60, Standard 2). .PARAMETER PerShare Zeigt zusaetzlich die einzelnen SMB-Freigaben statt nur _Total. .PARAMETER TopN Anzahl der Zeilen in den Live-Listen (Clients/Benutzer/Freigaben, 1-20, Standard 5). .EXAMPLE Watch-SmbPulse .EXAMPLE Watch-SmbPulse -IntervalSeconds 1 -PerShare -TopN 10 #> [CmdletBinding()] param( [ValidateRange(1,60)] [int]$IntervalSeconds = 2, [switch]$PerShare, [ValidateRange(1,20)] [int]$TopN = 5 ) $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.' } $AllSets = @(Get-Counter -ListSet *) $NetSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'Netzwerkschnittstelle' -or $_.CounterSetName -eq 'Network Interface' } | Select-Object -First 1 $NetCounterPaths = @(Get-WildcardCounterPaths -CounterSet $NetSet -CounterRegex '\\(Gesamtanzahl Bytes/s|Bytes Total/sec)$') $ClientSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Clientkontexte' -or $_.CounterSetName -eq 'SMB Client Contexts' } | Select-Object -First 1 $ShareSet = @($AllSets) | Where-Object { $_.CounterSetName -eq 'SMB-Clientfreigaben' -or $_.CounterSetName -eq 'SMB Client Shares' } | Select-Object -First 1 $ClientCounterPaths = @(Get-WildcardCounterPaths -CounterSet $ClientSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') $ShareCounterPaths = @(Get-WildcardCounterPaths -CounterSet $ShareSet -CounterRegex '\\(Datenbytes/Sek\.|Data bytes/sec)$') $refLinkMbps = 1000.0 $linkSpeeds = @{} try { $upAdapters = @(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.Status -eq 'Up' }) foreach ($adapter in $upAdapters) { $mbit = 0.0 if ($adapter.LinkSpeed -match '(\d+(?:\.\d+)?)\s*Gbps') { $mbit = [double]$Matches[1] * 1000 } elseif ($adapter.LinkSpeed -match '(\d+(?:\.\d+)?)\s*Mbps') { $mbit = [double]$Matches[1] } foreach ($key in @($adapter.Name, $adapter.InterfaceDescription, $adapter.InterfaceAlias, $adapter.ifAlias)) { if ($key) { $linkSpeeds[$key] = $mbit } } } $candidates = @($linkSpeeds.Values | Where-Object { $_ -gt 0 }) if ($candidates.Count -gt 0) { $refLinkMbps = [double](($candidates | Measure-Object -Maximum).Maximum) } } catch { Write-Warn 'Get-NetAdapter nicht verfuegbar - Massstab faellt auf 1 Gbit/s zurueck.' } function Write-LiveRow { param( [string]$Label, [string]$Kind, [double]$MBPS, [double]$MbitSec, [double]$RefMbps ) $percent = if ($RefMbps -gt 0) { ($MbitSec / $RefMbps) * 100 } else { 0 } $color = if ($percent -ge 90) { 'Red' } elseif ($percent -ge 70) { 'Yellow' } else { 'Green' } Write-Host (" {0,-24}" -f $Label) -NoNewline Write-Host ("{0,-10}" -f $Kind) -NoNewline -ForegroundColor DarkCyan Write-Host ("{0,10}" -f ('{0:N2}' -f $MBPS)) -NoNewline -ForegroundColor $color Write-Host ("{0,10}" -f ('{0:N2}' -f $MbitSec)) -NoNewline -ForegroundColor $color Write-Host (" {0,6:N1} % " -f $percent) -NoNewline -ForegroundColor $color Write-Host (Get-UsageBar -Percent $percent) -ForegroundColor $color } Write-MonitorBanner -Title 'SMBpulse Live Monitor' -Subtitle 'SMB-, Netz- und Client-Durchsatz in Echtzeit' Write-Host "Intervall: $IntervalSeconds Sekunde(n)" Write-Host "Link-Norm: {0:N0} Mbit/s (via Get-NetAdapter)" -f $refLinkMbps 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 } } } } ) $netRows = @() if ($NetCounterPaths.Count -gt 0) { try { $netSample = Get-Counter -Counter $NetCounterPaths -ErrorAction Stop foreach ($counter in $netSample.CounterSamples) { $value = [double]$counter.CookedValue if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) { continue } $nic = [string]$counter.InstanceName $linkMbps = $refLinkMbps foreach ($key in $linkSpeeds.Keys) { if ($nic -like ('*' + $key + '*')) { $linkMbps = $linkSpeeds[$key] break } } $netRows += [PSCustomObject]@{ Nic = $nic MBPS = $value / 1MB MbitSec = ($value * 8) / 1000000 LinkMbps = [double]$linkMbps } } } catch { $netRows = @() } } $clientRows = @() $userRows = @() if ($ClientCounterPaths.Count -gt 0) { try { $clientSample = Get-Counter -Counter $ClientCounterPaths -ErrorAction Stop $clientMap = @{} $userMap = @{} foreach ($counter in $clientSample.CounterSamples) { $value = [double]$counter.CookedValue if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) { continue } $parsed = Get-SmbClientParts -Instance ([string]$counter.InstanceName) if (-not $parsed) { continue } $mbitSec = ($value * 8) / 1000000 if (-not $clientMap.ContainsKey($parsed.Client)) { $clientMap[$parsed.Client] = 0.0 } $clientMap[$parsed.Client] += $mbitSec if ($parsed.User) { if (-not $userMap.ContainsKey($parsed.User)) { $userMap[$parsed.User] = 0.0 } $userMap[$parsed.User] += $mbitSec } } $clientRows = @( $clientMap.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First $TopN | ForEach-Object { [PSCustomObject]@{ Name = $_.Key; MbitSec = $_.Value } } ) $userRows = @( $userMap.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First $TopN | ForEach-Object { [PSCustomObject]@{ Name = $_.Key; MbitSec = $_.Value } } ) } catch { $clientRows = @() $userRows = @() } } $shareRows = @() if ($ShareCounterPaths.Count -gt 0) { try { $shareSample = Get-Counter -Counter $ShareCounterPaths -ErrorAction Stop $shareMap = @{} foreach ($counter in $shareSample.CounterSamples) { $value = [double]$counter.CookedValue if ([double]::IsNaN($value) -or [double]::IsInfinity($value) -or $value -lt 0) { continue } $share = ($counter.InstanceName -split '\\')[-1] if (-not $share) { continue } $mbitSec = ($value * 8) / 1000000 if (-not $shareMap.ContainsKey($share)) { $shareMap[$share] = 0.0 } $shareMap[$share] += $mbitSec } $shareRows = @( $shareMap.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First $TopN | ForEach-Object { [PSCustomObject]@{ Name = $_.Key; MbitSec = $_.Value } } ) } catch { $shareRows = @() } } Clear-Host Write-Host 'SMBpulse Live Monitor' -ForegroundColor Cyan Write-Host ('=' * 100) -ForegroundColor Cyan Write-Host ("Zeit: {0}" -f (Get-Date -Format 'dd.MM.yyyy HH:mm:ss')) Write-Host ("Intervall: {0} Sekunde(n) | Link-Norm: {1:N0} Mbit/s" -f $IntervalSeconds, $refLinkMbps) Write-Host 'Beenden: STRG+C' Write-Host '' Write-SectionTitle 'SMB-Durchsatz' if ($sorted.Count -eq 0) { Write-Host ' (momentan keine verwertbaren Messwerte)' -ForegroundColor Yellow } else { foreach ($row in $sorted) { Write-LiveRow -Label $row.Freigabe -Kind $row.Typ -MBPS $row.MBPS -MbitSec $row.MbitSec -RefMbps $refLinkMbps } } Write-Host '' Write-SectionTitle 'Netzwerk (je Schnittstelle)' if ($netRows.Count -eq 0) { Write-Host ' (keine Netzwerk-Gesamtwerte verfuegbar)' -ForegroundColor DarkGray } else { Write-Host (" {0,-24}{1,-10}{2,10}{3,10}{4,11}{5,-5}{6}" -f 'Schnittstelle', 'Link', 'MB/s', 'Mbit/s', 'Auslast.', '', 'Balken') -ForegroundColor DarkGray foreach ($row in $netRows) { $percent = if ($row.LinkMbps -gt 0) { ($row.MbitSec / $row.LinkMbps) * 100 } else { 0 } $color = if ($percent -ge 90) { 'Red' } elseif ($percent -ge 70) { 'Yellow' } else { 'Green' } Write-Host (" {0,-24}" -f $row.Nic) -NoNewline Write-Host ("{0,-10}" -f ('{0:N0}' -f $row.LinkMbps)) -NoNewline -ForegroundColor DarkCyan Write-Host ("{0,10}" -f ('{0:N2}' -f $row.MBPS)) -NoNewline -ForegroundColor $color Write-Host ("{0,10}" -f ('{0:N2}' -f $row.MbitSec)) -NoNewline -ForegroundColor $color Write-Host (" {0,6:N1} % " -f $percent) -NoNewline -ForegroundColor $color Write-Host (Get-UsageBar -Percent $percent) -ForegroundColor $color } } Write-Host '' Write-SectionTitle ("Top-Clients (aktuell, Top {0})" -f $TopN) if ($clientRows.Count -eq 0) { Write-Host ' (keine Client-Daten verfuegbar)' -ForegroundColor DarkGray } else { Write-Host (" {0,-24}{1,10}{2,11}" -f 'Client', 'Mbit/s', 'Auslast.') -ForegroundColor DarkGray foreach ($row in $clientRows) { $percent = ($row.MbitSec / $refLinkMbps) * 100 $color = if ($percent -ge 90) { 'Red' } elseif ($percent -ge 70) { 'Yellow' } else { 'Green' } Write-Host (" {0,-24}" -f $row.Name) -NoNewline Write-Host ("{0,10}" -f ('{0:N2}' -f $row.MbitSec)) -NoNewline -ForegroundColor $color Write-Host (" {0,6:N1} % " -f $percent) -NoNewline -ForegroundColor $color Write-Host (Get-UsageBar -Percent $percent) -ForegroundColor $color } } Write-Host '' Write-SectionTitle ("Top-Benutzer (aktuell, Top {0})" -f $TopN) if ($userRows.Count -eq 0) { Write-Host ' (keine Benutzer-Daten verfuegbar)' -ForegroundColor DarkGray } else { Write-Host (" {0,-24}{1,10}{2,11}" -f 'Benutzer', 'Mbit/s', 'Auslast.') -ForegroundColor DarkGray foreach ($row in $userRows) { $percent = ($row.MbitSec / $refLinkMbps) * 100 $color = if ($percent -ge 90) { 'Red' } elseif ($percent -ge 70) { 'Yellow' } else { 'Green' } Write-Host (" {0,-24}" -f $row.Name) -NoNewline Write-Host ("{0,10}" -f ('{0:N2}' -f $row.MbitSec)) -NoNewline -ForegroundColor $color Write-Host (" {0,6:N1} % " -f $percent) -NoNewline -ForegroundColor $color Write-Host (Get-UsageBar -Percent $percent) -ForegroundColor $color } } Write-Host '' Write-SectionTitle ("Top-Freigaben (aktuell, Top {0})" -f $TopN) if ($shareRows.Count -eq 0) { Write-Host ' (keine Freigaben-Daten verfuegbar)' -ForegroundColor DarkGray } else { Write-Host (" {0,-24}{1,10}{2,11}" -f 'Freigabe', 'Mbit/s', 'Auslast.') -ForegroundColor DarkGray foreach ($row in $shareRows) { $percent = ($row.MbitSec / $refLinkMbps) * 100 $color = if ($percent -ge 90) { 'Red' } elseif ($percent -ge 70) { 'Yellow' } else { 'Green' } Write-Host (" {0,-24}" -f $row.Name) -NoNewline Write-Host ("{0,10}" -f ('{0:N2}' -f $row.MbitSec)) -NoNewline -ForegroundColor $color Write-Host (" {0,6:N1} % " -f $percent) -NoNewline -ForegroundColor $color Write-Host (Get-UsageBar -Percent $percent) -ForegroundColor $color } } Write-Host '' Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Green Write-Host '< 70 %' -NoNewline -ForegroundColor DarkGray Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Yellow Write-Host '70-90 %' -NoNewline -ForegroundColor DarkGray Write-Host (' ' + [char]0x25A0 + ' ') -NoNewline -ForegroundColor Red Write-Host ('> 90 % (Ma\u00dfstab: {0:N0} Mbit/s)' -f $refLinkMbps) -ForegroundColor DarkGray Start-Sleep -Seconds $IntervalSeconds } catch { Clear-Host Write-Host 'SMBpulse Live Monitor' -ForegroundColor Cyan Write-Host ('=' * 100) -ForegroundColor Cyan Write-Warn "Fehler beim Lesen der Performance-Counter: $($_.Exception.Message)" Write-Host '' Start-Sleep -Seconds $IntervalSeconds } } } # ---------------------------------------------------------------------------- # Interne Funktionen der Auswertung und des HTML-Reports # ---------------------------------------------------------------------------- function Get-SmbClientParts { param( [Parameter(Mandatory)] [string]$Instance ) $parts = @($Instance -split '\\' | Where-Object { $_ -ne '' }) if ($parts.Count -lt 2) { return $null } $artifactMatch = '(?i)(StatusFiles|LocalUser|Logon|Disconnected|Anonymous)' $user = $parts[$parts.Count - 2] + '\' + $parts[$parts.Count - 1] $tokens = $parts[$parts.Count - 3] -split '[:;]' $client = $tokens[0] if (-not $client -or $client -notmatch '\d+\.\d+\.\d+\.\d+') { $client = ($Instance -replace ':\d+$', '') } if ($parts.Count -ge 3 -and $user -notmatch $artifactMatch -and $client -notmatch $artifactMatch) { return @{ Client = $client User = $user } } if ($parts.Count -lt 3) { return $null } return $null } function Get-WildcardCounterPaths { param( [Object]$CounterSet, [string]$CounterRegex ) if (-not $CounterSet) { return @() } return @( $CounterSet.PathsWithInstances | Where-Object { $_ -match $CounterRegex } | ForEach-Object { $_ -replace '\([^)]*\)', '(*)' } | Sort-Object -Unique ) } function Get-UsageBar { param([double]$Percent) if ($Percent -gt 100) { $Percent = 100 } $full = [Math]::Floor($Percent / 10) if ($full -gt 10) { $full = 10 } return ([string][char]0x2588 * $full) + ([string][char]0x2591 * (10 - $full)) } function Get-SampleIntervalSeconds { param( [object[]]$OrderedSamples ) $count = $OrderedSamples.Count if ($count -lt 2) { return 15.0 } $deltas = [System.Collections.Generic.List[double]]::new() for ($i = 1; $i -lt $count; $i++) { $delta = ($OrderedSamples[$i].Timestamp - $OrderedSamples[$i - 1].Timestamp).TotalSeconds if ($delta -gt 0) { $deltas.Add($delta) } } if ($deltas.Count -eq 0) { return 15.0 } $sorted = @($deltas | Sort-Object) if ($sorted.Count % 2 -eq 0) { $upper = [int][Math]::Floor($sorted.Count / 2) return ([double]$sorted[$upper - 1] + [double]$sorted[$upper]) / 2.0 } return [double]$sorted[[int][Math]::Floor($sorted.Count / 2)] } function Get-TimeBucketStart { param( [datetime]$Timestamp, [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity ) switch ($Granularity) { 'Hour' { return $Timestamp.Date.AddHours($Timestamp.Hour) } 'Day' { return $Timestamp.Date } 'Week' { $date = $Timestamp.Date $delta = ([DayOfWeek]::Monday - $date.DayOfWeek) if ($delta -gt 0) { $delta -= 7 } return $date.AddDays($delta) } } } function Get-TimeBucketLabel { param( [datetime]$Timestamp, [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity ) switch ($Granularity) { 'Hour' { return $Timestamp.ToString('dd.MM.yyyy HH:00') } 'Day' { return $Timestamp.ToString('dd.MM.yyyy') } 'Week' { $week = [Globalization.CultureInfo]::InvariantCulture.Calendar.GetWeekOfYear( $Timestamp, [Globalization.CalendarWeekRule]::FirstFourDayWeek, [DayOfWeek]::Monday ) return '{0:D4}-W{1:00}' -f $Timestamp.Year, $week } } } function Get-UsageRows { param( [ValidateSet('Hour', 'Day', 'Week')] [string]$Granularity, [object[]]$SmbSamples, [object[]]$NetSamples, [double]$SmbIntervalSeconds, [double]$NetIntervalSeconds ) $buckets = @{} foreach ($sample in $SmbSamples) { $key = Get-TimeBucketStart -Timestamp $sample.Timestamp -Granularity $Granularity if (-not $buckets.ContainsKey($key)) { $buckets[$key] = @{ SmbBytes = 0.0; NetBytes = 0.0 } } $buckets[$key].SmbBytes += $sample.BytesSec } foreach ($sample in $NetSamples) { $key = Get-TimeBucketStart -Timestamp $sample.Timestamp -Granularity $Granularity if (-not $buckets.ContainsKey($key)) { $buckets[$key] = @{ SmbBytes = 0.0; NetBytes = 0.0 } } $buckets[$key].NetBytes += $sample.BytesSec } $rows = foreach ($key in @($buckets.Keys | Sort-Object)) { $bucket = $buckets[$key] [PSCustomObject]@{ Zeitraum = Get-TimeBucketLabel -Timestamp $key -Granularity $Granularity 'SMB gesamt (GB)' = [Math]::Round(($bucket.SmbBytes * $SmbIntervalSeconds) / 1GB, 2) 'Netzwerk gesamt (GB)' = [Math]::Round(($bucket.NetBytes * $NetIntervalSeconds) / 1GB, 2) } } return @($rows) } function Get-SmbPulseUsage { param( [object[]]$Samples ) $smbSamples = @( $Samples | Where-Object { $_.Type -eq 'SMB' -and $_.Group -eq 'SMB Gesamt' } | Sort-Object Timestamp ) $netSamples = @( $Samples | Where-Object { $_.Type -eq 'Netzwerk' -and $_.Group -like 'Netzwerk Gesamt - *' } | Sort-Object Timestamp ) $smbInterval = Get-SampleIntervalSeconds -OrderedSamples $smbSamples $netInterval = Get-SampleIntervalSeconds -OrderedSamples $netSamples return @{ Hourly = Get-UsageRows -Granularity Hour -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval Daily = Get-UsageRows -Granularity Day -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval Weekly = Get-UsageRows -Granularity Week -SmbSamples $smbSamples -NetSamples $netSamples -SmbIntervalSeconds $smbInterval -NetIntervalSeconds $netInterval } } function Write-UsageTable { param( [string]$Title, [object[]]$Rows ) Write-SectionTitle $Title if (-not $Rows -or $Rows.Count -eq 0) { Write-Warn ' keine Daten vorhanden' Write-Host '' return } Write-Host (' {0,-22} {1,18} {2,18}' -f 'Zeitraum', 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)') -ForegroundColor DarkGray foreach ($row in $Rows) { Write-Host (' {0,-22} {1,18:N2} {2,18:N2}' -f $row.Zeitraum, $row.'SMB gesamt (GB)', $row.'Netzwerk gesamt (GB)') } Write-Host '' } function Write-TopTable { param( [string]$Title, [object[]]$Rows, [string]$NameHeader ) Write-SectionTitle $Title if (-not $Rows -or $Rows.Count -eq 0) { Write-Warn " keine $NameHeader-Daten vorhanden (Collector ohne Client-Counter oder noch kein Segment abgeschlossen)" Write-Host '' return } Write-Host (' {0,-32}{1,16}{2,12}' -f $NameHeader, 'Datenmenge (GB)', 'Anteil (%)') -ForegroundColor DarkGray foreach ($row in $Rows) { Write-Host (' {0,-32}{1,16:N2}{2,11:N1} %' -f $row.Name, $row.'Datenmenge (GB)', $row.'Anteil (%)') } Write-Host '' } function ConvertTo-HtmlText { param([string]$Value) return [Net.WebUtility]::HtmlEncode([string]$Value) } function Format-Decimal { param( [double]$Value, [int]$Digits = 2 ) return $Value.ToString('N' + $Digits, [Globalization.CultureInfo]::InvariantCulture) } function Get-UsageTableHtml { param( [object[]]$Rows ) $max = 0.0 foreach ($row in $Rows) { $smb = [double]$row.'SMB gesamt (GB)' $net = [double]$row.'Netzwerk gesamt (GB)' if ($smb -gt $max) { $max = $smb } if ($net -gt $max) { $max = $net } } if ($max -le 0) { $max = 1 } $html = [Text.StringBuilder]::new() [void]$html.Append('<table><thead><tr><th>Zeitraum</th><th>SMB gesamt (GB)</th><th>Netzwerk gesamt (GB)</th></tr></thead><tbody>') foreach ($row in $Rows) { $smb = [double]$row.'SMB gesamt (GB)' $net = [double]$row.'Netzwerk gesamt (GB)' $smbPercent = [Math]::Round(($smb / $max) * 100, 1) $netPercent = [Math]::Round(($net / $max) * 100, 1) [void]$html.Append('<tr>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $row.Zeitraum) + '</td>') [void]$html.Append(('<td><div class="num" data-color="smb">{0} GB</div><div class="bar" data-color="smb"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $smb), $smbPercent)) [void]$html.Append(('<td><div class="num" data-color="net">{0} GB</div><div class="bar" data-color="net"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $net), $netPercent)) [void]$html.Append('</tr>') } [void]$html.Append('</tbody></table>') return $html.ToString() } function Get-PeaksTableHtml { param( [object[]]$Peaks ) $html = [Text.StringBuilder]::new() [void]$html.Append('<table><thead><tr><th>Nr.</th><th>Zeitpunkt</th><th>Messgruppe</th><th>Instanz</th><th>MB/s</th><th>Mbit/s</th></tr></thead><tbody>') $index = 1 foreach ($peak in $Peaks) { [void]$html.Append('<tr>') [void]$html.Append('<td>' + $index + '</td>') [void]$html.Append('<td>' + $peak.Timestamp.ToString('dd.MM.yyyy HH:mm') + '</td>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $peak.Group) + '</td>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $peak.Instance) + '</td>') [void]$html.Append('<td>' + (Format-Decimal $peak.MBsec) + '</td>') [void]$html.Append('<td>' + (Format-Decimal $peak.MbitSec) + '</td>') [void]$html.Append('</tr>') $index++ } [void]$html.Append('</tbody></table>') return $html.ToString() } function Get-TopTableHtml { param( [object[]]$Rows, [string]$NameHeader ) if (-not $Rows -or $Rows.Count -eq 0) { return '<div class="hint">keine Daten vorhanden</div>' } $max = [double]$Rows[0].'Datenmenge (GB)' if ($max -le 0) { $max = 1 } $html = [Text.StringBuilder]::new() [void]$html.Append(('<table><thead><tr><th>{0}</th><th>Datenmenge (GB)</th><th>Anteil (%)</th></tr></thead><tbody>' -f $NameHeader)) foreach ($row in $Rows) { $gb = [double]$row.'Datenmenge (GB)' $pct = [double]$row.'Anteil (%)' $width = [Math]::Round(($gb / $max) * 100, 1) [void]$html.Append('<tr>') [void]$html.Append('<td>' + (ConvertTo-HtmlText -Value $row.Name) + '</td>') [void]$html.Append(('<td><div class="num" data-color="smb">{0} GB</div><div class="bar" data-color="smb"><span style="width:{1}%"></span></div></td>' -f (Format-Decimal $gb), $width)) [void]$html.Append(('<td>{0}</td>' -f (Format-Decimal $pct))) [void]$html.Append('</tr>') } [void]$html.Append('</tbody></table>') return $html.ToString() } function New-SmbPulseHtmlReport { param( [Parameter(Mandatory)] [string]$OutputPath, [string]$LogoPath, [hashtable]$Summary, [object[]]$Daily, [object[]]$Weekly, [object[]]$Hourly, [object[]]$Peaks, [object[]]$TopClients, [object[]]$TopUsers, [object[]]$TopShares ) $css = @' body { margin: 0; padding: 0; font-family: 'Segoe UI', system-ui, -apple-system, Arial, sans-serif; background: #eef2f7; color: #1f2937; } .wrap { max-width: 980px; margin: 0 auto; padding: 24px 16px 48px; } .hero { background: linear-gradient(135deg, #4f46e5, #0ea5e9); color: #ffffff; border-radius: 16px; padding: 22px 26px; display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; box-shadow: 0 8px 24px rgba(79, 70, 229, 0.25); } .logo { height: 56px; width: auto; border-radius: 10px; background: #ffffff; padding: 6px; } .hero-text h1 { margin: 0; font-size: 24px; } .hero-text p { margin: 4px 0 0; opacity: 0.92; font-size: 14px; } .meta { text-align: right; font-size: 12px; opacity: 0.85; line-height: 1.5; } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px; margin: 22px 0; } .card { background: #ffffff; border-radius: 14px; padding: 16px 18px; box-shadow: 0 2px 8px rgba(31, 41, 55, 0.06); } .card .kpi { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; } .card .val { font-size: 22px; font-weight: 700; margin-top: 6px; } .card .sub { font-size: 12px; color: #9ca3af; margin-top: 2px; } section.box { background: #ffffff; border-radius: 14px; padding: 18px 22px; margin: 18px 0; box-shadow: 0 2px 8px rgba(31, 41, 55, 0.06); } section.box h2 { margin: 0 0 12px; font-size: 18px; } .hint { font-size: 12px; color: #9ca3af; margin-top: 8px; } table { width: 100%; border-collapse: collapse; font-size: 14px; } th { text-align: left; color: #6b7280; font-weight: 600; padding: 8px 10px; border-bottom: 2px solid #e5e7eb; } td { padding: 8px 10px; border-bottom: 1px solid #f0f2f5; } tbody tr:nth-child(even) td { background: #fafbfc; } .num { font-weight: 600; } .num[data-color="smb"] { color: #2563eb; } .num[data-color="net"] { color: #10b981; } .bar { background: #eef2f7; border-radius: 999px; height: 8px; margin-top: 5px; overflow: hidden; } .bar span { display: block; height: 100%; border-radius: 999px; } .bar[data-color="smb"] span { background: #2563eb; } .bar[data-color="net"] span { background: #10b981; } details { margin-top: 4px; } summary { cursor: pointer; font-weight: 600; color: #2563eb; padding: 6px 0; } footer { text-align: center; color: #9ca3af; font-size: 12px; margin-top: 26px; } '@ $logoImage = '' if (Test-Path -LiteralPath $LogoPath -PathType Leaf) { $bytes = [IO.File]::ReadAllBytes($LogoPath) $logoImage = '<img src="data:image/png;base64,' + [Convert]::ToBase64String($bytes) + '" alt="SMBpulse" class="logo">' } $html = [Text.StringBuilder]::new() [void]$html.Append('<!DOCTYPE html>') [void]$html.Append('<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>SMBpulse Report</title><style>') [void]$html.Append($css) [void]$html.Append('</style></head><body><div class="wrap">') [void]$html.Append('<header class="hero">') [void]$html.Append($logoImage) [void]$html.Append('<div class="hero-text"><h1>SMBpulse - Durchsatz-Report</h1><p>Langzeitmessung des SMB- und Netzwerkdurchsatzes</p></div>') [void]$html.Append(('<div class="meta">Erstellt am {0}<br>Zeitraum: {1} - {2}</div>' -f (ConvertTo-HtmlText -Value $Summary.Generated), (ConvertTo-HtmlText -Value $Summary.PeriodFrom), (ConvertTo-HtmlText -Value $Summary.PeriodTo))) [void]$html.Append('</header>') $cards = @( @{ Kpi = 'SMB gesamt'; Val = (Format-Decimal $Summary.SmbTotalGb) + ' GB'; Sub = $Summary.DurationText } @{ Kpi = 'Netzwerk gesamt'; Val = (Format-Decimal $Summary.NetTotalGb) + ' GB'; Sub = 'alle Interfaces' } @{ Kpi = 'Durchschnitt'; Val = (Format-Decimal $Summary.AverageMbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'P95'; Val = (Format-Decimal $Summary.P95Mbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'Maximum'; Val = (Format-Decimal $Summary.MaximumMbs) + ' MB/s'; Sub = 'SMB' } @{ Kpi = 'Messdauer'; Val = $Summary.DurationText; Sub = 'abgeschlossene Segmente' } ) [void]$html.Append('<div class="cards">') foreach ($card in $cards) { [void]$html.Append('<div class="card">') [void]$html.Append('<div class="kpi">' + (ConvertTo-HtmlText -Value $card.Kpi) + '</div>') [void]$html.Append('<div class="val">' + (ConvertTo-HtmlText -Value $card.Val) + '</div>') [void]$html.Append('<div class="sub">' + (ConvertTo-HtmlText -Value $card.Sub) + '</div>') [void]$html.Append('</div>') } [void]$html.Append('</div>') [void]$html.Append('<section class="box"><h2>Tagesuebersicht</h2>') [void]$html.Append((Get-UsageTableHtml -Rows $Daily)) [void]$html.Append('<div class="hint">Balken relativ zum groessten Tageswert.</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Wochenuebersicht</h2>') [void]$html.Append((Get-UsageTableHtml -Rows $Weekly)) [void]$html.Append('<div class="hint">Kalenderwochen nach ISO 8601 (Montag als Wochenbeginn).</div>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Stundenuebersicht</h2>') [void]$html.Append(('<details><summary>Stundenwerte anzeigen ({0} Stunden)</summary>' -f $Hourly.Count)) [void]$html.Append((Get-UsageTableHtml -Rows $Hourly)) [void]$html.Append('</details>') [void]$html.Append('</section>') [void]$html.Append('<section class="box"><h2>Spitzenwerte</h2>') [void]$html.Append((Get-PeaksTableHtml -Peaks $Peaks)) [void]$html.Append('</section>') if ($TopClients -and $TopClients.Count -gt 0) { [void]$html.Append('<section class="box"><h2>Top Clients</h2>') [void]$html.Append((Get-TopTableHtml -Rows $TopClients -NameHeader 'Client')) [void]$html.Append('<div class="hint">Uebertragene Datenmenge je Client ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') } if ($TopUsers -and $TopUsers.Count -gt 0) { [void]$html.Append('<section class="box"><h2>Top Benutzer</h2>') [void]$html.Append((Get-TopTableHtml -Rows $TopUsers -NameHeader 'Benutzer')) [void]$html.Append('<div class="hint">Uebertragene Datenmenge je Benutzer (Domaene\Benutzer) ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') } if ($TopShares -and $TopShares.Count -gt 0) { [void]$html.Append('<section class="box"><h2>Top Freigaben</h2>') [void]$html.Append((Get-TopTableHtml -Rows $TopShares -NameHeader 'Freigabe')) [void]$html.Append('<div class="hint">Uebertragene Datenmenge je SMB-Freigabe ueber den gesamten Messzeitraum.</div>') [void]$html.Append('</section>') } [void]$html.Append(('<footer>SMBpulse {0} - {1}</footer>' -f (ConvertTo-HtmlText -Value $Summary.Version), (ConvertTo-HtmlText -Value $Summary.Generated))) [void]$html.Append('</div></body></html>') $parent = Split-Path -Path $OutputPath -Parent if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } [IO.File]::WriteAllText($OutputPath, $html.ToString(), [Text.UTF8Encoding]::new($false)) } # ---------------------------------------------------------------------------- # 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 - Zeitaufschluesselung (stundenweise, tagesweise, wochenweise) - Top Clients / Top Benutzer / Top Freigaben (Gesamtzeitraum) - SMBpulse-Report.csv - SMBpulse-Peaks.csv mit den groessten Einzelspitzen - SMBpulse-Stunden.csv / SMBpulse-Tage.csv / SMBpulse-Wochen.csv - SMBpulse-Clients.csv / SMBpulse-Benutzer.csv / SMBpulse-Freigaben.csv - SMBpulse-Report.html (selbst-enthaltender HTML-Report) .PARAMETER TopPeaks Anzahl der groessten Einzelspitzen in der Peaks-CSV (1-500, Standard 50). .PARAMETER TopClients Anzahl der groessten Clients/Benutzer/Freigaben in den Top-Listen und CSVs (1-100, Standard 20). .EXAMPLE Get-SmbPulseReport .EXAMPLE Get-SmbPulseReport -TopPeaks 100 .EXAMPLE Get-SmbPulseReport -TopClients 10 #> [CmdletBinding()] param( [ValidateRange(1,500)] [int]$TopPeaks = 50, [ValidateRange(1,100)] [int]$TopClients = 20 ) $OutputDir = $Script:OutputDir $SummaryCsv = $Script:SummaryCsv $PeaksCsv = $Script:PeaksCsv $HourCsv = Join-Path $OutputDir 'SMBpulse-Stunden.csv' $DayCsv = Join-Path $OutputDir 'SMBpulse-Tage.csv' $WeekCsv = Join-Path $OutputDir 'SMBpulse-Wochen.csv' $ClientCsv = Join-Path $OutputDir 'SMBpulse-Clients.csv' $UserCsv = Join-Path $OutputDir 'SMBpulse-Benutzer.csv' $ShareCsv = Join-Path $OutputDir 'SMBpulse-Freigaben.csv' $HtmlPath = Join-Path $OutputDir 'SMBpulse-Report.html' $LogoPath = Join-Path $PSScriptRoot 'logo.png' 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 } } } if ($path -match 'SMB-Clientkontexte|SMB Client Contexts') { if ($path -match '\\(Datenbytes/Sek\.|Data bytes/sec)$') { $parsed = Get-SmbClientParts -Instance $instance if (-not $parsed) { return $null } return @{ Group = "Client $($parsed.Client)" Type = 'Client' Instance = $instance Client = $parsed.Client User = $parsed.User } } } if ($path -match 'SMB-Clientfreigaben|SMB Client Shares') { if ($path -match '\\(Datenbytes/Sek\.|Data bytes/sec)$') { $share = ($instance -split '\\')[-1] if (-not $share) { return $null } return @{ Group = "Freigabe $share" Type = 'Share' Instance = $instance Share = $share } } } 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 Client = $info.Client User = $info.User Share = $info.Share 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 $usage = Get-SmbPulseUsage -Samples $samples $hourRows = $usage.Hourly $dayRows = $usage.Daily $weekRows = $usage.Weekly Write-UsageTable -Title 'Datenmenge nach Stunde' -Rows $hourRows Write-UsageTable -Title 'Datenmenge nach Tag' -Rows $dayRows Write-UsageTable -Title 'Datenmenge nach Woche' -Rows $weekRows $hourRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $HourCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $dayRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $DayCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $weekRows | Select-Object Zeitraum, 'SMB gesamt (GB)', 'Netzwerk gesamt (GB)' | Export-Csv -Path $WeekCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 $clientSamples = @($samples | Where-Object { $_.Type -eq 'Client' } | Sort-Object Timestamp) $shareSamples = @($samples | Where-Object { $_.Type -eq 'Share' } | Sort-Object Timestamp) $clientDt = Get-SampleIntervalSeconds -OrderedSamples $clientSamples $shareDt = Get-SampleIntervalSeconds -OrderedSamples $shareSamples $topClients = @( if ($clientSamples.Count -gt 0) { $clientGroups = @($clientSamples | Group-Object Client) $clientTotal = [double](($clientGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $clientGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $clientDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($clientTotal -gt 0) { [Math]::Round(($gb / $clientTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) $topUsers = @( if ($clientSamples.Count -gt 0) { $userSamples = @($clientSamples | Where-Object { $_.User }) $userGroups = @($userSamples | Group-Object User) $userTotal = [double](($userGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $userGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $clientDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($userTotal -gt 0) { [Math]::Round(($gb / $userTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) $topShares = @( if ($shareSamples.Count -gt 0) { $shareGroups = @($shareSamples | Group-Object Share) $shareTotal = [double](($shareGroups | ForEach-Object { [double](($_.Group | Measure-Object BytesSec -Sum).Sum) } | Measure-Object -Sum).Sum) $shareGroups | ForEach-Object { $gb = [double](($_.Group | Measure-Object BytesSec -Sum).Sum) * $shareDt / 1GB [PSCustomObject]@{ Name = $_.Name 'Datenmenge (GB)' = [Math]::Round($gb, 2) 'Anteil (%)' = if ($shareTotal -gt 0) { [Math]::Round(($gb / $shareTotal) * 100, 1) } else { 0 } } } | Sort-Object 'Datenmenge (GB)' -Descending | Select-Object -First $TopClients } ) Write-TopTable -Title 'Top Clients (nach Datenmenge)' -Rows $topClients -NameHeader 'Client' Write-TopTable -Title 'Top Benutzer (nach Datenmenge)' -Rows $topUsers -NameHeader 'Benutzer' Write-TopTable -Title 'Top Freigaben (nach Datenmenge)' -Rows $topShares -NameHeader 'Freigabe' if ($topClients.Count -gt 0) { $topClients | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $ClientCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } if ($topUsers.Count -gt 0) { $topUsers | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $UserCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } if ($topShares.Count -gt 0) { $topShares | Select-Object Name, 'Datenmenge (GB)', 'Anteil (%)' | Export-Csv -Path $ShareCsv -Delimiter ';' -NoTypeInformation -Encoding UTF8 } $smbResult = @($results | Where-Object { $_.Counter -eq 'SMB Gesamt' }) | Select-Object -First 1 $averageMbs = 0.0 $p95Mbs = 0.0 $maxMbs = 0.0 if ($smbResult) { $averageMbs = [double]$smbResult.'Durchschnitt MB/s' $p95Mbs = [double]$smbResult.'P95 MB/s' $maxMbs = [double]$smbResult.'Maximum MB/s' } $allSamples = @($samples | Sort-Object Timestamp) $periodFrom = $allSamples[0].Timestamp $periodTo = $allSamples[$allSamples.Count - 1].Timestamp $periodLength = $periodTo - $periodFrom if ($periodLength.TotalDays -ge 1) { $durationText = '{0} Tage {1} Std' -f [int][Math]::Floor($periodLength.TotalDays), $periodLength.Hours } else { $durationText = '{0} Std {1} Min' -f $periodLength.Hours, $periodLength.Minutes } $smbTotalGb = [Math]::Round([double](($hourRows | Measure-Object -Property 'SMB gesamt (GB)' -Sum).Sum), 2) $netTotalGb = [Math]::Round([double](($hourRows | Measure-Object -Property 'Netzwerk gesamt (GB)' -Sum).Sum), 2) $htmlSummary = @{ SmbTotalGb = $smbTotalGb NetTotalGb = $netTotalGb AverageMbs = $averageMbs P95Mbs = $p95Mbs MaximumMbs = $maxMbs DurationText = $durationText PeriodFrom = $periodFrom.ToString('dd.MM.yyyy HH:mm') PeriodTo = $periodTo.ToString('dd.MM.yyyy HH:mm') Generated = (Get-Date).ToString('dd.MM.yyyy HH:mm') Version = $MyInvocation.MyCommand.Module.Version.ToString() } New-SmbPulseHtmlReport -OutputPath $HtmlPath -LogoPath $LogoPath -Summary $htmlSummary -Daily $dayRows -Weekly $weekRows -Hourly $hourRows -Peaks $peaks -TopClients $topClients -TopUsers $topUsers -TopShares $topShares Write-SectionTitle 'Ergebnisdateien' Write-Ok " $SummaryCsv" Write-Ok " $PeaksCsv" Write-Ok " $HourCsv" Write-Ok " $DayCsv" Write-Ok " $WeekCsv" if ($topClients.Count -gt 0) { Write-Ok " $ClientCsv" } if ($topUsers.Count -gt 0) { Write-Ok " $UserCsv" } if ($topShares.Count -gt 0) { Write-Ok " $ShareCsv" } Write-Ok " $HtmlPath" 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' |