(projectRoot: string)
| 641 | * drive that's strictly better than the daemon never starting at all. |
| 642 | */ |
| 643 | export function tryAcquireDaemonLock(projectRoot: string): AcquireResult { |
| 644 | const pidPath = getDaemonPidPath(projectRoot); |
| 645 | // Make sure the .codegraph/ directory exists — the daemon may be the first |
| 646 | // thing to touch it on a fresh-clone-but-already-initialized checkout. |
| 647 | fs.mkdirSync(path.dirname(pidPath), { recursive: true }); |
| 648 | |
| 649 | const info: DaemonLockInfo = { |
| 650 | pid: process.pid, |
| 651 | version: CodeGraphPackageVersion, |
| 652 | socketPath: getDaemonSocketPath(projectRoot), |
| 653 | startedAt: Date.now(), |
| 654 | }; |
| 655 | |
| 656 | // Temp name is pid-scoped so racing candidates never collide on it. |
| 657 | const tmp = `${pidPath}.${process.pid}.tmp`; |
| 658 | let acquired = false; |
| 659 | try { |
| 660 | fs.writeFileSync(tmp, encodeLockInfo(info), { mode: 0o600 }); |
| 661 | try { |
| 662 | fs.linkSync(tmp, pidPath); // atomic + exclusive (race-free; see must-fix 1) |
| 663 | acquired = true; |
| 664 | } catch (err: unknown) { |
| 665 | if ((err as NodeJS.ErrnoException).code === 'EEXIST') { |
| 666 | // Lost the race — another candidate already holds it. Fall through to read. |
| 667 | } else { |
| 668 | // link() failed for a non-conflict reason — nearly always "this filesystem |
| 669 | // has no hard links" (ExFAT/FAT external volumes, some network mounts), |
| 670 | // which surfaces as a DIFFERENT errno on every OS: ENOTSUP on macOS, EPERM |
| 671 | // on Linux, EISDIR on Windows (#997). Enumerating them is whack-a-mole and |
| 672 | // unnecessary: the `tmp` write above already proved this directory is |
| 673 | // writable, so an O_EXCL create is a valid atomic+exclusive substitute. If |
| 674 | // IT fails too, that's a genuine error and propagates. EEXIST ⇒ taken. |
| 675 | acquired = acquireLockViaExclusiveOpen(pidPath, info); |
| 676 | } |
| 677 | } |
| 678 | } finally { |
| 679 | try { fs.unlinkSync(tmp); } catch { /* temp already gone */ } |
| 680 | } |
| 681 | |
| 682 | if (acquired) return { kind: 'acquired', pidPath, info }; |
| 683 | |
| 684 | // Taken. Because the pidfile was link'd atomically it always holds a complete |
| 685 | // record — `existing` is null only for a genuinely corrupt leftover, never a |
| 686 | // mid-write race. |
| 687 | let existing: DaemonLockInfo | null = null; |
| 688 | let lockContents: string | null = null; |
| 689 | try { |
| 690 | lockContents = fs.readFileSync(pidPath, 'utf8'); |
| 691 | existing = decodeLockInfo(lockContents); |
| 692 | } catch { /* unreadable lockfile — treat as malformed */ } |
| 693 | return { kind: 'taken', existing, lockContents, pidPath }; |
| 694 | } |
| 695 | |
| 696 | /** |
| 697 | * Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the |
no test coverage detected