← back to Letsbegin

app/api/live/check/route.ts

179 lines

import { NextRequest, NextResponse } from 'next/server'
import { exec } from 'child_process'
import { promisify } from 'util'

const execAsync = promisify(exec)

export async function POST(request: NextRequest) {
  try {
    const { pid, action } = await request.json()

    if (!pid) {
      return NextResponse.json({ error: 'PID required' }, { status: 400 })
    }

    // Check if process exists
    let processExists = false
    let processInfo: any = null

    try {
      const { stdout } = await execAsync(`ps -p ${pid} -o pid,state,pcpu,pmem,etime,comm --no-headers 2>/dev/null`)
      if (stdout.trim()) {
        const parts = stdout.trim().split(/\s+/)
        processExists = true
        processInfo = {
          pid: parseInt(parts[0]),
          state: parts[1],
          cpu: parseFloat(parts[2]),
          memory: parseFloat(parts[3]),
          elapsed: parts[4],
          command: parts.slice(5).join(' ')
        }
      }
    } catch {
      processExists = false
    }

    if (!processExists) {
      return NextResponse.json({
        status: 'dead',
        message: `Process ${pid} is not running`,
        processInfo: null
      })
    }

    // Get detailed process info
    let detailedInfo: any = {}

    try {
      // Get working directory
      const { stdout: cwdOut } = await execAsync(`readlink -f /proc/${pid}/cwd 2>/dev/null || echo "unknown"`)
      detailedInfo.cwd = cwdOut.trim()

      // Get command line
      const { stdout: cmdOut } = await execAsync(`cat /proc/${pid}/cmdline 2>/dev/null | tr '\\0' ' ' || echo "unknown"`)
      detailedInfo.cmdline = cmdOut.trim()

      // Get file descriptors count
      const { stdout: fdOut } = await execAsync(`ls /proc/${pid}/fd 2>/dev/null | wc -l || echo "0"`)
      detailedInfo.openFiles = parseInt(fdOut.trim())

      // Get threads count
      const { stdout: threadsOut } = await execAsync(`ls /proc/${pid}/task 2>/dev/null | wc -l || echo "0"`)
      detailedInfo.threads = parseInt(threadsOut.trim())

      // Get memory details
      const { stdout: memOut } = await execAsync(`cat /proc/${pid}/status 2>/dev/null | grep -E "^(VmRSS|VmSize|VmPeak):" || echo ""`)
      const memLines = memOut.trim().split('\n')
      memLines.forEach(line => {
        const [key, value] = line.split(':').map(s => s.trim())
        if (key && value) {
          detailedInfo[key] = value
        }
      })

      // Check for stuck indicators
      const stuckIndicators = []

      // Check CPU usage over time
      if (processInfo.cpu < 0.1 && processInfo.state === 'S') {
        // Get recent activity
        const { stdout: statOut } = await execAsync(`cat /proc/${pid}/stat 2>/dev/null || echo ""`)
        const statParts = statOut.split(' ')
        if (statParts.length > 13) {
          const utime = parseInt(statParts[13])
          const stime = parseInt(statParts[14])
          detailedInfo.cpuTicks = { user: utime, system: stime }
        }
      }

      // Check if it's a ralph process and get its status
      if (detailedInfo.cmdline.includes('ralph') || detailedInfo.cwd.includes('ralph')) {
        try {
          const { stdout: ralphOut } = await execAsync(`curl -s http://localhost:7300/api/ralph/status 2>/dev/null`)
          const ralphStatus = JSON.parse(ralphOut)
          detailedInfo.ralphStatus = {
            running: ralphStatus.running,
            currentStory: ralphStatus.currentStory,
            progress: `${Object.values(ralphStatus.storyResults || {}).filter((s: any) => s === 'pass').length}/${Object.keys(ralphStatus.storyResults || {}).length}`,
            lastLog: ralphStatus.logs?.slice(-3) || []
          }

          // Check if ralph is stuck (same story for too long with no progress)
          if (ralphStatus.running && ralphStatus.elapsed > 3600) {
            stuckIndicators.push('Ralph has been running for over an hour on the same story')
          }
        } catch {}
      }

      detailedInfo.stuckIndicators = stuckIndicators

    } catch (err) {
      console.error('Error getting detailed info:', err)
    }

    // Determine if process appears stuck
    const isStuck =
      processInfo.state === 'D' || // Uninterruptible sleep (usually I/O)
      processInfo.state === 'T' || // Stopped
      processInfo.state === 'Z' || // Zombie
      (detailedInfo.stuckIndicators && detailedInfo.stuckIndicators.length > 0)

    // Handle restart action
    if (action === 'restart') {
      try {
        // Kill the process
        await execAsync(`kill -9 ${pid}`)

        // If it's ralph, try to restart it
        if (detailedInfo.cmdline?.includes('ralph') || detailedInfo.cwd?.includes('ralph')) {
          // Get the project path from ralph status or cwd
          const projectPath = detailedInfo.cwd || '/root/Projects/angels-flowers'

          return NextResponse.json({
            status: 'restarted',
            message: `Process ${pid} was killed. Ralph processes need to be restarted manually via the UI.`,
            processInfo,
            detailedInfo
          })
        }

        return NextResponse.json({
          status: 'killed',
          message: `Process ${pid} was terminated`,
          processInfo,
          detailedInfo
        })
      } catch (err: any) {
        return NextResponse.json({
          status: 'error',
          message: `Failed to kill process: ${err.message}`,
          processInfo,
          detailedInfo
        })
      }
    }

    return NextResponse.json({
      status: isStuck ? 'stuck' : 'running',
      message: isStuck
        ? `Process ${pid} appears to be stuck (state: ${processInfo.state})`
        : `Process ${pid} is running normally`,
      processInfo,
      detailedInfo,
      stateDescription: ({
        'R': 'Running',
        'S': 'Sleeping (waiting for event)',
        'D': 'Uninterruptible sleep (usually I/O)',
        'T': 'Stopped',
        'Z': 'Zombie',
        'X': 'Dead'
      } as Record<string, string>)[processInfo.state] || processInfo.state
    })

  } catch (error: any) {
    console.error('Check process error:', error)
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}