Public/Start-WinslopFix.ps1
|
function Start-WinslopFix { <# .SYNOPSIS Starts the persistent WinslopFix monitoring loop. .DESCRIPTION Runs a continuous background monitor that periodically evaluates WorkloadsSessionHost processes and terminates those exceeding configured thresholds. Designed to run as a Scheduled Task under SYSTEM context. Configuration is loaded from config.json at startup and can be overridden via the ConfigPath parameter. The monitor writes all lifecycle and action events to the Windows Event Log under the 'WinslopFix' source. .PARAMETER ConfigPath Path to a custom config.json file. If not specified, the monitor loads configuration from the standard resolution chain: deployed config > bundled defaults > hardcoded fallbacks. .PARAMETER RunOnce Execute a single monitoring pass and exit. Useful for RMM one-shot deployments or testing without entering the persistent loop. .EXAMPLE Start-WinslopFix Start the persistent monitor with default configuration. .EXAMPLE Start-WinslopFix -RunOnce -Verbose Run a single monitoring pass with verbose diagnostic output. .EXAMPLE Start-WinslopFix -ConfigPath 'C:\CustomConfig\config.json' Start the monitor with a custom configuration file. .LINK https://github.com/DailenG/WinslopFix #> [CmdletBinding()] param( [Parameter()] [string]$ConfigPath, [Parameter()] [switch]$RunOnce ) process { # Load configuration $configParams = @{} if ($ConfigPath) { $configParams['ConfigPath'] = $ConfigPath } $config = Get-WinslopFixConfig @configParams Write-WinslopFixLog -Message ( "WinslopFix monitor started. Strategy: $($config.Strategy), " + "PerProcess: $($config.PerProcessThresholdMB)MB, " + "Aggregate: $($config.AggregateThresholdMB)MB, " + "Timer: $($config.TimerSeconds)s, " + "Interval: $($config.ScanIntervalSeconds)s, " + "RunOnce: $RunOnce" ) -EventId 1000 Write-WinslopFixLog -Message 'Configuration loaded successfully.' -EventId 1002 # Track detected PIDs for new-process detection logging $knownPIDs = [System.Collections.Generic.HashSet[int]]::new() # Build the parameter splat for Stop-WorkloadSession $stopParams = @{ Strategy = $config.Strategy PerProcessThresholdMB = $config.PerProcessThresholdMB AggregateThresholdMB = $config.AggregateThresholdMB TimerSeconds = $config.TimerSeconds ProcessName = $config.ProcessName Confirm = $false # Non-interactive in monitor mode } try { do { try { # Detect and log new processes $currentProcs = @(Get-WinslopFixProcess -ProcessName $config.ProcessName -ErrorAction SilentlyContinue) foreach ($proc in $currentProcs) { if ($knownPIDs.Add($proc.Id)) { $memMB = [math]::Round($proc.PrivateMemorySize64 / 1MB, 1) Write-WinslopFixLog -Message ( "New $($config.ProcessName) detected: PID $($proc.Id), Memory: ${memMB}MB" ) -EventId 2000 } } # Purge PIDs that exited naturally $activePIDs = $currentProcs | ForEach-Object { $_.Id } $exitedPIDs = @($knownPIDs | Where-Object { $_ -notin $activePIDs }) foreach ($exitedPid in $exitedPIDs) { $knownPIDs.Remove($exitedPid) | Out-Null Write-WinslopFixLog -Message ( "$($config.ProcessName) PID $exitedPid exited naturally." ) -EventId 2001 } # Execute the configured strategy $results = Stop-WorkloadSession @stopParams if ($results) { # Remove terminated PIDs from tracking foreach ($result in $results) { if ($result.Result -eq 'Terminated') { $knownPIDs.Remove($result.PID) | Out-Null } } } } catch { Write-WinslopFixLog -Message "Error in monitor loop: $($_.Exception.Message)" ` -EventId 9000 -EntryType Error Write-Warning "Monitor loop error: $($_.Exception.Message)" } if (-not $RunOnce) { # Aggressive garbage collection to prevent PS pipeline memory leaks over days of uptime $currentProcs = $null $results = $null [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() Start-Sleep -Seconds $config.ScanIntervalSeconds } } while (-not $RunOnce) } finally { Write-WinslopFixLog -Message 'WinslopFix monitor stopped.' -EventId 1001 } } } |