@@ -17,7 +17,7 @@ const {
1717 } ,
1818} = require ( 'internal/errors' ) ;
1919const { getValidatedPath } = require ( 'internal/fs/utils' ) ;
20- const { createIgnoreMatcher, kFSWatchStart, StatWatcher } = require ( 'internal/fs/watchers' ) ;
20+ const { createIgnoreMatcher, kFSWatchStart } = require ( 'internal/fs/watchers' ) ;
2121const { kEmptyObject } = require ( 'internal/util' ) ;
2222const { validateBoolean, validateAbortSignal, validateIgnoreOption } = require ( 'internal/validators' ) ;
2323const {
@@ -37,14 +37,22 @@ function lazyLoadFsSync() {
3737
3838let kResistStopPropagation ;
3939
40+ // Inotify reports changes to a directory's entries, with their names, on the
41+ // directory's own watch, so one watcher per directory is enough on Linux.
42+ // kqueue and event ports only report that the directory itself changed, so
43+ // elsewhere every file keeps a watcher of its own as well.
44+ const kDirectoryWatchReportsEntries = process . platform === 'linux' ;
45+
4046class FSWatcher extends EventEmitter {
4147 #options = null ;
4248 #closed = false ;
43- #files = new SafeMap ( ) ;
49+ // Every path below the root that has been reported (or existed at start).
50+ #entries = new SafeSet ( ) ;
51+ // One fs.watch() per directory and symbolic link (and per file where the
52+ // directory watch does not report its entries).
4453 #watchers = new SafeMap ( ) ;
45- #symbolicFiles = new SafeSet ( ) ;
54+ #symbolicLinks = new SafeSet ( ) ;
4655 #rootPath = pathResolve ( ) ;
47- #watchingFile = false ;
4856 #ignoreMatcher = null ;
4957
5058 constructor ( options = kEmptyObject ) {
@@ -94,129 +102,168 @@ class FSWatcher extends EventEmitter {
94102
95103 this . #closed = true ;
96104
97- for ( const file of this . #files. keys ( ) ) {
98- this . #watchers. get ( file ) ?. close ( ) ;
99- this . #watchers. delete ( file ) ;
105+ for ( const watcher of this . #watchers. values ( ) ) {
106+ watcher . close ( ) ;
100107 }
101-
102- this . #files . clear ( ) ;
103- this . #symbolicFiles . clear ( ) ;
108+ this . #watchers . clear ( ) ;
109+ this . #entries . clear ( ) ;
110+ this . #symbolicLinks . clear ( ) ;
104111 this . emit ( 'close' ) ;
105112 }
106113
107- #unwatchFiles( file ) {
108- this . #symbolicFiles. delete ( file ) ;
114+ #emit( eventType , file ) {
115+ this . emit ( 'change' , eventType , pathRelative ( this . #rootPath, file ) ) ;
116+ }
109117
118+ #forget( file ) {
110119 const childPrefix = file + pathSep ;
111- for ( const filename of this . #files. keys ( ) ) {
112- if ( filename === file ||
113- StringPrototypeStartsWith ( filename , childPrefix ) ) {
114- this . #files. delete ( filename ) ;
115- this . #watchers. get ( filename ) ?. close ( ) ;
116- this . #watchers. delete ( filename ) ;
120+ for ( const entry of this . #entries) {
121+ if ( entry === file || StringPrototypeStartsWith ( entry , childPrefix ) ) {
122+ this . #entries. delete ( entry ) ;
123+ this . #symbolicLinks. delete ( entry ) ;
124+ const watcher = this . #watchers. get ( entry ) ;
125+ if ( watcher !== undefined ) {
126+ watcher . close ( ) ;
127+ this . #watchers. delete ( entry ) ;
128+ }
117129 }
118130 }
119131 }
120132
121- #watchFolder( folder ) {
122- const { readdirSync } = lazyLoadFsSync ( ) ;
123-
133+ // An entry that vanished between being listed and being watched is left to
134+ // the directory's own watcher to report.
135+ #watch( file , onChange ) {
136+ if ( this . #closed || this . #watchers. has ( file ) ) {
137+ return ;
138+ }
139+ const { watch } = lazyLoadFsSync ( ) ;
140+ let watcher ;
124141 try {
125- const files = readdirSync ( folder , {
126- withFileTypes : true ,
127- } ) ;
128-
129- for ( const file of files ) {
130- if ( this . #closed) {
131- break ;
132- }
133-
134- const f = pathJoin ( folder , file . name ) ;
135- const relativePath = pathRelative ( this . #rootPath, f ) ;
136-
137- // Skip watching ignored paths entirely to avoid kernel resource pressure
138- if ( this . #ignoreMatcher?. ( relativePath ) ) {
139- continue ;
140- }
141-
142- if ( ! this . #files. has ( f ) ) {
143- this . emit ( 'change' , 'rename' , relativePath ) ;
144-
145- if ( file . isSymbolicLink ( ) ) {
146- this . #symbolicFiles. add ( f ) ;
147- }
148-
149- try {
150- this . #watchFile( f ) ;
151- if ( file . isDirectory ( ) && ! file . isSymbolicLink ( ) ) {
152- this . #watchFolder( f ) ;
153- }
154- } catch ( err ) {
155- // Ignore ENOENT
156- if ( err . code !== 'ENOENT' ) {
157- throw err ;
158- }
159- }
160- }
142+ watcher = watch ( file , { persistent : this . #options. persistent } , onChange ) ;
143+ } catch ( err ) {
144+ if ( err . code === 'ENOENT' ) {
145+ return ;
161146 }
147+ throw err ;
148+ }
149+ this . #watchers. set ( file , watcher ) ;
150+ }
151+
152+ // Registers the entries of `folder` that are not known yet (emitting
153+ // 'rename' for them unless this is the initial scan) and arms one watcher
154+ // for the directory; #addEntry() descends into subdirectories.
155+ #scanFolder( folder , initial ) {
156+ const { readdirSync } = lazyLoadFsSync ( ) ;
157+ let entries ;
158+ try {
159+ entries = readdirSync ( folder , { withFileTypes : true } ) ;
162160 } catch ( error ) {
163161 if ( error . code !== 'ENOENT' ) {
164162 this . emit ( 'error' , error ) ;
165163 }
164+ return ;
165+ }
166+
167+ this . #watch( folder , ( eventType , filename ) => this . #onFolderEvent( folder , filename ) ) ;
168+
169+ for ( const entry of entries ) {
170+ if ( this . #closed) {
171+ break ;
172+ }
173+ const file = pathJoin ( folder , entry . name ) ;
174+ if ( ! this . #entries. has ( file ) && ! this . #ignoreMatcher?. ( pathRelative ( this . #rootPath, file ) ) ) {
175+ this . #addEntry( file , entry , initial ) ;
176+ }
177+ }
178+ }
179+
180+ // `entry` is the Dirent or the lstat() Stats of `file`.
181+ #addEntry( file , entry , initial ) {
182+ this . #entries. add ( file ) ;
183+ if ( ! initial ) {
184+ this . #emit( 'rename' , file ) ;
185+ }
186+ if ( entry . isSymbolicLink ( ) ) {
187+ // The link target is watched so that changes behind the link surface
188+ // as a 'rename' of the link, as they always have on this code path.
189+ this . #symbolicLinks. add ( file ) ;
190+ this . #watch( file , ( ) => this . #emit( 'rename' , file ) ) ;
191+ } else if ( entry . isDirectory ( ) ) {
192+ this . #scanFolder( file , initial ) ;
193+ } else if ( ! kDirectoryWatchReportsEntries ) {
194+ this . #watch( file , ( ) => this . #onEntryEvent( file ) ) ;
166195 }
167196 }
168197
169- #watchFile ( file ) {
198+ #onFolderEvent ( folder , filename ) {
170199 if ( this . #closed) {
171200 return ;
172201 }
202+ const { lstatSync, statSync } = lazyLoadFsSync ( ) ;
203+ if ( ! kDirectoryWatchReportsEntries || filename == null ) {
204+ // All that is known is that something about `folder` changed.
205+ if ( statSync ( folder , { throwIfNoEntry : false } ) === undefined ) {
206+ this . #emit( 'rename' , folder ) ;
207+ this . #forget( folder ) ;
208+ } else {
209+ this . #scanFolder( folder , false ) ;
210+ }
211+ return ;
212+ }
213+ // Events about the watched directory itself are reported under its own
214+ // name; those take the "unknown entry" path and are resolved by the parent.
215+ const file = pathJoin ( folder , filename ) ;
173216
174- const { watch, statSync } = lazyLoadFsSync ( ) ;
175-
176- if ( this . #files. has ( file ) ) {
217+ if ( ! this . #entries. has ( file ) ) {
218+ if ( this . #ignoreMatcher?. ( pathRelative ( this . #rootPath, file ) ) ) {
219+ return ;
220+ }
221+ const entry = lstatSync ( file , { throwIfNoEntry : false } ) ;
222+ if ( entry !== undefined ) {
223+ this . #addEntry( file , entry , false ) ;
224+ } else if ( folder === this . #rootPath && statSync ( folder , { throwIfNoEntry : false } ) === undefined ) {
225+ this . #emit( 'rename' , folder ) ;
226+ this . #forget( folder ) ;
227+ }
177228 return ;
178229 }
179230
180- {
181- const existingStat = statSync ( file ) ;
182- this . #files. set ( file , existingStat ) ;
231+ this . #onEntryEvent( file ) ;
232+ }
233+
234+ // Something happened to a known entry: work out what from its current state.
235+ #onEntryEvent( file ) {
236+ if ( this . #closed) {
237+ return ;
183238 }
239+ const { statSync } = lazyLoadFsSync ( ) ;
240+ const stats = statSync ( file , { throwIfNoEntry : false } ) ;
241+ if ( stats === undefined ) {
242+ this . #emit( 'rename' , file ) ;
243+ this . #forget( file ) ;
244+ } else if ( this . #symbolicLinks. has ( file ) ) {
245+ this . #emit( 'rename' , file ) ;
246+ } else if ( stats . isDirectory ( ) ) {
247+ this . #scanFolder( file , false ) ;
248+ } else {
249+ this . #emit( 'change' , file ) ;
250+ }
251+ }
184252
185- const watcher = watch ( file , {
186- persistent : this . #options. persistent ,
187- } , ( eventType , filename ) => {
188- const existingStat = this . #files. get ( file ) ;
189- let currentStats ;
190-
191- try {
192- currentStats = statSync ( file ) ;
193- this . #files. set ( file , currentStats ) ;
194- } catch {
195- // This happens if the file was removed
253+ #watchRootFile( file ) {
254+ const { statSync } = lazyLoadFsSync ( ) ;
255+ this . #entries. add ( file ) ;
256+ this . #watch( file , ( ) => {
257+ if ( this . #closed) {
258+ return ;
196259 }
197-
198- if ( currentStats === undefined || ( currentStats . birthtimeMs === 0 && existingStat . birthtimeMs !== 0 ) ) {
199- // The file is now deleted
200- this . #files. delete ( file ) ;
201- this . #watchers. delete ( file ) ;
202- watcher . close ( ) ;
203- this . emit ( 'change' , 'rename' , pathRelative ( this . #rootPath, file ) ) ;
204- this . #unwatchFiles( file ) ;
205- } else if ( file === this . #rootPath && this . #watchingFile) {
206- // This case will only be triggered when watching a file with fs.watch
207- this . emit ( 'change' , 'change' , pathBasename ( file ) ) ;
208- } else if ( this . #symbolicFiles. has ( file ) ) {
209- // Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
210- // Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
211- this . emit ( 'change' , 'rename' , pathRelative ( this . #rootPath, file ) ) ;
212- } else if ( currentStats . isDirectory ( ) ) {
213- this . #watchFolder( file ) ;
260+ if ( statSync ( file , { throwIfNoEntry : false } ) === undefined ) {
261+ this . #emit( 'rename' , file ) ;
262+ this . #forget( file ) ;
214263 } else {
215- // Watching a directory will trigger a change event for child files)
216- this . emit ( 'change' , 'change' , pathRelative ( this . #rootPath, file ) ) ;
264+ this . emit ( 'change' , 'change' , pathBasename ( file ) ) ;
217265 }
218266 } ) ;
219- this . #watchers. set ( file , watcher ) ;
220267 }
221268
222269 [ kFSWatchStart ] ( filename ) {
@@ -227,11 +274,11 @@ class FSWatcher extends EventEmitter {
227274
228275 this . #rootPath = filename ;
229276 this . #closed = false ;
230- this . #watchingFile = file . isFile ( ) ;
231277
232- this . #watchFile( filename ) ;
233278 if ( file . isDirectory ( ) ) {
234- this . #watchFolder( filename ) ;
279+ this . #scanFolder( filename , true ) ;
280+ } else {
281+ this . #watchRootFile( filename ) ;
235282 }
236283 } catch ( error ) {
237284 if ( this . #options. throwIfNoEntry || error . code !== 'ENOENT' ) {
@@ -244,19 +291,15 @@ class FSWatcher extends EventEmitter {
244291 }
245292
246293 ref ( ) {
247- this . #files. forEach ( ( file ) => {
248- if ( file instanceof StatWatcher ) {
249- file . ref ( ) ;
250- }
251- } ) ;
294+ for ( const watcher of this . #watchers. values ( ) ) {
295+ watcher . ref ( ) ;
296+ }
252297 }
253298
254299 unref ( ) {
255- this . #files. forEach ( ( file ) => {
256- if ( file instanceof StatWatcher ) {
257- file . unref ( ) ;
258- }
259- } ) ;
300+ for ( const watcher of this . #watchers. values ( ) ) {
301+ watcher . unref ( ) ;
302+ }
260303 }
261304
262305 [ SymbolAsyncIterator ] ( ) {
0 commit comments