Files
autodoc/core/locks.js
Iisyourdad dca3e042f2
Template tests / tests (push) Successful in 1m48s
Cleanup
2026-06-11 09:44:52 -05:00

68 lines
2.3 KiB
JavaScript

'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { nowIso, readJsonIfExists } = require('./util');
/**
* Advisory sidecar lock files for shared .sfgz guides on network folders.
* For `guide.sfgz` the lock is `guide.lock-sfgz` next to it. This is a
* coordination mechanism, not a security boundary (see docs/SECURITY.md).
*/
const STALE_AFTER_MS = 1000 * 60 * 60 * 8; // 8h: treat crashed holders as stale
function lockPathFor(archivePath) {
const dir = path.dirname(archivePath);
const base = path.basename(archivePath);
const stem = base.endsWith('.sfgz') ? base.slice(0, -'.sfgz'.length) : base;
return path.join(dir, `${stem}.lock-sfgz`);
}
function currentHolder() {
return { host: os.hostname(), user: os.userInfo().username, pid: process.pid };
}
function readLock(archivePath) {
return readJsonIfExists(lockPathFor(archivePath), null);
}
function sameHolder(a, b) {
return a && b && a.host === b.host && a.user === b.user && a.pid === b.pid;
}
function isStale(lock, now = Date.now()) {
const t = Date.parse(lock && lock.acquiredAt);
return !Number.isFinite(t) || now - t > STALE_AFTER_MS;
}
/**
* Try to take the lock. Returns { acquired: true, lock } or
* { acquired: false, conflict } when someone else holds a fresh lock.
* Pass force=true to steal (after the user confirmed in the conflict dialog).
*/
function acquireLock(archivePath, { force = false } = {}) {
const file = lockPathFor(archivePath);
const existing = readLock(archivePath);
const me = currentHolder();
if (existing && !sameHolder(existing, me) && !isStale(existing) && !force) {
return { acquired: false, conflict: existing };
}
const lock = { ...me, acquiredAt: nowIso() };
fs.writeFileSync(file, JSON.stringify(lock, null, 2));
return { acquired: true, lock };
}
/** Release only if we are the holder (or force). */
function releaseLock(archivePath, { force = false } = {}) {
const file = lockPathFor(archivePath);
const existing = readLock(archivePath);
if (!existing) return true;
if (!force && !sameHolder(existing, currentHolder())) return false;
fs.rmSync(file, { force: true });
return true;
}
module.exports = { lockPathFor, readLock, acquireLock, releaseLock, isStale, STALE_AFTER_MS };