Private/Invoke-SystemMonitorWorker.ps1
|
function Invoke-SystemMonitorWorker { param($Sync) $Session = $null $LastUserFetch = [DateTime]::MinValue $LastAppFetch = [DateTime]::MinValue $IsLocal = ($Sync.TargetComputer -eq "localhost" -or $Sync.TargetComputer -eq $env:COMPUTERNAME) #region CONNECTION SETUP try { if (-not $IsLocal) { $SessParams = @{ ComputerName = $Sync.TargetComputer ErrorAction = 'Stop' SessionOption = (New-PSSessionOption -OperationTimeout 5000) } if ($Sync.Credential) { $SessParams['Credential'] = $Sync.Credential } $Session = New-PSSession @SessParams } } catch { $Sync.Error = "Connection Failed: $($_.Exception.Message)" $Sync.CriticalError = $true return } # Optimized Runner function Run-Script { param($Script, $Arguments) if ($IsLocal) { if ($Arguments) { & $Script @Arguments } else { & $Script } } else { if ($Session) { Invoke-Command -Session $Session -ScriptBlock $Script -ArgumentList $Arguments } } } #endregion #region PRE-FETCH STATIC while ($null -eq $Sync.StaticData -and $Sync.Running) { try { $Sync.DebugLog = "WMI Warmup..." $ScriptBlock = { $CPU = Get-CimInstance Win32_Processor -ErrorAction Stop $OS = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop $CS = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop $CpuName = $CPU.Name -replace "\s+", " " -replace "\(R\)", "" -replace "\(TM\)", "" # GPU Static Info Fetch $GpuName = $null try { $Video = Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue | Select-Object -First 1 if ($Video) { $GpuName = $Video.Name } } catch {} [PSCustomObject]@{ CpuName = $CpuName GpuName = $GpuName Cores = $CS.NumberOfLogicalProcessors TotalRam = [math]::Round($OS.TotalVisibleMemorySize / 1024 / 1024, 1) BootTime = $OS.LastBootUpTime } } if ($IsLocal) { $Sync.StaticData = (& $ScriptBlock) } else { $Sync.StaticData = Invoke-Command -Session $Session -ScriptBlock $ScriptBlock } } catch { $Sync.Error = "Waiting for WMI..." Start-Sleep -Seconds 2 } } #endregion #region MAIN DATA LOOP $CachedGpuLoad = 0 $GpuCycleCount = 0 $CycleCount = 0 $ConsecutiveErrors = 0 $MaxConsecutiveErrors = 3 while ($Sync.Running) { $CycleStart = [DateTime]::UtcNow try { # --- ACTION PROCESSING --- $PidToKill = 0 while ($Sync.KillQueue.TryDequeue([ref]$PidToKill)) { if ($IsLocal) { Stop-Process -Id $PidToKill -Force -ErrorAction SilentlyContinue } else { Invoke-Command -Session $Session -ScriptBlock { Stop-Process -Id $args[0] -Force } -ArgumentList $PidToKill } $Sync.ActionStatus = "Killed PID $PidToKill" } $SessionToLogoff = 0 while ($Sync.LogoffQueue.TryDequeue([ref]$SessionToLogoff)) { $Sb = { $res = logoff $args[0] 2>&1 if ($LASTEXITCODE -ne 0) { throw "Exit Code $LASTEXITCODE" } } if ($IsLocal) { & $Sb $SessionToLogoff } else { Invoke-Command -Session $Session -ScriptBlock $Sb -ArgumentList $SessionToLogoff } $Sync.ActionStatus = "Logoff sent to Session $SessionToLogoff" } $SvcAction = $null while ($Sync.ServiceQueue.TryDequeue([ref]$SvcAction)) { $Sb = { param($N,$A) if($A -eq "Start"){Start-Service $N} elseif($A -eq "Stop"){Stop-Service $N -Force} else{Restart-Service $N -Force} } if ($IsLocal) { & $Sb $SvcAction.Name $SvcAction.Action } else { Invoke-Command -Session $Session -ScriptBlock $Sb -ArgumentList $SvcAction.Name, $SvcAction.Action } $Sync.ActionStatus = "$($SvcAction.Action) $($SvcAction.Name)" } $TaskToStart = $null while ($Sync.TaskQueue.TryDequeue([ref]$TaskToStart)) { $Sb = { param($name, $path) if ($path) { Start-ScheduledTask -TaskName $name -TaskPath $path } else { Start-ScheduledTask -TaskName $name } } if ($IsLocal) { & $Sb $TaskToStart.Name $TaskToStart.Path } else { Invoke-Command -Session $Session -ScriptBlock $Sb -ArgumentList $TaskToStart.Name, $TaskToStart.Path } $Sync.ActionStatus = "Started $($TaskToStart.Name)" } # --- DATA FETCHING CONFIG --- $FetchUsers = $false if ($Sync.UserModeActive -or ((Get-Date) - $LastUserFetch).TotalSeconds -gt 30) { $FetchUsers = $true $LastUserFetch = Get-Date } $FetchApps = $false if ($Sync.AppModeActive -or ((Get-Date) - $LastAppFetch).TotalSeconds -gt 60) { $FetchApps = $true $LastAppFetch = Get-Date } # GPU: only query every 5 cycles (~5s) since it's the slowest WMI class $GpuCycleCount++ $DoGpu = ($GpuCycleCount -ge 5) if ($DoGpu) { $GpuCycleCount = 0 } $DoSvcHost = ($CycleCount % 5 -eq 0) # Pass logical core count so the Run-Script block can normalize per-core CPU % $CoreCount = if ($Sync.StaticData -and $Sync.StaticData.Cores -gt 0) { [int]$Sync.StaticData.Cores } else { 1 } $ArgsArray = @([bool]$FetchUsers, [bool]$Sync.ServiceModeActive, [bool]$Sync.TaskModeActive, [bool]$DoGpu, [bool]$FetchApps, [int]$CoreCount, [bool]($CycleCount -eq 0), [bool]$DoSvcHost) $Result = Run-Script -Script { param($DoUsers, $DoServices, $DoTasks, $DoGpu, $DoApps, $CoreCount, $IsFirstCycle, $DoSvcHost) $DebugStr = "" # 1. Performance Data (V71: Optimized with -Filter to avoid piping all instances) $CpuTotal = Get-CimInstance Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" -ErrorAction SilentlyContinue $RAM = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $DiskIO = Get-CimInstance Win32_PerfFormattedData_PerfDisk_LogicalDisk -Filter "Name='_Total'" -ErrorAction SilentlyContinue # Network Stats (Robust Initialization & Error Handling) $TotalSent = 0 $TotalRecv = 0 try { $NetStats = Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface -ErrorAction Stop if ($NetStats) { $TotalSent = ($NetStats | Measure-Object -Property BytesSentPersec -Sum).Sum $TotalRecv = ($NetStats | Measure-Object -Property BytesReceivedPersec -Sum).Sum } } catch {} # Silent fail to 0 if WMI error # GPU Load (only queried every 5 cycles as this WMI class is very slow) $GpuLoad = 0 if ($DoGpu) { try { $GpuCounters = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine -ErrorAction SilentlyContinue if ($GpuCounters) { $GpuLoad = ($GpuCounters | Measure-Object -Property UtilizationPercentage -Average).Average } } catch {} } # Normalize per-core CPU% to 0-100% range. # Win32_PerfFormattedData_PerfProc_Process reports PercentProcessorTime # as (usage * LogicalCores), so divide by core count to get true %. $Procs = Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "Idle" -and $_.Name -ne "_Total" } | ForEach-Object { # Divide by core count and cap at 100.0 $CpuVal = 0.0 if (-not $IsFirstCycle) { $CpuVal = [math]::Round([double]$_.PercentProcessorTime / $CoreCount, 1) if ($CpuVal -gt 100.0) { $CpuVal = 100.0 } if ($CpuVal -lt 0.0) { $CpuVal = 0.0 } } [PSCustomObject]@{ Name = [string]$_.Name IDProcess = [int]$_.IDProcess PercentProcessorTime = $CpuVal WorkingSet = [long]$_.WorkingSet ThreadCount = [int]$_.ThreadCount HandleCount = [int]$_.HandleCount } } # 2. User Data $UserList = $null if ($DoUsers) { $UserList = @() try { $quserOut = query user 2>&1 if ($quserOut -is [string] -or $quserOut -is [Array]) { $quserOut | Select-Object -Skip 1 | ForEach-Object { $line = $_.Trim() if ($line -match '^>?([^\s]+)\s+([^\s]*)\s+(\d+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$') { $UserList += [PSCustomObject]@{ UserName = $Matches[1] SessionName = if ($Matches[2]) { $Matches[2] } else { "console" } SessionId = [int]$Matches[3] State = $Matches[4] IdleTime = $Matches[5] LogonTime = $Matches[6] } } } } $DebugStr += "Usr: $($UserList.Count) " } catch { $DebugStr += "Usr:Err " } } # 3. Services Data $SvcList = $null if ($DoServices) { $SvcList = Get-Service | Select-Object Name, @{N='Status';E={$_.Status.ToString()}}, @{N='StartType';E={$_.StartType.ToString()}}, DisplayName, Description $DebugStr += "Svc: $($SvcList.Count) " } # 4. Tasks Data $TaskList = $null if ($DoTasks) { try { if (-not (Get-Module -Name ScheduledTasks)) { Import-Module ScheduledTasks -ErrorAction SilentlyContinue } $RawTasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Select-Object -First 100 if ($RawTasks) { $TaskList = $RawTasks | ForEach-Object { $taskState = "Unknown" try { if ($_.State) { $taskState = $_.State.ToString() } } catch {} $lastRun = "--" $lastRes = "--" try { $info = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue if ($info) { $lastRun = $info.LastRunTime $lastRes = $info.LastTaskResult } } catch {} [PSCustomObject]@{ TaskName = [string]$_.TaskName TaskPath = [string]$_.TaskPath State = [string]$taskState LastRunTime = $lastRun LastTaskResult = [string]$lastRes Triggers = if ($_.Triggers) { "Yes" } else { "No" } Actions = if ($_.Actions) { "Yes" } else { "No" } } } } $DebugStr += "Tsk: $($TaskList.Count) " } catch { $DebugStr += "Tsk:Err " } } # 5. Installed Apps Data $AppList = $null if ($DoApps) { try { # Load and register Shlwapi type if not already loaded (for indirect string resolution) if (-not ("Win32.Shlwapi" -as [type])) { try { $MethodDefinition = @' [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int SHLoadIndirectString(string pszSource, System.Text.StringBuilder pszOutBuf, int cchOutBuf, System.IntPtr ppvReserved); '@ Add-Type -MemberDefinition $MethodDefinition -Name "Shlwapi" -Namespace "Win32" -ErrorAction SilentlyContinue } catch {} } $GetFriendlyName = { param([string]$RawString) if ($RawString -and $RawString.StartsWith("@") -and ("Win32.Shlwapi" -as [type])) { try { $OutBuf = [System.Text.StringBuilder]::new(1024) $Result = [Win32.Shlwapi]::SHLoadIndirectString($RawString, $OutBuf, $OutBuf.Capacity, [IntPtr]::Zero) if ($Result -eq 0) { return $OutBuf.ToString() } } catch {} } return $RawString } $LocalApps = @() # A. Registry entries (Classic) $RegPaths = @( "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" ) $RegApps = Get-ItemProperty $RegPaths -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and -not $_.SystemComponent -and $_.ParentKeyName -eq $null } | ForEach-Object { $Date = $_.InstallDate if ($Date -match '^\d{8}$') { $Date = "$($Date.Substring(0,4))-$($Date.Substring(4,2))-$($Date.Substring(6,2))" } $DispName = & $GetFriendlyName $_.DisplayName $PubName = & $GetFriendlyName $_.Publisher [PSCustomObject]@{ DisplayName = [string]$DispName DisplayVersion = if ($_.DisplayVersion) { [string]$_.DisplayVersion } else { "" } Publisher = [string]$PubName AppType = "Classic" InstallDate = if ($Date) { [string]$Date } else { "" } } } if ($RegApps) { $LocalApps += $RegApps } # B. AppX Packages (Store) $StoreApps = Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { -not $_.IsFramework -and $_.SignatureKind -in 'Store', 'Developer', 'None' } | ForEach-Object { $Name = $_.Name # Strip hex-like publisher hash or numeric domain prefixes (e.g. AD2F1837.HPPrinterControl -> HPPrinterControl, 19453.net.Rufus -> Rufus) $Name = $Name -replace '^[a-fA-F0-9]{8}[. -]', '' -replace '^\d+\.net\.', '' $Name = $Name -replace '\.', ' ' $Name = $Name -creplace '(?<=[a-z])(?=[A-Z])', ' ' $Name = $Name -replace '\bMicrosoft Microsoft\b', 'Microsoft' $Pub = $_.Publisher if ($Pub -match 'CN=([^,]+)') { $Pub = $Matches[1] } $Pub = & $GetFriendlyName $Pub [PSCustomObject]@{ DisplayName = [string]$Name DisplayVersion = if ($_.Version) { [string]$_.Version } else { "" } Publisher = if ($Pub) { [string]$Pub } else { "" } AppType = "Store" InstallDate = "" } } if ($StoreApps) { $LocalApps += $StoreApps } # Deduplicate by DisplayName and sort $AppList = $LocalApps | Group-Object DisplayName | ForEach-Object { $_.Group[0] } $DebugStr += "App: $($AppList.Count) " } catch { $DebugStr += "App:Err " } } $ServiceHostMap = @{} $ProcessMap = @{} if ($DoSvcHost) { try { $RunningServices = Get-CimInstance Win32_Service -Filter "State='Running' and ProcessId > 0" -ErrorAction SilentlyContinue foreach ($Svc in $RunningServices) { $SvcPid = [int]$Svc.ProcessId $SvcDisp = if ($Svc.DisplayName) { [string]$Svc.DisplayName } else { [string]$Svc.Name } if (-not $ServiceHostMap.ContainsKey($SvcPid)) { $ServiceHostMap[$SvcPid] = @() } $ServiceHostMap[$SvcPid] += $SvcDisp } } catch {} try { Get-Process -ErrorAction SilentlyContinue | ForEach-Object { if ($_.Description -and $_.Description -ne $_.ProcessName) { $ProcessMap[$_.Id] = [string]$_.Description } } } catch {} } # V71: Safety clamps on total system stats to avoid overflow anomalies $SafeCpuLoad = if ($CpuTotal) { [math]::Min(100, [math]::Max(0, [math]::Round([double]$CpuTotal.PercentProcessorTime))) } else { 0 } $SafeDiskLoad = if ($DiskIO) { [math]::Min(100, [math]::Max(0, [math]::Round([double]$DiskIO.PercentDiskTime))) } else { 0 } $SafeDiskR = if ($DiskIO) { [math]::Round([double]$DiskIO.DiskReadBytesPersec / 1MB, 1) } else { 0 } $SafeDiskW = if ($DiskIO) { [math]::Round([double]$DiskIO.DiskWriteBytesPersec / 1MB, 1) } else { 0 } # Compute thread count from the process data we already have (avoids a second expensive WMI call) $ThreadCnt = if ($Procs) { ($Procs | Measure-Object -Property ThreadCount -Sum).Sum } else { 0 } [PSCustomObject]@{ CpuLoad = $SafeCpuLoad GpuLoad = [math]::Round([double]$GpuLoad) DiskLoad = $SafeDiskLoad TotalRam = [math]::Round([double]$RAM.TotalVisibleMemorySize / 1024) FreeRam = [math]::Round([double]$RAM.FreePhysicalMemory / 1024) UpMbps = [math]::Round(([double]$TotalSent * 8) / 1000000, 1) DnMbps = [math]::Round(([double]$TotalRecv * 8) / 1000000, 1) DiskRead = $SafeDiskR DiskWrite = $SafeDiskW Processes = $Procs UserList = $UserList SvcList = $SvcList TaskList = $TaskList AppList = $AppList DebugStr = $DebugStr ThreadCount = $ThreadCnt ServiceHostMap = $ServiceHostMap ProcessMap = $ProcessMap } } -Arguments $ArgsArray $Sync.SysData = $Result $Sync.RawProcessList = $Result.Processes # Cache GPU load for non-GPU cycles if ($DoGpu) { $CachedGpuLoad = $Result.GpuLoad } elseif ($Result) { $Result.GpuLoad = $CachedGpuLoad; $Sync.SysData = $Result } # FORCE UPDATE SHARED MEMORY if ($Result.UserList) { $Sync.UserData = @($Result.UserList) } if ($Result.SvcList) { $Sync.ServiceData = @($Result.SvcList) } if ($Result.TaskList) { $Sync.TaskData = @($Result.TaskList) } if ($Result.AppList) { $Sync.AppData = @($Result.AppList) } if ($DoSvcHost -and $Result.ServiceHostMap) { $Sync.ServiceHostMap = $Result.ServiceHostMap } if ($DoSvcHost -and $Result.ProcessMap) { $Sync.ProcessMap = $Result.ProcessMap } if ($Result.DebugStr) { $Sync.DebugLog = $Result.DebugStr } $Sync.LastUpdate = Get-Date $CycleCount++ $ConsecutiveErrors = 0 # reset on successful cycle } catch { $ConsecutiveErrors++ $errMsg = $_.Exception.Message # Check if PSSession is definitively broken $sessionLost = $false if (-not $IsLocal -and $Session) { if ($Session.State -ne [System.Management.Automation.Runspaces.RunspaceState]::Opened) { $sessionLost = $true } } $shouldReconnect = $sessionLost -or ($ConsecutiveErrors -ge $MaxConsecutiveErrors) if (-not $IsLocal -and $shouldReconnect) { # --- RECONNECT LOOP --- # Signal the TUI to show the reconnect overlay, but keep Running=true # so the TUI stays alive and the user can choose to wait or quit. try { if ($Session) { Remove-PSSession $Session -ErrorAction SilentlyContinue } } catch {} $Session = $null $ConsecutiveErrors = 0 $Sync.Disconnected = $true $Sync.DisconnectTime = [DateTime]::UtcNow $Sync.ReconnectAttempt = 0 $Sync.ReconnectMessage = "Lost connection to '$($Sync.TargetComputer)': $errMsg" $reconnected = $false while ($Sync.Running -and -not $reconnected) { $Sync.ReconnectAttempt++ $Sync.ReconnectMessage = "Attempting to reconnect to '$($Sync.TargetComputer)'... (attempt $($Sync.ReconnectAttempt))" try { $SessParams = @{ ComputerName = $Sync.TargetComputer ErrorAction = 'Stop' SessionOption = (New-PSSessionOption -OperationTimeout 5000) } if ($Sync.Credential) { $SessParams['Credential'] = $Sync.Credential } $Session = New-PSSession @SessParams # Re-fetch static data on the new session $Sync.StaticData = Invoke-Command -Session $Session -ScriptBlock { $CPU = Get-CimInstance Win32_Processor $OS = Get-CimInstance Win32_OperatingSystem $CS = Get-CimInstance Win32_ComputerSystem $GpuName = $null try { $Vid = Get-CimInstance Win32_VideoController -EA SilentlyContinue | Select -First 1; if ($Vid) { $GpuName = $Vid.Name } } catch {} [PSCustomObject]@{ CpuName = $CPU.Name -replace '\s+', ' ' -replace '\(R\)', '' -replace '\(TM\)', '' GpuName = $GpuName Cores = $CS.NumberOfLogicalProcessors TotalRam = [math]::Round($OS.TotalVisibleMemorySize / 1024 / 1024, 1) BootTime = $OS.LastBootUpTime } } # Connection restored — clear the disconnected state and resume $Sync.Disconnected = $false $Sync.ReconnectAttempt = 0 $Sync.ReconnectMessage = "" $reconnected = $true } catch { # Reconnect failed — wait 5 seconds then retry (if user hasn't quit) $Sync.ReconnectMessage = "Attempt $($Sync.ReconnectAttempt) failed. Retrying in 5s... ($($_.Exception.Message))" $waitEnd = [DateTime]::UtcNow.AddSeconds(5) while ([DateTime]::UtcNow -lt $waitEnd -and $Sync.Running) { Start-Sleep -Milliseconds 250 } } } # If user pressed Q while we were reconnecting, exit cleanly if (-not $Sync.Running) { break } # Otherwise we reconnected — fall through to continue the main loop } else { # Local error or minor transient — log and retry next cycle $Sync.DebugLog = "ERR ($ConsecutiveErrors/$MaxConsecutiveErrors): $errMsg" } } # Dynamic sleep: target 1-second total cycle time, accounting for query duration $Elapsed = ([DateTime]::UtcNow - $CycleStart).TotalMilliseconds $SleepMs = [math]::Max(100, 1000 - $Elapsed) Start-Sleep -Milliseconds $SleepMs } # CLEANUP REMOTE SESSION if ($Session) { Remove-PSSession $Session } #endregion } |