portio
Version:
A beautiful terminal UI for managing processes on network ports (Windows only)
316 lines (315 loc) • 13.1 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import os from 'os';
const execAsync = promisify(exec);
const DEV_PORTS = [
3000, 3001, 3002, 3003, 3004, 3005,
4000, 4001, 4200, 4201,
5000, 5001, 5173, 5174, 5175,
8000, 8001, 8080, 8081, 8082, 8083,
8888, 9000, 9001, 9200, 9229,
19000, 19001, 19002
];
export function isDevPort(port) {
return DEV_PORTS.includes(port);
}
async function getPortsWindows() {
try {
const { stdout } = await execAsync('netstat -ano');
const lines = stdout.split('\n');
const processes = [];
const seenPorts = new Set();
for (const line of lines) {
if (line.includes('LISTENING')) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 5) {
const localAddress = parts[1];
const pidStr = parts[4];
if (!localAddress || !pidStr)
continue;
const pid = parseInt(pidStr, 10);
if (isNaN(pid))
continue;
const portMatch = localAddress.match(/:(\d+)$/);
if (portMatch && portMatch[1]) {
const port = parseInt(portMatch[1], 10);
const key = `${port}-${pid}`;
if (!seenPorts.has(key)) {
seenPorts.add(key);
processes.push({
pid,
port,
processName: '',
command: ''
});
}
}
}
}
}
// Batch query all PIDs at once for better performance
if (processes.length > 0) {
const pids = processes.map(p => p.pid);
// Create a filter string like: (ProcessId=123 or ProcessId=456 or ProcessId=789)
const pidFilter = pids.map(pid => `ProcessId=${pid}`).join(' or ');
try {
const { stdout: taskInfo } = await execAsync(`wmic process where "(${pidFilter})" get ProcessId,Name,CommandLine /format:list`);
// Parse WMIC output - it comes in blocks separated by blank lines
const lines = taskInfo.split(/\r?\n/);
let currentPid = 0;
let currentName = '';
let currentCmdLine = '';
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('ProcessId=')) {
// If we have a previous process data, save it before starting new one
if (currentPid > 0) {
const process = processes.find(p => p.pid === currentPid);
if (process) {
process.processName = currentName || 'Unknown';
process.fullCommand = currentCmdLine;
process.command = extractCommand(currentCmdLine);
}
}
currentPid = parseInt(trimmedLine.replace('ProcessId=', '').trim(), 10);
currentName = '';
currentCmdLine = '';
}
else if (trimmedLine.startsWith('Name=')) {
currentName = trimmedLine.replace('Name=', '').trim();
}
else if (trimmedLine.startsWith('CommandLine=')) {
currentCmdLine = trimmedLine.replace('CommandLine=', '').trim();
}
}
// Don't forget the last process
if (currentPid > 0) {
const process = processes.find(p => p.pid === currentPid);
if (process) {
process.processName = currentName || 'Unknown';
process.fullCommand = currentCmdLine;
process.command = extractCommand(currentCmdLine);
}
}
}
catch (error) {
// Fallback to individual queries if batch fails
for (const process of processes) {
try {
const { stdout: taskInfo } = await execAsync(`wmic process where ProcessId=${process.pid} get Name,CommandLine /format:list`);
const lines = taskInfo.split('\n');
for (const line of lines) {
if (line.startsWith('Name=')) {
process.processName = line.replace('Name=', '').trim();
}
else if (line.startsWith('CommandLine=')) {
const cmdLine = line.replace('CommandLine=', '').trim();
process.fullCommand = cmdLine;
process.command = extractCommand(cmdLine);
}
}
}
catch {
process.processName = 'Unknown';
process.command = '';
}
}
}
}
return processes;
}
catch (error) {
console.error('Error getting Windows ports:', error);
return [];
}
}
async function getPortsUnix() {
try {
const command = os.platform() === 'darwin'
? 'lsof -iTCP -sTCP:LISTEN -n -P'
: 'ss -ltnp';
const { stdout } = await execAsync(command);
const lines = stdout.split('\n');
const processes = [];
const seenPorts = new Set();
if (os.platform() === 'darwin') {
for (const line of lines.slice(1)) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 9) {
const pidStr = parts[1];
const addressPart = parts[8];
if (!pidStr || !addressPart)
continue;
const pid = parseInt(pidStr, 10);
if (isNaN(pid))
continue;
const portMatch = addressPart.match(/:(\d+)$/);
if (portMatch && portMatch[1]) {
const port = parseInt(portMatch[1], 10);
const key = `${port}-${pid}`;
if (!seenPorts.has(key)) {
seenPorts.add(key);
const processName = parts[0] || 'Unknown';
processes.push({
pid,
port,
processName: processName || 'Unknown',
command: await getCommandFromPid(pid)
});
}
}
}
}
}
else {
for (const line of lines.slice(1)) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 4) {
const addressPart = parts[3];
const processPart = parts[6];
if (!addressPart || !processPart)
continue;
const portMatch = addressPart.match(/:(\d+)$/);
const pidMatch = processPart.match(/pid=(\d+)/);
if (portMatch && portMatch[1] && pidMatch && pidMatch[1]) {
const port = parseInt(portMatch[1], 10);
const pid = parseInt(pidMatch[1], 10);
const key = `${port}-${pid}`;
if (!seenPorts.has(key)) {
seenPorts.add(key);
const processMatch = processPart.match(/"([^"]+)"/);
const processName = processMatch ? processMatch[1] : undefined;
processes.push({
pid,
port,
processName: processName || 'Unknown',
command: await getCommandFromPid(pid)
});
}
}
}
}
}
return processes;
}
catch (error) {
console.error('Error getting Unix ports:', error);
return [];
}
}
async function getCommandFromPid(pid) {
try {
const { stdout } = await execAsync(`ps -p ${pid} -o command=`);
const fullCommand = stdout.trim();
return extractCommand(fullCommand);
}
catch {
return '';
}
}
function extractCommand(fullCommand) {
if (!fullCommand)
return '';
const patterns = [
/\b(next|nuxt|vite|webpack|nodemon|ts-node|node|npm|yarn|pnpm|bun|deno)\b/i,
/\b(react|vue|angular|svelte|gatsby|remix|astro)\b/i,
/\b(express|fastify|koa|hapi|nest|strapi)\b/i,
/\b(dev|start|serve|watch|run)\b/i
];
for (const pattern of patterns) {
const match = fullCommand.match(pattern);
if (match) {
const context = fullCommand.substring(Math.max(0, fullCommand.indexOf(match[0]) - 20), Math.min(fullCommand.length, fullCommand.indexOf(match[0]) + match[0].length + 30));
return context.replace(/\s+/g, ' ').trim();
}
}
const parts = fullCommand.split(/\s+/);
const relevantParts = parts.slice(0, 3).join(' ');
return relevantParts.substring(0, 50);
}
export async function getProcessesOnPorts(showAll = true) {
const platform = os.platform();
let processes;
if (platform === 'win32') {
processes = await getPortsWindows();
}
else {
processes = await getPortsUnix();
}
if (!showAll) {
processes = processes.filter(p => isDevPort(p.port));
}
return processes.sort((a, b) => a.port - b.port);
}
export async function getProcessOnPort(port) {
const processes = await getProcessesOnPorts(true);
return processes.find(p => p.port === port) || null;
}
export async function killProcess(pid) {
try {
const command = os.platform() === 'win32'
? `taskkill /F /PID ${pid}`
: `kill -9 ${pid}`;
await execAsync(command);
return true;
}
catch {
return false;
}
}
export async function checkProcessExists(pid) {
try {
if (os.platform() === 'win32') {
const { stdout } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV`);
// If process exists, tasklist will return it, otherwise it returns "INFO: No tasks are running..."
return !stdout.includes('INFO:') && stdout.includes(pid.toString());
}
else {
// For Unix, check if process exists
const { stdout } = await execAsync(`ps -p ${pid} -o pid=`);
return stdout.trim() === pid.toString();
}
}
catch {
return false;
}
}
export async function killProcessElevated(pid, processName) {
if (os.platform() !== 'win32') {
// For Unix systems, use sudo
const command = `sudo kill -9 ${pid}`;
exec(command, (error) => {
if (error) {
console.error('Failed to kill process with sudo:', error);
}
});
return;
}
// For Windows, create a simpler PowerShell script since result will show in TUI
const psScript = `
$Host.UI.RawUI.WindowTitle = 'PORTIO - Admin Kill'
Write-Host 'PORTIO - Killing process with admin privileges...' -ForegroundColor Yellow
Write-Host ''
Write-Host 'Process: ${processName} (PID: ${pid})' -ForegroundColor Cyan
Write-Host ''
taskkill /F /PID ${pid}
Write-Host ''
Write-Host 'Command executed. Check PORTIO for result.' -ForegroundColor Green
Start-Sleep -Seconds 1
`.replace(/\n\t\t/g, '\n').trim();
// Encode the script as base64 to avoid escaping issues
const encodedScript = Buffer.from(psScript, 'utf16le').toString('base64');
// Create the command to launch elevated PowerShell with the encoded script
const psCommand = `Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', '${encodedScript}'`;
try {
// Launch PowerShell with the elevated command
exec(`powershell -Command "${psCommand}"`, (error) => {
if (error && error.code !== 1) { // Code 1 is normal for UAC cancel
console.error('Failed to launch elevated prompt:', error);
}
});
}
catch (error) {
console.error('Error launching elevated terminal:', error);
}
}