/** * kill-ports.js * Cross-platform port killer — ONLY kills Flo Desktop processes. * Uses an allowlist approach: identifies Flo by process name/cmdline, * then kills only those processes that hold the target ports. * * Usage: node kill-ports.js 3001 3002 3003 */ const { execSync, exec } = require('child_process'); const os = require('os'); // ── Validate args ─────────────────────────────────────────────────────────── const ports = process.argv.slice(2) .map((p) => parseInt(p, 10)) .filter((p) => Number.isInteger(p) && p >= 1 && p <= 65535); if (ports.length === 0 && require.main === module) { console.log('[kill-ports] No valid ports specified. Usage: node kill-ports.js 3001 3002 3003'); process.exit(0); } const isWindows = os.platform() === 'win32'; const isMac = os.platform() === 'darwin'; const isLinux = os.platform() === 'linux'; // ── Identity: how to recognize a Flo Desktop process ──────────────────────── // These patterns match the process command line on all platforms. // In dev: `electron .` with app.name = 'flo-desktop' // Packaged: // - Linux: executableName "flocafe" (snap/AppImage/deb binary path) // - Mac/Windows: productName "Flo Cafe" const FLO_PATTERNS = [ /(?:^|[\s\\/])flocafe(?:\.exe)?(?:$|\s)/i, /(?:^|[\s\\/])Flo[\s_\-]*Cafe(?:\.exe)?(?:$|\s)/i, /(?:^|[\s\\/])Flo Cafe\.app(?:[\\/]Contents[\\/]MacOS[\\/]Flo Cafe)?(?:$|\s)/i, /(?:^|\s)com\.flo\.desktop(?:\.\S*)?(?:$|\s)/i, /(?:^|\s)flo[_\-]?pos(?:-service)?(?:\.exe)?(?:$|\s)/i, /(?:^|\s)electron(?:\s+\S+)*\s+--appName=flo[_\-]?desktop(?:$|\s)/i, /(?:^|\s)(?:node|nodejs)(?:\s+\S+)*[\\/]FloCafe[\\/](?:dev-server\.js|dist[\\/](?:main[\\/])?index\.js)(?:$|\s)/i, /(?:^|\s)(?:node|nodejs)(?:\s+\S+)*\s+dev-server\.js(?:$|\s)/i, ]; function isFloProcess(cmdline) { if (!cmdline) return false; return FLO_PATTERNS.some((pat) => pat.test(cmdline)); } function isValidPid(pid) { return typeof pid === 'string' && /^\d+$/.test(pid) && Number(pid) > 0; } // ── Find processes on a port (cross-platform) ─────────────────────────────── // Returns Array<{ pid: string, cmdline: string }> function getProcessesOnPort(port) { const results = []; if (isWindows) { try { // netstat gives PIDs listening on the port const out = execSync( `netstat -aon | findstr "LISTENING" | findstr ":${port} "`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ); const pids = new Set(); for (const line of out.split('\n')) { const m = line.trim().match(/(\d+)\s*$/); if (m) pids.add(m[1]); } for (const pid of pids) { if (!isValidPid(pid)) continue; let cmdline = ''; try { const cmdOut = execSync( `wmic process where "ProcessId=${pid}" get CommandLine / value 2>nul`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ); const m = cmdOut.match(/CommandLine=(.*)/i); cmdline = m?.[1]?.trim() || ''; } catch { /* WMIC is absent on newer Windows installations. */ } if (!cmdline) { try { cmdline = execSync( `powershell.exe -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 } ).trim(); } catch { /* process metadata is unavailable; fail closed below */ } } results.push({ pid, cmdline }); } } catch { /* port is free */ } return results; } // Unix: lsof → ss → fuser fallback if (hasLsof) { try { // -F pC: output pid and command name fields const out = execSync( `lsof -i :${port} -P -n -F pC 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ); const pids = new Set(); for (const line of out.split('\n')) { if (line.startsWith('p')) pids.add(line.slice(1)); } for (const pid of pids) { if (!isValidPid(pid)) continue; const cmdline = getCmdline(pid); results.push({ pid, cmdline }); } if (results.length > 0) return results; } catch { /* fall through */ } } if (hasSs) { try { const out = execSync( `ss -tlnp 'sport = :${port}' 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ); const pidMatches = [...out.matchAll(/pid=(\d+)/g)]; for (const m of pidMatches) { const pid = m[1]; if (!isValidPid(pid)) continue; const cmdline = getCmdline(pid); results.push({ pid, cmdline }); } if (results.length > 0) return results; } catch { /* fall through */ } } if (hasFuser) { try { const out = execSync( `fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ); for (const pid of out.trim().split(/\s+/)) { if (!isValidPid(pid)) continue; const cmdline = getCmdline(pid); results.push({ pid, cmdline }); } } catch { /* fall through */ } } return results; } // ── Read /proc//cmdline (Linux) or ps (macOS) ────────────────────────── function getCmdline(pid) { if (isLinux) { try { return execSync( `cat /proc/${pid}/cmdline 2>/dev/null | tr '\\0' ' '`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); } catch { return ''; } } if (isMac) { try { return execSync( `ps -p ${pid} -o command= 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); } catch { return ''; } } return ''; } // ── Detect available tools on Unix ────────────────────────────────────────── function hasCommand(cmd) { try { execSync(`command -v ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; } } const hasLsof = !isWindows && hasCommand('lsof'); const hasSs = !isWindows && hasCommand('ss'); const hasFuser = !isWindows && hasCommand('fuser'); // ── Graceful kill: SIGTERM → wait → SIGKILL ───────────────────────────────── function gracefulKill(pid) { return new Promise((resolve) => { if (!isValidPid(pid)) return resolve(false); // Try SIGTERM first exec(`kill ${pid} 2>/dev/null`, { shell: '/bin/sh' }, () => { // Wait 2 seconds for graceful shutdown setTimeout(() => { // Check if still alive exec(`kill -0 ${pid} 2>/dev/null`, { shell: '/bin/sh' }, (err) => { if (!err) { // Still alive — escalate to SIGKILL exec(`kill -9 ${pid} 2>/dev/null`, { shell: '/bin/sh' }, (killError) => { if (killError) return resolve(false); exec(`kill -0 ${pid} 2>/dev/null`, { shell: '/bin/sh' }, (stillAlive) => resolve(Boolean(stillAlive))); }); } else { resolve(true); } }); }, 2000); }); }); } function gracefulKillWindows(pid) { return new Promise((resolve) => { if (!isValidPid(pid)) return resolve(false); // taskkill without /F sends WM_CLOSE (graceful) exec(`taskkill /PID ${pid} 2>nul`, { shell: 'cmd.exe' }, () => { setTimeout(() => { exec(`tasklist /FI "PID eq ${pid}" /NH`, { shell: 'cmd.exe' }, (checkError, stdout) => { if (checkError || !new RegExp(`\\b${pid}\\b`).test(stdout)) return resolve(true); exec(`taskkill /F /PID ${pid} 2>nul`, { shell: 'cmd.exe' }, (killError) => { if (killError) return resolve(false); exec(`tasklist /FI "PID eq ${pid}" /NH`, { shell: 'cmd.exe' }, (afterError, afterStdout) => { resolve(Boolean(afterError) || !new RegExp(`\\b${pid}\\b`).test(afterStdout)); }); }); }); }, 2000); }); }); } // ── Kill a port ───────────────────────────────────────────────────────────── async function killPort(port) { const procs = getProcessesOnPort(port); if (procs.length === 0) { console.log(`[kill-ports] Port ${port} is free.`); return; } const floProcs = procs.filter((p) => isFloProcess(p.cmdline)); const otherProcs = procs.filter((p) => !isFloProcess(p.cmdline)); // Report what we found but won't touch for (const p of otherProcs) { const cmd = p.cmdline || 'unknown process'; console.log( `[kill-ports] Port ${port}: SKIP — PID ${p.pid} (${cmd}) is not a Flo process.` ); } if (floProcs.length === 0) { console.log( `[kill-ports] Port ${port}: no Flo processes found. ${procs.length} other process(es) using this port.` ); console.log( `[kill-ports] Note: Non-Flo process is holding port ${port}. FloCafe will attempt fallback ports automatically on startup, or you can configure custom ports (PORT, KDS_PORT, SERVER_APP_PORT) in .env.` ); return; } // Kill Flo processes for (const p of floProcs) { const cmd = p.cmdline || 'electron'; console.log(`[kill-ports] Port ${port}: killing Flo process PID ${p.pid} (${cmd})...`); const stopped = isWindows ? await gracefulKillWindows(p.pid) : await gracefulKill(p.pid); if (stopped) { console.log(`[kill-ports] Port ${port}: PID ${p.pid} stopped.`); } else { console.warn(`[kill-ports] Port ${port}: could not stop PID ${p.pid}.`); } } } // ── Main / Export ─────────────────────────────────────────────────────────── if (require.main === module) { (async () => { for (const port of ports) { await killPort(port); } })(); } else { module.exports = { isFloProcess, FLO_PATTERNS, getProcessesOnPort, killPort, }; }