Skip to content

Commit 15a7f21

Browse files
mcollinatargos
authored andcommitted
fs: remove race condition for recursive watch on Linux
Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #51406 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
1 parent d8e1058 commit 15a7f21

8 files changed

Lines changed: 182 additions & 198 deletions

‎lib/internal/fs/promises.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@ async function* _watch(filename, options = kEmptyObject) {
12551255
// e.g. Linux due to the limitations of inotify.
12561256
if(options.recursive&&!isOSX&&!isWindows){
12571257
constwatcher=newnonNativeWatcher.FSWatcher(options);
1258-
awaitwatcher[kFSWatchStart](filename);
1258+
watcher[kFSWatchStart](filename);
12591259
yield*watcher;
12601260
return;
12611261
}

‎lib/internal/fs/recursive_watch.js‎

Lines changed: 44 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
'use strict';
22

33
const{
4-
ArrayPrototypePush,
5-
SafePromiseAllReturnVoid,
64
Promise,
7-
PromisePrototypeThen,
85
SafeMap,
96
SafeSet,
107
StringPrototypeStartsWith,
@@ -31,47 +28,19 @@ const {
3128
}=require('path');
3229

3330
letinternalSync;
34-
letinternalPromises;
35-
36-
functionlazyLoadFsPromises(){
37-
internalPromises??=require('fs/promises');
38-
returninternalPromises;
39-
}
4031

4132
functionlazyLoadFsSync(){
4233
internalSync??=require('fs');
4334
returninternalSync;
4435
}
45-
letkResistStopPropagation;
46-
47-
asyncfunctiontraverse(dir,files=newSafeMap(),symbolicLinks=newSafeSet()){
48-
const{ opendir }=lazyLoadFsPromises();
49-
50-
constfilenames=awaitopendir(dir);
51-
constsubdirectories=[];
52-
53-
forawait(constfileoffilenames){
54-
constf=pathJoin(dir,file.name);
55-
56-
files.set(f,file);
57-
58-
// Do not follow symbolic links
59-
if(file.isSymbolicLink()){
60-
symbolicLinks.add(f);
61-
}elseif(file.isDirectory()){
62-
ArrayPrototypePush(subdirectories,traverse(f,files));
63-
}
64-
}
65-
66-
awaitSafePromiseAllReturnVoid(subdirectories);
6736

68-
returnfiles;
69-
}
37+
letkResistStopPropagation;
7038

7139
classFSWatcherextendsEventEmitter{
7240
#options =null;
7341
#closed =false;
7442
#files =newSafeMap();
43+
#watchers =newSafeMap();
7544
#symbolicFiles =newSafeSet();
7645
#rootPath =pathResolve();
7746
#watchingFile =false;
@@ -111,11 +80,11 @@ class FSWatcher extends EventEmitter {
11180
return;
11281
}
11382

114-
const{ unwatchFile }=lazyLoadFsSync();
11583
this.#closed =true;
11684

11785
for(constfileofthis.#files.keys()){
118-
unwatchFile(file);
86+
this.#watchers.get(file).close();
87+
this.#watchers.delete(file);
11988
}
12089

12190
this.#files.clear();
@@ -124,24 +93,26 @@ class FSWatcher extends EventEmitter {
12493
}
12594

12695
#unwatchFiles(file){
127-
const{ unwatchFile }=lazyLoadFsSync();
128-
12996
this.#symbolicFiles.delete(file);
13097

13198
for(constfilenameofthis.#files.keys()){
13299
if(StringPrototypeStartsWith(filename,file)){
133-
unwatchFile(filename);
100+
this.#files.delete(filename);
101+
this.#watchers.get(filename).close();
102+
this.#watchers.delete(filename);
134103
}
135104
}
136105
}
137106

138-
async#watchFolder(folder){
139-
const{opendir}=lazyLoadFsPromises();
107+
#watchFolder(folder){
108+
const{readdirSync}=lazyLoadFsSync();
140109

141110
try{
142-
constfiles=awaitopendir(folder);
111+
constfiles=readdirSync(folder,{
112+
withFileTypes: true,
113+
});
143114

144-
forawait(constfileoffiles){
115+
for(constfileoffiles){
145116
if(this.#closed){
146117
break;
147118
}
@@ -155,11 +126,9 @@ class FSWatcher extends EventEmitter {
155126
this.#symbolicFiles.add(f);
156127
}
157128

158-
this.#files.set(f,file);
159-
if(file.isFile()){
160-
this.#watchFile(f);
161-
}elseif(file.isDirectory()&&!file.isSymbolicLink()){
162-
awaitthis.#watchFolder(f);
129+
this.#watchFile(f);
130+
if(file.isDirectory()&&!file.isSymbolicLink()){
131+
this.#watchFolder(f);
163132
}
164133
}
165134
}
@@ -173,22 +142,30 @@ class FSWatcher extends EventEmitter {
173142
return;
174143
}
175144

176-
const{ watchFile }=lazyLoadFsSync();
177-
constexistingStat=this.#files.get(file);
145+
const{ watch, statSync }=lazyLoadFsSync();
146+
147+
if(this.#files.has(file)){
148+
return;
149+
}
150+
151+
{
152+
constexistingStat=statSync(file);
153+
this.#files.set(file,existingStat);
154+
}
178155

179-
watchFile(file,{
156+
constwatcher=watch(file,{
180157
persistent: this.#options.persistent,
181-
},(currentStats,previousStats)=>{
182-
if(existingStat&&!existingStat.isDirectory()&&
183-
currentStats.nlink!==0&&existingStat.mtimeMs===currentStats.mtimeMs){
184-
return;
185-
}
158+
},(eventType,filename)=>{
159+
constexistingStat=this.#files.get(file);
160+
constcurrentStats=statSync(file);
186161

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

189-
if(currentStats.birthtimeMs===0&&previousStats.birthtimeMs!==0){
164+
if(currentStats.birthtimeMs===0&&existingStat.birthtimeMs!==0){
190165
// The file is now deleted
191166
this.#files.delete(file);
167+
this.#watchers.delete(file);
168+
watcher.close();
192169
this.emit('change','rename',pathRelative(this.#rootPath,file));
193170
this.#unwatchFiles(file);
194171
}elseif(file===this.#rootPath &&this.#watchingFile){
@@ -205,6 +182,7 @@ class FSWatcher extends EventEmitter {
205182
this.emit('change','change',pathRelative(this.#rootPath,file));
206183
}
207184
});
185+
this.#watchers.set(file,watcher);
208186
}
209187

210188
[kFSWatchStart](filename){
@@ -217,19 +195,9 @@ class FSWatcher extends EventEmitter {
217195
this.#closed =false;
218196
this.#watchingFile =file.isFile();
219197

198+
this.#watchFile(filename);
220199
if(file.isDirectory()){
221-
this.#files.set(filename,file);
222-
223-
PromisePrototypeThen(
224-
traverse(filename,this.#files,this.#symbolicFiles),
225-
()=>{
226-
for(constfofthis.#files.keys()){
227-
this.#watchFile(f);
228-
}
229-
},
230-
);
231-
}else{
232-
this.#watchFile(filename);
200+
this.#watchFolder(filename);
233201
}
234202
}catch(error){
235203
if(error.code==='ENOENT'){
@@ -264,7 +232,10 @@ class FSWatcher extends EventEmitter {
264232
resolve({__proto__: null,value: { eventType, filename }});
265233
});
266234
} : (resolve,reject)=>{
267-
constonAbort=()=>reject(newAbortError(undefined,{cause: signal.reason}));
235+
constonAbort=()=>{
236+
this.close();
237+
reject(newAbortError(undefined,{cause: signal.reason}));
238+
};
268239
if(signal.aborted)returnonAbort();
269240
kResistStopPropagation??=require('internal/event_target').kResistStopPropagation;
270241
signal.addEventListener('abort',onAbort,{__proto__: null,once: true,[kResistStopPropagation]: true});
@@ -277,6 +248,10 @@ class FSWatcher extends EventEmitter {
277248
next: ()=>(this.#closed ?
278249
{__proto__: null,done: true} :
279250
newPromise(promiseExecutor)),
251+
return: ()=>{
252+
this.close();
253+
return{__proto__: null,done: true};
254+
},
280255
[SymbolAsyncIterator](){returnthis;},
281256
};
282257
}
Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
constcommon=require('../common');
4-
const{ setTimeout }=require('timers/promises');
54

65
if(common.isIBMi)
76
common.skip('IBMi does not support `fs.watch()`');
@@ -21,39 +20,36 @@ const tmpdir = require('../common/tmpdir');
2120
consttestDir=tmpdir.path;
2221
tmpdir.refresh();
2322

24-
(async()=>{
25-
// Add a file to subfolder of a watching folder
23+
// Add a file to subfolder of a watching folder
2624

27-
constrootDirectory=fs.mkdtempSync(testDir+path.sep);
28-
consttestDirectory=path.join(rootDirectory,'test-4');
29-
fs.mkdirSync(testDirectory);
25+
constrootDirectory=fs.mkdtempSync(testDir+path.sep);
26+
consttestDirectory=path.join(rootDirectory,'test-4');
27+
fs.mkdirSync(testDirectory);
3028

31-
constfile='folder-5';
32-
constfilePath=path.join(testDirectory,file);
33-
fs.mkdirSync(filePath);
29+
constfile='folder-5';
30+
constfilePath=path.join(testDirectory,file);
31+
fs.mkdirSync(filePath);
3432

35-
constsubfolderPath=path.join(filePath,'subfolder-6');
36-
fs.mkdirSync(subfolderPath);
33+
constsubfolderPath=path.join(filePath,'subfolder-6');
34+
fs.mkdirSync(subfolderPath);
3735

38-
constchildrenFile='file-7.txt';
39-
constchildrenAbsolutePath=path.join(subfolderPath,childrenFile);
40-
constrelativePath=path.join(file,path.basename(subfolderPath),childrenFile);
36+
constchildrenFile='file-7.txt';
37+
constchildrenAbsolutePath=path.join(subfolderPath,childrenFile);
38+
constrelativePath=path.join(file,path.basename(subfolderPath),childrenFile);
4139

42-
constwatcher=fs.watch(testDirectory,{recursive: true});
43-
letwatcherClosed=false;
44-
watcher.on('change',function(event,filename){
45-
assert.strictEqual(event,'rename');
40+
constwatcher=fs.watch(testDirectory,{recursive: true});
41+
letwatcherClosed=false;
42+
watcher.on('change',function(event,filename){
43+
assert.strictEqual(event,'rename');
4644

47-
if(filename===relativePath){
48-
watcher.close();
49-
watcherClosed=true;
50-
}
51-
});
45+
if(filename===relativePath){
46+
watcher.close();
47+
watcherClosed=true;
48+
}
49+
});
5250

53-
awaitsetTimeout(common.platformTimeout(100));
54-
fs.writeFileSync(childrenAbsolutePath,'world');
51+
fs.writeFileSync(childrenAbsolutePath,'world');
5552

56-
process.once('exit',function(){
57-
assert(watcherClosed,'watcher Object was not closed');
58-
});
59-
})().then(common.mustCall());
53+
process.once('exit',function(){
54+
assert(watcherClosed,'watcher Object was not closed');
55+
});
Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
constcommon=require('../common');
4-
const{ setTimeout }=require('timers/promises');
54

65
if(common.isIBMi)
76
common.skip('IBMi does not support `fs.watch()`');
@@ -21,37 +20,33 @@ const tmpdir = require('../common/tmpdir');
2120
consttestDir=tmpdir.path;
2221
tmpdir.refresh();
2322

24-
(async()=>{
25-
// Add a file to newly created folder to already watching folder
23+
// Add a file to newly created folder to already watching folder
2624

27-
constrootDirectory=fs.mkdtempSync(testDir+path.sep);
28-
consttestDirectory=path.join(rootDirectory,'test-3');
29-
fs.mkdirSync(testDirectory);
25+
constrootDirectory=fs.mkdtempSync(testDir+path.sep);
26+
consttestDirectory=path.join(rootDirectory,'test-3');
27+
fs.mkdirSync(testDirectory);
3028

31-
constfilePath=path.join(testDirectory,'folder-3');
29+
constfilePath=path.join(testDirectory,'folder-3');
3230

33-
constchildrenFile='file-4.txt';
34-
constchildrenAbsolutePath=path.join(filePath,childrenFile);
35-
constchildrenRelativePath=path.join(path.basename(filePath),childrenFile);
31+
constchildrenFile='file-4.txt';
32+
constchildrenAbsolutePath=path.join(filePath,childrenFile);
33+
constchildrenRelativePath=path.join(path.basename(filePath),childrenFile);
3634

37-
constwatcher=fs.watch(testDirectory,{recursive: true});
38-
letwatcherClosed=false;
39-
watcher.on('change',function(event,filename){
40-
assert.strictEqual(event,'rename');
41-
assert.ok(filename===path.basename(filePath)||filename===childrenRelativePath);
35+
constwatcher=fs.watch(testDirectory,{recursive: true});
36+
letwatcherClosed=false;
37+
watcher.on('change',function(event,filename){
38+
assert.strictEqual(event,'rename');
39+
assert.ok(filename===path.basename(filePath)||filename===childrenRelativePath);
4240

43-
if(filename===childrenRelativePath){
44-
watcher.close();
45-
watcherClosed=true;
46-
}
47-
});
41+
if(filename===childrenRelativePath){
42+
watcher.close();
43+
watcherClosed=true;
44+
}
45+
});
4846

49-
awaitsetTimeout(common.platformTimeout(100));
50-
fs.mkdirSync(filePath);
51-
awaitsetTimeout(common.platformTimeout(100));
52-
fs.writeFileSync(childrenAbsolutePath,'world');
47+
fs.mkdirSync(filePath);
48+
fs.writeFileSync(childrenAbsolutePath,'world');
5349

54-
process.once('exit',function(){
55-
assert(watcherClosed,'watcher Object was not closed');
56-
});
57-
})().then(common.mustCall());
50+
process.once('exit',function(){
51+
assert(watcherClosed,'watcher Object was not closed');
52+
});

0 commit comments

Comments
 (0)