daemon-paths.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * Daemon socket + lockfile path helpers — issue #411.
  3. *
  4. * One shared `codegraph serve --mcp` daemon per project root means we need a
  5. * stable, project-keyed rendezvous between cooperating processes. The IPC
  6. * surface area is just two file paths:
  7. *
  8. * - `daemon.sock` — Unix domain socket / named pipe the daemon listens on.
  9. * - `daemon.pid` — atomic-create lockfile holding the daemon's pid + version.
  10. *
  11. * Both live under `.codegraph/` so the project-scoped uninstall (`codegraph
  12. * uninit`) sweeps them up for free.
  13. *
  14. * Special-case: Unix domain socket paths have a hard length limit (~104 on
  15. * macOS, ~108 on Linux); when the in-project path exceeds it we fall back to
  16. * an absolute-path hash under `os.tmpdir()`. The pidfile always stays in the
  17. * project (it doesn't have a length limit) — and acts as the authoritative
  18. * pointer to the socket path the daemon chose.
  19. */
  20. import * as crypto from 'crypto';
  21. import * as os from 'os';
  22. import * as path from 'path';
  23. import { getCodeGraphDir } from '../directory';
  24. /** Soft upper bound for in-project socket paths. */
  25. const POSIX_SOCKET_PATH_LIMIT = 100;
  26. /** Short stable identifier for a project root — used in tmpdir/pipe names. */
  27. function projectHash(projectRoot: string): string {
  28. return crypto.createHash('sha256').update(path.resolve(projectRoot)).digest('hex').slice(0, 16);
  29. }
  30. /**
  31. * Compute the socket / named-pipe path the daemon should listen on (and the
  32. * proxy should connect to) for `projectRoot`. Deterministic given a project
  33. * root, so independent processes converge without coordination.
  34. */
  35. export function getDaemonSocketPath(projectRoot: string): string {
  36. if (process.platform === 'win32') {
  37. return `\\\\.\\pipe\\codegraph-${projectHash(projectRoot)}`;
  38. }
  39. const inProject = path.join(getCodeGraphDir(projectRoot), 'daemon.sock');
  40. if (inProject.length <= POSIX_SOCKET_PATH_LIMIT) return inProject;
  41. // Long project paths (deep monorepos, Bazel out dirs) need tmpdir fallback
  42. // or `bind` returns EADDRINUSE / ENAMETOOLONG. Hash keeps it project-scoped.
  43. return path.join(os.tmpdir(), `codegraph-${projectHash(projectRoot)}.sock`);
  44. }
  45. /** Absolute path to the daemon pid lockfile for `projectRoot`. */
  46. export function getDaemonPidPath(projectRoot: string): string {
  47. return path.join(getCodeGraphDir(projectRoot), 'daemon.pid');
  48. }
  49. /** Structured contents of the pid lockfile. */
  50. export interface DaemonLockInfo {
  51. pid: number;
  52. version: string;
  53. socketPath: string;
  54. startedAt: number;
  55. }
  56. /**
  57. * Serialize a {@link DaemonLockInfo} for writing to the pidfile. JSON for
  58. * human readability — operators occasionally `cat` this when debugging.
  59. */
  60. export function encodeLockInfo(info: DaemonLockInfo): string {
  61. return JSON.stringify(info, null, 2) + '\n';
  62. }
  63. /**
  64. * Parse a pidfile body. Tolerant of old-format pidfiles (plain decimal pid) so
  65. * a 0.10.x daemon doesn't trip over a 0.9.x lockfile if that ever happens —
  66. * we treat such a lockfile as "process is unknown version, refuse to share."
  67. */
  68. export function decodeLockInfo(raw: string): DaemonLockInfo | null {
  69. const trimmed = raw.trim();
  70. if (!trimmed) return null;
  71. try {
  72. const parsed = JSON.parse(trimmed);
  73. if (
  74. parsed &&
  75. typeof parsed.pid === 'number' &&
  76. typeof parsed.version === 'string' &&
  77. typeof parsed.socketPath === 'string' &&
  78. typeof parsed.startedAt === 'number'
  79. ) {
  80. return parsed as DaemonLockInfo;
  81. }
  82. return null;
  83. } catch {
  84. // Fall through to legacy plain-pid handling.
  85. }
  86. const pid = Number(trimmed);
  87. if (Number.isFinite(pid) && pid > 0) {
  88. return { pid, version: 'unknown', socketPath: '', startedAt: 0 };
  89. }
  90. return null;
  91. }