@oxog/port-terminator
Version:
Cross-platform utility to terminate processes on ports with zero dependencies
213 lines • 7.96 kB
JavaScript
;
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