forked from lunanting/cangku
164 lines
3.7 KiB
JavaScript
164 lines
3.7 KiB
JavaScript
'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,
|
|
};
|