mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-30 22:13:28 +08:00
fix: preserve gateguard concurrent state writes (#1623)
This commit is contained in:
@@ -129,12 +129,33 @@ function saveState(state) {
|
|||||||
const stateFile = getStateFile();
|
const stateFile = getStateFile();
|
||||||
let tmpFile = null;
|
let tmpFile = null;
|
||||||
try {
|
try {
|
||||||
state.last_active = Date.now();
|
|
||||||
state.checked = pruneCheckedEntries(state.checked);
|
|
||||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
let mergedChecked = Array.isArray(state.checked) ? state.checked : [];
|
||||||
|
let mergedLastActive = typeof state.last_active === 'number' ? state.last_active : 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(stateFile)) {
|
||||||
|
const diskState = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
|
if (Array.isArray(diskState.checked)) {
|
||||||
|
mergedChecked = Array.from(new Set([...diskState.checked, ...mergedChecked]));
|
||||||
|
}
|
||||||
|
if (typeof diskState.last_active === 'number') {
|
||||||
|
mergedLastActive = Math.max(mergedLastActive, diskState.last_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore malformed or transient disk state */
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalState = {
|
||||||
|
checked: pruneCheckedEntries(mergedChecked),
|
||||||
|
last_active: Math.max(mergedLastActive, Date.now())
|
||||||
|
};
|
||||||
|
|
||||||
// Atomic write: temp file + rename prevents partial reads
|
// Atomic write: temp file + rename prevents partial reads
|
||||||
tmpFile = stateFile + '.tmp.' + process.pid;
|
tmpFile = `${stateFile}.tmp.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
|
||||||
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8');
|
fs.writeFileSync(tmpFile, JSON.stringify(finalState, null, 2), 'utf8');
|
||||||
try {
|
try {
|
||||||
fs.renameSync(tmpFile, stateFile);
|
fs.renameSync(tmpFile, stateFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -149,6 +170,7 @@ function saveState(state) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tmpFile = null;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (tmpFile) {
|
if (tmpFile) {
|
||||||
try {
|
try {
|
||||||
@@ -183,7 +205,8 @@ function isChecked(key) {
|
|||||||
const files = fs.readdirSync(STATE_DIR);
|
const files = fs.readdirSync(STATE_DIR);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
if (!f.startsWith('state-') || !f.endsWith('.json')) continue;
|
const isStateFile = f.startsWith('state-') && (f.endsWith('.json') || f.includes('.json.tmp.'));
|
||||||
|
if (!isStateFile) continue;
|
||||||
const fp = path.join(STATE_DIR, f);
|
const fp = path.join(STATE_DIR, f);
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fp);
|
const stat = fs.statSync(fp);
|
||||||
|
|||||||
@@ -752,6 +752,58 @@ function runTests() {
|
|||||||
assert.ok(reason.includes('evil.js'), 'sanitized path should retain visible filename text');
|
assert.ok(reason.includes('evil.js'), 'sanitized path should retain visible filename text');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 28: saveState preserves concurrent disk updates ---
|
||||||
|
clearState();
|
||||||
|
if (test('merges state written by another process during save', () => {
|
||||||
|
const hook = loadDirectHook();
|
||||||
|
const originalMkdirSync = fs.mkdirSync;
|
||||||
|
let injected = false;
|
||||||
|
|
||||||
|
fs.mkdirSync = function patchedMkdirSync(target) {
|
||||||
|
const result = originalMkdirSync.apply(fs, arguments);
|
||||||
|
if (!injected && path.resolve(String(target)) === path.resolve(stateDir)) {
|
||||||
|
injected = true;
|
||||||
|
fs.writeFileSync(stateFile, JSON.stringify({
|
||||||
|
checked: ['/src/concurrent.js'],
|
||||||
|
last_active: Date.now()
|
||||||
|
}), 'utf8');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = hook.run({
|
||||||
|
tool_name: 'Edit',
|
||||||
|
tool_input: { file_path: '/src/new-edit.js', old_string: 'a', new_string: 'b' }
|
||||||
|
});
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny', 'first edit should still be gated');
|
||||||
|
} finally {
|
||||||
|
fs.mkdirSync = originalMkdirSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
|
assert.ok(persisted.checked.includes('/src/concurrent.js'), 'concurrent disk entry should be preserved');
|
||||||
|
assert.ok(persisted.checked.includes('/src/new-edit.js'), 'new in-memory entry should be persisted');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 29: stale temp files from interrupted writes are pruned ---
|
||||||
|
clearState();
|
||||||
|
if (test('prunes stale state temp files at module load', () => {
|
||||||
|
fs.mkdirSync(stateDir, { recursive: true });
|
||||||
|
const staleTmp = path.join(stateDir, `${path.basename(stateFile)}.tmp.1234.abcd`);
|
||||||
|
const freshState = path.join(stateDir, 'state-fresh-session.json');
|
||||||
|
fs.writeFileSync(staleTmp, '{}', 'utf8');
|
||||||
|
fs.writeFileSync(freshState, '{}', 'utf8');
|
||||||
|
const staleTime = new Date(Date.now() - (61 * 60 * 1000));
|
||||||
|
fs.utimesSync(staleTmp, staleTime, staleTime);
|
||||||
|
|
||||||
|
loadDirectHook();
|
||||||
|
|
||||||
|
assert.ok(!fs.existsSync(staleTmp), 'stale temp state file should be pruned');
|
||||||
|
assert.ok(fs.existsSync(freshState), 'fresh state file should remain');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Cleanup only the temp directory created by this test file.
|
// Cleanup only the temp directory created by this test file.
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(stateDir)) {
|
if (fs.existsSync(stateDir)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user