src/PowerShell/StatusBatch.ps1

# cdp PowerShell domain: StatusBatch.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function Get-CdpStatusIntegerSetting {
    param(
        [Parameter(Mandatory = $true)][string]$Name,
        [Parameter(Mandatory = $true)][int]$Default,
        [Parameter(Mandatory = $true)][int]$Minimum,
        [Parameter(Mandatory = $true)][int]$Maximum
    )

    $parsed = 0
    $raw = [Environment]::GetEnvironmentVariable($Name)
    if (-not [int]::TryParse($raw, [ref]$parsed)) { $parsed = $Default }
    [Math]::Max($Minimum, [Math]::Min($Maximum, $parsed))
}

function Resolve-CdpStatusThrottleLimit {
    param([int]$Value = 0)

    if ($Value -gt 0) { return [Math]::Max(1, [Math]::Min(16, $Value)) }
    $default = [Math]::Max(1, [Math]::Min(4, [Environment]::ProcessorCount))
    Get-CdpStatusIntegerSetting -Name 'CDP_STATUS_CONCURRENCY' -Default $default -Minimum 1 -Maximum 16
}

function Get-CdpStatusCacheKey {
    param([Parameter(Mandatory = $true)][object]$Project)

    $name = ([string]$Project.name).ToUpperInvariant()
    $resolution = Resolve-CdpProjectPath -Project $Project
    if ($resolution.ErrorCode) {
        return "$name`0$($resolution.Profile)`0invalid:$($resolution.Source):$($resolution.RawPath)"
    }
    $path = Get-CdpComparablePath -Path ([string]$resolution.ResolvedPath)
    "$name`0$($resolution.Profile)`0$path"
}

function Get-CdpCachedGitProjectInfo {
    param(
        [Parameter(Mandatory = $true)][object]$Project,
        [Parameter(Mandatory = $true)][int]$TtlSeconds,
        [switch]$Refresh
    )

    $key = Get-CdpStatusCacheKey -Project $Project
    if ($Refresh) { [void]$script:CdpStatusCache.Remove($key); return $null }
    if ($TtlSeconds -le 0) { return $null }
    $entry = $script:CdpStatusCache[$key]
    if (-not $entry) { return $null }
    if (([DateTime]::UtcNow - $entry.CachedAt).TotalSeconds -ge $TtlSeconds) {
        [void]$script:CdpStatusCache.Remove($key)
        return $null
    }
    $entry.Value
}

function Set-CdpCachedGitProjectInfo {
    param(
        [Parameter(Mandatory = $true)][object]$Project,
        [Parameter(Mandatory = $true)][object]$Info,
        [Parameter(Mandatory = $true)][int]$TtlSeconds
    )

    if ($TtlSeconds -le 0) { return }
    $script:CdpStatusCache[(Get-CdpStatusCacheKey -Project $Project)] = [PSCustomObject]@{
        CachedAt = [DateTime]::UtcNow
        Value = $Info
    }
}

function New-CdpFailedGitProjectInfo {
    param(
        [Parameter(Mandatory = $true)][object]$Project,
        [Parameter(Mandatory = $true)][string]$StatusLabel
    )

    $info = New-CdpGitProjectInfo -Project $Project
    if (-not $info.PathResolutionError) {
        $info.PathExists = Test-Path -LiteralPath ([string]$info.ResolvedPath)
    }
    $info.StatusLabel = $StatusLabel
    $info.NeedsAttention = $true
    $info
}

function Get-CdpStatusWorkerHelperScript {
    @(
        'function New-CdpGitProjectInfo {'
        (Get-Command New-CdpGitProjectInfo).ScriptBlock.ToString()
        '}'
        'function Convert-WindowsPathToWSL {'
        (Get-Command Convert-WindowsPathToWSL).ScriptBlock.ToString()
        '}'
        'function Get-CdpDetectedPathProfile {'
        (Get-Command Get-CdpDetectedPathProfile).ScriptBlock.ToString()
        '}'
        'function Get-CdpCurrentPathProfile {'
        (Get-Command Get-CdpCurrentPathProfile).ScriptBlock.ToString()
        '}'
        'function Get-CdpProjectPathProperty {'
        (Get-Command Get-CdpProjectPathProperty).ScriptBlock.ToString()
        '}'
        'function New-CdpPathResolution {'
        (Get-Command New-CdpPathResolution).ScriptBlock.ToString()
        '}'
        'function Get-CdpInvalidKnownPathProfile {'
        (Get-Command Get-CdpInvalidKnownPathProfile).ScriptBlock.ToString()
        '}'
        'function Resolve-CdpProjectPath {'
        (Get-Command Resolve-CdpProjectPath).ScriptBlock.ToString()
        '}'
        'function ConvertFrom-CdpGitStatusPorcelainV2 {'
        (Get-Command ConvertFrom-CdpGitStatusPorcelainV2).ScriptBlock.ToString()
        '}'
        'function Set-CdpGitProjectStatusLabel {'
        (Get-Command Set-CdpGitProjectStatusLabel).ScriptBlock.ToString()
        '}'
        'function Get-CdpGitProjectInfo {'
        (Get-Command Get-CdpGitProjectInfo).ScriptBlock.ToString()
        '}'
    ) -join "`n"
}

function Start-CdpStatusWorker {
    param(
        [Parameter(Mandatory = $true)][object]$Project,
        [Parameter(Mandatory = $true)][System.Management.Automation.Runspaces.RunspacePool]$Pool,
        [Parameter(Mandatory = $true)][string]$HelperScript,
        [Parameter(Mandatory = $true)][scriptblock]$CollectorScript
    )

    $pipeline = [PowerShell]::Create()
    $pipeline.RunspacePool = $Pool
    $workerScript = {
        param($Project, $HelperScript, $CollectorText)
        # Helper text is assembled only from trusted module-owned function bodies.
        Invoke-Expression $HelperScript
        & ([scriptblock]::Create($CollectorText)) $Project
    }
    [void]$pipeline.AddScript($workerScript.ToString())
    [void]$pipeline.AddArgument($Project)
    [void]$pipeline.AddArgument($HelperScript)
    [void]$pipeline.AddArgument($CollectorScript.ToString())
    [PSCustomObject]@{
        Project = $Project
        PowerShell = $pipeline
        Async = $pipeline.BeginInvoke()
    }
}

function Complete-CdpStatusWorker {
    param(
        [Parameter(Mandatory = $true)][object]$Task,
        [Parameter(Mandatory = $true)][DateTime]$Deadline,
        [Parameter(Mandatory = $true)][int]$TtlSeconds
    )

    try {
        $remaining = [Math]::Max(0, [int][Math]::Ceiling(($Deadline - [DateTime]::UtcNow).TotalMilliseconds))
        if (-not $Task.Async.AsyncWaitHandle.WaitOne($remaining)) {
            try { $Task.PowerShell.Stop() } catch {}
            $info = New-CdpFailedGitProjectInfo -Project $Task.Project -StatusLabel 'status timed out'
        } else {
            try {
                $values = @($Task.PowerShell.EndInvoke($Task.Async))
                $info = if ($values.Count -gt 0) { $values[-1] } else {
                    New-CdpFailedGitProjectInfo -Project $Task.Project -StatusLabel 'status failed'
                }
            } catch {
                $info = New-CdpFailedGitProjectInfo -Project $Task.Project -StatusLabel 'status failed'
            }
        }
        Set-CdpCachedGitProjectInfo -Project $Task.Project -Info $info -TtlSeconds $TtlSeconds
        $info
    } finally {
        $Task.PowerShell.Dispose()
    }
}

function Get-CdpGitProjectInfoBatch {
    param(
        [Parameter(Mandatory = $true)][object[]]$Projects,
        [int]$ThrottleLimit = 0,
        [int]$TimeoutSeconds = 0,
        [switch]$Refresh,
        [scriptblock]$CollectorScript = { param($Project) Get-CdpGitProjectInfo -Project $Project }
    )

    if ($Projects.Count -eq 0) { return }
    $limit = Resolve-CdpStatusThrottleLimit -Value $ThrottleLimit
    if ($TimeoutSeconds -le 0) {
        $TimeoutSeconds = Get-CdpStatusIntegerSetting -Name 'CDP_STATUS_TIMEOUT_SECONDS' -Default 10 -Minimum 1 -Maximum 60
    }
    $ttl = Get-CdpStatusIntegerSetting -Name 'CDP_STATUS_CACHE_TTL' -Default 0 -Minimum 0 -Maximum 60
    $pool = [RunspaceFactory]::CreateRunspacePool(1, [Math]::Min($limit, $Projects.Count))
    try {
        $pool.Open()
        $helperScript = Get-CdpStatusWorkerHelperScript
        for ($offset = 0; $offset -lt $Projects.Count; $offset += $limit) {
            $tasks = New-Object 'System.Collections.Generic.List[object]'
            $end = [Math]::Min($Projects.Count, $offset + $limit)
            for ($index = $offset; $index -lt $end; $index++) {
                $project = $Projects[$index]
                $cached = Get-CdpCachedGitProjectInfo -Project $project -TtlSeconds $ttl -Refresh:$Refresh
                if ($cached) {
                    $tasks.Add([PSCustomObject]@{ Project = $project; Cached = $cached })
                } else {
                    $tasks.Add((Start-CdpStatusWorker -Project $project -Pool $pool `
                        -HelperScript $helperScript -CollectorScript $CollectorScript))
                }
            }
            $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
            foreach ($task in $tasks) {
                if ($task.Cached) { $task.Cached } else {
                    Complete-CdpStatusWorker -Task $task -Deadline $deadline -TtlSeconds $ttl
                }
            }
        }
    } finally {
        $pool.Dispose()
    }
}