UNPKG

@oxog/port-terminator

Version:

Cross-platform utility to terminate processes on ports with zero dependencies

213 lines 7.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LinuxPlatform = void 0; const child_process_1 = require("child_process"); const errors_1 = require("../errors"); class LinuxPlatform { async findProcessesByPort(port, protocol = 'both') { let processes = []; try { processes = await this.findWithLsof(port, protocol); } catch (error) { processes = await this.findWithNetstat(port, protocol); } return this.deduplicateProcesses(processes); } async killProcess(pid, force = false) { try { if (!force) { try { await this.executeCommand('kill', ['-TERM', pid.toString()]); await this.waitForProcessToExit(pid, 5000); return true; } catch { // Fall through to force kill } } await this.executeCommand('kill', ['-KILL', pid.toString()]); await this.waitForProcessToExit(pid, 2000); return true; } catch (error) { if (error instanceof errors_1.CommandExecutionError) { if (error.stderr.includes('Operation not permitted')) { throw new errors_1.PermissionError(`Permission denied when trying to kill process ${pid}`, pid); } if (error.stderr.includes('No such process')) { return true; } } throw new errors_1.ProcessKillError(pid, force ? 'SIGKILL' : 'SIGTERM'); } } async isPortAvailable(port, protocol = 'both') { const processes = await this.findProcessesByPort(port, protocol); return processes.length === 0; } async findWithLsof(port, protocol) { const processes = []; const protocols = protocol === 'both' ? ['tcp', 'udp'] : [protocol]; for (const proto of protocols) { try { const lsofResult = await this.executeCommand('lsof', [ '-i', `${proto}:${port}`, '-P', '-n', ]); const lines = lsofResult.stdout.split('\n'); for (const line of lines) { if (!line.trim() || line.startsWith('COMMAND')) { continue; } const parts = line.trim().split(/\s+/); if (parts.length < 9) continue; const [command, pidStr, user, , , , , , name] = parts; const pid = parseInt(pidStr, 10); if (isNaN(pid)) continue; if (!name.includes(`:${port}`)) continue; const processCommand = await this.getProcessCommand(pid); processes.push({ pid, name: command, port, protocol: proto, command: processCommand, user, }); } } catch (error) { // Continue with next protocol } } return processes; } async findWithNetstat(port, protocol) { const processes = []; try { const args = ['-tulpn']; if (protocol !== 'both') { args.push(`--${protocol}`); } const netstatResult = await this.executeCommand('netstat', args); const lines = netstatResult.stdout.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('Active') || trimmed.startsWith('Proto')) { continue; } const parts = trimmed.split(/\s+/); if (parts.length < 7) continue; const [proto, , , localAddress, , , processInfo] = parts; if (protocol !== 'both' && !proto.toLowerCase().includes(protocol)) { continue; } const portMatch = localAddress.match(/:(\d+)$/); if (!portMatch) continue; const localPort = parseInt(portMatch[1], 10); if (localPort !== port) continue; let pid = 0; let processName = 'Unknown'; if (processInfo && processInfo !== '-') { const processMatch = processInfo.match(/^(\d+)\/(.+)$/); if (processMatch) { pid = parseInt(processMatch[1], 10); processName = processMatch[2]; } } const processCommand = pid > 0 ? await this.getProcessCommand(pid) : undefined; const processUser = pid > 0 ? await this.getProcessUser(pid) : undefined; processes.push({ pid, name: processName, port: localPort, protocol: proto.toLowerCase(), command: processCommand, user: processUser, }); } return processes; } catch (error) { return []; } } async executeCommand(command, args) { return new Promise((resolve, reject) => { const child = (0, child_process_1.spawn)(command, args, { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr, exitCode: code }); } else { reject(new errors_1.CommandExecutionError(`${command} ${args.join(' ')}`, code || 1, stderr)); } }); child.on('error', (error) => { reject(new errors_1.CommandExecutionError(`${command} ${args.join(' ')}`, 1, error.message)); }); }); } async getProcessCommand(pid) { try { const result = await this.executeCommand('ps', ['-p', pid.toString(), '-o', 'command=']); return result.stdout.trim() || undefined; } catch { return undefined; } } async getProcessUser(pid) { try { const result = await this.executeCommand('ps', ['-p', pid.toString(), '-o', 'user=']); return result.stdout.trim() || undefined; } catch { return undefined; } } async waitForProcessToExit(pid, timeout) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { await this.executeCommand('kill', ['-0', pid.toString()]); await new Promise((resolve) => setTimeout(resolve, 100)); } catch { return; } } } deduplicateProcesses(processes) { const seen = new Set(); return processes.filter((process) => { const key = `${process.pid}-${process.port}-${process.protocol}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } } exports.LinuxPlatform = LinuxPlatform; //# sourceMappingURL=linux.js.map