diff --git a/bin/lib/admin.js b/bin/lib/admin.js index defc15f..6ab7d5f 100644 --- a/bin/lib/admin.js +++ b/bin/lib/admin.js @@ -7,6 +7,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import { getLogsForAPI, LogFileWatcher } from './logs.js'; import { startService, stopService, restartService, getServiceStatus, getAllServiceStatuses, validateServiceCanRun } from './service-manager.js'; import { initializePlugins, callHook } from './plugin-system.js'; +import { getResourceMonitor } from './resources.js'; // ws helper function sendWebSocketMessage(ws, message) { @@ -23,6 +24,7 @@ function sendWebSocketMessage(ws, message) { // Global log watcher instance let globalLogWatcher = null; let wsServer = null; +let globalResourceMonitor = null; async function handleWebSocketMessage(ws, message) { if (message.type === 'start_log_stream') { @@ -43,6 +45,70 @@ async function handleWebSocketMessage(ws, message) { sendWebSocketMessage(ws, { type: 'log_data', service: ws.serviceFilter, logs }); } else if (message.type === 'stop_log_stream') { ws.serviceFilter = null; + } else if (message.type === 'start_metrics_stream') { + console.log('🔧 WebSocket: start_metrics_stream received, service:', message.service); + ws.metricsFilter = message.service || 'all'; + console.log('🔧 WebSocket: metricsFilter set to:', ws.metricsFilter); + if (!globalResourceMonitor) { + sendWebSocketMessage(ws, { type: 'error', message: 'Resource monitor not initialized' }); + return; + } + // Send current metrics in the expected format + const serviceFilter = ws.metricsFilter === 'all' ? null : ws.metricsFilter; + const currentMetrics = globalResourceMonitor.getCurrentMetrics(serviceFilter); + console.log('🔧 WebSocket: getCurrentMetrics returned:', serviceFilter ? 'single service' : Object.keys(currentMetrics || {})); + const services = []; + + // Convert metrics to array format expected by frontend + if (serviceFilter && currentMetrics) { + // Single service requested + services.push({ + serviceName: serviceFilter, + timestamp: currentMetrics.timestamp, + cpu: { usage: currentMetrics.cpu || 0 }, + memory: { percentage: currentMetrics.memory || 0 }, + network: currentMetrics.network || { rx: 0, tx: 0 }, + status: currentMetrics.status || 'unknown' + }); + } else if (!serviceFilter && currentMetrics) { + // All services requested + for (const [serviceName, metrics] of Object.entries(currentMetrics)) { + if (metrics) { + services.push({ + serviceName: serviceName, + timestamp: metrics.timestamp, + cpu: { usage: metrics.cpu || 0 }, + memory: { percentage: metrics.memory || 0 }, + network: metrics.network || { rx: 0, tx: 0 }, + status: metrics.status || 'unknown' + }); + } + } + } + + console.log('🔧 WebSocket: Sending metrics_data with', services.length, 'services'); + sendWebSocketMessage(ws, { + type: 'metrics_data', + service: ws.metricsFilter, + services: services, + systemInfo: globalResourceMonitor.getSystemInfo() + }); + } else if (message.type === 'stop_metrics_stream') { + ws.metricsFilter = null; + } else if (message.type === 'get_metrics_history') { + if (!globalResourceMonitor) { + sendWebSocketMessage(ws, { type: 'error', message: 'Resource monitor not initialized' }); + return; + } + const history = globalResourceMonitor.getMetricsHistory(message.service, { + since: message.since, + limit: message.limit || 100 + }); + sendWebSocketMessage(ws, { + type: 'metrics_history', + service: message.service, + history + }); } } @@ -72,6 +138,34 @@ function broadcastLogEvent(event, payload) { }); } +// Set up listener for real-time metrics updates +function broadcastMetricsEvent(event, payload) { + if (!wsServer) return; + console.log(`🔧 Broadcasting ${event} to ${wsServer.clients.size} clients`); + wsServer.clients.forEach(ws => { + if (ws.readyState !== WebSocket.OPEN) return; + console.log(`🔧 Client metricsFilter: ${ws.metricsFilter}`); + // Only send to clients that requested metrics stream + if (!ws.metricsFilter) return; + // Filter by service if client requested specific service + if (ws.metricsFilter !== 'all' && payload.services) { + const filteredServices = payload.services.filter(s => s.serviceName === ws.metricsFilter); + if (filteredServices.length === 0) return; + payload = { ...payload, services: filteredServices }; + } + + if (event === 'metricsUpdate') { + console.log(`🔧 Sending metrics_update to client with ${payload.services?.length || 0} services`); + sendWebSocketMessage(ws, { + type: 'metrics_update', + timestamp: payload.timestamp, + services: payload.services, + system: payload.system + }); + } + }); +} + async function checkServiceStatus(service) { return new Promise((resolve) => { const timeout = setTimeout(() => { @@ -145,6 +239,7 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) { Polyglot Admin Dashboard + @@ -843,6 +1392,78 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) { `} + +
+
+

Resource Monitoring

+
+ + + +
+
+
+
+
+

CPU Usage

+ +
+ +
+
+ Current + -- +
+
+ Average + -- +
+
+
+
+
+

Memory Usage

+ 🧠 +
+ +
+
+ Current + -- +
+
+ Average + -- +
+
+
+
+
+

Network I/O

+ 🌐 +
+ +
+
+ Received + -- +
+
+ Transmitted + -- +
+
+
+
+
+
@@ -915,6 +1536,22 @@ export async function startAdminDashboard(options = {}) { console.log(chalk.gray(' Logs will be read from files on demand')); } + // Initialize resource monitor + console.log('🔧 Initializing resource monitor...'); + globalResourceMonitor = getResourceMonitor({ + collectInterval: 5000, + maxHistorySize: 720 + }); + try { + console.log('🔧 Calling globalResourceMonitor.initialize()...'); + await globalResourceMonitor.initialize(); + console.log(chalk.green('✅ Resource monitor initialized')); + } catch (error) { + console.warn(chalk.yellow('⚠️ Failed to initialize resource monitor:', error.message)); + console.log(chalk.gray(' Resource metrics will not be available')); + console.error('Resource monitor error:', error); + } + const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${port}`); @@ -972,6 +1609,44 @@ export async function startAdminDashboard(options = {}) { return; } + if (url.pathname === '/api/metrics') { + // API endpoint for resource metrics + try { + const serviceName = url.searchParams.get('service'); + const since = url.searchParams.get('since'); + const limit = url.searchParams.get('limit') || '100'; + + let metrics = {}; + if (globalResourceMonitor) { + if (serviceName) { + metrics = globalResourceMonitor.getMetricsHistory(serviceName, { + since, + limit: parseInt(limit) + }); + } else { + metrics = globalResourceMonitor.getCurrentMetrics(); + } + } + + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }); + res.end(JSON.stringify({ + metrics, + systemInfo: globalResourceMonitor?.getSystemInfo() || null + }, null, 2)); + } catch (e) { + console.error('❌ Metrics API error:', e.message); + res.writeHead(500, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }); + res.end(JSON.stringify({ error: e.message })); + } + return; + } + // Service management endpoints if (url.pathname === '/api/services/start' && req.method === 'POST') { try { @@ -1085,6 +1760,49 @@ export async function startAdminDashboard(options = {}) { options, services: cfg.services }); + + // Start resource monitoring if available + if (globalResourceMonitor) { + console.log('🔧 Starting resource monitoring...'); + // Start with initial services (PIDs might be null initially) + const servicesWithPids = cfg.services.map(service => { + const status = getServiceStatus(service.name); + console.log(`🔧 Service ${service.name}: status=${status.status}, pid=${status.pid}`); + return { ...service, pid: status.pid }; + }); + + console.log(`🔧 Starting resource monitor with ${servicesWithPids.length} services`); + globalResourceMonitor.startCollecting(servicesWithPids).catch(console.error); + + // Set up event listener for metrics updates + globalResourceMonitor.on('metricsUpdate', (data) => { + console.log(`📊 Received metricsUpdate for ${data.services?.length || 0} services`); + broadcastMetricsEvent('metricsUpdate', data); + }); + + // Update service PIDs periodically as services start up + const updateServicePids = async () => { + const updatedServicesWithPids = cfg.services.map(service => { + const status = getServiceStatus(service.name); + return { ...service, pid: status.pid }; + }); + + // Always update services to let resource monitor detect PIDs + try { + await globalResourceMonitor.updateServices(updatedServicesWithPids); + // Update the stored services + servicesWithPids.length = 0; + servicesWithPids.push(...updatedServicesWithPids); + } catch (error) { + console.error('Error updating service PIDs:', error.message); + } + }; + + // Check for PID updates every 10 seconds + setInterval(updateServicePids, 10000); + + console.log(chalk.green('📊 Resource monitoring started')); + } // Auto-open browser if requested if (options.open !== false) { @@ -1115,7 +1833,9 @@ export async function startAdminDashboard(options = {}) { }, 30000); wsServer.on('connection', (ws) => { + console.log('🔧 New WebSocket connection established'); ws.serviceFilter = null; // default: all services + ws.metricsFilter = null; // default: no metrics filter yet ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); ws.on('message', (raw) => { @@ -1131,6 +1851,11 @@ export async function startAdminDashboard(options = {}) { if (!ws.serviceFilter && ws.readyState === WebSocket.OPEN) { handleWebSocketMessage(ws, { type: 'start_log_stream' }); } + // Also auto-start metrics stream + if (!ws.metricsFilter && ws.readyState === WebSocket.OPEN) { + console.log('🔧 Auto-starting metrics stream for new connection'); + handleWebSocketMessage(ws, { type: 'start_metrics_stream', service: 'all' }); + } }, 250); }); @@ -1150,6 +1875,16 @@ export async function startAdminDashboard(options = {}) { } globalLogWatcher = null; } + + // Stop resource monitor if active + if (globalResourceMonitor) { + try { + globalResourceMonitor.stopCollecting(); + } catch (e) { + console.warn(chalk.yellow('⚠️ Error stopping resource monitor:'), e.message); + } + globalResourceMonitor = null; + } if (wsServer) { try { diff --git a/bin/lib/dev.js b/bin/lib/dev.js index bc45966..156da30 100644 --- a/bin/lib/dev.js +++ b/bin/lib/dev.js @@ -5,6 +5,7 @@ import { spawn } from 'node:child_process'; import http from 'http'; import { initializeServiceLogs } from './logs.js'; import { initializePlugins, callHook } from './plugin-system.js'; +import { registerRunningProcess, unregisterRunningProcess } from './service-manager.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); @@ -152,9 +153,15 @@ const args = ['run', useScript]; const child = spawn(cmd, args, { cwd: svcPath, env: { ...process.env, PORT: String(svc.port) }, shell: true }); procs.push(child); + + // Register with service manager for PID tracking + registerRunningProcess(svc.name, child, svc); + child.stdout.on('data', d => process.stdout.write(color(`[${svc.name}] `) + d.toString())); child.stderr.on('data', d => process.stderr.write(color(`[${svc.name}] `) + d.toString())); child.on('exit', code => { + // Unregister when process exits + unregisterRunningProcess(svc.name); process.stdout.write(color(`[${svc.name}] exited with code ${code}\n`)); }); // health check @@ -193,7 +200,15 @@ const args = ['run', useScript]; docker, mode: docker ? 'docker' : 'local' }); - procs.forEach(p => p.kill('SIGINT')); + + // Cleanup service registrations and kill processes + procs.forEach(p => { + if (p.serviceName && p.pid) { + unregisterRunningProcess(p.serviceName); + } + p.kill('SIGINT'); + }); + await callHook('after:dev:stop', { projectDir: cwd, docker, diff --git a/bin/lib/resources.js b/bin/lib/resources.js new file mode 100644 index 0000000..e14182c --- /dev/null +++ b/bin/lib/resources.js @@ -0,0 +1,455 @@ +import pidusage from 'pidusage'; +import si from 'systeminformation'; +import { EventEmitter } from 'events'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +/** + * Resource monitoring class for collecting system metrics per service + */ +export class ResourceMonitor extends EventEmitter { + constructor(options = {}) { + super(); + this.collectInterval = options.collectInterval || 5000; // 5 seconds default + this.maxHistorySize = options.maxHistorySize || 720; // 1 hour at 5s intervals + this.isCollecting = false; + this.intervalId = null; + this.currentServices = []; // Store current services being monitored + + // In-memory storage for metrics history + this.metricsHistory = new Map(); // service name -> array of metrics + + // Cache for system-wide info + this.systemInfo = { + cpu: { cores: 0, model: '' }, + memory: { total: 0 }, + disk: { total: 0, available: 0 }, + network: { interfaces: [] } + }; + + this.lastNetworkStats = new Map(); + } + + /** + * Initialize the resource monitor + */ + async initialize() { + console.log('🔧 ResourceMonitor.initialize() called'); + try { + console.log('🔧 Getting system information...'); + // Get basic system information + const [cpu, memory, disk, networkInterfaces] = await Promise.all([ + si.cpu(), + si.mem(), + si.fsSize(), + si.networkInterfaces() + ]); + + console.log('🔧 System info collected:', { + cores: cpu.cores, + memory: `${(memory.total / 1024 / 1024 / 1024).toFixed(1)}GB` + }); + + this.systemInfo = { + cpu: { cores: cpu.cores, model: cpu.model }, + memory: { total: memory.total }, + disk: { + total: disk.reduce((acc, d) => acc + d.size, 0), + available: disk.reduce((acc, d) => acc + d.available, 0) + }, + network: { + interfaces: networkInterfaces.filter(iface => + !iface.internal && iface.operstate === 'up' + ).map(iface => ({ name: iface.iface, type: iface.type })) + } + }; + + console.log('🔍 Resource monitor initialized'); + return this.systemInfo; + } catch (error) { + console.warn('⚠️ Failed to initialize resource monitor:', error.message); + throw error; + } + } + + /** + * Find PIDs for services by process name and port + */ + async findServicePids(services) { + const servicesWithPids = []; + + for (const service of services) { + let pid = service.pid; + + // If no PID provided, try to find it by process name and port + if (!pid) { + try { + // Try to find Node.js processes on the service port + if (service.type === 'node' || service.type === 'frontend') { + const { stdout } = await execAsync(`lsof -t -i:${service.port} 2>/dev/null || echo ""`); + const pids = stdout.trim().split('\n').filter(p => p && p !== ''); + if (pids.length > 0) { + pid = parseInt(pids[0]); // Take the first PID + } + } + } catch (error) { + // Ignore errors, pid will remain null + } + } + + servicesWithPids.push({ ...service, pid }); + } + + return servicesWithPids; + } + + /** + * Start collecting metrics for services + */ + async startCollecting(services = []) { + if (this.isCollecting) { + console.log('Resource monitoring already running'); + return; + } + + this.isCollecting = true; + console.log(`📊 Starting resource monitoring for ${services.length} services`); + + // Find actual PIDs for services + this.currentServices = await this.findServicePids(services); + + // Collect metrics immediately, then on interval + this.collectMetrics(this.currentServices); + this.intervalId = setInterval(async () => { + // Re-detect PIDs on each collection in case services restart + this.currentServices = await this.findServicePids(this.currentServices); + this.collectMetrics(this.currentServices); + }, this.collectInterval); + } + + /** + * Update the services being monitored (e.g., when PIDs change) + */ + async updateServices(services = []) { + this.currentServices = await this.findServicePids(services); // Find PIDs for updated services + } + + /** + * Stop collecting metrics + */ + stopCollecting() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.isCollecting = false; + this.currentServices = []; + console.log('📊 Resource monitoring stopped'); + } + + /** + * Collect metrics for all services + */ + async collectMetrics(services) { + const timestamp = new Date(); + const metricsCollection = []; + + console.log(`📊 Starting metrics collection for ${services.length} services at ${timestamp}`); + + try { + // Get system-wide metrics + const [systemCpu, systemMemory, networkStats] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.networkStats() + ]); + + console.log('📊 System metrics collected:', { + cpu: systemCpu.currentload, + memory: `${(systemMemory.used / 1024 / 1024 / 1024).toFixed(1)}GB / ${(systemMemory.total / 1024 / 1024 / 1024).toFixed(1)}GB`, + networkInterfaces: networkStats?.length || 0 + }); + + // Process each service + for (const service of services) { + try { + const serviceMetrics = await this.collectServiceMetrics( + service, + systemCpu, + systemMemory, + networkStats, + timestamp + ); + + if (serviceMetrics) { + metricsCollection.push(serviceMetrics); + this.storeMetrics(service.name, serviceMetrics); + console.log(`📊 Stored metrics for ${service.name} in history`); + } + } catch (error) { + // Don't fail entire collection if one service fails + console.debug(`❌ Failed to collect metrics for ${service.name}:`, error.message); + } + } + + // Emit metrics update event + if (metricsCollection.length > 0) { + console.log(`📊 Emitting metrics update with ${metricsCollection.length} services`); + this.emit('metricsUpdate', { + timestamp, + services: metricsCollection, + system: { + cpu: systemCpu.currentload, + memory: { + used: systemMemory.used, + total: systemMemory.total, + percentage: (systemMemory.used / systemMemory.total) * 100 + } + } + }); + } else { + console.log('⚠️ No metrics collected - empty metricsCollection'); + } + + } catch (error) { + console.error('❌ Error collecting system metrics:', error.message); + } + } + + /** + * Collect metrics for a specific service + */ + async collectServiceMetrics(service, systemCpu, systemMemory, networkStats, timestamp) { + const { name, type, port, pid } = service; + + console.log(`🔍 Collecting metrics for ${name}: pid=${pid}, type=${type}`); + + if (!pid) { + console.log(`⚠️ No PID for service ${name}, returning stopped status`); + return { + serviceName: name, + type, + port, + timestamp, + status: 'stopped', + cpu: { usage: 0 }, + memory: { usage: 0, percentage: 0 }, + network: { rx: 0, tx: 0 } + }; + } + + try { + // Get process-specific metrics using pidusage + console.log(`📊 Getting pidusage for PID ${pid}`); + const processStats = await pidusage(pid); + console.log(`📊 Process stats for ${name}:`, { cpu: processStats.cpu, memory: processStats.memory }); + + // Calculate network metrics for the service (approximation) + const networkMetrics = this.calculateNetworkMetrics(service, networkStats); + + const result = { + serviceName: name, + type, + port, + pid, + timestamp, + status: 'running', + cpu: { + usage: processStats.cpu, // CPU percentage + time: processStats.ctime // CPU time in ms + }, + memory: { + usage: processStats.memory, // Memory in bytes + percentage: (processStats.memory / systemMemory.total) * 100 + }, + network: networkMetrics, + uptime: Date.now() - processStats.elapsed // Process uptime in ms + }; + + console.log(`✅ Collected metrics for ${name}:`, { + cpu: result.cpu.usage, + memory: result.memory.percentage, + status: result.status, + network: result.network + }); + + return result; } catch (error) { + // Process might have stopped + return { + serviceName: name, + type, + port, + timestamp, + status: 'error', + error: error.message, + cpu: { usage: 0 }, + memory: { usage: 0, percentage: 0 }, + network: { rx: 0, tx: 0 } + }; + } + } + + /** + * Calculate network metrics (approximation based on port usage) + */ + calculateNetworkMetrics(service, networkStats) { + const { port } = service; + + console.log(`🌐 Calculating network metrics for ${service.name}, interfaces: ${networkStats?.length || 0}`); + + // This is an approximation - in a real implementation, you might want to + // use more sophisticated methods to track per-process network usage + let totalRx = 0; + let totalTx = 0; + + if (networkStats && networkStats.length > 0) { + // Sum up network stats from all active interfaces + networkStats.forEach(stat => { + totalRx += stat.rx_bytes || 0; + totalTx += stat.tx_bytes || 0; + }); + console.log(`🌐 Total network bytes: RX=${totalRx}, TX=${totalTx}`); + } + + const key = `${service.name}_${port}`; + const lastStats = this.lastNetworkStats.get(key); + + let rxRate = 0; + let txRate = 0; + + if (lastStats) { + const timeDiff = (Date.now() - lastStats.timestamp) / 1000; // seconds + rxRate = Math.max(0, (totalRx - lastStats.rx) / timeDiff); + txRate = Math.max(0, (totalTx - lastStats.tx) / timeDiff); + } + + this.lastNetworkStats.set(key, { + rx: totalRx, + tx: totalTx, + timestamp: Date.now() + }); + + const result = { + rx: rxRate, // bytes per second received + tx: txRate // bytes per second transmitted + }; + + console.log(`🌐 Network rates for ${service.name}: RX=${rxRate.toFixed(2)} B/s, TX=${txRate.toFixed(2)} B/s`); + + return result; + } + + /** + * Store metrics in history with size limit + */ + storeMetrics(serviceName, metrics) { + if (!this.metricsHistory.has(serviceName)) { + this.metricsHistory.set(serviceName, []); + } + + const history = this.metricsHistory.get(serviceName); + history.push(metrics); + + // Keep only the last N metrics + if (history.length > this.maxHistorySize) { + history.splice(0, history.length - this.maxHistorySize); + } + } + + /** + * Get historical metrics for a service + */ + getMetricsHistory(serviceName, options = {}) { + const { since, limit } = options; + let history = this.metricsHistory.get(serviceName) || []; + + if (since) { + const sinceTime = new Date(since); + history = history.filter(metric => new Date(metric.timestamp) >= sinceTime); + } + + if (limit) { + history = history.slice(-limit); + } + + return history; + } + + /** + * Get current metrics for all services or a specific service + */ + getCurrentMetrics(serviceName = null) { + console.log('📊 getCurrentMetrics called'); + console.log('📊 metricsHistory size:', this.metricsHistory.size); + + if (serviceName) { + // Return metrics for a specific service + const history = this.metricsHistory.get(serviceName); + const latest = history ? history[history.length - 1] : null; + console.log(`📊 Service ${serviceName}: ${history ? history.length : 0} entries, latest:`, latest ? latest.status : 'none'); + return latest; + } + + // Return metrics for all services + const result = {}; + for (const [serviceName, history] of this.metricsHistory.entries()) { + const latest = history[history.length - 1]; + if (latest) { + result[serviceName] = latest; + } + console.log(`📊 Service ${serviceName}: ${history.length} entries, latest:`, latest ? latest.status : 'none'); + } + + console.log('📊 Returning metrics for services:', Object.keys(result)); + return result; + } + + /** + * Clear metrics history for a service or all services + */ + clearMetricsHistory(serviceName = null) { + if (serviceName) { + this.metricsHistory.delete(serviceName); + } else { + this.metricsHistory.clear(); + } + } + + /** + * Get system information + */ + getSystemInfo() { + return this.systemInfo; + } + + /** + * Format bytes for human reading + */ + static formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } + + /** + * Format percentage for display + */ + static formatPercentage(value, decimals = 1) { + return `${value.toFixed(decimals)}%`; + } +} + +// Export singleton instance +let resourceMonitor = null; + +export function getResourceMonitor(options = {}) { + if (!resourceMonitor) { + resourceMonitor = new ResourceMonitor(options); + } + return resourceMonitor; +} \ No newline at end of file diff --git a/bin/lib/service-manager.js b/bin/lib/service-manager.js index a667577..b42f335 100644 --- a/bin/lib/service-manager.js +++ b/bin/lib/service-manager.js @@ -366,4 +366,18 @@ export async function installServiceDependencies(service, serviceDir) { default: return { success: true }; } +} + +// Helper functions for registering externally started processes +export function registerRunningProcess(serviceName, process, serviceConfig) { + runningProcesses.set(serviceName, { + process, + service: serviceConfig, + startTime: new Date(), + status: 'running' + }); +} + +export function unregisterRunningProcess(serviceName) { + runningProcesses.delete(serviceName); } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4743335..5747eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "fs-extra": "^11.3.2", "hookable": "^5.5.3", "lodash": "^4.17.21", + "pidusage": "^3.0.2", "prompts": "^2.4.2", + "systeminformation": "^5.23.5", "ws": "^8.16.0" }, "bin": { @@ -165,6 +167,7 @@ "version": "5.39.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.39.0", "@algolia/requester-browser-xhr": "5.39.0", @@ -337,6 +340,74 @@ } } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "cpu": [ @@ -352,6 +423,312 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.54", "dev": true, @@ -381,6 +758,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.52.4", "cpu": [ @@ -393,6 +798,272 @@ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "license": "MIT" @@ -869,6 +1540,7 @@ "version": "5.39.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.5.0", "@algolia/client-abtesting": "5.39.0", @@ -1223,6 +1895,7 @@ "version": "7.6.5", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.2.0" } @@ -1712,6 +2385,18 @@ "dev": true, "license": "ISC" }, + "node_modules/pidusage": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz", + "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "dev": true, @@ -1893,6 +2578,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/search-insights": { "version": "2.17.3", "dev": true, @@ -2030,6 +2735,32 @@ "node": ">=16" } }, + "node_modules/systeminformation": { + "version": "5.27.11", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.11.tgz", + "integrity": "sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tabbable": { "version": "6.2.0", "dev": true, @@ -2190,6 +2921,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2461,6 +3193,7 @@ "version": "3.5.22", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.22", "@vue/compiler-sfc": "3.5.22", diff --git a/package.json b/package.json index 5d47343..7682559 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,9 @@ "fs-extra": "^11.3.2", "hookable": "^5.5.3", "lodash": "^4.17.21", + "pidusage": "^3.0.2", "prompts": "^2.4.2", + "systeminformation": "^5.23.5", "ws": "^8.16.0" }, "devDependencies": { diff --git a/tests/resource-monitoring-integration.test.js b/tests/resource-monitoring-integration.test.js new file mode 100644 index 0000000..f76c9ad --- /dev/null +++ b/tests/resource-monitoring-integration.test.js @@ -0,0 +1,277 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { execa } from 'execa'; +import fs from 'fs'; +import path from 'path'; +import http from 'http'; + +describe('Resource Monitoring Integration', () => { + const testWorkspace = path.join(process.cwd(), 'test-workspace', 'integration-test'); + let adminProcess = null; + + beforeEach(async () => { + // Clean up previous test workspace + if (fs.existsSync(testWorkspace)) { + fs.rmSync(testWorkspace, { recursive: true, force: true }); + } + + // Create test workspace + fs.mkdirSync(testWorkspace, { recursive: true }); + + // Create a simple polyglot project + const polyglotConfig = { + name: 'integration-test', + services: [ + { name: 'test-node', type: 'node', port: 4001, path: 'services/test-node' } + ] + }; + + fs.writeFileSync( + path.join(testWorkspace, 'polyglot.json'), + JSON.stringify(polyglotConfig, null, 2) + ); + + // Create service directory structure + const serviceDir = path.join(testWorkspace, 'services', 'test-node'); + fs.mkdirSync(serviceDir, { recursive: true }); + + // Create basic package.json for the service + const packageJson = { + name: 'test-node', + version: '1.0.0', + scripts: { + dev: 'node index.js', + start: 'node index.js' + }, + dependencies: {} + }; + + fs.writeFileSync( + path.join(serviceDir, 'package.json'), + JSON.stringify(packageJson, null, 2) + ); + + // Create basic service file + const serviceCode = ` +const http = require('http'); +const port = process.env.PORT || 4001; + +const server = http.createServer((req, res) => { + if (req.url === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() })); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Test Node Service Running'); + } +}); + +server.listen(port, () => { + console.log(\`Test service running on port \${port}\`); +}); + +process.on('SIGTERM', () => { + console.log('Received SIGTERM, shutting down gracefully'); + server.close(() => { + process.exit(0); + }); +}); +`; + + fs.writeFileSync(path.join(serviceDir, 'index.js'), serviceCode); + }); + + afterEach(async () => { + // Stop admin dashboard if running + if (adminProcess && !adminProcess.killed) { + adminProcess.kill('SIGTERM'); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + // Clean up test workspace + if (fs.existsSync(testWorkspace)) { + fs.rmSync(testWorkspace, { recursive: true, force: true }); + } + }); + + it('should start admin dashboard with resource monitoring enabled', async () => { + // Start admin dashboard + adminProcess = execa('node', [ + path.join(process.cwd(), 'bin', 'index.js'), + 'admin', + '--port', '9999' + ], { + cwd: testWorkspace, + stdio: 'pipe' + }); + + // Wait for dashboard to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + expect(adminProcess.killed).toBe(false); + + // Test if dashboard is responding + const response = await makeRequest('GET', 'http://localhost:9999/', {}, 5000); + expect(response.statusCode).toBe(200); + expect(response.body).toContain('Polyglot Admin Dashboard'); + + // Test if resource monitoring UI is included + expect(response.body).toContain('Resource Monitoring'); + expect(response.body).toContain('CPU Usage'); + expect(response.body).toContain('Memory Usage'); + expect(response.body).toContain('Network I/O'); + // Disk I/O removed - only 3 tiles now + }, 15000); + + it('should provide metrics API endpoint', async () => { + // Start admin dashboard + adminProcess = execa('node', [ + path.join(process.cwd(), 'bin', 'index.js'), + 'admin', + '--port', '9998' + ], { + cwd: testWorkspace, + stdio: 'pipe' + }); + + // Wait for dashboard to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Test metrics API endpoint + const response = await makeRequest('GET', 'http://localhost:9998/api/metrics'); + expect(response.statusCode).toBe(200); + + const data = JSON.parse(response.body); + expect(data).toHaveProperty('metrics'); + expect(data).toHaveProperty('systemInfo'); + + if (data.systemInfo) { + expect(data.systemInfo).toHaveProperty('cpu'); + expect(data.systemInfo).toHaveProperty('memory'); + } + }, 15000); + + it('should provide service status API with resource monitoring integration', async () => { + // Start admin dashboard + adminProcess = execa('node', [ + path.join(process.cwd(), 'bin', 'index.js'), + 'admin', + '--port', '9997' + ], { + cwd: testWorkspace, + stdio: 'pipe' + }); + + // Wait for dashboard to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Test service status API + const response = await makeRequest('GET', 'http://localhost:9997/api/status'); + expect(response.statusCode).toBe(200); + + const services = JSON.parse(response.body); + expect(Array.isArray(services)).toBe(true); + expect(services.length).toBeGreaterThan(0); + + const service = services[0]; + expect(service).toHaveProperty('name'); + expect(service).toHaveProperty('type'); + expect(service).toHaveProperty('port'); + expect(service).toHaveProperty('status'); + }, 15000); + + it('should handle graceful shutdown without errors', async () => { + // Start admin dashboard + adminProcess = execa('node', [ + path.join(process.cwd(), 'bin', 'index.js'), + 'admin', + '--port', '9996' + ], { + cwd: testWorkspace, + stdio: 'pipe' + }); + + // Wait for dashboard to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Send SIGTERM to gracefully shut down + adminProcess.kill('SIGTERM'); + + // Wait for process to exit + const { exitCode } = await adminProcess; + + // Should exit cleanly + expect(exitCode).toBe(0); + }, 15000); + + it('should include Chart.js library for metrics visualization', async () => { + // Start admin dashboard + adminProcess = execa('node', [ + path.join(process.cwd(), 'bin', 'index.js'), + 'admin', + '--port', '9995' + ], { + cwd: testWorkspace, + stdio: 'pipe' + }); + + // Wait for dashboard to start + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Test if Chart.js is included in the HTML + const response = await makeRequest('GET', 'http://localhost:9995/'); + expect(response.statusCode).toBe(200); + expect(response.body).toContain('chart.js'); + // Test that dashboard contains expected chart elements (3-tile layout) + expect(response.body).toContain('canvas id="cpu-chart"'); + expect(response.body).toContain('canvas id="memory-chart"'); + expect(response.body).toContain('canvas id="network-chart"'); + // Disk chart removed - only 3 tiles now + }, 15000); +}); + +// Helper function to make HTTP requests with timeout +function makeRequest(method, url, data = null, timeout = 10000) { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const options = { + hostname: urlObj.hostname, + port: urlObj.port, + path: urlObj.pathname + urlObj.search, + method: method, + timeout: timeout, + headers: { + 'User-Agent': 'test-client', + } + }; + + if (data && method !== 'GET') { + const postData = typeof data === 'string' ? data : JSON.stringify(data); + options.headers['Content-Type'] = 'application/json'; + options.headers['Content-Length'] = Buffer.byteLength(postData); + } + + const req = http.request(options, (res) => { + let body = ''; + res.on('data', (chunk) => body += chunk); + res.on('end', () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: body + }); + }); + }); + + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error(`Request timeout after ${timeout}ms`)); + }); + + if (data && method !== 'GET') { + req.write(typeof data === 'string' ? data : JSON.stringify(data)); + } + + req.end(); + }); +} \ No newline at end of file diff --git a/tests/resource-monitoring.test.js b/tests/resource-monitoring.test.js new file mode 100644 index 0000000..3d245d5 --- /dev/null +++ b/tests/resource-monitoring.test.js @@ -0,0 +1,196 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { getResourceMonitor, ResourceMonitor } from '../bin/lib/resources.js'; +import fs from 'fs'; +import path from 'path'; + +describe('Resource Monitoring', () => { + let resourceMonitor; + const testWorkspace = path.join(process.cwd(), 'test-workspace', 'resource-test'); + + beforeEach(async () => { + // Create test workspace + if (!fs.existsSync(testWorkspace)) { + fs.mkdirSync(testWorkspace, { recursive: true }); + } + + // Create basic polyglot.json + const polyglotConfig = { + projectName: 'resource-test', + services: [ + { name: 'test-service', type: 'node', port: 3001 } + ] + }; + + fs.writeFileSync( + path.join(testWorkspace, 'polyglot.json'), + JSON.stringify(polyglotConfig, null, 2) + ); + + resourceMonitor = getResourceMonitor({ + collectInterval: 1000, // 1 second for faster testing + maxHistorySize: 10 + }); + }); + + afterEach(() => { + if (resourceMonitor) { + resourceMonitor.stopCollecting(); + resourceMonitor.clearMetricsHistory(); + } + + // Clean up test workspace + if (fs.existsSync(testWorkspace)) { + fs.rmSync(testWorkspace, { recursive: true, force: true }); + } + }); + + it('should initialize resource monitor', async () => { + expect(resourceMonitor).toBeDefined(); + + const systemInfo = await resourceMonitor.initialize(); + expect(systemInfo).toBeDefined(); + expect(systemInfo.cpu).toBeDefined(); + expect(systemInfo.memory).toBeDefined(); + expect(systemInfo.cpu.cores).toBeGreaterThan(0); + expect(systemInfo.memory.total).toBeGreaterThan(0); + }); + + it('should start and stop collecting metrics', () => { + const services = [{ name: 'test-service', type: 'node', port: 3001, pid: process.pid }]; + + expect(resourceMonitor.isCollecting).toBe(false); + + resourceMonitor.startCollecting(services); + expect(resourceMonitor.isCollecting).toBe(true); + + resourceMonitor.stopCollecting(); + expect(resourceMonitor.isCollecting).toBe(false); + }); + + it('should collect metrics for a running process', async () => { + const services = [{ name: 'test-service', type: 'node', port: 3001, pid: process.pid }]; + + // Set up event listener to capture metrics + let metricsReceived = false; + let metricsData = null; + + resourceMonitor.on('metricsUpdate', (data) => { + metricsReceived = true; + metricsData = data; + }); + + await resourceMonitor.initialize(); + resourceMonitor.startCollecting(services); + + // Wait for metrics to be collected + await new Promise(resolve => setTimeout(resolve, 2000)); + + expect(metricsReceived).toBe(true); + expect(metricsData).toBeDefined(); + expect(metricsData.services).toBeDefined(); + expect(metricsData.services.length).toBeGreaterThan(0); + + const serviceMetrics = metricsData.services[0]; + expect(serviceMetrics.serviceName).toBe('test-service'); + expect(serviceMetrics.status).toBe('running'); + expect(serviceMetrics.cpu).toBeDefined(); + expect(serviceMetrics.memory).toBeDefined(); + expect(serviceMetrics.cpu.usage).toBeGreaterThanOrEqual(0); + expect(serviceMetrics.memory.usage).toBeGreaterThan(0); + }); + + it('should store and retrieve metrics history', async () => { + const services = [{ name: 'test-service', type: 'node', port: 3001, pid: process.pid }]; + + await resourceMonitor.initialize(); + + // Manually store some test metrics + const testMetrics = { + serviceName: 'test-service', + timestamp: new Date(), + status: 'running', + cpu: { usage: 25.5 }, + memory: { usage: 1024 * 1024, percentage: 10.0 }, + disk: { read: 100, write: 200 }, + network: { rx: 500, tx: 300 } + }; + + resourceMonitor.storeMetrics('test-service', testMetrics); + + const history = resourceMonitor.getMetricsHistory('test-service'); + expect(history).toBeDefined(); + expect(history.length).toBe(1); + expect(history[0]).toEqual(testMetrics); + + const current = resourceMonitor.getCurrentMetrics('test-service'); + expect(current).toEqual(testMetrics); + }); + + it('should format bytes correctly', () => { + expect(ResourceMonitor.formatBytes).toBeDefined(); + + expect(ResourceMonitor.formatBytes(0)).toBe('0 Bytes'); + expect(ResourceMonitor.formatBytes(1024)).toBe('1 KB'); + expect(ResourceMonitor.formatBytes(1024 * 1024)).toBe('1 MB'); + expect(ResourceMonitor.formatBytes(1024 * 1024 * 1024)).toBe('1 GB'); + }); + + it('should format percentage correctly', () => { + expect(ResourceMonitor.formatPercentage).toBeDefined(); + + expect(ResourceMonitor.formatPercentage(25.5)).toBe('25.5%'); + expect(ResourceMonitor.formatPercentage(100.0)).toBe('100.0%'); + expect(ResourceMonitor.formatPercentage(0.0)).toBe('0.0%'); + }); + + it('should handle missing process ID gracefully', async () => { + const services = [{ name: 'test-service', type: 'node', port: 3001 }]; // No PID + + await resourceMonitor.initialize(); + + const mockSystemCpu = { currentload: 50 }; + const mockSystemMemory = { used: 1024 * 1024, total: 8 * 1024 * 1024 * 1024 }; + const mockNetworkStats = []; + const timestamp = new Date(); + + const metrics = await resourceMonitor.collectServiceMetrics( + services[0], + mockSystemCpu, + mockSystemMemory, + mockNetworkStats, + timestamp + ); + + expect(metrics).toBeDefined(); + expect(metrics.status).toBe('stopped'); + expect(metrics.cpu.usage).toBe(0); + expect(metrics.memory.usage).toBe(0); + }); + + it('should clear metrics history', async () => { + await resourceMonitor.initialize(); + + // Add some test data + const testMetrics = { + serviceName: 'test-service', + timestamp: new Date(), + cpu: { usage: 25.5 }, + memory: { usage: 1024 * 1024 } + }; + + resourceMonitor.storeMetrics('test-service', testMetrics); + resourceMonitor.storeMetrics('other-service', testMetrics); + + expect(resourceMonitor.getMetricsHistory('test-service').length).toBe(1); + expect(resourceMonitor.getMetricsHistory('other-service').length).toBe(1); + + // Clear specific service + resourceMonitor.clearMetricsHistory('test-service'); + expect(resourceMonitor.getMetricsHistory('test-service').length).toBe(0); + expect(resourceMonitor.getMetricsHistory('other-service').length).toBe(1); + + // Clear all + resourceMonitor.clearMetricsHistory(); + expect(resourceMonitor.getMetricsHistory('other-service').length).toBe(0); + }); +}, 15000); // 15 second timeout for integration tests \ No newline at end of file