mission-control/src/app/api/gateways/discover/route.ts

99 lines
3.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { requireRole } from '@/lib/auth'
interface DiscoveredGateway {
user: string
port: number
active: boolean
description: string
}
/**
* GET /api/gateways/discover
* Discovers OpenClaw gateways via systemd services and port scanning.
* Does not require filesystem access to other users' configs.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const discovered: DiscoveredGateway[] = []
// Parse systemd services for openclaw-gateway instances
try {
const output = execFileSync('systemctl', [
'list-units', '--type=service', '--plain', '--no-legend', '--no-pager',
], { encoding: 'utf-8', timeout: 3000 })
const gwLines = output.split('\n').filter(l => l.includes('openclaw') && l.includes('gateway'))
for (const line of gwLines) {
// e.g. "openclaw-gateway@quant.service loaded active running OpenClaw Gateway (quant)"
const parts = line.trim().split(/\s+/)
const serviceName = parts[0] || ''
const state = parts[2] || '' // active/inactive
const description = parts.slice(4).join(' ') // "OpenClaw Gateway (quant)"
// Extract user from service name
let user = ''
const templateMatch = serviceName.match(/openclaw-gateway@(\w+)\.service/)
if (templateMatch) {
user = templateMatch[1]
} else {
// Custom service name like "openclaw-leads-gateway.service"
const customMatch = serviceName.match(/openclaw-(\w+)-gateway\.service/)
if (customMatch) user = customMatch[1]
}
if (!user) continue
// Find the port by checking what openclaw-gateway processes are listening on
let port = 0
try {
const configPath = `/home/${user}/.openclaw/openclaw.json`
const raw = readFileSync(configPath, 'utf-8')
const config = JSON.parse(raw)
if (typeof config?.gateway?.port === 'number') port = config.gateway.port
} catch {
// Can't read config — try to detect from ss output
}
// If we couldn't read config, try finding port via ss for the service PID
if (!port) {
try {
const pidOutput = execFileSync('systemctl', [
'show', serviceName, '--property=ExecMainPID', '--value',
], { encoding: 'utf-8', timeout: 2000 }).trim()
const pid = parseInt(pidOutput, 10)
if (pid > 0) {
const ssOutput = execFileSync('ss', ['-ltnp'], {
encoding: 'utf-8', timeout: 2000,
})
const pidPattern = `pid=${pid},`
for (const ssLine of ssOutput.split('\n')) {
if (ssLine.includes(pidPattern)) {
const portMatch = ssLine.match(/:(\d+)\s/)
if (portMatch) { port = parseInt(portMatch[1], 10); break }
}
}
}
} catch { /* ignore */ }
}
if (!port) continue
discovered.push({
user,
port,
active: state === 'active',
description: description.replace(/[()]/g, '').trim(),
})
}
} catch {
// systemctl not available or failed — fall back silently
}
return NextResponse.json({ gateways: discovered })
}