Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
major rewrite of Linux recursive watch
Signed-off-by: Matteo Collina <hello@matteocollina.com>
  • Loading branch information
mcollina committed Jan 15, 2024
commit faf947d69034a6d36bcdaf55110af2f683889d1b
61 changes: 35 additions & 26 deletions lib/internal/fs/recursive_watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class FSWatcher extends EventEmitter {
#options = null;
#closed = false;
#files = new SafeMap();
#watchers = new SafeMap();
#symbolicFiles = new SafeSet();
#rootPath = pathResolve();
#watchingFile = false;
Expand Down Expand Up @@ -79,11 +80,11 @@ class FSWatcher extends EventEmitter {
return;
}

const { unwatchFile } = lazyLoadFsSync();
this.#closed = true;

for (const file of this.#files.keys()) {
unwatchFile(file);
this.#watchers.get(file).close();
this.#watchers.delete(file);
}

this.#files.clear();
Expand All @@ -92,13 +93,13 @@ class FSWatcher extends EventEmitter {
}

#unwatchFiles(file) {
const { unwatchFile } = lazyLoadFsSync();

this.#symbolicFiles.delete(file);

for (const filename of this.#files.keys()) {
if (StringPrototypeStartsWith(filename, file)) {
unwatchFile(filename);
this.#files.delete(filename);
this.#watchers.get(filename).close();
this.#watchers.delete(filename);
}
}
}
Expand All @@ -125,10 +126,8 @@ class FSWatcher extends EventEmitter {
this.#symbolicFiles.add(f);
}

this.#files.set(f, file);
if (file.isFile()) {
this.#watchFile(f);
} else if (file.isDirectory() && !file.isSymbolicLink()) {
this.#watchFile(f);
if (file.isDirectory() && !file.isSymbolicLink()) {
this.#watchFolder(f);
}
}
Expand All @@ -143,22 +142,30 @@ class FSWatcher extends EventEmitter {
return;
}

const { watchFile } = lazyLoadFsSync();
const existingStat = this.#files.get(file);
const { watch, statSync } = lazyLoadFsSync();

if (this.#files.has(file)) {
return;
}

{
const existingStat = statSync(file);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern is that, for several places, we make statSync calls twice. One in the parent caller and one in this function.

this.#files.set(file, existingStat);
}

watchFile(file, {
const watcher = watch(file, {
persistent: this.#options.persistent,
}, (currentStats, previousStats) => {
if (existingStat && !existingStat.isDirectory() &&
currentStats.nlink !== 0 && existingStat.mtimeMs === currentStats.mtimeMs) {
return;
}
}, (eventType, filename) => {
const existingStat = this.#files.get(file);
const currentStats = statSync(file);

this.#files.set(file, currentStats);

if (currentStats.birthtimeMs === 0 && previousStats.birthtimeMs !== 0) {
if (currentStats.birthtimeMs === 0 && existingStat.birthtimeMs !== 0) {
// The file is now deleted
this.#files.delete(file);
this.#watchers.delete(file);
watcher.close();
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
this.#unwatchFiles(file);
} else if (file === this.#rootPath && this.#watchingFile) {
Expand All @@ -175,6 +182,7 @@ class FSWatcher extends EventEmitter {
this.emit('change', 'change', pathRelative(this.#rootPath, file));
}
});
this.#watchers.set(file, watcher);
}

[kFSWatchStart](filename) {
Expand All @@ -187,15 +195,9 @@ class FSWatcher extends EventEmitter {
this.#closed = false;
this.#watchingFile = file.isFile();

this.#watchFile(filename);
if (file.isDirectory()) {
this.#files.set(filename, file);

this.#watchFolder(filename);
for (const f of this.#files.keys()) {
this.#watchFile(f);
}
} else {
this.#watchFile(filename);
}
} catch (error) {
if (error.code === 'ENOENT') {
Expand Down Expand Up @@ -230,7 +232,10 @@ class FSWatcher extends EventEmitter {
resolve({ __proto__: null, value: { eventType, filename } });
});
} : (resolve, reject) => {
const onAbort = () => reject(new AbortError(undefined, { cause: signal.reason }));
const onAbort = () => {
this.close();
reject(new AbortError(undefined, { cause: signal.reason }));
};
if (signal.aborted) return onAbort();
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
signal.addEventListener('abort', onAbort, { __proto__: null, once: true, [kResistStopPropagation]: true });
Expand All @@ -243,6 +248,10 @@ class FSWatcher extends EventEmitter {
next: () => (this.#closed ?
{ __proto__: null, done: true } :
new Promise(promiseExecutor)),
return: () => {
this.close();
return { __proto__: null, done: true };
},
[SymbolAsyncIterator]() { return this; },
};
}
Expand Down
4 changes: 0 additions & 4 deletions test/parallel/test-fs-recursive-sync-write.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,3 @@ const watcher = watch(tmpDir, { recursive: true }, common.mustCall((eventType, _
}));

writeFileSync(filename, 'foobar2');

process.once('exit', function() {
assert(watcherClosed, 'watcher Object was not closed');
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-fs-watch-recursive-add-file-with-url.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-fs-watch-recursive-add-file.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-fs-watch-recursive-add-folder.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
1 change: 0 additions & 1 deletion test/parallel/test-fs-watch-recursive-symlink.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down
3 changes: 0 additions & 3 deletions test/parallel/test-fs-watch-recursive-update-file.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const common = require('../common');
const { setTimeout } = require('timers/promises');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');
Expand Down Expand Up @@ -31,14 +30,12 @@ const testFile = path.join(testDirectory, 'file-1.txt');
fs.writeFileSync(testFile, 'hello');

const watcher = fs.watch(testDirectory, { recursive: true });
let watcherClosed = false;
watcher.on('change', common.mustCallAtLeast(function(event, filename) {
// Libuv inconsistenly emits a rename event for the file we are watching
assert.ok(event === 'change' || event === 'rename');

if (filename === path.basename(testFile)) {
watcher.close();
watcherClosed = true;
}
}));

Expand Down