190 lines
5.8 KiB
JavaScript
190 lines
5.8 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const DEFAULT_LOCK_WAIT_MS = 30_000;
|
|
const DEFAULT_LOCK_POLL_MS = 200;
|
|
const DEFAULT_LOCK_POLL_MAX_MS = 2_000;
|
|
const INVALID_LOCK_GRACE_MS = 2_000;
|
|
|
|
|
|
function positiveInt(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
if (!Number.isFinite(parsed) || parsed < min) return fallback;
|
|
return Math.min(parsed, max);
|
|
}
|
|
|
|
function resolveCodexHome(env = process.env) {
|
|
const explicit = String(env?.CODEX_HOME || '').trim();
|
|
if (explicit) return path.resolve(explicit);
|
|
const home = String(env?.HOME || env?.USERPROFILE || os.homedir() || '').trim();
|
|
return home ? path.join(home, '.codex') : '';
|
|
}
|
|
|
|
function readProcessStartTime(pid) {
|
|
if (!Number.isInteger(pid) || pid <= 0 || process.platform === 'win32') return null;
|
|
try {
|
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
const closingParen = stat.lastIndexOf(')');
|
|
if (closingParen < 0) return null;
|
|
const fields = stat.slice(closingParen + 1).trim().split(/\s+/);
|
|
// The remainder starts at stat field 3; field 22 is index 19 here.
|
|
return fields[19] || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function currentProcessOwner() {
|
|
return {
|
|
pid: process.pid,
|
|
startTime: readProcessStartTime(process.pid),
|
|
token: crypto.randomBytes(16).toString('hex'),
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function readLock(lockPath) {
|
|
try {
|
|
const stat = fs.statSync(lockPath);
|
|
let owner = null;
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
if (parsed && typeof parsed === 'object') owner = parsed;
|
|
} catch {}
|
|
return { owner, mtimeMs: stat.mtimeMs };
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') return null;
|
|
return { owner: null, mtimeMs: 0 };
|
|
}
|
|
}
|
|
|
|
function processAlive(pid) {
|
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return error?.code === 'EPERM';
|
|
}
|
|
}
|
|
|
|
function lockOwnerActive(owner) {
|
|
if (!owner || !Number.isInteger(Number(owner.pid))) return false;
|
|
const pid = Number(owner.pid);
|
|
if (!processAlive(pid)) return false;
|
|
const recordedStart = owner.startTime == null ? null : String(owner.startTime);
|
|
const currentStart = readProcessStartTime(pid);
|
|
if (recordedStart && currentStart && recordedStart !== currentStart) return false;
|
|
// On platforms without /proc, a live PID is the strongest available signal.
|
|
return true;
|
|
}
|
|
|
|
function lockOwnerSummary(owner) {
|
|
if (!owner || typeof owner !== 'object') return null;
|
|
return {
|
|
pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
|
|
startTime: owner.startTime || null,
|
|
createdAt: owner.createdAt || null,
|
|
};
|
|
}
|
|
|
|
function wait(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function acquireCodexHomeLock(options = {}) {
|
|
const lockPath = options.lockPath
|
|
? path.resolve(String(options.lockPath))
|
|
: (() => {
|
|
const home = resolveCodexHome(options.env || process.env);
|
|
return home ? path.join(home, '.cc-web-codex.lock') : '';
|
|
})();
|
|
if (!lockPath) {
|
|
return { path: '', owner: null, release: async () => {} };
|
|
}
|
|
|
|
const waitMs = positiveInt(
|
|
options.waitMs ?? options.env?.CC_WEB_CODEX_LOCK_WAIT_MS,
|
|
DEFAULT_LOCK_WAIT_MS,
|
|
{ min: 0, max: 10 * 60 * 1000 },
|
|
);
|
|
const pollMs = positiveInt(options.pollMs, DEFAULT_LOCK_POLL_MS, { min: 10, max: 5000 });
|
|
const pollMaxMs = positiveInt(options.pollMaxMs, DEFAULT_LOCK_POLL_MAX_MS, { min: pollMs, max: 10_000 });
|
|
const owner = currentProcessOwner();
|
|
const startedAt = Date.now();
|
|
let lastOwner = null;
|
|
|
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
while (true) {
|
|
try {
|
|
const fd = fs.openSync(lockPath, 'wx', 0o600);
|
|
try {
|
|
fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`, 'utf8');
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
return createLockHandle(lockPath, owner);
|
|
} catch (error) {
|
|
if (error?.code !== 'EEXIST') throw error;
|
|
const existing = readLock(lockPath);
|
|
lastOwner = existing?.owner || null;
|
|
const ageMs = existing ? Math.max(0, Date.now() - Number(existing.mtimeMs || 0)) : 0;
|
|
const stale = existing && (
|
|
(existing.owner && !lockOwnerActive(existing.owner))
|
|
|| (!existing.owner && ageMs >= INVALID_LOCK_GRACE_MS)
|
|
);
|
|
if (stale) {
|
|
try {
|
|
const latest = readLock(lockPath);
|
|
if (latest?.owner?.token === existing.owner?.token || (!latest?.owner && ageMs >= INVALID_LOCK_GRACE_MS)) {
|
|
fs.unlinkSync(lockPath);
|
|
continue;
|
|
}
|
|
} catch (unlinkError) {
|
|
if (unlinkError?.code === 'ENOENT') continue;
|
|
}
|
|
}
|
|
|
|
if (Date.now() - startedAt >= waitMs) {
|
|
const err = new Error(`Codex CODEX_HOME 已被占用: ${lockPath}`);
|
|
err.code = 'CODEX_HOME_LOCK_BUSY';
|
|
err.lockPath = lockPath;
|
|
err.owner = lockOwnerSummary(lastOwner);
|
|
throw err;
|
|
}
|
|
const elapsed = Date.now() - startedAt;
|
|
const base = Math.min(pollMaxMs, pollMs * (2 ** Math.min(4, Math.floor(elapsed / 1000))));
|
|
const jitter = Math.floor(Math.random() * Math.max(1, Math.min(100, base * 0.2)));
|
|
await wait(Math.min(pollMaxMs, base + jitter));
|
|
}
|
|
}
|
|
}
|
|
|
|
function createLockHandle(lockPath, owner) {
|
|
let released = false;
|
|
return {
|
|
path: lockPath,
|
|
owner,
|
|
release: async () => {
|
|
if (released) return;
|
|
released = true;
|
|
try {
|
|
const onDisk = readLock(lockPath)?.owner;
|
|
if (onDisk?.token === owner.token) fs.unlinkSync(lockPath);
|
|
} catch {}
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_LOCK_WAIT_MS,
|
|
acquireCodexHomeLock,
|
|
lockOwnerActive,
|
|
readProcessStartTime,
|
|
resolveCodexHome,
|
|
};
|