Skip to content

Commit c2f0377

Browse files
anonrigdanielleadams
authored andcommitted
fs: update todo message
PR-URL: #45265 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]> Reviewed-By: Moshe Atlow <[email protected]>
1 parent 03a3f30 commit c2f0377

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

lib/internal/fs/recursive_watch.js

+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
'use strict';
2+
3+
const {
4+
ArrayPrototypePush,
5+
SafePromiseAllReturnVoid,
6+
Promise,
7+
PromisePrototypeThen,
8+
SafeMap,
9+
SafeSet,
10+
StringPrototypeStartsWith,
11+
SymbolAsyncIterator,
12+
} = primordials;
13+
14+
const { EventEmitter } = require('events');
15+
const assert = require('internal/assert');
16+
const {
17+
AbortError,
18+
codes: {
19+
ERR_INVALID_ARG_VALUE,
20+
},
21+
} = require('internal/errors');
22+
const { getValidatedPath } = require('internal/fs/utils');
23+
const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
24+
const { kEmptyObject } = require('internal/util');
25+
const { validateBoolean, validateAbortSignal } = require('internal/validators');
26+
const {
27+
basename: pathBasename,
28+
join: pathJoin,
29+
relative: pathRelative,
30+
resolve: pathResolve,
31+
} = require('path');
32+
33+
let internalSync;
34+
let internalPromises;
35+
36+
function lazyLoadFsPromises() {
37+
internalPromises ??= require('fs/promises');
38+
return internalPromises;
39+
}
40+
41+
function lazyLoadFsSync() {
42+
internalSync ??= require('fs');
43+
return internalSync;
44+
}
45+
46+
async function traverse(dir, files = new SafeMap(), symbolicLinks = new SafeSet()) {
47+
const { opendir } = lazyLoadFsPromises();
48+
49+
const filenames = await opendir(dir);
50+
const subdirectories = [];
51+
52+
for await (const file of filenames) {
53+
const f = pathJoin(dir, file.name);
54+
55+
files.set(f, file);
56+
57+
// Do not follow symbolic links
58+
if (file.isSymbolicLink()) {
59+
symbolicLinks.add(f);
60+
} else if (file.isDirectory()) {
61+
ArrayPrototypePush(subdirectories, traverse(f, files));
62+
}
63+
}
64+
65+
await SafePromiseAllReturnVoid(subdirectories);
66+
67+
return files;
68+
}
69+
70+
class FSWatcher extends EventEmitter {
71+
#options = null;
72+
#closed = false;
73+
#files = new SafeMap();
74+
#symbolicFiles = new SafeSet();
75+
#rootPath = pathResolve();
76+
#watchingFile = false;
77+
78+
constructor(options = kEmptyObject) {
79+
super();
80+
81+
assert(typeof options === 'object');
82+
83+
const { persistent, recursive, signal, encoding } = options;
84+
85+
// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
86+
if (recursive != null) {
87+
validateBoolean(recursive, 'options.recursive');
88+
}
89+
90+
if (persistent != null) {
91+
validateBoolean(persistent, 'options.persistent');
92+
}
93+
94+
if (signal != null) {
95+
validateAbortSignal(signal, 'options.signal');
96+
}
97+
98+
if (encoding != null) {
99+
// This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE
100+
if (typeof encoding !== 'string') {
101+
throw new ERR_INVALID_ARG_VALUE(encoding, 'options.encoding');
102+
}
103+
}
104+
105+
this.#options = { persistent, recursive, signal, encoding };
106+
}
107+
108+
close() {
109+
if (this.#closed) {
110+
return;
111+
}
112+
113+
const { unwatchFile } = lazyLoadFsSync();
114+
this.#closed = true;
115+
116+
for (const file of this.#files.keys()) {
117+
unwatchFile(file);
118+
}
119+
120+
this.#files.clear();
121+
this.#symbolicFiles.clear();
122+
this.emit('close');
123+
}
124+
125+
#unwatchFiles(file) {
126+
const { unwatchFile } = lazyLoadFsSync();
127+
128+
this.#symbolicFiles.delete(file);
129+
130+
for (const filename of this.#files.keys()) {
131+
if (StringPrototypeStartsWith(filename, file)) {
132+
unwatchFile(filename);
133+
}
134+
}
135+
}
136+
137+
async #watchFolder(folder) {
138+
const { opendir } = lazyLoadFsPromises();
139+
140+
try {
141+
const files = await opendir(folder);
142+
143+
for await (const file of files) {
144+
if (this.#closed) {
145+
break;
146+
}
147+
148+
const f = pathJoin(folder, file.name);
149+
150+
if (!this.#files.has(f)) {
151+
this.emit('change', 'rename', pathRelative(this.#rootPath, f));
152+
153+
if (file.isSymbolicLink()) {
154+
this.#symbolicFiles.add(f);
155+
}
156+
157+
if (file.isFile()) {
158+
this.#watchFile(f);
159+
} else {
160+
this.#files.set(f, file);
161+
162+
if (file.isDirectory() && !file.isSymbolicLink()) {
163+
await this.#watchFolder(f);
164+
}
165+
}
166+
}
167+
}
168+
} catch (error) {
169+
this.emit('error', error);
170+
}
171+
}
172+
173+
#watchFile(file) {
174+
if (this.#closed) {
175+
return;
176+
}
177+
178+
const { watchFile } = lazyLoadFsSync();
179+
const existingStat = this.#files.get(file);
180+
181+
watchFile(file, {
182+
persistent: this.#options.persistent,
183+
}, (currentStats, previousStats) => {
184+
if (existingStat && !existingStat.isDirectory() &&
185+
currentStats.nlink !== 0 && existingStat.mtimeMs === currentStats.mtimeMs) {
186+
return;
187+
}
188+
189+
this.#files.set(file, currentStats);
190+
191+
if (currentStats.birthtimeMs === 0 && previousStats.birthtimeMs !== 0) {
192+
// The file is now deleted
193+
this.#files.delete(file);
194+
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
195+
this.#unwatchFiles(file);
196+
} else if (file === this.#rootPath && this.#watchingFile) {
197+
// This case will only be triggered when watching a file with fs.watch
198+
this.emit('change', 'change', pathBasename(file));
199+
} else if (this.#symbolicFiles.has(file)) {
200+
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
201+
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
202+
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
203+
} else if (currentStats.isDirectory()) {
204+
this.#watchFolder(file);
205+
}
206+
});
207+
}
208+
209+
[kFSWatchStart](filename) {
210+
filename = pathResolve(getValidatedPath(filename));
211+
212+
try {
213+
const file = lazyLoadFsSync().statSync(filename);
214+
215+
this.#rootPath = filename;
216+
this.#closed = false;
217+
this.#watchingFile = file.isFile();
218+
219+
if (file.isDirectory()) {
220+
this.#files.set(filename, file);
221+
222+
PromisePrototypeThen(
223+
traverse(filename, this.#files, this.#symbolicFiles),
224+
() => {
225+
for (const f of this.#files.keys()) {
226+
this.#watchFile(f);
227+
}
228+
},
229+
);
230+
} else {
231+
this.#watchFile(filename);
232+
}
233+
} catch (error) {
234+
if (error.code === 'ENOENT') {
235+
error.filename = filename;
236+
throw error;
237+
}
238+
}
239+
240+
}
241+
242+
ref() {
243+
this.#files.forEach((file) => {
244+
if (file instanceof StatWatcher) {
245+
file.ref();
246+
}
247+
});
248+
}
249+
250+
unref() {
251+
this.#files.forEach((file) => {
252+
if (file instanceof StatWatcher) {
253+
file.unref();
254+
}
255+
});
256+
}
257+
258+
[SymbolAsyncIterator]() {
259+
const { signal } = this.#options;
260+
const promiseExecutor = signal == null ?
261+
(resolve) => {
262+
this.once('change', (eventType, filename) => {
263+
resolve({ __proto__: null, value: { eventType, filename } });
264+
});
265+
} : (resolve, reject) => {
266+
const onAbort = () => reject(new AbortError(undefined, { cause: signal.reason }));
267+
if (signal.aborted) return onAbort();
268+
signal.addEventListener('abort', onAbort, { __proto__: null, once: true });
269+
this.once('change', (eventType, filename) => {
270+
signal.removeEventListener('abort', onAbort);
271+
resolve({ __proto__: null, value: { eventType, filename } });
272+
});
273+
};
274+
return {
275+
next: () => (this.#closed ?
276+
{ __proto__: null, done: true } :
277+
new Promise(promiseExecutor)),
278+
[SymbolAsyncIterator]() { return this; },
279+
};
280+
}
281+
}
282+
283+
module.exports = {
284+
FSWatcher,
285+
kFSWatchStart,
286+
};

0 commit comments

Comments
 (0)