UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

115 lines (114 loc) 3.35 kB
import { execFileSync } from 'node:child_process'; import * as path from 'node:path'; import { assertSignalablePid } from '../utils/pid.js'; export function decidePortHolder(holder, worktreeRoot, livePids) { if (holder === null) return { action: 'free' }; if (livePids.has(holder.pid)) { return { action: 'refuse', holder, reason: 'held by a live mesh dev session', }; } if (holder.cwd === null) { return { action: 'refuse', holder, reason: 'owner could not be identified (no cwd)', }; } if (!isInside(holder.cwd, worktreeRoot)) { return { action: 'refuse', holder, reason: `held by a process outside this worktree (cwd ${holder.cwd})`, }; } return { action: 'reclaim', holder, reason: 'orphaned service from an earlier boot of this worktree', }; } export function isInside(child, root) { const c = path.resolve(child); const r = path.resolve(root); return c === r || c.startsWith(r + path.sep); } export function describePortHolder(port) { const pid = firstListenerPid(port); if (pid === null) return null; return { pid, command: processCommand(pid), cwd: processCwd(pid) }; } function firstListenerPid(port) { try { const out = execFileSync('lsof', ['-tiTCP:' + port, '-sTCP:LISTEN'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); const first = out.trim().split('\n')[0]?.trim(); const pid = Number(first); return Number.isInteger(pid) && pid > 0 ? pid : null; } catch { return null; } } function processCommand(pid) { try { const out = execFileSync('ps', ['-o', 'comm=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); return out.trim().split('\n')[0]?.trim() || 'unknown'; } catch { return 'unknown'; } } export function parseLsofCwd(out) { for (const line of out.trim().split('\n')) { if (line.startsWith('n')) return line.slice(1).trim() || null; } return null; } function processCwd(pid) { try { const out = execFileSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); return parseLsofCwd(out); } catch { return null; } } export async function reclaimPort(port, holder, opts) { assertSignalablePid(holder.pid, { what: `the process holding port ${port}` }); const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal)); const deadline = Date.now() + (opts.timeoutMs ?? 5_000); try { kill(holder.pid, 'SIGTERM'); } catch { } let escalated = false; while (Date.now() < deadline) { if (await opts.isPortFree(port)) return true; if (!escalated && Date.now() > deadline - 2_000) { escalated = true; try { kill(holder.pid, 'SIGKILL'); } catch { } } await new Promise((r) => setTimeout(r, 150)); } return opts.isPortFree(port); }