Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## v1.115.0

Network throughput benchmark — a new interactive diagnostic under **Network Diagnostics → [14] Network Throughput Benchmark (file copy)**.

It generates a test file (default 256 MB, 16–4096 MB), copies it to a target folder and back, and times each transfer to report **write and read throughput** in MB/s and Mbps. A UNC share (`\\server\share`) is the typical target; a local path measures the local disk. The figure reflects the full network + remote storage path, not pure wire speed.

The benchmark is **in-box only** — it uses built-in cmdlets and a stopwatch, with no external binary (`ntttcp`/`iperf` are not shipped with Windows and are not auto-downloaded). The payload buffer is filled with pseudo-random bytes so it is not trivially compressible, the target folder is validated before any write, and all three temp files (local source, remote copy, local read-back) are removed in a `finally` even if a transfer fails.

Interactive diagnostic (needs a target path), so no new CLI action. Addition to 58-NetworkDiagnostics. CLI actions: unchanged at 197.

## v1.114.0

Print server cleanup — a new Operations-menu utility for maintaining the local print spooler, plus a read-only CLI action.
Expand Down
2 changes: 1 addition & 1 deletion Header.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
7h3 4b1d3r

.VERSION
1.114.0
1.115.0

.LAST UPDATED
05/23/2026
Expand Down
2 changes: 1 addition & 1 deletion Modules/00-Initialization.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) {
if (-not $script:ModuleRoot -and $script:ScriptPath) {
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
}
$script:ScriptVersion = "1.114.0"
$script:ScriptVersion = "1.115.0"
$script:ScriptStartTime = Get-Date

# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
Expand Down
100 changes: 99 additions & 1 deletion Modules/58-NetworkDiagnostics.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ function Show-NetworkDiagnostics {
Write-MenuItem "[11] Path MTU Discovery"
Write-MenuItem "[12] Gateway Connectivity Test"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-OutputColor " │$(" PERFORMANCE".PadRight(72))│" -color "Info"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-MenuItem "[14] Network Throughput Benchmark (file copy)"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-OutputColor " │$(" REPAIR".PadRight(72))│" -color "Info"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-MenuItem "[13] Reset Network Stack"
Expand Down Expand Up @@ -58,12 +62,13 @@ function Show-NetworkDiagnostics {
"11" { Invoke-PathMtuDiscovery }
"12" { Test-GatewayConnectivity }
"13" { Invoke-NetworkStackReset }
"14" { Invoke-NetworkThroughputBenchmark }
"b" { return }
"B" { return }
"m" { $script:ReturnToMainMenu = $true; return }
"M" { $script:ReturnToMainMenu = $true; return }
default {
Write-OutputColor " Invalid choice. Enter 1-13 or B." -color "Error"
Write-OutputColor " Invalid choice. Enter 1-14 or B." -color "Error"
Start-Sleep -Seconds 1
}
}
Expand Down Expand Up @@ -1096,4 +1101,97 @@ function Invoke-NetworkStackReset {

Write-PressEnter
}

# In-box network throughput benchmark: copies a generated test file to a target
# path and back, timing each transfer. Measures the combined network + remote
# storage path (not pure wire speed) using only built-in cmdlets — no external
# binary (ntttcp/iperf are not in-box and are not auto-downloaded). All temp
# files (local source, remote copy, local read-back) are removed in a finally.
function Invoke-NetworkThroughputBenchmark {
Clear-Host
Write-CenteredOutput "Network Throughput Benchmark" -color "Info"
Write-OutputColor "" -color "Info"
Write-OutputColor " Measures real write + read throughput by copying a test file to a target" -color "Info"
Write-OutputColor " folder and back. This reflects the full network + remote storage path," -color "Info"
Write-OutputColor " not a pure wire-speed figure. A UNC share (\\server\share) is the typical" -color "Info"
Write-OutputColor " target; a local path measures the local disk." -color "Info"
Write-OutputColor "" -color "Info"

$target = Read-Host " Enter target folder (UNC or local path)"
$navResult = Test-NavigationCommand -UserInput $target
if ($navResult.ShouldReturn) { return }
if ([string]::IsNullOrWhiteSpace($target)) { return }
$target = $target.Trim('"').TrimEnd('\')

if (-not (Test-Path -LiteralPath $target -PathType Container)) {
Write-OutputColor " Target folder not found or not accessible: $target" -color "Error"
Write-PressEnter; return
}

$sizeMB = 256
$sizeInput = Read-Host " Test file size in MB (default 256, 16-4096)"
if (-not [string]::IsNullOrWhiteSpace($sizeInput)) {
$parsed = 0
if ([int]::TryParse($sizeInput.Trim(), [ref]$parsed)) {
$sizeMB = [math]::Max(16, [math]::Min(4096, $parsed))
}
}

$localTemp = Join-Path $env:TEMP ("rs_thrput_src_{0}.tmp" -f $PID)
$remoteFile = Join-Path $target ("rs_thrput_{0}.tmp" -f $PID)
$readBack = Join-Path $env:TEMP ("rs_thrput_dst_{0}.tmp" -f $PID)
$writeMBps = 0; $readMBps = 0; $ok = $false

try {
Write-OutputColor "" -color "Info"
Write-OutputColor " Generating ${sizeMB} MB test file..." -color "Info"
# Fill a 4 MB buffer with pseudo-random bytes (so the payload is not
# trivially compressible) and write it enough times to reach the size.
$bufSize = 4MB
$buffer = New-Object byte[] $bufSize
(New-Object System.Random).NextBytes($buffer)
$iterations = [math]::Ceiling(($sizeMB * 1MB) / $bufSize)
$fs = [System.IO.File]::Create($localTemp)
try { for ($i = 0; $i -lt $iterations; $i++) { $fs.Write($buffer, 0, $bufSize) } }
finally { $fs.Close() }
$bytes = (Get-Item -LiteralPath $localTemp).Length

Write-OutputColor " Measuring WRITE throughput (local -> target)..." -color "Info"
$sw = [System.Diagnostics.Stopwatch]::StartNew()
Copy-Item -LiteralPath $localTemp -Destination $remoteFile -Force -ErrorAction Stop
$sw.Stop()
$writeMBps = [math]::Round(($bytes / 1MB) / [math]::Max($sw.Elapsed.TotalSeconds, 0.001), 1)

Write-OutputColor " Measuring READ throughput (target -> local)..." -color "Info"
$sw.Restart()
Copy-Item -LiteralPath $remoteFile -Destination $readBack -Force -ErrorAction Stop
$sw.Stop()
$readMBps = [math]::Round(($bytes / 1MB) / [math]::Max($sw.Elapsed.TotalSeconds, 0.001), 1)
$ok = $true
}
catch {
Write-OutputColor " Throughput test failed: $($_.Exception.Message)" -color "Error"
}
finally {
foreach ($p in @($localTemp, $remoteFile, $readBack)) {
if ($p -and (Test-Path -LiteralPath $p)) { Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue }
}
}

if ($ok) {
$writeMbps = [math]::Round($writeMBps * 8, 0)
$readMbps = [math]::Round($readMBps * 8, 0)
$title = " THROUGHPUT: $target"
if ($title.Length -gt 69) { $title = $title.Substring(0, 69) + "..." }
Write-OutputColor "" -color "Info"
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
Write-OutputColor " │$($title.PadRight(72))│" -color "Info"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-OutputColor " │$(" Test size: ${sizeMB} MB".PadRight(72))│" -color "Info"
Write-OutputColor " │$(" Write: ${writeMBps} MB/s (${writeMbps} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " │$(" Read: ${readMBps} MB/s (${readMbps} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
}
Write-PressEnter
}
#endregion
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<a href="https://www.bestpractices.dev/projects/12921"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/12921/badge"></a>
<a href="https://codecov.io/gh/TheAbider/RackStack"><img alt="codecov" src="https://codecov.io/gh/TheAbider/RackStack/branch/master/graph/badge.svg"></a>
<img alt="PSScriptAnalyzer 0 errors" src="https://img.shields.io/badge/PSScriptAnalyzer-0%20errors-brightgreen">
<img alt="5075 structural tests" src="https://img.shields.io/badge/structural%20tests-5075-brightgreen">
<img alt="5085 structural tests" src="https://img.shields.io/badge/structural%20tests-5085-brightgreen">
<img alt="Pester 312 tests" src="https://img.shields.io/badge/Pester-312%20tests-brightgreen">
<img alt="SLSA Level 3" src="https://slsa.dev/images/gh-badge-level3.svg">
</p>
Expand Down
2 changes: 1 addition & 1 deletion RackStack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Environment-specific settings are configured via defaults.json.

.VERSION
1.114.0
1.115.0

.NOTES
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)
Expand Down
2 changes: 1 addition & 1 deletion RackStack.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'RackStack.psm1'
ModuleVersion = '1.114.0'
ModuleVersion = '1.115.0'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
Expand Down
33 changes: 31 additions & 2 deletions Tests/Run-Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9169,6 +9169,35 @@ catch {
Write-TestResult "Print Server Cleanup Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 182: NETWORK THROUGHPUT BENCHMARK (v1.115.0, 58-NetworkDiagnostics)
# ============================================================================
Write-SectionHeader "SECTION 182: NETWORK THROUGHPUT BENCHMARK (58-NetworkDiagnostics)"

try {
$ntC = Get-Content "$modulesPath\58-NetworkDiagnostics.ps1" -Raw
Write-TestResult "58-NetDiag: Invoke-NetworkThroughputBenchmark exists" ($ntC -match 'function\s+Invoke-NetworkThroughputBenchmark\b')
# In-box only: must not actually invoke ntttcp / iperf (mentions in comments are fine;
# what matters is that no external benchmark binary is executed).
Write-TestResult "58-NetDiag: throughput test is in-box (no ntttcp/iperf invocation)" (-not ($ntC -match 'ntttcp\.exe|iperf\.exe|&\s*[''"]?\.?\\?ntttcp|Start-Process[^\r\n]*ntttcp'))
# Measures both directions with a stopwatch.
Write-TestResult "58-NetDiag: measures write + read throughput" ($ntC -match 'Measuring WRITE throughput' -and $ntC -match 'Measuring READ throughput')
Write-TestResult "58-NetDiag: uses a stopwatch for timing" ($ntC -match '\[System\.Diagnostics\.Stopwatch\]')
# Temp files are always cleaned up in a finally (local source, remote copy, read-back).
Write-TestResult "58-NetDiag: cleans up temp files in finally" ($ntC -match 'finally\s*\{[\s\S]{0,200}foreach \(\$p in[\s\S]{0,250}Remove-Item -LiteralPath \$p')
# Validates the target folder before writing.
Write-TestResult "58-NetDiag: validates target folder exists" ($ntC -match 'Test-Path -LiteralPath \$target -PathType Container')
# Follows the menu navigation convention.
Write-TestResult "58-NetDiag: throughput prompt has nav check" ($ntC -match 'Enter target folder[\s\S]{0,120}Test-NavigationCommand')
# Menu wiring.
Write-TestResult "58-NetDiag: menu item [14] present" ($ntC -match '\[14\]\s*Network Throughput Benchmark')
Write-TestResult "58-NetDiag: dispatch case 14 wired" ($ntC -match '"14"\s*\{\s*Invoke-NetworkThroughputBenchmark')
Write-TestResult "58-NetDiag: invalid msg bumped to 1-14" ($ntC -match 'Enter 1-14 or B')
}
catch {
Write-TestResult "Network Throughput Benchmark Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase)
# ============================================================================
Expand Down Expand Up @@ -9510,7 +9539,7 @@ Write-TestResult "45-Config: baseline second number nav check" ($ceContent2 -mat
$dcContent = Get-Content -LiteralPath "$modulesPath\20-DiskCleanup.ps1" -Raw
Write-TestResult "20-DiskCleanup: specific invalid msg" ($dcContent -match 'Enter 1-13 or B')
$ndContent = Get-Content -LiteralPath "$modulesPath\58-NetworkDiagnostics.ps1" -Raw
Write-TestResult "58-NetworkDiagnostics: specific invalid msg" ($ndContent -match 'Enter 1-13 or B')
Write-TestResult "58-NetworkDiagnostics: specific invalid msg" ($ndContent -match 'Enter 1-14 or B')

# AD DS promotion menu — no double Write-PressEnter (sub-functions have their own)
$adContent = Get-Content -LiteralPath "$modulesPath\61-ActiveDirectory.ps1" -Raw
Expand Down Expand Up @@ -9873,7 +9902,7 @@ Write-TestResult "58-NetDiag: network reset uses netsh winsock reset" ($ndConten
Write-TestResult "58-NetDiag: network reset uses netsh int ip reset" ($ndContent3 -match 'netsh int ip reset')
Write-TestResult "58-NetDiag: network reset sets RebootNeeded flag" ($ndContent3 -match 'RebootNeeded.*=.*\$true')
Write-TestResult "58-NetDiag: network reset has confirmation prompt" ($ndContent3 -match 'Are you sure')
Write-TestResult "58-NetDiag: menu has 13 options" ($ndContent3 -match 'Enter 1-13 or B')
Write-TestResult "58-NetDiag: menu has 14 options" ($ndContent3 -match 'Enter 1-14 or B')
Write-TestResult "58-NetDiag: menu has REPAIR section" ($ndContent3 -match 'REPAIR')

# 48-MenuDisplay: Invoke-WithTimeout function existence (critical helper)
Expand Down
Loading