mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-10 11:47:54 +02:00
fix(plan-canvas): inspect startup lock safely
This commit is contained in:
@@ -25,8 +25,8 @@ artifact as a real PDF file without sending the plan to an external converter.
|
||||
- RED: the focused server suite produced 30 passes and 2 failures because the
|
||||
Canvas had no Download PDF control or PDF endpoint.
|
||||
- GREEN: renderer unit tests pass 7/7, Plan Canvas server tests pass 34/34,
|
||||
and the end-to-end review workflow passes 11/11.
|
||||
- FULL SUITE: the final review-hardened implementation passes all 4,007
|
||||
and the end-to-end review workflow passes 12/12.
|
||||
- FULL SUITE: the final review-hardened implementation passes all 4,008
|
||||
discovered tests; hosted security reruns are recorded on PR #2894.
|
||||
- COVERAGE: `npm run coverage` passes 4,003/4,003 with 88.97% statements,
|
||||
80.58% branches, 94.22% functions, and 88.97% lines. The Plan Canvas
|
||||
|
||||
+17
-4
@@ -187,12 +187,25 @@ function processIsAlive(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
function readServerStartLock(lockPath) {
|
||||
let fd;
|
||||
try {
|
||||
fd = fs.openSync(lockPath, 'r');
|
||||
const stat = fs.fstatSync(fd);
|
||||
let owner = null;
|
||||
try { owner = JSON.parse(fs.readFileSync(fd, 'utf8')); } catch { /* incomplete lock owner */ }
|
||||
return { mtimeMs: stat.mtimeMs, owner };
|
||||
} finally {
|
||||
if (fd !== undefined) {
|
||||
try { fs.closeSync(fd); } catch { /* best-effort lock inspection */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeStaleServerStartLock(lockPath, staleAfterMs = 60 * 1000) {
|
||||
try {
|
||||
const stat = fs.statSync(lockPath);
|
||||
let owner = null;
|
||||
try { owner = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch { /* incomplete lock owner */ }
|
||||
const oldEnough = Date.now() - stat.mtimeMs > staleAfterMs;
|
||||
const { mtimeMs, owner } = readServerStartLock(lockPath);
|
||||
const oldEnough = Date.now() - mtimeMs > staleAfterMs;
|
||||
if (!oldEnough && (!owner || processIsAlive(owner.pid))) return false;
|
||||
fs.rmSync(lockPath, { force: true });
|
||||
return true;
|
||||
|
||||
@@ -158,6 +158,18 @@ async function main() {
|
||||
assert.ok(!fs.readdirSync(tmp).some(name => name.endsWith('.lock')));
|
||||
});
|
||||
|
||||
await test('port-scoped startup lock recovers a dead owner', async () => {
|
||||
const lockPort = port + 3;
|
||||
const userId = typeof process.getuid === 'function' ? process.getuid() : 'user';
|
||||
const lockPath = path.join(tmp, `ecc-plan-canvas-${userId}-${lockPort}.lock`);
|
||||
fs.writeFileSync(lockPath, JSON.stringify({ pid: 2147483647, token: 'dead-owner' }));
|
||||
assert.strictEqual(
|
||||
await withServerStartLock(lockPort, async () => 'recovered', { lockDir: tmp, timeoutMs: 2000 }),
|
||||
'recovered'
|
||||
);
|
||||
assert.ok(!fs.existsSync(lockPath));
|
||||
});
|
||||
|
||||
await test('concurrent opens serialize replacement of a same-version legacy server', async () => {
|
||||
const legacyPort = port + 1;
|
||||
const legacyStateDir = path.join(tmp, 'legacy-state');
|
||||
|
||||
Reference in New Issue
Block a user