@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
201 lines (200 loc) • 6.55 kB
JavaScript
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as path from 'node:path';
import { execFileSync } from 'node:child_process';
export function mintRunnerToken() {
return crypto.randomBytes(16).toString('hex');
}
export function readRunnerManifest(file) {
if (!fs.existsSync(file))
return null;
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
catch {
return null;
}
}
export function writeRunnerManifest(file, manifest) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(manifest, null, 2), { mode: 0o600 });
}
export function clearRunnerManifest(file, token) {
if (token !== undefined && readRunnerManifest(file)?.token !== token)
return;
try {
fs.unlinkSync(file);
}
catch {
}
}
export function getProcessGroupId(pid) {
try {
const out = execFileSync('ps', ['-o', 'pgid=', '-p', String(pid)], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
const pgid = Number(out);
return Number.isInteger(pgid) && pgid > 0 ? pgid : null;
}
catch {
return null;
}
}
export function parseRunnerPidsFromPs(psOutput, tenant, exclude) {
const pids = [];
for (const line of psOutput.split('\n')) {
if (!/vpn\s+tunnel\s+(?:__run|__supervise)\s+/.test(line))
continue;
const m = line.trim().match(/^(\d+)\s+(.*)$/);
if (!m)
continue;
const argv = m[2].split(/\s+/);
const i = argv.findIndex((a) => a === '__run' || a === '__supervise');
if (i === -1 || argv[i + 1] !== tenant)
continue;
const pid = Number(m[1]);
if (pid !== exclude)
pids.push(pid);
}
return pids;
}
export function discoverRunnerPids(tenant, exclude) {
try {
const out = execFileSync('ps', ['ax', '-o', 'pid=,command='], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
return parseRunnerPidsFromPs(out, tenant, exclude);
}
catch {
return [];
}
}
export function startControlServer(payload) {
return new Promise((resolve, reject) => {
const server = net.createServer((socket) => {
socket.on('error', () => { });
try {
socket.end(`${JSON.stringify(payload())}\n`);
}
catch {
socket.destroy();
}
});
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
resolve({ server, port: server.address().port });
});
});
}
export function queryControl(port, timeoutMs = 700) {
return new Promise((resolve) => {
const s = new net.Socket();
let buf = '';
let done = false;
const fin = (v) => {
if (done)
return;
done = true;
s.destroy();
resolve(v);
};
s.setTimeout(timeoutMs);
s.once('timeout', () => fin(null));
s.once('error', () => fin(null));
s.on('data', (d) => {
buf += d.toString();
});
s.once('close', () => {
try {
fin(JSON.parse(buf));
}
catch {
fin(null);
}
});
s.connect(port, '127.0.0.1');
});
}
function defaultPidAlive(pid) {
try {
process.kill(pid, 0);
return true;
}
catch {
return false;
}
}
export async function verifyRunnerOwnership(args) {
const { manifest } = args;
if (!manifest)
return { ok: false, reason: 'no-manifest' };
const alive = args.isPidAlive ?? defaultPidAlive;
if (!alive(manifest.pid))
return { ok: false, reason: 'runner-dead' };
const resp = await (args.query ?? queryControl)(manifest.controlPort);
if (!resp)
return { ok: false, reason: 'no-control-answer' };
if (resp.token !== manifest.token)
return { ok: false, reason: 'token-mismatch', resp };
if (resp.socksPort !== args.expectedSocksPort) {
return { ok: false, reason: 'socks-port-mismatch', resp };
}
const listening = new Set(resp.listening);
const missing = args.expectedPorts.filter((p) => !listening.has(p));
if (missing.length > 0) {
return { ok: false, reason: `ports-not-listening:${missing.join(',')}`, resp };
}
return { ok: true, reason: '', resp };
}
export async function pollOwnership(check, deadlineMs) {
let last = { ok: false, reason: 'not-checked' };
const end = Date.now() + deadlineMs;
do {
last = await check();
if (last.ok)
return last;
await new Promise((r) => setTimeout(r, 250));
} while (Date.now() < end);
return last;
}
export function describePortOwner(port) {
try {
const out = execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fcp'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
const pid = out.match(/^p(\d+)$/m)?.[1];
const cmd = out.match(/^c(.+)$/m)?.[1];
if (!pid && !cmd)
return null;
return `${cmd ?? '?'} (pid ${pid ?? '?'})`;
}
catch {
return null;
}
}
export async function findSquattedPorts(ports, accepts) {
const squatted = [];
for (const port of ports) {
if (await accepts(port))
squatted.push({ port, owner: describePortOwner(port) });
}
return squatted;
}
export function buildSquatterError(tenant, squatted, detail) {
const lines = squatted.map((s) => ` 127.0.0.1:${s.port} held by ${s.owner ?? 'an unidentified process'}`);
const first = squatted[0]?.port ?? '<port>';
return [
`Tunnel port(s) for tenant '${tenant}' are bound by a process mesh does not own${detail ? ` (${detail})` : ''}:`,
...lines,
` Refusing to reuse a foreign listener — traffic would silently flow to the wrong upstream.`,
` To fix:`,
` mesh vpn -t ${tenant} tunnel down --stop # tear down mesh-owned runners + daemon`,
` lsof -nP -iTCP:${first} -sTCP:LISTEN # identify what still holds the port`,
` then stop that process and re-run.`,
` (If lsof names another mesh runner, it belongs to a different tenant — tear that one down instead.)`,
].join('\n');
}