Scripts/spike-etw-share.ps1

#Requires -Version 5.1
# Run on the file server (powershell.exe 5.1, Administrator).
# Phase 0 gate: prove SMB2 Response Read LengthRead tracks a large sequential copy.
#
# Abort: event 111 LengthRead stays near 0 while _listen445 / NIC Out show the copy.
# Go: LengthRead is in the same ballpark as TCP 445 (not exact).
#
# Sequence:
# 1. powershell.exe -File spike-etw-share.ps1 -DumpKeywords
# 2. On the CLIENT: net use * /delete (or disconnect the share), then net use again
# so TreeConnect events fire. Warm sessions have no event 4/104/600.
# 3. powershell.exe -File spike-etw-share.ps1 -Seconds 180
# 4. During those 180s, copy the large file from \\FILESERVER\Storage
# 5. Compare LengthReadSum to _listen445 / NIC Out. Keep the keyword dump.

[CmdletBinding(DefaultParameterSetName = 'Help')]
param(
    [Parameter(ParameterSetName = 'Dump')]
    [switch]$DumpKeywords,

    [Parameter(ParameterSetName = 'Run')]
    [int]$Seconds = 120,

    [Parameter(ParameterSetName = 'Logman')]
    [switch]$LogmanStart,

    [Parameter(ParameterSetName = 'LogmanStop')]
    [switch]$LogmanStop
)

$ErrorActionPreference = 'Continue'
$provider = 'Microsoft-Windows-SMBServer'
$session = 'SMBeatEtwSpike'
$etl = Join-Path $env:TEMP 'SMBeat-etw-spike.etl'

function Show-SMBeatEtwSpikeHelp {
    Write-Host 'Usage (Administrator, on the file server):'
    Write-Host ' .\spike-etw-share.ps1 -DumpKeywords'
    Write-Host ' .\spike-etw-share.ps1 -Seconds 180 # then remount + copy'
    Write-Host ' .\spike-etw-share.ps1 -LogmanStart # file session if in-process is not wanted'
    Write-Host ' .\spike-etw-share.ps1 -LogmanStop'
    Write-Host ''
    Write-Host 'Gate: remount the share first (new TreeConnect), then copy ~5.5 GiB.'
    Write-Host 'Abort if LengthRead ~ 0 while _listen445 shows the copy.'
}

function Dump-SMBeatEtwKeywords {
    Write-Host '=== wevtutil gp Microsoft-Windows-SMBServer /ge /gm ==='
    Write-Host 'Record keyword names and masks for Performance vs Analytic.'
    Write-Host 'Do not guess masks from blogs. Event 40000 (packet bytes) must stay out of the collector.'
    Write-Host ''
    & wevtutil.exe gp $provider /ge /gm
}

if ($PSCmdlet.ParameterSetName -eq 'Help') {
    Show-SMBeatEtwSpikeHelp
    return
}

if ($DumpKeywords) {
    Dump-SMBeatEtwKeywords
    return
}

if ($LogmanStart) {
    if (Test-Path -LiteralPath $etl) {
        Remove-Item -LiteralPath $etl -Force -ErrorAction SilentlyContinue
    }
    & logman.exe stop $session -ets 2>$null | Out-Null
    & logman.exe create trace $session -p $provider 0xffffffffffffffff 5 -o $etl -ets
    Write-Host ('Started {0} -> {1}' -f $session, $etl)
    Write-Host 'Remount the share, copy the file, then: .\spike-etw-share.ps1 -LogmanStop'
    return
}

if ($LogmanStop) {
    & logman.exe stop $session -ets
    Write-Host ('Stopped. ETL: {0}' -f $etl)
    Write-Host 'Inspect with tracerpt or Get-WinEvent -Path if the ETL is readable.'
    Write-Host 'Count events 4, 104, 111, 112, 600. Sum 111 LengthRead vs _listen445.'
    return
}

$modRoot = Split-Path -Parent $PSScriptRoot
$manifest = Join-Path $modRoot 'SMBeat.psd1'
if (-not (Test-Path -LiteralPath $manifest)) {
    Write-Host 'SMBeat.psd1 not found next to Scripts. Copy this repo onto the server or Install-Module SMBeat.'
    return
}

Import-Module $manifest -Force
& (Get-Module SMBeat) { Initialize-SMBeatEtwShareNative } | Out-Null
$err = [SMBeat.Native.EtwShare]::Start()
if ($err -ne 0) {
    Write-Host ('FAIL: ETW session start returned {0}. Run as Administrator. Stop leftover SMBeat-SMBServer if needed.' -f $err)
    return
}

Write-Host ('ETW session running for {0}s. Remount Storage, then copy the large file NOW.' -f $Seconds)
Start-Sleep -Seconds $Seconds

$snap = [SMBeat.Native.EtwShare]::Snapshot()
[SMBeat.Native.EtwShare]::Stop()

Write-Host ''
Write-Host '=== spike result ==='
Write-Host ('Event 4 TreeConnect request : {0}' -f $snap.Count4)
Write-Host ('Event 104 TreeConnect response: {0}' -f $snap.Count104)
Write-Host ('Event 111 Response Read : {0}' -f $snap.Count111)
Write-Host ('Event 112 Response Write : {0}' -f $snap.Count112)
Write-Host ('Event 600 TreeConnect alloc : {0}' -f $snap.Count600)
Write-Host ('Event 552 Session auth : {0}' -f $snap.Count552)
Write-Host ('LengthRead sum : {0} bytes ({1:N3} GiB)' -f $snap.LengthReadTotal, ($snap.LengthReadTotal / 1GB))
Write-Host ('LengthWritten sum : {0} bytes ({1:N3} GiB)' -f $snap.LengthWrittenTotal, ($snap.LengthWrittenTotal / 1GB))
Write-Host ('Mapped shares : {0}' -f $snap.MappedTrees)
Write-Host ('Unknown trees : {0}' -f $snap.UnknownTrees)
Write-Host ('Mapped sessions : {0}' -f $snap.MappedSessions)
Write-Host ('Unknown sessions : {0}' -f $snap.UnknownSessions)
Write-Host ('LostEvents : {0}' -f $snap.LostEvents)
Write-Host ('LastError : {0}' -f $snap.LastError)
Write-Host ''
Write-Host 'Shares flushed this window:'
[SMBeat.Native.EtwShare]::Flush() | Format-Table Name, Sent, Received -AutoSize
Write-Host ''
Write-Host 'Users flushed this window:'
[SMBeat.Native.EtwShare]::FlushUsers() | Format-Table Name, Sent, Received -AutoSize

Write-Host ''
Write-Host 'Compare LengthRead sum to the last _listen445 flush (Scripts\check.ps1).'
Write-Host 'ABORT if LengthRead is ~0 while TCP 445 shows the copy. Do not ship a fake share source.'
Write-Host 'GO if LengthRead is near the copy size (headers and other sessions explain a gap).'
Write-Host 'Warm sessions without a new net use land on (unknown) until reconnect.'