| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- /**
- * Daemon socket + lockfile path helpers — issue #411.
- *
- * One shared `codegraph serve --mcp` daemon per project root means we need a
- * stable, project-keyed rendezvous between cooperating processes. The IPC
- * surface area is just two file paths:
- *
- * - `daemon.sock` — Unix domain socket / named pipe the daemon listens on.
- * - `daemon.pid` — atomic-create lockfile holding the daemon's pid + version.
- *
- * Both live under `.codegraph/` so the project-scoped uninstall (`codegraph
- * uninit`) sweeps them up for free.
- *
- * Special-case: Unix domain socket paths have a hard length limit (~104 on
- * macOS, ~108 on Linux); when the in-project path exceeds it we fall back to
- * an absolute-path hash under `os.tmpdir()`. The pidfile always stays in the
- * project (it doesn't have a length limit) — and acts as the authoritative
- * pointer to the socket path the daemon chose.
- */
- import * as crypto from 'crypto';
- import * as os from 'os';
- import * as path from 'path';
- import { getCodeGraphDir } from '../directory';
- /** Soft upper bound for in-project socket paths. */
- const POSIX_SOCKET_PATH_LIMIT = 100;
- /** Short stable identifier for a project root — used in tmpdir/pipe names. */
- function projectHash(projectRoot: string): string {
- return crypto.createHash('sha256').update(path.resolve(projectRoot)).digest('hex').slice(0, 16);
- }
- /**
- * Compute the socket / named-pipe path the daemon should listen on (and the
- * proxy should connect to) for `projectRoot`. Deterministic given a project
- * root, so independent processes converge without coordination.
- */
- export function getDaemonSocketPath(projectRoot: string): string {
- if (process.platform === 'win32') {
- return `\\\\.\\pipe\\codegraph-${projectHash(projectRoot)}`;
- }
- const inProject = path.join(getCodeGraphDir(projectRoot), 'daemon.sock');
- if (inProject.length <= POSIX_SOCKET_PATH_LIMIT) return inProject;
- // Long project paths (deep monorepos, Bazel out dirs) need tmpdir fallback
- // or `bind` returns EADDRINUSE / ENAMETOOLONG. Hash keeps it project-scoped.
- return path.join(os.tmpdir(), `codegraph-${projectHash(projectRoot)}.sock`);
- }
- /** Absolute path to the daemon pid lockfile for `projectRoot`. */
- export function getDaemonPidPath(projectRoot: string): string {
- return path.join(getCodeGraphDir(projectRoot), 'daemon.pid');
- }
- /** Structured contents of the pid lockfile. */
- export interface DaemonLockInfo {
- pid: number;
- version: string;
- socketPath: string;
- startedAt: number;
- }
- /**
- * Serialize a {@link DaemonLockInfo} for writing to the pidfile. JSON for
- * human readability — operators occasionally `cat` this when debugging.
- */
- export function encodeLockInfo(info: DaemonLockInfo): string {
- return JSON.stringify(info, null, 2) + '\n';
- }
- /**
- * Parse a pidfile body. Tolerant of old-format pidfiles (plain decimal pid) so
- * a 0.10.x daemon doesn't trip over a 0.9.x lockfile if that ever happens —
- * we treat such a lockfile as "process is unknown version, refuse to share."
- */
- export function decodeLockInfo(raw: string): DaemonLockInfo | null {
- const trimmed = raw.trim();
- if (!trimmed) return null;
- try {
- const parsed = JSON.parse(trimmed);
- if (
- parsed &&
- typeof parsed.pid === 'number' &&
- typeof parsed.version === 'string' &&
- typeof parsed.socketPath === 'string' &&
- typeof parsed.startedAt === 'number'
- ) {
- return parsed as DaemonLockInfo;
- }
- return null;
- } catch {
- // Fall through to legacy plain-pid handling.
- }
- const pid = Number(trimmed);
- if (Number.isFinite(pid) && pid > 0) {
- return { pid, version: 'unknown', socketPath: '', startedAt: 0 };
- }
- return null;
- }
|