UNPKG

@tryloop/oats

Version:

🌾 OATS - OpenAPI TypeScript Sync. The missing link between your OpenAPI specs and TypeScript applications. Automatically watch, generate, and sync TypeScript clients from your API definitions.

241 lines 10.7 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import detectPort from 'detect-port'; import chalk from 'chalk'; import ora from 'ora'; import { PlatformUtils } from './platform.js'; import { Logger } from './logger.js'; const execAsync = promisify(exec); const logger = new Logger('PortManager'); export class PortManager { /** * Force kill a process using system commands */ static async forceKillProcess(pid) { try { if (process.platform === 'darwin' || process.platform === 'linux') { // Try multiple methods to ensure process termination const commands = [ `kill -9 ${pid}`, `pkill -9 -P ${pid}`, // Kill child processes too ]; for (const cmd of commands) { try { await execAsync(cmd); logger.debug(`Force killed process ${pid} with: ${cmd}`); } catch (err) { // Continue trying other methods } } } else if (process.platform === 'win32') { // Windows: Use taskkill with tree flag to kill process tree await execAsync(`taskkill /F /T /PID ${pid}`); logger.debug(`Force killed process tree ${pid} on Windows`); } } catch (error) { logger.debug(`Failed to force kill process ${pid}:`, error); } } /** * Check if a port is in use */ static async isPortInUse(port) { try { // First try OS-level check for more reliable results if (process.platform === 'darwin' || process.platform === 'linux') { const { stdout } = await execAsync(`lsof -i :${port} -t 2>/dev/null || true`); const inUse = stdout.trim() !== ''; logger.debug(`Port ${port}: ${inUse ? 'IN USE' : 'FREE'} (lsof)`); return inUse; } else if (process.platform === 'win32') { const { stdout } = await execAsync(`netstat -an | findstr :${port}`); const inUse = stdout.includes('LISTENING'); logger.debug(`Port ${port}: ${inUse ? 'IN USE' : 'FREE'} (netstat)`); return inUse; } } catch (error) { logger.debug(`OS-level port check failed, using detectPort fallback`); } // Fallback to detectPort try { const availablePort = await detectPort(port); const inUse = availablePort !== port; logger.debug(`Port ${port}: ${inUse ? 'IN USE' : 'FREE'} (detectPort)`); return inUse; } catch (error) { logger.error(`Failed to check port ${port}:`, error); return false; } } /** * Get process IDs using a specific port */ static async getProcessesUsingPort(port) { const pids = []; try { if (process.platform === 'darwin' || process.platform === 'linux') { // Try multiple approaches to find all processes const commands = [ `lsof -i :${port} -t 2>/dev/null || true`, `lsof -i tcp:${port} -t 2>/dev/null || true`, `lsof -i udp:${port} -t 2>/dev/null || true`, ]; for (const cmd of commands) { try { const { stdout } = await execAsync(cmd); const foundPids = stdout .trim() .split('\n') .filter((pid) => pid && /^\d+$/.test(pid)); foundPids.forEach((pid) => { if (!pids.includes(pid)) { pids.push(pid); } }); } catch (err) { logger.debug(`Command failed: ${cmd}`, err); } } } else if (process.platform === 'win32') { const { stdout } = await execAsync(`netstat -ano | findstr :${port}`); const lines = stdout.trim().split('\n'); const pidSet = new Set(); lines.forEach((line) => { const parts = line.trim().split(/\s+/); const pid = parts[parts.length - 1]; if (pid && /^\d+$/.test(pid) && pid !== '0') { pidSet.add(pid); } }); pids.push(...Array.from(pidSet)); } } catch (error) { logger.debug(`Failed to get processes for port ${port}:`, error); } return pids; } /** * Free a port by killing processes using it with exponential backoff */ static async freePort(port, serviceName) { const isInUse = await this.isPortInUse(port); if (!isInUse) { logger.debug(`Port ${port} is already free`); return; } console.log(chalk.yellow(`⚠️ Port ${port} is already in use for ${serviceName}`)); const spinner = ora(`Attempting to free port ${port}...`).start(); // Exponential backoff configuration const maxAttempts = 5; const initialDelay = 500; // 500ms const maxDelay = 5000; // 5 seconds const backoffMultiplier = 2; try { const pids = await this.getProcessesUsingPort(port); if (pids.length === 0) { spinner.fail(`Port ${port} is in use but could not find process using it`); logger.warn(`Port ${port} is in use but no PIDs found`); throw new Error(`Port ${port} is in use but could not identify the process`); } spinner.text = `Found ${pids.length} process(es) using port ${port}. Terminating...`; // Try to free the port with exponential backoff let attempt = 0; let delay = initialDelay; let portFreed = false; while (attempt < maxAttempts && !portFreed) { attempt++; logger.debug(`Attempt ${attempt}/${maxAttempts} to free port ${port}`); // Kill processes (use force kill after first attempt) for (const pid of pids) { const pidNum = parseInt(pid, 10); logger.debug(`Killing process ${pid} using port ${port} (attempt ${attempt})`); try { if (attempt === 1) { // First attempt: graceful termination await PlatformUtils.killProcess(pidNum); } else if (attempt === 2) { // Second attempt: SIGKILL if (process.platform === 'win32') { await PlatformUtils.killProcess(pidNum, 'SIGKILL'); } else { await PlatformUtils.killProcess(pidNum, 'SIGKILL'); } } else { // Third+ attempts: use force kill with system commands await this.forceKillProcess(pidNum); } } catch (err) { logger.debug(`Failed to kill process ${pid}:`, err); } } spinner.text = `Waiting ${delay}ms for port ${port} to be released (attempt ${attempt}/${maxAttempts})...`; // Wait with current delay await new Promise((resolve) => setTimeout(resolve, delay)); // Check if port is now free portFreed = !(await this.isPortInUse(port)); if (portFreed) { spinner.succeed(`Port ${port} is now free (freed after ${attempt} attempt${attempt > 1 ? 's' : ''})`); logger.debug(`Port ${port} successfully freed after ${attempt} attempts`); return; } // Update pids for next attempt (in case new processes took over) const newPids = await this.getProcessesUsingPort(port); if (newPids.length > 0 && newPids.join(',') !== pids.join(',')) { logger.debug(`New processes detected on port ${port}: ${newPids.join(', ')}`); pids.length = 0; pids.push(...newPids); } // Calculate next delay with exponential backoff delay = Math.min(delay * backoffMultiplier, maxDelay); } // If we get here, all attempts failed spinner.fail(`Failed to free port ${port} after ${maxAttempts} attempts`); // Check if we need elevated privileges const hasElevated = await PlatformUtils.hasElevatedPrivileges(); if (!hasElevated) { console.log(chalk.yellow('\n💡 Tip: Try running with elevated privileges:')); if (process.platform === 'darwin' || process.platform === 'linux') { console.log(chalk.cyan(' sudo oats start\n')); } else if (process.platform === 'win32') { console.log(chalk.cyan(' Run as Administrator\n')); } } throw new Error(`Failed to free port ${port} after ${maxAttempts} attempts with exponential backoff`); } catch (error) { if (spinner.isSpinning) { spinner.fail(`Failed to free port ${port}`); } throw error; } } /** * Find an available port starting from the given port */ static async findAvailablePort(startPort, maxAttempts = 10) { for (let i = 0; i < maxAttempts; i++) { const portToCheck = startPort + i; const inUse = await this.isPortInUse(portToCheck); if (!inUse) { logger.debug(`Found available port: ${portToCheck}`); return portToCheck; } } throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`); } } //# sourceMappingURL=port-manager.js.map