(projectRoot: string)
| 523 | * drive that's strictly better than the daemon never starting at all. |
| 524 | */ |
| 525 | export function tryAcquireDaemonLock(projectRoot: string): AcquireResult { |
| 526 | const pidPath = getDaemonPidPath(projectRoot); |
| 527 | // Make sure the .codegraph/ directory exists — the daemon may be the first |
| 528 | // thing to touch it on a fresh-clone-but-already-initialized checkout. |
| 529 | fs.mkdirSync(path.dirname(pidPath), { recursive: true }); |
| 530 | |
| 531 | const info: DaemonLockInfo = { |
| 532 | pid: process.pid, |
| 533 | version: CodeGraphPackageVersion, |
| 534 | socketPath: getDaemonSocketPath(projectRoot), |
| 535 | startedAt: Date.now(), |
| 536 | }; |
| 537 | |
| 538 | // Temp name is pid-scoped so racing candidates never collide on it. |
| 539 | const tmp = `${pidPath}.${process.pid}.tmp`; |
| 540 | let acquired = false; |
| 541 | try { |
| 542 | fs.writeFileSync(tmp, encodeLockInfo(info), { mode: 0o600 }); |
| 543 | try { |
| 544 | fs.linkSync(tmp, pidPath); // atomic + exclusive (race-free; see must-fix 1) |
| 545 | acquired = true; |
| 546 | } catch (err: unknown) { |
| 547 | if ((err as NodeJS.ErrnoException).code === 'EEXIST') { |
| 548 | // Lost the race — another candidate already holds it. Fall through to read. |
| 549 | } else { |
| 550 | // link() failed for a non-conflict reason — nearly always "this filesystem |
| 551 | // has no hard links" (ExFAT/FAT external volumes, some network mounts), |
| 552 | // which surfaces as a DIFFERENT errno on every OS: ENOTSUP on macOS, EPERM |
| 553 | // on Linux, EISDIR on Windows (#997). Enumerating them is whack-a-mole and |
| 554 | // unnecessary: the `tmp` write above already proved this directory is |
| 555 | // writable, so an O_EXCL create is a valid atomic+exclusive substitute. If |
| 556 | // IT fails too, that's a genuine error and propagates. EEXIST ⇒ taken. |
| 557 | acquired = acquireLockViaExclusiveOpen(pidPath, info); |
| 558 | } |
| 559 | } |
| 560 | } finally { |
| 561 | try { fs.unlinkSync(tmp); } catch { /* temp already gone */ } |
| 562 | } |
| 563 | |
| 564 | if (acquired) return { kind: 'acquired', pidPath, info }; |
| 565 | |
| 566 | // Taken. Because the pidfile was link'd atomically it always holds a complete |
| 567 | // record — `existing` is null only for a genuinely corrupt leftover, never a |
| 568 | // mid-write race. |
| 569 | let existing: DaemonLockInfo | null = null; |
| 570 | try { |
| 571 | existing = decodeLockInfo(fs.readFileSync(pidPath, 'utf8')); |
| 572 | } catch { /* unreadable lockfile — treat as malformed */ } |
| 573 | return { kind: 'taken', existing, pidPath }; |
| 574 | } |
| 575 | |
| 576 | /** |
| 577 | * Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the |
no test coverage detected