UNPKG

web-terminal-server

Version:

Professional web-based terminal server with persistent sessions, live sharing, smart port detection, Cloudflare tunnels, and full CLI support

452 lines (401 loc) 11.6 kB
const { exec } = require('child_process'); const { promisify } = require('util'); const path = require('path'); const execAsync = promisify(exec); /** * ProcessTracker - Cross-platform process information tracker */ class ProcessTracker { constructor() { this.platform = process.platform; this.processCache = new Map(); // pid -> processInfo cache this.cacheTimeout = 5000; // 5 seconds cache } /** * Get detailed process information */ async getProcessInfo(pid) { if (!pid || pid === 0) { return null; } // Check cache const cached = this.processCache.get(pid); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.data; } try { let info; switch (this.platform) { case 'darwin': info = await this.getProcessInfoMacOS(pid); break; case 'linux': info = await this.getProcessInfoLinux(pid); break; case 'win32': info = await this.getProcessInfoWindows(pid); break; default: throw new Error(`Unsupported platform: ${this.platform}`); } // Cache the result this.processCache.set(pid, { data: info, timestamp: Date.now() }); return info; } catch (error) { console.error(`Failed to get process info for PID ${pid}:`, error.message); return null; } } /** * Get process info on macOS */ async getProcessInfoMacOS(pid) { try { // Get basic process info const { stdout: psOutput } = await execAsync(`ps -p ${pid} -o pid,ppid,user,comm,args`); const lines = psOutput.trim().split('\n'); if (lines.length < 2) { return null; } // Parse ps output const dataLine = lines[1]; const parts = dataLine.trim().split(/\s+/); // Get working directory let cwd = ''; try { const { stdout: lsofOutput } = await execAsync(`lsof -p ${pid} | grep cwd | head -1`); const cwdMatch = lsofOutput.match(/\s+(\/.*)$/); if (cwdMatch) { cwd = cwdMatch[1]; } } catch {} // Get memory and CPU usage let memoryMB = 0; let cpu = 0; try { const { stdout: topOutput } = await execAsync(`ps -p ${pid} -o %mem,%cpu | tail -1`); const topParts = topOutput.trim().split(/\s+/); if (topParts.length >= 2) { memoryMB = parseFloat(topParts[0]); cpu = parseFloat(topParts[1]); } } catch {} // Get command line arguments let commandLine = parts.slice(4).join(' '); // Detect application type const appType = this.detectAppType(commandLine); const framework = this.detectFramework(commandLine, cwd); return { pid: parseInt(pid), ppid: parseInt(parts[1]), user: parts[2], name: parts[3], commandLine, cwd, memoryMB, cpu, appType, framework, platform: 'darwin' }; } catch (error) { throw error; } } /** * Get process info on Linux */ async getProcessInfoLinux(pid) { try { // Read from /proc filesystem const fs = require('fs').promises; // Get command line let commandLine = ''; try { const cmdlineBuffer = await fs.readFile(`/proc/${pid}/cmdline`); commandLine = cmdlineBuffer.toString().replace(/\0/g, ' ').trim(); } catch {} // Get working directory let cwd = ''; try { cwd = await fs.readlink(`/proc/${pid}/cwd`); } catch {} // Get process status let status = {}; try { const statusContent = await fs.readFile(`/proc/${pid}/status`, 'utf8'); const lines = statusContent.split('\n'); for (const line of lines) { const [key, value] = line.split(':').map(s => s.trim()); if (key && value) { status[key] = value; } } } catch {} // Get memory and CPU from stat let memoryMB = 0; let cpu = 0; try { const { stdout } = await execAsync(`ps -p ${pid} -o %mem,%cpu | tail -1`); const parts = stdout.trim().split(/\s+/); if (parts.length >= 2) { memoryMB = parseFloat(parts[0]); cpu = parseFloat(parts[1]); } } catch {} // Detect application type const appType = this.detectAppType(commandLine); const framework = this.detectFramework(commandLine, cwd); return { pid: parseInt(pid), ppid: parseInt(status.PPid || 0), user: status.Uid || 'unknown', name: status.Name || 'unknown', commandLine, cwd, memoryMB, cpu, appType, framework, platform: 'linux' }; } catch (error) { throw error; } } /** * Get process info on Windows */ async getProcessInfoWindows(pid) { try { // Use wmic to get process info const { stdout } = await execAsync( `wmic process where ProcessId=${pid} get Name,ParentProcessId,CommandLine,WorkingSetSize,PageFileUsage /format:csv` ); const lines = stdout.trim().split('\r\n').filter(line => line.trim()); if (lines.length < 2) { return null; } // Parse CSV output const headers = lines[lines.length - 2].split(','); const values = lines[lines.length - 1].split(','); const data = {}; headers.forEach((header, index) => { data[header] = values[index]; }); // Get additional info using tasklist let cpu = 0; try { const { stdout: taskOutput } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV /V`); // Parse CPU time from tasklist output // This is approximate since Windows doesn't easily provide CPU percentage } catch {} const commandLine = data.CommandLine || ''; const appType = this.detectAppType(commandLine); const framework = this.detectFramework(commandLine, ''); return { pid: parseInt(pid), ppid: parseInt(data.ParentProcessId || 0), user: 'N/A', // Would need additional WMI query name: data.Name || 'unknown', commandLine, cwd: '', // Not easily available on Windows memoryMB: data.WorkingSetSize ? parseInt(data.WorkingSetSize) / 1048576 : 0, cpu, appType, framework, platform: 'win32' }; } catch (error) { throw error; } } /** * Detect application type from command line */ detectAppType(commandLine) { const cmd = commandLine.toLowerCase(); // Programming languages and runtimes if (cmd.includes('node') || cmd.includes('npm') || cmd.includes('yarn') || cmd.includes('pnpm')) { return 'nodejs'; } if (cmd.includes('python') || cmd.includes('py.exe')) { return 'python'; } if (cmd.includes('java') || cmd.includes('javaw')) { return 'java'; } if (cmd.includes('ruby')) { return 'ruby'; } if (cmd.includes('php')) { return 'php'; } if (cmd.includes('dotnet') || cmd.includes('.dll')) { return 'dotnet'; } if (cmd.includes('go run') || cmd.includes('golang')) { return 'golang'; } if (cmd.includes('cargo') || cmd.includes('rustc')) { return 'rust'; } // Databases if (cmd.includes('mysql') || cmd.includes('mysqld')) { return 'mysql'; } if (cmd.includes('postgres') || cmd.includes('postgresql')) { return 'postgresql'; } if (cmd.includes('mongod') || cmd.includes('mongodb')) { return 'mongodb'; } if (cmd.includes('redis')) { return 'redis'; } // Web servers if (cmd.includes('nginx')) { return 'nginx'; } if (cmd.includes('apache') || cmd.includes('httpd')) { return 'apache'; } // Containers if (cmd.includes('docker')) { return 'docker'; } if (cmd.includes('containerd')) { return 'container'; } return 'unknown'; } /** * Detect framework from command line and working directory */ detectFramework(commandLine, cwd) { const cmd = commandLine.toLowerCase(); // Node.js frameworks if (cmd.includes('next')) { return 'nextjs'; } if (cmd.includes('vite')) { return 'vite'; } if (cmd.includes('react-scripts')) { return 'create-react-app'; } if (cmd.includes('vue-cli')) { return 'vue-cli'; } if (cmd.includes('ng serve') || cmd.includes('angular')) { return 'angular'; } if (cmd.includes('gatsby')) { return 'gatsby'; } if (cmd.includes('nuxt')) { return 'nuxt'; } if (cmd.includes('webpack-dev-server')) { return 'webpack'; } if (cmd.includes('parcel')) { return 'parcel'; } if (cmd.includes('express')) { return 'express'; } if (cmd.includes('fastify')) { return 'fastify'; } if (cmd.includes('nest')) { return 'nestjs'; } // Python frameworks if (cmd.includes('django') || cmd.includes('manage.py runserver')) { return 'django'; } if (cmd.includes('flask')) { return 'flask'; } if (cmd.includes('fastapi') || cmd.includes('uvicorn')) { return 'fastapi'; } if (cmd.includes('streamlit')) { return 'streamlit'; } if (cmd.includes('jupyter')) { return 'jupyter'; } // Ruby frameworks if (cmd.includes('rails')) { return 'rails'; } if (cmd.includes('sinatra')) { return 'sinatra'; } // PHP frameworks if (cmd.includes('laravel') || cmd.includes('artisan')) { return 'laravel'; } if (cmd.includes('symfony')) { return 'symfony'; } // Java frameworks if (cmd.includes('spring')) { return 'spring'; } if (cmd.includes('tomcat')) { return 'tomcat'; } return 'unknown'; } /** * Get framework-specific port detection regex */ getFrameworkPortRegex(framework) { const patterns = { // Node.js frameworks 'vite': /Local:.*:(\d+)/, 'nextjs': /started server on.*:(\d+)|ready on.*:(\d+)/i, 'create-react-app': /On Your Network:.*:(\d+)|Local:.*:(\d+)/, 'angular': /Angular Live Development Server.*:(\d+)/, 'express': /listening on port (\d+)|server.*:(\d+)/i, 'fastify': /Server listening at.*:(\d+)/, // Python frameworks 'django': /Starting development server at.*:(\d+)/, 'flask': /Running on.*:(\d+)/, 'fastapi': /Uvicorn running on.*:(\d+)/, 'streamlit': /Network URL:.*:(\d+)|Local URL:.*:(\d+)/, // Ruby 'rails': /Listening on.*:(\d+)/, // PHP 'laravel': /Laravel development server started.*:(\d+)/, // Default 'default': /(?:port|listening|server|running on|started at).*?(\d{4,5})/i }; return patterns[framework] || patterns.default; } /** * Clear process cache */ clearCache() { this.processCache.clear(); } /** * Get all cached processes */ getCachedProcesses() { const processes = []; for (const [pid, cache] of this.processCache) { if (Date.now() - cache.timestamp < this.cacheTimeout) { processes.push(cache.data); } } return processes; } } module.exports = ProcessTracker;