Skip to content

Commit 0c106f0

Browse files
committed
fs: add recursive watch to linux
1 parent fdadea8 commit 0c106f0

File tree

4 files changed

+172
-19
lines changed

4 files changed

+172
-19
lines changed

doc/api/fs.md

-4
Original file line numberDiff line numberDiff line change
@@ -4377,10 +4377,6 @@ the returned {fs.FSWatcher}.
43774377
The `fs.watch` API is not 100% consistent across platforms, and is
43784378
unavailable in some situations.
43794379
4380-
The recursive option is only supported on macOS and Windows.
4381-
An `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` exception will be thrown
4382-
when the option is used on a platform that does not support it.
4383-
43844380
On Windows, no events will be emitted if the watched directory is moved or
43854381
renamed. An `EPERM` error is reported when the watched directory is deleted.
43864382

lib/fs.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const {
5757

5858
const pathModule = require('path');
5959
const { isArrayBufferView } = require('internal/util/types');
60+
const linuxWatcher = require('internal/fs/linux_recursive_watcher');
6061

6162
// We need to get the statValues from the binding at the callsite since
6263
// it's re-initialized after deserialization.
@@ -68,7 +69,6 @@ const {
6869
codes: {
6970
ERR_FS_FILE_TOO_LARGE,
7071
ERR_INVALID_ARG_VALUE,
71-
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
7272
},
7373
AbortError,
7474
uvErrmapGet,
@@ -161,7 +161,7 @@ let FileReadStream;
161161
let FileWriteStream;
162162

163163
const isWindows = process.platform === 'win32';
164-
const isOSX = process.platform === 'darwin';
164+
const isLinux = process.platform === 'linux';
165165

166166

167167
function showTruncateDeprecation() {
@@ -2297,13 +2297,22 @@ function watch(filename, options, listener) {
22972297

22982298
if (options.persistent === undefined) options.persistent = true;
22992299
if (options.recursive === undefined) options.recursive = false;
2300-
if (options.recursive && !(isOSX || isWindows))
2301-
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('watch recursively');
2302-
const watcher = new watchers.FSWatcher();
2303-
watcher[watchers.kFSWatchStart](filename,
2304-
options.persistent,
2305-
options.recursive,
2306-
options.encoding);
2300+
2301+
let watcher;
2302+
2303+
// TODO(anonrig): Remove this when/if libuv supports it.
2304+
// libuv does not support recursive file watch on Linux due to
2305+
// the limitations of inotify.
2306+
if (options.recursive && isLinux) {
2307+
watcher = new linuxWatcher.FSWatcher(options);
2308+
watcher[linuxWatcher.kFSWatchStart](filename);
2309+
} else {
2310+
watcher = new watchers.FSWatcher();
2311+
watcher[watchers.kFSWatchStart](filename,
2312+
options.persistent,
2313+
options.recursive,
2314+
options.encoding);
2315+
}
23072316

23082317
if (listener) {
23092318
watcher.addListener('change', listener);
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use strict';
2+
3+
const { EventEmitter } = require('events');
4+
const path = require('path');
5+
const { Symbol, ObjectKeys } = primordials;
6+
7+
const kFSWatchStart = Symbol('kFSWatchStart');
8+
9+
let internalSync;
10+
let internalPromises;
11+
12+
function lazyLoadFsPromises() {
13+
internalPromises ??= require('fs/promises');
14+
return internalPromises;
15+
}
16+
17+
function lazyLoadFsSync() {
18+
internalSync ??= require('fs');
19+
return internalSync;
20+
}
21+
22+
async function traverse(dir, files = {}) {
23+
const { stat, readdir } = lazyLoadFsPromises();
24+
25+
files[dir] = await stat(dir);
26+
27+
try {
28+
const directoryFiles = await readdir(dir);
29+
30+
for (const file of directoryFiles) {
31+
const f = path.join(dir, file);
32+
33+
try {
34+
const stats = await stat(f);
35+
36+
files[f] = stats;
37+
38+
if (stats.isDirectory()) {
39+
await traverse(f, files);
40+
}
41+
} catch (error) {
42+
if (error.code !== 'ENOENT' || error.code !== 'EPERM') {
43+
throw error;
44+
}
45+
}
46+
47+
}
48+
} catch (error) {
49+
if (error.code !== 'EACCES') {
50+
throw error;
51+
}
52+
}
53+
54+
return files;
55+
}
56+
57+
class FSWatcher extends EventEmitter {
58+
#options = null;
59+
#closed = false;
60+
#files = {};
61+
62+
/**
63+
* @param {{
64+
* persistent?: boolean;
65+
* recursive?: boolean;
66+
* encoding?: string;
67+
* signal?: AbortSignal;
68+
* }} [options]
69+
*/
70+
constructor(options) {
71+
super();
72+
73+
this.#options = options || {};
74+
}
75+
76+
async close() {
77+
const { unwatchFile } = lazyLoadFsPromises();
78+
this.#closed = true;
79+
80+
for (const file of ObjectKeys(this.#files)) {
81+
await unwatchFile(file);
82+
}
83+
84+
this.emit('close');
85+
}
86+
87+
/**
88+
* @param {string} file
89+
*/
90+
#watchFile(file) {
91+
const { readdir } = lazyLoadFsPromises();
92+
const { stat, watchFile } = lazyLoadFsSync();
93+
94+
watchFile(file, this.#options, (event, payload) => {
95+
const existingStat = this.#files[file];
96+
97+
if (existingStat && !existingStat.isDirectory() &&
98+
event.nlink !== 0 && existingStat.mtime.getTime() === event.mtime.getTime()) {
99+
return;
100+
}
101+
102+
this.#files[file] = event;
103+
104+
if (!event.isDirectory()) {
105+
this.emit(event, payload);
106+
} else {
107+
readdir(file)
108+
.then((files) => {
109+
for (const subfile of files) {
110+
const f = path.join(file, subfile);
111+
112+
if (!this.#files[f]) {
113+
stat(f, (error, stat) => {
114+
if (error) {
115+
return;
116+
}
117+
118+
this.#files[f] = stat;
119+
this.#watchFile(f);
120+
});
121+
}
122+
}
123+
});
124+
}
125+
});
126+
}
127+
128+
/**
129+
* @param {string | Buffer | URL} filename
130+
*/
131+
async [kFSWatchStart](filename) {
132+
this.#closed = false;
133+
this.#files = await traverse(filename);
134+
135+
this.#watchFile(filename);
136+
137+
for (const f in this.#files) {
138+
this.#watchFile(f);
139+
}
140+
}
141+
142+
/**
143+
* @param {string} name
144+
* @param {Function=} callback
145+
*/
146+
addEventListener(name, callback) {
147+
this.on(name, (...args) => callback(...args));
148+
}
149+
}
150+
151+
module.exports = {
152+
FSWatcher,
153+
kFSWatchStart,
154+
};

test/parallel/test-fs-watch-recursive.js

-6
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ tmpdir.refresh();
1717
const testsubdir = fs.mkdtempSync(testDir + path.sep);
1818
const relativePathOne = path.join(path.basename(testsubdir), filenameOne);
1919
const filepathOne = path.join(testsubdir, filenameOne);
20-
21-
if (!common.isOSX && !common.isWindows) {
22-
assert.throws(() => { fs.watch(testDir, { recursive: true }); },
23-
{ code: 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM' });
24-
return;
25-
}
2620
const watcher = fs.watch(testDir, { recursive: true });
2721

2822
let watcherClosed = false;

0 commit comments

Comments
 (0)