Public/Start-SystemMonitor.ps1

<#
.SYNOPSIS
    A high-performance, asynchronous TUI System Monitor and Task Manager.

.DESCRIPTION
    Start-SystemMonitor is a terminal-based system dashboard.

.PARAMETER ComputerName
    The target computer to monitor. Defaults to "localhost".

.PARAMETER PageSize
    Optional. Sets a fixed number of rows to display in lists.

.PARAMETER Credential
    Optional. PSCredential object to use for remote connections.
    
.PARAMETER Diagnostics
    Optional Switch. Displays the yellow diagnostic bar showing internal worker thread counters.

.EXAMPLE
    Start-SystemMonitor -Diagnostics
#>

function Start-SystemMonitor {
    [CmdletBinding()]
    param (
        [Parameter(Position=0)]
        [string]$ComputerName = "localhost",

        [Parameter(Position=1)]
        [ValidateRange(5,100)]
        [int]$PageSize = 0,

        [Parameter(HelpMessage="Credentials for remote connection")]
        [System.Management.Automation.PSCredential]$Credential = $null,
        
        [Parameter(HelpMessage="Enable diagnostic output bar")]
        [switch]$Diagnostics
    )

    $isWindowsOS = $true
    if ($PSVersionTable.PSEdition -eq 'Core' -and -not $IsWindows) {
        $isWindowsOS = $false
    }
    if (-not $isWindowsOS) {
        Write-Error "Start-SystemMonitor requires Windows as it relies on WMI."
        return
    }

    $UseFixedPageSize = $PSBoundParameters.ContainsKey('PageSize')
    
    #region 0. ADMIN CHECK
    $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $IsAdmin) {
        Write-Warning "You are NOT running as Administrator. Actions (Kill, Logoff, Services, Tasks) may fail."
        Start-Sleep -Seconds 1
    }
    #endregion
    #region 0b. REMOTE HOST REACHABILITY PRE-CHECK
    $IsLocal = ($ComputerName -eq "localhost" -or $ComputerName -eq "." -or $ComputerName -eq $env:COMPUTERNAME)
    if (-not $IsLocal) {
        Write-Host ""
        Write-Host " Checking reachability of '$ComputerName'..." -ForegroundColor DarkGray
        
        # First try a fast TCP connect to WinRM port 5985 (2 second timeout)
        $tcpClient = New-Object System.Net.Sockets.TcpClient
        $connected = $false
        try {
            $connectResult = $tcpClient.BeginConnect($ComputerName, 5985, $null, $null)
            $connected = $connectResult.AsyncWaitHandle.WaitOne(2000, $false)
            if ($connected -and $tcpClient.Connected) {
                $tcpClient.EndConnect($connectResult)
            } else {
                $connected = $false
            }
        } catch { $connected = $false }
        finally { $tcpClient.Dispose() }

        if (-not $connected) {
            # TCP failed — try ICMP ping to distinguish host-down from WinRM-not-configured
            $pingReachable = $false
            try {
                $ping = New-Object System.Net.NetworkInformation.Ping
                $pingResult = $ping.Send($ComputerName, 1500)
                $pingReachable = ($pingResult.Status -eq [System.Net.NetworkInformation.IPStatus]::Success)
            } catch { $pingReachable = $false }

            Write-Host ""
            if ($pingReachable) {
                Write-Host " HOST REACHABLE but WinRM is not responding on port 5985." -ForegroundColor Yellow
                Write-Host " Ensure WinRM is enabled on '$ComputerName':" -ForegroundColor Yellow
                Write-Host " Enable-PSRemoting -Force (run on the remote machine)" -ForegroundColor DarkGray
            } else {
                Write-Host " HOST UNREACHABLE: '$ComputerName' did not respond." -ForegroundColor Red
                Write-Host " Verify the hostname/IP is correct and the machine is online." -ForegroundColor DarkGray
            }
            Write-Host ""
            return
        }

        Write-Host " Host reachable. Connecting..." -ForegroundColor DarkGray
    }
    #endregion

    $SyncHash = [hashtable]::Synchronized(@{
        TargetComputer    = $ComputerName
        Credential        = $Credential
        Running           = $true
        # Optimization Flags
        UserModeActive    = $false
        ServiceModeActive = $false
        TaskModeActive    = $false
        AppModeActive     = $false
        # Queues
        KillQueue         = [System.Collections.Concurrent.ConcurrentQueue[int]]::new()
        LogoffQueue       = [System.Collections.Concurrent.ConcurrentQueue[int]]::new()
        ServiceQueue      = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
        TaskQueue         = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
        # Data
        SysData           = $null
        UserData          = @()   
        ServiceData       = @()
        TaskData          = @()
        AppData           = @()
        StaticData        = $null        
        RawProcessList    = $null
        LastUpdate        = [DateTime]::MinValue
        # Status
        Error             = $null
        CriticalError     = $false
        Disconnected      = $false
        ReconnectAttempt  = 0
        ReconnectMessage  = ""
        DisconnectTime    = [DateTime]::MinValue
        ActionStatus      = ""
        DebugLog          = "Init..."
        SpeedTest         = [hashtable]::Synchronized(@{
            TestMode        = "Local"
            TargetHost      = if ($ComputerName -ne "localhost") { $ComputerName } else { "" }
            Threads         = 8
            TimeoutSeconds  = 15
            Port            = 5201
            Running         = $false
            ActivePhase     = "Idle"
            ProgressPercent = 0.0
            Results         = [hashtable]::Synchronized(@{ Latency = $null; Download = $null; Upload = $null; Error = "" })
        })
    })
    #endregion

    #region 4. UI THREAD STARTUP
    $PS = [PowerShell]::Create()
    
    # Pass the function script block to the Runspace
    $WorkerScript = ${function:Invoke-SystemMonitorWorker}
    $null = $PS.AddScript($WorkerScript).AddArgument($SyncHash)
    $AsyncHandle = $PS.BeginInvoke()

    try { [Console]::CursorVisible = $false } catch {}
    Clear-Host

    $UILoaded = $false
    $SyncHash.SpeedTest.TestMode = if ($IsLocal) { "Local" } else { "Remote" }

    # Initial Wait
    try {
        $LoadWidth = [math]::Min($Host.UI.RawUI.WindowSize.Width, 42)
        Write-Host ""
        Write-Host ("═" * $LoadWidth) -ForegroundColor Cyan
        Write-Host " ⚡ SYSTEM MONITOR: $ComputerName" -ForegroundColor Cyan
        Write-Host ("═" * $LoadWidth) -ForegroundColor Cyan
        Write-Host " 🌐 Connecting..." -ForegroundColor DarkGray

        while (-not $UILoaded) {
            if ($SyncHash.CriticalError) { break }
            if ($SyncHash.SysData -and $SyncHash.StaticData) { $UILoaded = $true; break }
            if ($SyncHash.Error -like "Waiting*") {
                [Console]::SetCursorPosition(0, 3)
                Write-Host " ⏳ $($SyncHash.Error)".PadRight($LoadWidth) -ForegroundColor Cyan
            }
            Start-Sleep -Milliseconds 100
        }
        if ($SyncHash.CriticalError) {
            Clear-Host
            Write-Host ""
            Write-Host " CONNECTION FAILED" -ForegroundColor Red
            Write-Host " $($SyncHash.Error)" -ForegroundColor Yellow
            Write-Host ""
            return
        }
        Clear-Host
    #endregion

        # Load BrowserHistory script and sqlite3 base64 path into SyncHash
        # Resolve module root dynamically - works on any computer regardless of install path.
        # Strategy 1: ScriptBlock.File from Get-Command - most reliable, returns the actual .ps1 source path
        $ModuleRoot = $null
        $dbgLog = "PathRes: "
        try {
            $cmd = Get-Command Start-SystemMonitor -ErrorAction SilentlyContinue
            $sbFile = if ($cmd) { $cmd.ScriptBlock.File } else { $null }
            if ($sbFile) {
                # ScriptBlock.File = .../Public/Start-SystemMonitor.ps1 -> parent's parent = module root
                $ModuleRoot = Split-Path -Parent (Split-Path -Parent $sbFile)
                $dbgLog += "SBF=$sbFile->$ModuleRoot"
            } else {
                $dbgLog += "SBF=null"
            }
        } catch { $dbgLog += "SBF threw: $_" }

        # Strategy 2: Module.ModuleBase from Get-Command
        if (-not $ModuleRoot) {
            try {
                $cmd = Get-Command Start-SystemMonitor -ErrorAction SilentlyContinue
                if ($cmd -and $cmd.Module -and $cmd.Module.ModuleBase) {
                    $ModuleRoot = $cmd.Module.ModuleBase
                    $dbgLog += " | GCM=$ModuleRoot"
                } else { $dbgLog += " | GCM=noMod" }
            } catch { $dbgLog += " | GCM threw: $_" }
        }

        # Strategy 3: PSScriptRoot parent (works when dot-sourced directly)
        if (-not $ModuleRoot -and $PSScriptRoot) {
            $ModuleRoot = Split-Path -Parent $PSScriptRoot
            $dbgLog += " | PSR=$ModuleRoot"
        }

        $HistoryScriptPath = Join-Path $ModuleRoot "Public\Get-BrowserHistory.ps1"
        $dbgLog += " | HP=$(Test-Path $HistoryScriptPath):$HistoryScriptPath"
        if (Test-Path $HistoryScriptPath) {
            $SyncHash.BrowserHistoryScript = Get-Content $HistoryScriptPath -Raw
            $dbgLog += " LOADED"
        }

        $B64Path = Join-Path $ModuleRoot "Private\sqlite3.exe.b64"
        $dbgLog += " | B64=$(Test-Path $B64Path)"
        if (Test-Path $B64Path) {
            $SyncHash.SqliteB64Path = (Resolve-Path $B64Path).Path
        }

        $SyncHash.DebugLog = $dbgLog

        # Compile TuiEngine if not loaded
        $TuiEnginePath = Join-Path $ModuleRoot "Private\TuiEngine.cs"
        if (Test-Path $TuiEnginePath) {
            if (-not ("OmniAdmin.TuiEngine" -as [type])) {
                $TuiCode = Get-Content $TuiEnginePath -Raw -Encoding UTF8
                Add-Type -TypeDefinition $TuiCode
            }
        } else {
            throw "Could not find TuiEngine.cs at: $TuiEnginePath"
        }

        # Define Speed Test background worker script block
        $InitNetEngineSB = ${function:Initialize-NetworkEngine}
        $STWorker = {
            param($SyncHash, $InitNetEngineSB, $ComputerName)
            try {
                # $InitNetEngineSB arrives as a plain string when launched from C# TuiEngine.
                # Wrap in [scriptblock]::Create() so & executes it as code, not a command name.
                $InitSB = [scriptblock]::Create($InitNetEngineSB)
                function Initialize-NetworkEngine { & $InitSB }
                if (-not ("NativeNetworkTest" -as [type])) {
                    Initialize-NetworkEngine
                }
                
                $Threads = $SyncHash.SpeedTest.Threads
                $Timeout = $SyncHash.SpeedTest.TimeoutSeconds
                $Port = $SyncHash.SpeedTest.Port
                $Target = $ComputerName
                $Mode = $SyncHash.SpeedTest.TestMode
                
                # --- PHASE 1: LATENCY ---
                $SyncHash.SpeedTest.ActivePhase = "Latency"
                $SyncHash.SpeedTest.ProgressPercent = 10
                
                $PingTarget = if ($Mode -eq "Local") { "speed.cloudflare.com" } else { $Target }
                $PingPort = if ($Mode -eq "Local") { 443 } else { 5985 }
                
                if ($Mode -eq "P2P" -and -not $Target) {
                    throw "Target Host is required for Peer-to-Peer test."
                }
                if ($Mode -eq "Remote" -and -not $Target) {
                    throw "Target Host is required for Remote test."
                }
                
                $measurements = @()
                for ($i = 0; $i -lt 10; $i++) {
                    $tcp = New-Object System.Net.Sockets.TcpClient
                    try {
                        $sw = [System.Diagnostics.Stopwatch]::StartNew()
                        $tcp.Connect($PingTarget, $PingPort)
                        $sw.Stop()
                        $measurements += $sw.Elapsed.TotalMilliseconds
                    }
                    catch {
                        # Ignore single connection failures
                    }
                    finally { $tcp.Dispose() }
                    $SyncHash.SpeedTest.ProgressPercent = 10 + ($i * 10)
                    Start-Sleep -Milliseconds 100
                }
                if ($measurements.Count -gt 0) {
                    $SyncHash.SpeedTest.Results.Latency = [math]::Round(($measurements | Measure-Object -Average).Average, 1)
                } else {
                    $SyncHash.SpeedTest.Results.Latency = 0.0
                }
                
                # --- PHASE 2: DOWNLOAD ---
                $SyncHash.SpeedTest.ActivePhase = "Download"
                $SyncHash.SpeedTest.ProgressPercent = 0.0
                $SyncHash.SpeedTest.PhaseStartTime = [DateTime]::UtcNow
                
                $dlSpeed = 0.0
                if ($Mode -eq "Local") {
                    $downUrl = "https://speed.cloudflare.com/__down?bytes=50000000"
                    $dlSpeed = [NativeNetworkTest]::Download($downUrl, $Threads, $Timeout)
                }
                elseif ($Mode -eq "Remote") {
                    # Build remote scriptblock via string concat to avoid here-string/interpolation issues
                    $initBody = $InitNetEngineSB.ToString()
                    $remoteScriptText = "function Initialize-NetworkEngine { " + $initBody + " }`n" +
                                        "Initialize-NetworkEngine`n" +
                                        "[NativeNetworkTest]::Download('https://speed.cloudflare.com/__down?bytes=50000000', $Threads, $Timeout)"
                    $remoteScript = [scriptblock]::Create($remoteScriptText)
                    $params = @{
                        ComputerName = $Target
                        ScriptBlock  = $remoteScript
                    }
                    if ($SyncHash.Credential) { $params['Credential'] = $SyncHash.Credential }
                    $dlSpeed = Invoke-Command @params
                }
                elseif ($Mode -eq "P2P") {
                    # Local IP
                    $udp = New-Object System.Net.Sockets.UdpClient
                    $udp.Connect($Target, 1)
                    $localIP = $udp.Client.LocalEndPoint.Address.ToString()
                    $udp.Close()
                    
                    # Local firewall rule
                    New-NetFirewallRule -DisplayName "OmniAdmin-PeerTest" -Direction Inbound -Protocol TCP -LocalPort $Port -Action Allow | Out-Null
                    $fwCreated = $true
                    
                    # Remote worker — build via string concat to avoid here-string issues
                    $initBody = $InitNetEngineSB.ToString()
                    $workerLifetime = ($Timeout * 3) + 60
                    $totalConnections = ($Threads * 2) + 1
                    $workerScriptText = "function Initialize-NetworkEngine { " + $initBody + " }`n" +
                                        "Initialize-NetworkEngine`n" +
                                        "[PeerSpeedTest]::WorkerConnect('$localIP', $Port, $totalConnections, $workerLifetime)"
                    $workerScript = [scriptblock]::Create($workerScriptText)
                    $jobParams = @{
                        ComputerName = $Target
                        ScriptBlock  = $workerScript
                        AsJob        = $true
                    }
                    if ($SyncHash.Credential) { $jobParams['Credential'] = $SyncHash.Credential }
                    $workerJob = Invoke-Command @jobParams
                    
                    # Measure download
                    $dlSpeed = [PeerSpeedTest]::MeasureDownload($Port, $Threads, $Timeout)
                    
                    # Cleanup
                    try { [PeerSpeedTest]::KillWorkers($Port) } catch {}
                    if ($workerJob) { Remove-Job -Force $workerJob -ErrorAction SilentlyContinue }
                    if ($fwCreated) { Remove-NetFirewallRule -DisplayName "OmniAdmin-PeerTest" -ErrorAction SilentlyContinue; $fwCreated = $false }
                }
                $SyncHash.SpeedTest.Results.Download = $dlSpeed
                $SyncHash.SpeedTest.ProgressPercent = 100.0
                
                # --- PHASE 3: UPLOAD ---
                $SyncHash.SpeedTest.ActivePhase = "Upload"
                $SyncHash.SpeedTest.ProgressPercent = 0.0
                $SyncHash.SpeedTest.PhaseStartTime = [DateTime]::UtcNow
                
                $ulSpeed = 0.0
                if ($Mode -eq "Local") {
                    $upUrl = "https://speed.cloudflare.com/__up"
                    $ulSpeed = [NativeNetworkTest]::Upload($upUrl, $Threads, $Timeout)
                }
                elseif ($Mode -eq "Remote") {
                    $initBody = $InitNetEngineSB.ToString()
                    $remoteScriptText = "function Initialize-NetworkEngine { " + $initBody + " }`n" +
                                        "Initialize-NetworkEngine`n" +
                                        "[NativeNetworkTest]::Upload('https://speed.cloudflare.com/__up', $Threads, $Timeout)"
                    $remoteScript = [scriptblock]::Create($remoteScriptText)
                    $params = @{
                        ComputerName = $Target
                        ScriptBlock  = $remoteScript
                    }
                    if ($SyncHash.Credential) { $params['Credential'] = $SyncHash.Credential }
                    $ulSpeed = Invoke-Command @params
                }
                elseif ($Mode -eq "P2P") {
                    # Local IP
                    $udp = New-Object System.Net.Sockets.UdpClient
                    $udp.Connect($Target, 1)
                    $localIP = $udp.Client.LocalEndPoint.Address.ToString()
                    $udp.Close()
                    
                    # Local firewall rule
                    New-NetFirewallRule -DisplayName "OmniAdmin-PeerTest" -Direction Inbound -Protocol TCP -LocalPort $Port -Action Allow | Out-Null
                    $fwCreated = $true
                    
                    # Remote worker
                    $initBody = $InitNetEngineSB.ToString()
                    $workerLifetime = ($Timeout * 3) + 60
                    $totalConnections = ($Threads * 2) + 1
                    $workerScriptText = "function Initialize-NetworkEngine { " + $initBody + " }`n" +
                                        "Initialize-NetworkEngine`n" +
                                        "[PeerSpeedTest]::WorkerConnect('$localIP', $Port, $totalConnections, $workerLifetime)"
                    $workerScript = [scriptblock]::Create($workerScriptText)
                    $jobParams = @{
                        ComputerName = $Target
                        ScriptBlock  = $workerScript
                        AsJob        = $true
                    }
                    if ($SyncHash.Credential) { $jobParams['Credential'] = $SyncHash.Credential }
                    $workerJob = Invoke-Command @jobParams
                    
                    # Measure upload
                    $ulSpeed = [PeerSpeedTest]::MeasureUpload($Port, $Threads, $Timeout)
                    
                    # Cleanup
                    try { [PeerSpeedTest]::KillWorkers($Port) } catch {}
                    if ($workerJob) { Remove-Job -Force $workerJob -ErrorAction SilentlyContinue }
                    if ($fwCreated) { Remove-NetFirewallRule -DisplayName "OmniAdmin-PeerTest" -ErrorAction SilentlyContinue; $fwCreated = $false }
                }
                $SyncHash.SpeedTest.Results.Upload = $ulSpeed
                $SyncHash.SpeedTest.ProgressPercent = 100.0
                $SyncHash.SpeedTest.ActivePhase = "Done"
            }
            catch {
                $SyncHash.SpeedTest.Results.Error = $_.Exception.Message
                $SyncHash.SpeedTest.ActivePhase = "Error"
                if ($fwCreated) { Remove-NetFirewallRule -DisplayName "OmniAdmin-PeerTest" -ErrorAction SilentlyContinue }
            }
            finally {
                $SyncHash.SpeedTest.Running = $false
            }
        }

        # Run compiled C# TUI Loop
        [OmniAdmin.TuiEngine]::Run(
            $SyncHash,
            $ComputerName,
            $PageSize,
            $UseFixedPageSize,
            $Diagnostics,
            $Credential,
            $STWorker.ToString(),
            $InitNetEngineSB.ToString()
        )
    }
    #region CLEANUP
    finally {
        Write-Host "`nStopping..." -ForegroundColor DarkGray
        $SyncHash.Running = $false
        try { $PS.EndInvoke($AsyncHandle) } catch {}
        try { $PS.Dispose() } catch {}
        try { [Console]::CursorVisible = $true } catch {}
        Clear-Host
    }
    #endregion
}