Spaces:
Runtime error
Runtime error
| 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 }) | |
| } | |