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