|
/** |
|
* @parcel/watcher DirTree Cache Bug - Proof of Concept |
|
* |
|
* This demonstrates a bug where DirTree is cached by directory path only, |
|
* not by ignore patterns. This causes incorrect behavior when multiple |
|
* watchers with different ignore patterns watch the same directory. |
|
* |
|
* BUG AFFECTS: Linux (inotify), likely Windows (ReadDirectoryChangesW) |
|
* NOT AFFECTED: macOS (FSEvents creates fresh DirTree per subscription) |
|
* |
|
* Run on Linux with: node bug-poc-final.js |
|
*/ |
|
|
|
const watcher = require('@parcel/watcher'); |
|
const fs = require('fs'); |
|
const path = require('path'); |
|
|
|
const WATCH_DIR = path.join(__dirname, 'test-dir'); |
|
const SUBDIR = path.join(WATCH_DIR, 'subdir'); |
|
const TEST_FILE = path.join(SUBDIR, 'test.txt'); |
|
const ROOT_FILE = path.join(WATCH_DIR, 'root.txt'); |
|
|
|
async function sleep(ms) { |
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
} |
|
|
|
async function main() { |
|
// Setup |
|
fs.rmSync(WATCH_DIR, { recursive: true, force: true }); |
|
fs.mkdirSync(SUBDIR, { recursive: true }); |
|
fs.writeFileSync(TEST_FILE, 'initial'); |
|
fs.writeFileSync(ROOT_FILE, 'initial'); |
|
|
|
console.log('╔══════════════════════════════════════════════════════════════╗'); |
|
console.log('║ @parcel/watcher DirTree Cache Bug - Proof of Concept ║'); |
|
console.log('╚══════════════════════════════════════════════════════════════╝\n'); |
|
|
|
console.log(`Platform: ${process.platform}`); |
|
console.log(`Node: ${process.version}\n`); |
|
|
|
if (process.platform !== 'linux') { |
|
console.log('⚠️ WARNING: This bug only affects Linux (inotify backend).'); |
|
console.log(' macOS uses FSEvents which creates a fresh DirTree per watcher.'); |
|
console.log(' Running anyway to show expected behavior on this platform.\n'); |
|
} |
|
|
|
console.log('Directory structure:'); |
|
console.log(` ${WATCH_DIR}/`); |
|
console.log(` ├── root.txt`); |
|
console.log(` └── subdir/`); |
|
console.log(` └── test.txt\n`); |
|
|
|
// Track events for each watcher |
|
let watcher1Events = []; |
|
let watcher2Events = []; |
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
// STEP 1: First watcher ignores subdir |
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
console.log('─'.repeat(66)); |
|
console.log('STEP 1: Subscribe Watcher 1 (ignores "subdir")\n'); |
|
console.log(' On Linux, this:'); |
|
console.log(' 1. Calls DirTree::getCached("/test-dir") - cache miss'); |
|
console.log(' 2. Calls readTree() which uses FTS_SKIP for ignored paths'); |
|
console.log(' 3. DirTree is built WITHOUT subdir/'); |
|
console.log(' 4. Tree is cached and marked isComplete = true'); |
|
console.log(' 5. inotify_add_watch() called for /test-dir only\n'); |
|
|
|
const sub1 = await watcher.subscribe( |
|
WATCH_DIR, |
|
(err, events) => { |
|
for (const e of events) { |
|
console.log(` [W1] ${e.type}: ${path.relative(WATCH_DIR, e.path)}`); |
|
watcher1Events.push(e); |
|
} |
|
}, |
|
{ ignore: ['subdir'] } |
|
); |
|
|
|
await sleep(500); |
|
console.log(' ✓ Watcher 1 subscribed\n'); |
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
// STEP 2: Second watcher with NO ignores |
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
console.log('─'.repeat(66)); |
|
console.log('STEP 2: Subscribe Watcher 2 (NO ignores) - WHILE Watcher 1 active\n'); |
|
console.log(' On Linux, this:'); |
|
console.log(' 1. Calls DirTree::getCached("/test-dir") - CACHE HIT!'); |
|
console.log(' 2. Returns tree built by Watcher 1 (missing subdir/)'); |
|
console.log(' 3. tree->isComplete is true, so readTree() is SKIPPED'); |
|
console.log(' 4. inotify_add_watch() called for /test-dir only'); |
|
console.log(' 5. *** NO watch created for subdir/ ***\n'); |
|
|
|
const sub2 = await watcher.subscribe( |
|
WATCH_DIR, |
|
(err, events) => { |
|
for (const e of events) { |
|
console.log(` [W2] ${e.type}: ${path.relative(WATCH_DIR, e.path)}`); |
|
watcher2Events.push(e); |
|
} |
|
}, |
|
{ ignore: [] } // No ignores - should watch EVERYTHING |
|
); |
|
|
|
await sleep(500); |
|
console.log(' ✓ Watcher 2 subscribed\n'); |
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
// STEP 3: Unsubscribe Watcher 1 |
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
console.log('─'.repeat(66)); |
|
console.log('STEP 3: Unsubscribe Watcher 1\n'); |
|
|
|
await sub1.unsubscribe(); |
|
console.log(' ✓ Watcher 1 unsubscribed'); |
|
console.log(' Watcher 2 is now the only active watcher.\n'); |
|
|
|
await sleep(500); |
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
// STEP 4: Trigger file changes |
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
console.log('─'.repeat(66)); |
|
console.log('STEP 4: Modify files\n'); |
|
|
|
// Clear events from setup |
|
watcher2Events = []; |
|
|
|
// Modify root file (should always work) |
|
console.log(' Writing to root.txt...'); |
|
fs.writeFileSync(ROOT_FILE, 'modified-' + Date.now()); |
|
|
|
await sleep(1000); |
|
|
|
// Modify subdir file (this is the bug!) |
|
console.log(' Writing to subdir/test.txt...'); |
|
fs.writeFileSync(TEST_FILE, 'modified-' + Date.now()); |
|
|
|
console.log('\n Waiting for events...\n'); |
|
await sleep(2000); |
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
// RESULTS |
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
console.log('═'.repeat(66)); |
|
console.log('RESULTS\n'); |
|
|
|
const rootEvents = watcher2Events.filter(e => e.path.includes('root.txt')); |
|
const subdirEvents = watcher2Events.filter(e => e.path.includes('subdir')); |
|
|
|
console.log(` Events from root.txt: ${rootEvents.length > 0 ? '✓ YES' : '✗ NO'}`); |
|
console.log(` Events from subdir/: ${subdirEvents.length > 0 ? '✓ YES' : '✗ NO'}`); |
|
|
|
console.log('\n' + '─'.repeat(66)); |
|
|
|
if (subdirEvents.length === 0 && rootEvents.length > 0) { |
|
console.log('\n 🐛 BUG CONFIRMED!\n'); |
|
console.log(' Watcher 2 (with no ignores) did NOT receive events from subdir/'); |
|
console.log(' even though it should be watching the entire directory tree.\n'); |
|
console.log(' Root cause: DirTree cache key is only the directory path.'); |
|
console.log(' Watcher 2 inherited the filtered tree from Watcher 1.\n'); |
|
} else if (subdirEvents.length > 0) { |
|
console.log('\n ✓ Bug NOT reproduced on this platform.\n'); |
|
if (process.platform === 'darwin') { |
|
console.log(' This is expected on macOS - FSEvents creates a fresh DirTree'); |
|
console.log(' per subscription, avoiding the caching issue.\n'); |
|
} |
|
} else { |
|
console.log('\n ⚠️ No events received at all - something else may be wrong.\n'); |
|
} |
|
|
|
// Cleanup |
|
await sub2.unsubscribe(); |
|
fs.rmSync(WATCH_DIR, { recursive: true, force: true }); |
|
} |
|
|
|
main().catch(err => { |
|
console.error('Error:', err); |
|
process.exit(1); |
|
}); |