'use strict'; const crypto = require('crypto'); const fs = require('fs/promises'); const os = require('os'); const path = require('path'); const DEFAULT_LOCK_PATH = path.resolve(__dirname, '../../runtime/hsds/hsds_account.lock.json'); const DEFAULT_STALE_MS = Math.max( Number.parseInt(process.env.HSDS_ACCOUNT_LOCK_STALE_MS || '43200000', 10) || 43200000, 60000 ); function normalizeText(value, fallback = '') { const text = value === undefined || value === null ? '' : String(value).trim(); return text || fallback; } function isProcessAlive(pid) { const numericPid = Number.parseInt(pid, 10); if (!Number.isFinite(numericPid) || numericPid <= 0) { return false; } try { process.kill(numericPid, 0); return true; } catch (error) { return error?.code === 'EPERM'; } } async function ensureLockDirectory(lockPath) { await fs.mkdir(path.dirname(lockPath), { recursive: true }); } async function readLockPayload(lockPath) { try { const text = await fs.readFile(lockPath, 'utf8'); return JSON.parse(text); } catch (_error) { return null; } } function isLockPayloadStale(payload) { if (!payload || typeof payload !== 'object') { return true; } if (isProcessAlive(payload.pid)) { return false; } const currentHostname = os.hostname(); const lockHostname = normalizeText(payload.hostname); if (!lockHostname || lockHostname === currentHostname) { return true; } const acquiredAt = Date.parse(payload.acquiredAt || ''); if (Number.isFinite(acquiredAt)) { return Date.now() - acquiredAt > DEFAULT_STALE_MS; } return true; } async function releaseOwnedLock(lockPath, token) { if (!token) { return; } const payload = await readLockPayload(lockPath); if (payload?.token !== token) { return; } await fs.rm(lockPath, { force: true }); } async function acquireHsdsAccountLock(trigger = 'manual', options = {}) { const lockPath = path.resolve(options.lockPath || DEFAULT_LOCK_PATH); await ensureLockDirectory(lockPath); for (let attempt = 0; attempt < 2; attempt += 1) { const token = crypto.randomUUID(); const payload = { token, pid: process.pid, hostname: os.hostname(), trigger: normalizeText(trigger, 'manual'), command: process.argv.join(' ').trim(), acquiredAt: new Date().toISOString(), }; try { const handle = await fs.open(lockPath, 'wx'); try { await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, 'utf8'); } finally { await handle.close(); } return { acquired: true, lockPath, payload, async release() { await releaseOwnedLock(lockPath, token); }, }; } catch (error) { if (error?.code !== 'EEXIST') { throw error; } const holder = await readLockPayload(lockPath); if (isLockPayloadStale(holder)) { await fs.rm(lockPath, { force: true }); continue; } return { acquired: false, lockPath, holder, }; } } const holder = await readLockPayload(lockPath); return { acquired: false, lockPath, holder, }; } async function withHsdsAccountLock(trigger, runner, options = {}) { const lockState = await acquireHsdsAccountLock(trigger, options); if (!lockState.acquired) { return { success: false, skipped: true, reason: 'hsds_account_busy', trigger: normalizeText(trigger, 'manual'), lockPath: lockState.lockPath, lockHolder: lockState.holder || null, }; } try { return await runner(); } finally { await lockState.release(); } } module.exports = { acquireHsdsAccountLock, withHsdsAccountLock, };