Keep legacy graph owners protected and root reads lightweight

This commit is contained in:
Forge
2026-09-22 03:49:35 +00:00
parent 4c13016561
commit 9ab6959d26
3 changed files with 21 additions and 4 deletions
@@ -183,6 +183,12 @@ if (process.argv[2] === '--child') {
await run(conflict); clean(conflict); assert.equal(JSON.parse(config(conflict)).resolvedByUser, true);
receipt({ action: 'conflicting plugin edit', boundary, decision: 'no overwrite; actionable repair then recovery', passed: true });
}
const graphLocked = await project(true), graphLock = path.join(graphLocked, '.codegraph/codegraph.lock');
fs.writeFileSync(graphLock, String(process.pid)); fs.utimesSync(graphLock, 1, 1);
assert.match((await run(graphLocked, 'update', 1)).stderr, /legacy graph owner/);
assert.equal(fs.readFileSync(graphLock, 'utf8'), String(process.pid)); fs.unlinkSync(graphLock);
fs.writeFileSync(graphLock, '{'); assert.match((await run(graphLocked, 'index', 1)).stderr, /unknown legacy graph lock/); fs.unlinkSync(graphLock);
receipt({ action: 'live aged/torn legacy graph lock', decision: 'no age-based stealing or guessing', passed: true });
const settings = await project(true); await killAt(settings, 'update', 'config_written');
const changedSettings = JSON.parse(config(settings)); changedSettings.exclude = ['handlers.py'];
const changedRaw = JSON.stringify(changedSettings); fs.writeFileSync(path.join(settings, 'codegraph.json'), changedRaw);
+1 -2
View File
@@ -274,7 +274,7 @@ export class CodeGraph {
* file can't be unlinked there, and st_ino is unreliable).
*/
reopenIfReplaced(): boolean {
this.ensureExtensionState();
recoverExtensions(this.projectRoot);
if (!this.db.isReplacedOnDisk()) return false;
const dbPath = this.db.getPath();
// Open the live file FIRST — if that throws (e.g. mid-recreate), the old
@@ -503,7 +503,6 @@ export class CodeGraph {
* Get the project root directory
*/
getProjectRoot(): string {
this.ensureExtensionState();
return this.projectRoot;
}
+14 -2
View File
@@ -101,6 +101,17 @@ function checkOwner(root: string, journal?: RecordV1): void {
}
fail(root, 'torn/unknown owner record without a valid transaction; confirm no legacy installer is running before archiving the owner record');
}
function checkGraphOwner(root: string, transactionPid?: number): void {
const lock = path.join(root, '.codegraph', 'codegraph.lock'), raw = read(lock)?.trim();
if (raw === undefined) return;
if (transactionPid && raw === String(transactionPid)) return; // orphan proved by the coordinator
if (/^[1-9]\d*$/.test(raw)) {
try { process.kill(Number(raw), 0); }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ESRCH') { remove(lock); return; } }
fail(root, `legacy graph owner ${raw} may be live (or PID reused); wait for it to finish before retrying, regardless of lock age`);
}
fail(root, 'unknown legacy graph lock; confirm its owner has stopped before archiving codegraph.lock');
}
function marker(root: string, graphExisted: boolean): string | null {
const file = path.join(root, '.codegraph', 'codegraph.db');
if (!fs.existsSync(file)) return null;
@@ -153,6 +164,7 @@ function verifyPackages(root: string, config: string | null, trust: string | nul
function reconcile(root: string): boolean {
const r = loadRecord(root); checkOwner(root, r);
if (!r) { remove(ownerPath(root)); return false; }
checkGraphOwner(root, r.pid);
const committed = marker(root, r.graphExisted) === r.id;
const configFile = path.join(root, 'codegraph.json'), trustFile = path.join(dir(root), 'trust.json');
const configRaw = read(configFile), trustRaw = read(trustFile);
@@ -202,7 +214,7 @@ export function withExtensionGuardSync<T>(root: string, work: () => T): T {
root = fs.realpathSync(root);
if (context.getStore() === root) return work();
const release = acquire(root);
try { return context.run(root, () => { reconcile(root); return work(); }); }
try { return context.run(root, () => { reconcile(root); checkGraphOwner(root); return work(); }); }
finally { release(); }
}
/** Indexers and lifecycle writers share this OS-released lock for their full run. */
@@ -210,7 +222,7 @@ export async function withExtensionGuard<T>(root: string, work: () => Promise<T>
root = fs.realpathSync(root);
if (context.getStore() === root) return work();
const release = acquire(root);
try { return await context.run(root, async () => { reconcile(root); return work(); }); }
try { return await context.run(root, async () => { reconcile(root); checkGraphOwner(root); return work(); }); }
finally { release(); }
}
export class ExtensionTransaction {