portfree
Version:
A cross-platform CLI tool for managing processes running on specific ports
244 lines • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnixAdapter = void 0;
const base_adapter_1 = require("./base-adapter");
const child_process_1 = require("child_process");
const util_1 = require("util");
const process_1 = require("../models/process");
const errors_1 = require("../errors");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
/**
* Unix (Linux/macOS) platform adapter
* Implements platform-specific commands for Unix-like systems
*/
class UnixAdapter extends base_adapter_1.BaseAdapter {
/**
* Finds processes running on the specified port using lsof
* @param port - Port number to check
* @returns Array of process objects
*/
async findProcessByPort(port) {
try {
// Use lsof to find processes listening on the port
const command = `lsof -i :${port} -P -n`;
const { stdout } = await execAsync(command);
if (!stdout.trim()) {
return [];
}
const processes = [];
const lines = stdout.trim().split('\n');
const processMap = new Map(); // Track unique PIDs to avoid duplicates
// Skip header line
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
// Parse lsof output: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
const parts = line.split(/\s+/);
if (parts.length < 9)
continue;
const pid = parseInt(parts[1]);
const user = parts[2];
const name = parts[0];
// Connection info might be split across multiple parts, join the remaining parts
const connectionInfo = parts.slice(8).join(' '); // e.g., *:3000 (LISTEN) or 127.0.0.1:3000->127.0.0.1:54321 (ESTABLISHED)
// Skip if not a valid PID
if (isNaN(pid) || pid <= 0)
continue;
// Skip if we've already processed this PID (avoid duplicates from multiple connections)
if (processMap.has(pid))
continue;
// Determine protocol from TYPE column
let protocol = 'TCP'; // Default to TCP
if (parts[4] === 'IPv4' || parts[4] === 'IPv6') {
protocol = 'TCP';
}
else if (parts[7] && parts[7].includes('UDP')) {
protocol = 'UDP';
}
// Filter for relevant connections (LISTEN state or established connections)
const isListening = connectionInfo.includes('(LISTEN)');
const isEstablished = connectionInfo.includes('(ESTABLISHED)');
const isRelevant = isListening || isEstablished || protocol === 'UDP';
if (!isRelevant)
continue;
try {
const process = new process_1.Process({
pid,
name: name || 'Unknown',
user: user || 'Unknown',
protocol,
port,
command: name || 'Unknown'
});
processes.push(process);
processMap.set(pid, true);
}
catch (error) {
// Skip invalid process data but continue processing others
continue;
}
}
return processes;
}
catch (error) {
const err = error;
if (err.code === 1 && err.stderr && err.stderr.includes('No such file or directory')) {
throw new errors_1.SystemError('lsof command not found. Please install lsof to use this tool.', `lsof -i :${port}`, err.code);
}
if (err.code === 1 && !err.stdout) {
// lsof returns exit code 1 when no processes are found, which is normal
return [];
}
// Handle permission denied errors
if (err.stderr && err.stderr.includes('Permission denied')) {
throw new errors_1.PermissionError('Permission denied when checking port. Some processes may not be visible without elevated privileges.');
}
// Check for network-related errors
if (err.message.includes('network') || err.message.includes('connection') ||
err.message.includes('timeout') || err.stderr && err.stderr.includes('network')) {
throw new errors_1.NetworkError(`Network error while checking port ${port}: ${err.message}`, port, 'lsof lookup');
}
// Handle invalid port errors
if (err.stderr && err.stderr.includes('invalid')) {
throw new errors_1.ValidationError(`Invalid port number ${port} for lsof command`);
}
throw new errors_1.SystemError(`Failed to find processes on port ${port}: ${err.message}`, `lsof -i :${port}`, err.code);
}
}
/**
* Gets detailed information about a process by PID using ps
* @param pid - Process ID
* @returns Process details object
*/
async getProcessDetails(pid) {
try {
// Use ps to get detailed process information
const command = `ps -p ${pid} -o pid,user,comm,args --no-headers`;
const { stdout } = await execAsync(command);
if (!stdout.trim()) {
throw new errors_1.SystemError(`Process with PID ${pid} not found`, command, 1);
}
const line = stdout.trim();
const parts = line.split(/\s+/);
if (parts.length < 4) {
throw new errors_1.SystemError(`Invalid ps output format for PID ${pid}`, command);
}
const processPid = parseInt(parts[0]);
const user = parts[1];
const comm = parts[2];
const args = parts.slice(3).join(' ');
return {
pid: processPid,
user,
name: comm,
command: args || comm
};
}
catch (error) {
if (error instanceof errors_1.SystemError) {
throw error;
}
const err = error;
if (err.code === 1) {
throw new errors_1.SystemError(`Process with PID ${pid} not found`, `ps -p ${pid}`, err.code);
}
throw new errors_1.SystemError(`Failed to get process details for PID ${pid}: ${err.message}`, `ps -p ${pid}`, err.code);
}
}
/**
* Terminates a process by PID using kill command
* @param pid - Process ID to terminate
* @returns True if process was successfully terminated
*/
async killProcess(pid) {
try {
// First try graceful termination with SIGTERM
await this._killWithSignal(pid, 'TERM');
// Wait a moment to see if process terminates gracefully
await this._sleep(1000);
// Check if process is still running
const isRunning = await this._isProcessRunning(pid);
if (!isRunning) {
return true;
}
// If still running, force kill with SIGKILL
await this._killWithSignal(pid, 'KILL');
// Wait a moment and check again
await this._sleep(500);
const isStillRunning = await this._isProcessRunning(pid);
return !isStillRunning;
}
catch (error) {
if (error instanceof errors_1.PermissionError) {
throw error;
}
const err = error;
throw new errors_1.SystemError(`Failed to kill process ${pid}: ${err.message}`, `kill ${pid}`, err.code);
}
}
/**
* Kills a process with a specific signal
* @param pid - Process ID
* @param signal - Signal name (TERM, KILL, etc.)
* @private
*/
async _killWithSignal(pid, signal) {
try {
const command = `kill -${signal} ${pid}`;
await execAsync(command);
}
catch (error) {
const err = error;
if (err.code === 1 && err.stderr && err.stderr.includes('Operation not permitted')) {
throw new errors_1.PermissionError(`Permission denied when trying to kill process ${pid}. The process may be owned by another user or be a system process.`, pid);
}
if (err.code === 1 && err.stderr && err.stderr.includes('No such process')) {
// Process already terminated, which is fine
return;
}
// Provide more specific error messages
if (err.stderr && err.stderr.includes('Invalid argument')) {
const command = `kill -${signal} ${pid}`;
throw new errors_1.SystemError(`Invalid signal or process ID when trying to kill process ${pid}`, command, err.code);
}
throw error;
}
}
/**
* Checks if a process is still running
* @param pid - Process ID
* @returns True if process is running
* @private
*/
async _isProcessRunning(pid) {
try {
const command = `ps -p ${pid} --no-headers`;
const { stdout } = await execAsync(command);
return stdout.trim().length > 0;
}
catch {
// If ps fails, assume process is not running
return false;
}
}
/**
* Sleep for specified milliseconds
* @param ms - Milliseconds to sleep
* @returns Promise that resolves after the specified time
* @private
*/
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Validates if the adapter can run on Unix systems
* @returns True if adapter is compatible
*/
async isCompatible() {
const platform = process.platform;
return platform === 'linux' || platform === 'darwin';
}
}
exports.UnixAdapter = UnixAdapter;
//# sourceMappingURL=unix-adapter.js.map