scripts/internal/continuous-co-review/review-linux-runtime-port.ps1

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

if (-not (Get-Command -Name 'New-ReviewPosixRuntimePort' -ErrorAction SilentlyContinue)) {
    . (Join-Path $PSScriptRoot 'review-posix-runtime-common.ps1')
}

function Resolve-ReviewLinuxCgroupRoot {
    [CmdletBinding()]
    param([string]$CgroupRoot)
    if (-not $IsLinux) { throw 'linux-cgroup-wrong-platform' }
    $candidate = $CgroupRoot
    if ([string]::IsNullOrWhiteSpace($candidate)) { $candidate = $env:SPECREW_REVIEW_CGROUP_ROOT }
    if ([string]::IsNullOrWhiteSpace($candidate)) {
        $entry = Get-Content -LiteralPath '/proc/self/cgroup' -ErrorAction Stop | Where-Object { $_ -match '^0::(?<path>/.*)$' } | Select-Object -First 1
        if ([string]::IsNullOrWhiteSpace([string]$entry)) { throw 'linux-cgroup-v2-membership-missing' }
        $relative = ([regex]::Match([string]$entry, '^0::(?<path>/.*)$').Groups['path'].Value).TrimStart('/')
        $candidate = Join-Path '/sys/fs/cgroup' $relative
    }
    $resolved = [IO.Path]::GetFullPath($candidate)
    if (-not [IO.Directory]::Exists($resolved) -or -not [IO.File]::Exists((Join-Path $resolved 'cgroup.controllers'))) {
        throw "linux-cgroup-v2-root-invalid:$resolved"
    }
    return $resolved
}

function New-ReviewLinuxCgroupDescriptor {
    param([string]$CgroupRoot)
    $root = Resolve-ReviewLinuxCgroupRoot -CgroupRoot $CgroupRoot
    $path = Join-Path $root ('specrew-review-' + [guid]::NewGuid().ToString('N'))
    [void][IO.Directory]::CreateDirectory($path)
    foreach ($name in @('cgroup.procs', 'cgroup.events', 'cgroup.kill')) {
        if (-not [IO.File]::Exists((Join-Path $path $name))) {
            try { [IO.Directory]::Delete($path) } catch { $null = $_ }
            throw "linux-cgroup-required-interface-missing:$name"
        }
    }
    return [pscustomobject]@{ path = $path; root = $root; mode = 'cgroup-v2' }
}

function Get-ReviewLinuxCgroupPids {
    param([Parameter(Mandatory)]$Descriptor)
    $path = Join-Path ([string]$Descriptor.path) 'cgroup.procs'
    if (-not [IO.File]::Exists($path)) { return @() }
    $pids = [Collections.Generic.List[int]]::new()
    foreach ($line in @(Get-Content -LiteralPath $path -ErrorAction Stop)) {
        $observed = 0
        if ([int]::TryParse(([string]$line).Trim(), [ref]$observed) -and $observed -gt 0 -and -not $pids.Contains($observed)) { $pids.Add($observed) }
    }
    return @($pids)
}

function Wait-ReviewLinuxCgroupEmpty {
    param([Parameter(Mandatory)]$Descriptor, [ValidateRange(100, 30000)][int]$TimeoutMilliseconds = 5000)
    $watch = [Diagnostics.Stopwatch]::StartNew()
    while ($watch.ElapsedMilliseconds -lt $TimeoutMilliseconds) {
        if (-not [IO.Directory]::Exists([string]$Descriptor.path)) { return $true }
        try {
            $events = [IO.File]::ReadAllText((Join-Path ([string]$Descriptor.path) 'cgroup.events'))
            if ($events -match '(?m)^populated\s+0\s*$' -and @(Get-ReviewLinuxCgroupPids -Descriptor $Descriptor).Count -eq 0) { return $true }
        }
        catch { return $false }
        [Threading.Thread]::Sleep(25)
    }
    return $false
}

function Stop-ReviewLinuxCgroup {
    param([Parameter(Mandatory)]$Descriptor, [ValidateRange(0, 10)][int]$GraceSeconds = 5)
    if (-not [IO.Directory]::Exists([string]$Descriptor.path)) { return }
    $kill = Get-Command -Name kill -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($null -ne $kill) {
        foreach ($processId in @(Get-ReviewLinuxCgroupPids -Descriptor $Descriptor)) {
            try { & $kill.Source -TERM $processId 2>$null } catch { $null = $_ }
        }
    }
    if ($GraceSeconds -gt 0) { Start-Sleep -Seconds $GraceSeconds }
    try { [IO.File]::WriteAllText((Join-Path ([string]$Descriptor.path) 'cgroup.kill'), '1', [Text.UTF8Encoding]::new($false)) } catch { $null = $_ }
}

function Remove-ReviewLinuxCgroup {
    param([Parameter(Mandatory)]$Descriptor)
    if (-not [IO.Directory]::Exists([string]$Descriptor.path)) { return $true }
    if (-not (Wait-ReviewLinuxCgroupEmpty -Descriptor $Descriptor -TimeoutMilliseconds 1000)) { return $false }
    try { [IO.Directory]::Delete([string]$Descriptor.path); return -not [IO.Directory]::Exists([string]$Descriptor.path) }
    catch { return $false }
}

function Test-ReviewLinuxCgroupMembership {
    param([Parameter(Mandatory)]$Descriptor, [Parameter(Mandatory)]$Ready, [Parameter(Mandatory)][int]$ProcessId)
    if ([string]$Ready.containment_id -cne [string]$Descriptor.path) { return $false }
    return $ProcessId -in @(Get-ReviewLinuxCgroupPids -Descriptor $Descriptor)
}

function Test-ReviewLinuxCgroupAvailability {
    [CmdletBinding()]
    param([string]$CgroupRoot)
    if (-not $IsLinux) { return [pscustomobject]@{ ok = $false; reason = 'linux-cgroup-wrong-platform' } }
    $descriptor = $null; $probe = $null
    try {
        $root = Resolve-ReviewLinuxCgroupRoot -CgroupRoot $CgroupRoot
        $descriptor = New-ReviewLinuxCgroupDescriptor -CgroupRoot $root
        $sleep = Resolve-ReviewPosixExecutable -CommandName sleep
        $startInfo = [Diagnostics.ProcessStartInfo]::new(); $startInfo.FileName = $sleep; [void]$startInfo.ArgumentList.Add('30')
        $startInfo.UseShellExecute = $false; $startInfo.CreateNoWindow = $true
        $probe = [Diagnostics.Process]::new(); $probe.StartInfo = $startInfo
        if (-not $probe.Start()) { throw 'linux-cgroup-probe-start-failed' }
        [IO.File]::WriteAllText((Join-Path ([string]$descriptor.path) 'cgroup.procs'), [string]$probe.Id, [Text.UTF8Encoding]::new($false))
        if ($probe.Id -notin @(Get-ReviewLinuxCgroupPids -Descriptor $descriptor)) { throw 'linux-cgroup-probe-membership-failed' }
        Stop-ReviewLinuxCgroup -Descriptor $descriptor -GraceSeconds 0
        [void]$probe.WaitForExit(5000)
        if (-not (Wait-ReviewLinuxCgroupEmpty -Descriptor $descriptor -TimeoutMilliseconds 5000)) { throw 'linux-cgroup-probe-kill-failed' }
        return [pscustomobject]@{ ok = $true; reason = 'linux-cgroup-v2-ready'; root = $root }
    }
    catch { return [pscustomobject]@{ ok = $false; reason = ('linux-cgroup-v2-unavailable:' + $_.Exception.Message) } }
    finally {
        if ($null -ne $probe) {
            try { if (-not $probe.HasExited) { $probe.Kill($true); [void]$probe.WaitForExit(5000) } } catch { $null = $_ }
            try { $probe.Dispose() } catch { $null = $_ }
        }
        if ($null -ne $descriptor) { try { Remove-ReviewLinuxCgroup -Descriptor $descriptor | Out-Null } catch { $null = $_ } }
    }
}

function New-ReviewLinuxRuntimePort {
    [CmdletBinding()]
    param(
        [ValidateRange(1, 7200)][int]$TimeoutSeconds = 900,
        [ValidateRange(0, 10)][int]$TerminationGraceSeconds = 5,
        [string]$CgroupRoot,
        [scriptblock]$CapabilityProbe
    )
    $availabilityCommand = ${function:Test-ReviewLinuxCgroupAvailability}
    $newDescriptorCommand = ${function:New-ReviewLinuxCgroupDescriptor}
    $membershipCommand = ${function:Test-ReviewLinuxCgroupMembership}
    $stopCommand = ${function:Stop-ReviewLinuxCgroup}
    $waitCommand = ${function:Wait-ReviewLinuxCgroupEmpty}
    $cleanupCommand = ${function:Remove-ReviewLinuxCgroup}
    $resolveRootCommand = ${function:Resolve-ReviewLinuxCgroupRoot}
    if (-not $CapabilityProbe) { $CapabilityProbe = { & $availabilityCommand -CgroupRoot $CgroupRoot }.GetNewClosure() }
    $setup = { param($invocation) & $newDescriptorCommand -CgroupRoot $CgroupRoot }.GetNewClosure()
    $verify = { param($descriptor, $ready, $processId) & $membershipCommand -Descriptor $descriptor -Ready $ready -ProcessId $processId }.GetNewClosure()
    $stop = { param($descriptor, $grace) & $stopCommand -Descriptor $descriptor -GraceSeconds $grace }.GetNewClosure()
    $wait = { param($descriptor, $timeout) & $waitCommand -Descriptor $descriptor -TimeoutMilliseconds $timeout }.GetNewClosure()
    $cleanup = { param($descriptor) & $cleanupCommand -Descriptor $descriptor }.GetNewClosure()
    $recover = {
        param($receipt)
        if ($null -eq $receipt -or [string]$receipt.runtime_id -cne 'linux-cgroup-v2-runtime' -or [string]$receipt.platform -cne 'linux' -or [string]$receipt.containment_kind -cne 'cgroup-v2') {
            return [pscustomobject]@{ termination_verified = $false; containment = 'unknown'; process_tree_live = $null; failure_reason = 'linux-recovery-receipt-mismatch' }
        }
        try {
            $root = [IO.Path]::GetFullPath((& $resolveRootCommand -CgroupRoot $CgroupRoot)).TrimEnd([IO.Path]::DirectorySeparatorChar)
            $path = [IO.Path]::GetFullPath([string]$receipt.containment_id)
            $prefix = $root + [IO.Path]::DirectorySeparatorChar
            if (-not $path.StartsWith($prefix, [StringComparison]::Ordinal) -or [IO.Path]::GetDirectoryName($path) -cne $root -or [IO.Path]::GetFileName($path) -cnotmatch '^specrew-review-[a-f0-9]{32}$') {
                return [pscustomobject]@{ termination_verified = $false; containment = 'unknown'; process_tree_live = $null; failure_reason = 'linux-recovery-cgroup-path-invalid' }
            }
            if (-not [IO.Directory]::Exists($path)) {
                return [pscustomobject]@{ termination_verified = $true; containment = 'verified'; process_tree_live = $false; failure_reason = $null }
            }
            $descriptor = [pscustomobject]@{ path = $path; root = $root; mode = 'cgroup-v2' }
            & $stopCommand -Descriptor $descriptor -GraceSeconds $TerminationGraceSeconds
            $empty = [bool](& $waitCommand -Descriptor $descriptor -TimeoutMilliseconds 5000)
            $clean = $empty -and [bool](& $cleanupCommand -Descriptor $descriptor)
            return [pscustomobject]@{
                termination_verified = $clean; containment = $(if ($clean) { 'verified' } else { 'unknown' })
                process_tree_live = (-not $empty); failure_reason = $(if ($clean) { $null } else { 'linux-recovery-cgroup-not-empty-or-not-cleaned' })
            }
        }
        catch {
            return [pscustomobject]@{ termination_verified = $false; containment = 'unknown'; process_tree_live = $null; failure_reason = ('linux-recovery-failed:' + $_.Exception.Message) }
        }
    }.GetNewClosure()
    return New-ReviewPosixRuntimePort -RuntimeId 'linux-cgroup-v2-runtime' -Platform linux -Containment cgroup-v2 -HostMode linux-cgroup `
        -TimeoutSeconds $TimeoutSeconds -TerminationGraceSeconds $TerminationGraceSeconds -CapabilityProbe $CapabilityProbe `
        -SetupContainment $setup -VerifyContainment $verify -StopContainment $stop -WaitContainmentEmpty $wait -CleanupContainment $cleanup -RecoverContainment $recover
}