@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
115 lines (114 loc) • 3.35 kB
JavaScript
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);
}