PS-AdminTools.psm1

#Requires -Version 7.0
Set-StrictMode -Version Latest

# ── Resolve paths ─────────────────────────────────────────────────────────────
$ModuleRoot = $PSScriptRoot
$BinPath    = Join-Path $ModuleRoot 'Bin'

if (-not (Test-Path $BinPath)) {
    throw "Bin folder not found at: $BinPath"
}

# ── Store BinPath in AppDomain so the resolver closure can reach it ───────────
[System.AppDomain]::CurrentDomain.SetData('SysAdminToolsBinPath', $BinPath)

# ── Register assembly resolver once per process ───────────────────────────────
if (-not [System.AppDomain]::CurrentDomain.GetData('SysAdminToolsResolver')) {
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve(
        [System.ResolveEventHandler] {
            param($sender, $resolveArgs)
            try {
                $bin     = [System.AppDomain]::CurrentDomain.GetData('SysAdminToolsBinPath')
                $asmName = $resolveArgs.Name.Split(',')[0].Trim()
                $dllPath = [System.IO.Path]::Combine($bin, "$asmName.dll")
                if ([System.IO.File]::Exists($dllPath)) {
                    return [System.Reflection.Assembly]::LoadFrom($dllPath)
                }
            } catch {}
            return $null
        }
    )
    [System.AppDomain]::CurrentDomain.SetData('SysAdminToolsResolver', $true)
}

# ── Load DLLs in dependency order ─────────────────────────────────────────────
foreach ($dll in @('PacketDotNet.dll', 'SharpPcap.dll', 'BandwidthMonitor.dll')) {
    $fullPath = [System.IO.Path]::Combine($BinPath, $dll)
    if (-not [System.IO.File]::Exists($fullPath)) {
        throw "Missing required DLL: $fullPath"
    }
    [System.Reflection.Assembly]::LoadFrom($fullPath) | Out-Null
    Write-Verbose "Loaded: $fullPath"
}

# ── Verify type loaded correctly ──────────────────────────────────────────────
$script:BwMonitorType = $null
foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) {
    if ($asm.GetName().Name -eq 'BandwidthMonitor') {
        $t = $asm.GetType('BandwidthMonitor.BwMonitor')
        if ($t) {
            $script:BwMonitorType = $t
            Write-Verbose "Type resolved: $($t.FullName)"
            break
        }
    }
}

if (-not $script:BwMonitorType) {
    # Dump what actually loaded for diagnosis
    Write-Warning "Could not resolve BandwidthMonitor.BwMonitor. Loaded assemblies:"
    [System.AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { -not $_.IsDynamic } |
        ForEach-Object { Write-Warning " $($_.GetName().Name) → $($_.Location)" }
    throw "Failed to load BandwidthMonitor type."
}

# ── Start-BwMon ───────────────────────────────────────────────────────────────
function Start-BwMon {
    <#
    .SYNOPSIS
        Live per-process network bandwidth monitor.
 
    .DESCRIPTION
        Captures packets on a selected NIC using Npcap and displays
        real-time RX/TX Mbps per process in a live console table.
        Requires Administrator privileges and Npcap 1.00+.
 
    .EXAMPLE
        Start-BwMon
 
    .NOTES
        Keys while running:
          B = sort by Bandwidth (default)
          R = sort by RX
          T = sort by TX
          C = sort by CPU
          M = sort by Memory
          Type a PID + Enter = drill into process connections
          ESC = back to overview
          Ctrl+C = quit
    #>

    [CmdletBinding()]
    param()

    if (-not $IsWindows) {
        Write-Error "Start-BwMon requires Windows."
        return
    }

    $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-Warning "Start-BwMon should be run as Administrator for full packet capture access."
    }

    if (-not $script:BwMonitorType) {
        Write-Error "BandwidthMonitor type not loaded. Try: Import-Module ps-admintools -Force"
        return
    }

    try {
        $script:BwMonitorType.GetMethod('Start').Invoke($null, $null)
    }
    catch {
        $msg = if ($_.Exception.InnerException) {
            $_.Exception.InnerException.Message
        } else {
            $_.Exception.Message
        }
        Write-Error "BwMonitor error: $msg"
    }
}

Export-ModuleMember -Function 'Start-BwMon'