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
        }
    }
}