MCPcopy Create free account
hub / github.com/EvoMap/evolver / acquireLock

Function acquireLock

index.js:668–760  ·  view source on GitHub ↗
()

Source from the content-addressed store, hash-verified

666}
667
668function acquireLock() {
669 const lockFile = getLockFilePath();
670 // NOTE(windows): mode 0o700 / 0o600 are silently ignored on Windows.
671 // Lock directory and file permissions provide no OS-level isolation on
672 // Windows; rely on user-profile directory ACLs (%USERPROFILE%\.evomap).
673 try {
674 try { fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o700 }); } catch (_) {}
675 try {
676 fs.writeFileSync(lockFile, _lockPayload(), { flag: 'wx', mode: 0o600 });
677 return true;
678 } catch (exclErr) {
679 if (exclErr.code !== 'EEXIST') throw exclErr;
680 }
681 const payload = _readLockPayload(lockFile);
682 if (!payload || !Number.isFinite(payload.pid) || payload.pid <= 0) {
683 console.log('[Singleton] Corrupt lock file. Taking over.');
684 } else if (_lockIsStaleByLease(lockFile, payload)) {
685 // Round-9: a lease-aware daemon has not refreshed this lock's mtime
686 // within the stale TTL. Either it was SIGKILLed/crashed, or its PID
687 // has since been reused by an unrelated process (kill(0) below would
688 // then falsely report it alive and we would refuse to start forever).
689 // The expired lease is authoritative: take over.
690 console.log('[Singleton] Lock lease expired (PID ' + payload.pid + ', no mtime refresh for > ' +
691 Math.round(STALE_LOCK_TTL_MS / 60_000) + 'min). Taking over.');
692 } else {
693 try {
694 process.kill(payload.pid, 0);
695 // Process exists. Distinguish "alive, our user" (refuse) from
696 // "alive, different uid" (also refuse -- never barge into a root
697 // daemon under a user-launched evolver, etc.).
698 console.log(`[Singleton] Evolver loop already running (PID ${payload.pid}). Exiting.`);
699 return false;
700 } catch (e) {
701 if (e && e.code === 'EPERM') {
702 // PID exists but belongs to another user. Conservatively
703 // refuse: barging in would race the existing daemon for
704 // secret/heartbeat ownership.
705 console.warn(`[Singleton] Lock owned by PID ${payload.pid} (different user). Refusing to take over. ` +
706 `Remove ${lockFile} manually if the PID is actually dead.`);
707 return false;
708 }
709 console.log(`[Singleton] Stale lock found (PID ${payload.pid}). Taking over.`);
710 }
711 }
712 // Atomic takeover so two daemons that both observe the same stale PID
713 // and pass the kill(0) check cannot both end up "owning" the lock.
714 //
715 // Bug it fixes: the previous "unconditional unlinkSync then linkSync"
716 // pattern was NOT atomic across acquirers. Interleaving where P1 wins
717 // the linkSync but P2's unlinkSync then deletes P1's freshly-linked
718 // file (P2 never re-verifies it's deleting the same stale lock it
719 // observed) lets P2's subsequent linkSync also succeed. Both processes
720 // then return true and start a daemon, racing each other on the
721 // shared singleton secret store.
722 //
723 // renameSync is atomic at the filesystem level: only one of N racing
724 // acquirers can move the stale lockFile to a unique claim name, the
725 // rest see ENOENT and abort. After the claim succeeds, _writeLockAtomic

Callers 1

mainFunction · 0.85

Calls 4

getLockFilePathFunction · 0.85
_lockPayloadFunction · 0.85
_readLockPayloadFunction · 0.85
_writeLockAtomicFunction · 0.85

Tested by

no test coverage detected