Skip to content

Commit 82e2492

Browse files
fs: prevent spurious recursive watch events on prefix siblings
Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
1 parent b2f6aa3 commit 82e2492

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

lib/internal/fs/recursive_watch.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
join: pathJoin,
2626
relative: pathRelative,
2727
resolve: pathResolve,
28+
sep: pathSep,
2829
} = require('path');
2930

3031
let internalSync;
@@ -106,8 +107,10 @@ class FSWatcher extends EventEmitter {
106107
#unwatchFiles(file) {
107108
this.#symbolicFiles.delete(file);
108109

110+
const childPrefix = file + pathSep;
109111
for (const filename of this.#files.keys()) {
110-
if (StringPrototypeStartsWith(filename, file)) {
112+
if (filename === file ||
113+
StringPrototypeStartsWith(filename, childPrefix)) {
111114
this.#files.delete(filename);
112115
this.#watchers.get(filename)?.close();
113116
this.#watchers.delete(filename);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
// Regression test for https://github.com/nodejs/node/issues/58868
4+
5+
const common = require('../common');
6+
7+
if (common.isIBMi) {
8+
common.skip('IBMi does not support `fs.watch()`');
9+
}
10+
11+
if (common.isAIX) {
12+
common.skip('folder watch capability is limited in AIX.');
13+
}
14+
15+
// macOS and Windows use the native recursive watcher and are unaffected.
16+
if (common.isMacOS || common.isWindows) {
17+
common.skip('regression specific to the JS-based recursive watcher');
18+
}
19+
20+
const assert = require('assert');
21+
const fs = require('fs');
22+
const path = require('path');
23+
const { setTimeout } = require('timers/promises');
24+
25+
const tmpdir = require('../common/tmpdir');
26+
tmpdir.refresh();
27+
28+
(async () => {
29+
const root = fs.mkdtempSync(path.join(tmpdir.path, 'watch-prefix-'));
30+
31+
// Sibling names that share the prefix `foo` with the entry to delete.
32+
fs.mkdirSync(path.join(root, 'foo_bar'));
33+
fs.writeFileSync(path.join(root, 'foo_bar', 'file.txt'), '');
34+
fs.mkdirSync(path.join(root, 'foo_bar', 'somedir'));
35+
fs.mkdirSync(path.join(root, 'foo'));
36+
fs.writeFileSync(path.join(root, 'foo_'), '');
37+
38+
const events = [];
39+
const watcher = fs.watch(root, { recursive: true }, (eventType, filename) => {
40+
events.push({ eventType, filename });
41+
});
42+
43+
// Allow the watcher to fully attach to existing entries.
44+
await setTimeout(common.platformTimeout(200));
45+
46+
fs.rmdirSync(path.join(root, 'foo'));
47+
48+
// Wait long enough to capture any spurious follow-up events.
49+
await setTimeout(common.platformTimeout(500));
50+
51+
watcher.close();
52+
53+
const isSibling = (f) =>
54+
f === 'foo_' || f === 'foo_bar' ||
55+
f.startsWith('foo_bar' + path.sep);
56+
const spurious = events.filter((e) => isSibling(e.filename));
57+
assert.deepStrictEqual(
58+
spurious,
59+
[],
60+
`unexpected events for prefix-sibling entries: ${JSON.stringify(spurious)}`,
61+
);
62+
})().then(common.mustCall());

0 commit comments

Comments
 (0)