fix(plan-canvas): inspect startup lock safely

This commit is contained in:
haelyra
2026-08-28 17:24:19 -04:00
parent 8dd610f574
commit 6ca0540dba
3 changed files with 31 additions and 6 deletions
+2 -2
View File
@@ -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
View File
@@ -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;
+12
View File
@@ -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');