const dgram = require('dgram'); const state = require('../../infrastructure/state'); const { getMetrics, getHistoricalData, trackRequestWithTiming, trackRequest } = require('../../maintenance/metrics'); const { getHashForDomain } = require('../../core/core'); const { logDebug, logError } = require('../../infrastructure/logger'); const { createErrorResponse } = require('../../infrastructure/error_handler'); const { parseMinutesToMs } = require('../../infrastructure/utils'); const pidusage = require('pidusage'); async function handleStatsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; if (method === 'GET' && urlPath === '/api/stats') { try { const startTime = Date.now(); const stats = getMetrics(); const holesailChildren = []; const pidStatsPromises = []; const pidToChildMap = new Map(); for (const [id, child] of state.holesailChildren.entries()) { try { const opts = state.holesailOpts.get(id) || {}; const info = state.holesailInfos.get(id) || {}; const startTime = state.holesailChildStartTimes.get(id); const uptime = startTime ? Date.now() - startTime : 0; const status = child && !child.killed ? 'running' : 'stopped'; const pid = child ? child.pid : null; if (pid && child && !child.killed) { pidStatsPromises.push( pidusage(pid).then(stats => ({ id, type: 'server', stats })).catch(err => { logDebug('Admin', `Failed to get stats for server child ${id} (PID ${pid}): ${err.message}`); return { id, type: 'server', stats: null }; }) ); } pidToChildMap.set(id, { id, type: 'server', status, pid, uptime, cpuUsage: null, memoryUsage: null, opts: { port: opts.port, host: opts.host || '0.0.0.0', protocol: opts.udp ? 'udp' : 'tcp', secure: opts.secure || false, domain: opts.domain || null }, info: info }); } catch (err) { logError('Admin', `Error collecting stats for server child ${id}: ${err.message}`); } } for (const [id, child] of state.holesailClientChildren.entries()) { try { const opts = state.holesailClientOpts.get(id) || {}; const info = state.holesailClientInfos.get(id) || {}; const startTime = state.holesailChildStartTimes.get(id); const uptime = startTime ? Date.now() - startTime : 0; const status = child && !child.killed ? 'running' : 'stopped'; const pid = child ? child.pid : null; if (pid && child && !child.killed) { pidStatsPromises.push( pidusage(pid).then(stats => ({ id, type: 'client', stats })).catch(err => { logDebug('Admin', `Failed to get stats for client child ${id} (PID ${pid}): ${err.message}`); return { id, type: 'client', stats: null }; }) ); } pidToChildMap.set(id, { id, type: 'client', status, pid, uptime, cpuUsage: null, memoryUsage: null, opts: { domain: opts.domain || null, port: opts.port, host: opts.host || null, protocol: opts.protocol || 'tcp' }, info: info }); } catch (err) { logError('Admin', `Error collecting stats for client child ${id}: ${err.message}`); } } const pidStatsResults = await Promise.all(pidStatsPromises); for (const result of pidStatsResults) { if (result.stats) { const childData = pidToChildMap.get(result.id); if (childData) { childData.cpuUsage = { user: result.stats.cpu / 2, system: result.stats.cpu / 2, percentage: result.stats.cpu }; childData.memoryUsage = { rss: result.stats.memory, heapUsed: result.stats.memory * 0.8, heapTotal: result.stats.memory, external: 0, arrayBuffers: 0 }; } } } for (const childData of pidToChildMap.values()) { holesailChildren.push(childData); } const managedConnections = new Set(); for (const child of holesailChildren) { if (child.opts && child.opts.domain && child.opts.port) { managedConnections.add(`${child.opts.domain}:${child.opts.port}`); } } const p2pDomainEntries = []; const hashPromises = []; for (const [key, holesail] of state.holesails.entries()) { try { if (managedConnections.has(key)) { continue; } const keyParts = key.split(':'); if (keyParts.length !== 2) { logDebug('Admin', `Skipping invalid holesail key format: ${key}`); continue; } const domain = keyParts[0]; const port = parseInt(keyParts[1], 10); if (isNaN(port)) { logDebug('Admin', `Skipping holesail key with invalid port: ${key}`); continue; } const ip = state.domainToIPMap.get ? state.domainToIPMap.get(domain) : state.domainToIPMap[domain]; const isPersistent = state.persistentConnections && state.persistentConnections.has(key); const status = holesail && typeof holesail === 'object' ? 'running' : 'stopped'; let holesailInfo = null; try { if (holesail && typeof holesail === 'object' && holesail.info) { holesailInfo = holesail.info; } } catch (e) { // Info not available } p2pDomainEntries.push({ key, domain, port, ip, isPersistent, status, holesailInfo }); hashPromises.push( getHashForDomain(domain).catch(err => { logDebug('Admin', `Could not get hash for domain ${domain}: ${err.message}`); return null; }) ); } catch (err) { logError('Admin', `Error processing p2p domain connection ${key}: ${err.message}`); } } const hashResults = await Promise.all(hashPromises); let mainProcessStats = null; try { mainProcessStats = await pidusage(process.pid); } catch (err) { logDebug('Admin', `Failed to get main process stats for p2p connections: ${err.message}`); } const p2pConnectionCount = p2pDomainEntries.length || 1; let perConnectionCpu = null; let perConnectionMemory = null; if (mainProcessStats) { perConnectionCpu = { user: mainProcessStats.cpu / (p2pConnectionCount * 2), system: mainProcessStats.cpu / (p2pConnectionCount * 2), percentage: mainProcessStats.cpu / p2pConnectionCount }; perConnectionMemory = { rss: Math.floor(mainProcessStats.memory / p2pConnectionCount), heapUsed: Math.floor((mainProcessStats.memory * 0.8) / p2pConnectionCount), heapTotal: Math.floor(mainProcessStats.memory / p2pConnectionCount), external: 0, arrayBuffers: 0 }; } for (let i = 0; i < p2pDomainEntries.length; i++) { try { const entry = p2pDomainEntries[i]; const hash = hashResults[i]; const startTime = state.holesailStartTimes && state.holesailStartTimes.get(entry.key); const uptime = startTime ? Date.now() - startTime : 0; let timeRemaining = null; if (!entry.isPersistent && process.env.FULL_PERSISTENCE !== 'true' && startTime) { const timeoutDuration = parseMinutesToMs(process.env.HOLESAIL_TIMEOUT || '5'); const elapsed = Date.now() - startTime; const remaining = Math.max(0, timeoutDuration - elapsed); timeRemaining = remaining; } holesailChildren.push({ id: entry.key, type: 'p2p-domain', status: entry.status, pid: process.pid, uptime: uptime, timeRemaining: timeRemaining, cpuUsage: perConnectionCpu, memoryUsage: perConnectionMemory, opts: { domain: entry.domain, port: entry.port, host: entry.ip || null, protocol: 'tcp' }, persistent: entry.isPersistent, hash: hash || null, info: entry.holesailInfo || null }); } catch (err) { logError('Admin', `Error creating stats entry for p2p domain connection ${p2pDomainEntries[i].key}: ${err.message}`); } } stats.holesailChildren = holesailChildren; const responseTime = Date.now() - startTime; trackRequestWithTiming('/api/stats', true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(stats)); } catch (err) { logError('Admin', `Error in /api/stats: ${err.message}`, err); trackRequest('/api/stats', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } if (method === 'GET' && urlPath === '/api/stats/historical') { try { let minutes = 5; if (req.url.includes('?')) { const queryString = req.url.split('?')[1]; const params = new URLSearchParams(queryString); const minutesParam = params.get('minutes'); if (minutesParam) { minutes = parseInt(minutesParam, 10); if (isNaN(minutes) || minutes < 1) minutes = 5; if (minutes > 2880) minutes = 2880; } } const startTime = Date.now(); const historical = getHistoricalData(minutes); const responseTime = Date.now() - startTime; trackRequestWithTiming('/api/stats/historical', true, responseTime); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(historical)); } catch (err) { logError('Admin', `Error in /api/stats/historical: ${err.message}`, err); trackRequest('/api/stats/historical', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } return true; } return false; } module.exports = { handleStatsRoutes };