@debugg-ai/debugg-ai-mcp
Version:
Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.
110 lines (109 loc) • 4.13 kB
JavaScript
/**
* Cross-process tunnel registry.
*
* Lets multiple MCP server instances on the same machine discover and share
* ngrok tunnels instead of each provisioning a duplicate for the same port.
*
* The file registry uses an atomic rename-write so concurrent processes never
* see a partial JSON file. All operations are best-effort — errors are
* swallowed so a broken registry never blocks tunnel creation.
*/
import { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
// ── File-backed implementation (production) ───────────────────────────────────
const REGISTRY_FILE = join(tmpdir(), 'debugg-ai-tunnels.json');
export function createFileRegistry() {
const store = {
read() {
try {
if (!existsSync(REGISTRY_FILE))
return {};
return JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
}
catch {
return {};
}
},
write(data) {
const tmp = `${REGISTRY_FILE}.${process.pid}.tmp`;
try {
writeFileSync(tmp, JSON.stringify(data));
renameSync(tmp, REGISTRY_FILE);
}
catch {
// best-effort
}
},
isPidAlive(pid) {
return checkPid(pid);
},
prune(opts) {
return pruneRegistryData(store, opts);
},
};
return store;
}
// ── In-memory implementation (tests / injectable) ─────────────────────────────
export function createInMemoryRegistry(isPidAliveImpl) {
let data = {};
const store = {
read: () => ({ ...data }),
write: (next) => { data = { ...next }; },
isPidAlive: isPidAliveImpl ?? checkPid,
prune: (opts) => pruneRegistryData(store, opts),
};
return store;
}
// ── No-op implementation (tests that don't exercise registry) ─────────────────
export const noopRegistry = {
read: () => ({}),
write: () => { },
isPidAlive: () => false,
prune: () => ({ pruned: 0, remaining: 0 }),
};
// ── Default selection ─────────────────────────────────────────────────────────
/**
* Returns the appropriate registry for the current environment.
* Tests (NODE_ENV=test) get the no-op registry; production gets file-backed.
*/
export function getDefaultRegistry() {
return process.env.NODE_ENV === 'test' ? noopRegistry : createFileRegistry();
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function checkPid(pid) {
try {
process.kill(pid, 0); // signal 0 = existence check, no signal sent
return true;
}
catch {
return false;
}
}
/**
* Shared prune logic — read, filter, write back. Used by both the file-backed
* and in-memory implementations so the eviction policy lives in one place.
*
* Eviction rule: drop entries where EITHER the owner PID is dead OR the entry
* hasn't been touched within `staleAfterMs`. The freshness check is what
* defends against PID-reuse (bead 3th).
*/
function pruneRegistryData(store, opts) {
const now = opts.nowMs ?? Date.now();
const data = store.read();
const next = {};
let pruned = 0;
for (const [port, entry] of Object.entries(data)) {
const aliveAndFresh = store.isPidAlive(entry.ownerPid) &&
(now - entry.lastAccessedAt) <= opts.staleAfterMs;
if (aliveAndFresh) {
next[port] = entry;
}
else {
pruned++;
}
}
if (pruned > 0)
store.write(next);
return { pruned, remaining: Object.keys(next).length };
}