Skip to content

Commit 3d0097d

Browse files
marcopiracciniaduh95
authored andcommitted
fs: prevent spurious recursive watch events on prefix siblings
Signed-off-by: marcopiraccini <marco.piraccini@gmail.com> PR-URL: #63095 Fixes: #58868 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent e49d730 commit 3d0097d

2 files changed

Lines changed: 74 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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 entries 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.writeFileSync(path.join(root, 'foo_'), '');
36+
37+
// `foo` (empty) exercises the exact-match branch of `#unwatchFiles`.
38+
fs.mkdirSync(path.join(root, 'foo'));
39+
40+
// `foo2` has descendants and exercises the `file + sep` prefix branch.
41+
fs.mkdirSync(path.join(root, 'foo2'));
42+
fs.writeFileSync(path.join(root, 'foo2', 'inside.txt'), '');
43+
fs.mkdirSync(path.join(root, 'foo2', 'sub'));
44+
45+
const events = [];
46+
const watcher = fs.watch(root, { recursive: true }, (eventType, filename) => {
47+
events.push({ eventType, filename });
48+
});
49+
50+
// Allow the watcher to fully attach to existing entries.
51+
await setTimeout(common.platformTimeout(200));
52+
53+
fs.rmdirSync(path.join(root, 'foo'));
54+
fs.rmSync(path.join(root, 'foo2'), { recursive: true });
55+
56+
// Wait long enough to capture any spurious follow-up events.
57+
await setTimeout(common.platformTimeout(500));
58+
59+
watcher.close();
60+
61+
const isSibling = (f) =>
62+
f === 'foo_' || f === 'foo_bar' ||
63+
f.startsWith('foo_bar' + path.sep);
64+
const spurious = events.filter((e) => isSibling(e.filename));
65+
assert.deepStrictEqual(
66+
spurious,
67+
[],
68+
`unexpected events for prefix-sibling entries: ${JSON.stringify(spurious)}`,
69+
);
70+
})().then(common.mustCall());

0 commit comments

Comments
 (0)