mirror of
https://github.com/nodejs/node.git
synced 2025-05-03 02:06:12 +00:00

Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: https://github.com/nodejs/node/pull/51406 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
264 lines
6.7 KiB
JavaScript
264 lines
6.7 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
Promise,
|
|
SafeMap,
|
|
SafeSet,
|
|
StringPrototypeStartsWith,
|
|
SymbolAsyncIterator,
|
|
} = primordials;
|
|
|
|
const { EventEmitter } = require('events');
|
|
const assert = require('internal/assert');
|
|
const {
|
|
AbortError,
|
|
codes: {
|
|
ERR_INVALID_ARG_VALUE,
|
|
},
|
|
} = require('internal/errors');
|
|
const { getValidatedPath } = require('internal/fs/utils');
|
|
const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
|
|
const { kEmptyObject } = require('internal/util');
|
|
const { validateBoolean, validateAbortSignal } = require('internal/validators');
|
|
const {
|
|
basename: pathBasename,
|
|
join: pathJoin,
|
|
relative: pathRelative,
|
|
resolve: pathResolve,
|
|
} = require('path');
|
|
|
|
let internalSync;
|
|
|
|
function lazyLoadFsSync() {
|
|
internalSync ??= require('fs');
|
|
return internalSync;
|
|
}
|
|
|
|
let kResistStopPropagation;
|
|
|
|
class FSWatcher extends EventEmitter {
|
|
#options = null;
|
|
#closed = false;
|
|
#files = new SafeMap();
|
|
#watchers = new SafeMap();
|
|
#symbolicFiles = new SafeSet();
|
|
#rootPath = pathResolve();
|
|
#watchingFile = false;
|
|
|
|
constructor(options = kEmptyObject) {
|
|
super();
|
|
|
|
assert(typeof options === 'object');
|
|
|
|
const { persistent, recursive, signal, encoding } = options;
|
|
|
|
// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
|
|
if (recursive != null) {
|
|
validateBoolean(recursive, 'options.recursive');
|
|
}
|
|
|
|
if (persistent != null) {
|
|
validateBoolean(persistent, 'options.persistent');
|
|
}
|
|
|
|
if (signal != null) {
|
|
validateAbortSignal(signal, 'options.signal');
|
|
}
|
|
|
|
if (encoding != null) {
|
|
// This is required since on macOS and Windows it throws ERR_INVALID_ARG_VALUE
|
|
if (typeof encoding !== 'string') {
|
|
throw new ERR_INVALID_ARG_VALUE(encoding, 'options.encoding');
|
|
}
|
|
}
|
|
|
|
this.#options = { persistent, recursive, signal, encoding };
|
|
}
|
|
|
|
close() {
|
|
if (this.#closed) {
|
|
return;
|
|
}
|
|
|
|
this.#closed = true;
|
|
|
|
for (const file of this.#files.keys()) {
|
|
this.#watchers.get(file).close();
|
|
this.#watchers.delete(file);
|
|
}
|
|
|
|
this.#files.clear();
|
|
this.#symbolicFiles.clear();
|
|
this.emit('close');
|
|
}
|
|
|
|
#unwatchFiles(file) {
|
|
this.#symbolicFiles.delete(file);
|
|
|
|
for (const filename of this.#files.keys()) {
|
|
if (StringPrototypeStartsWith(filename, file)) {
|
|
this.#files.delete(filename);
|
|
this.#watchers.get(filename).close();
|
|
this.#watchers.delete(filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
#watchFolder(folder) {
|
|
const { readdirSync } = lazyLoadFsSync();
|
|
|
|
try {
|
|
const files = readdirSync(folder, {
|
|
withFileTypes: true,
|
|
});
|
|
|
|
for (const file of files) {
|
|
if (this.#closed) {
|
|
break;
|
|
}
|
|
|
|
const f = pathJoin(folder, file.name);
|
|
|
|
if (!this.#files.has(f)) {
|
|
this.emit('change', 'rename', pathRelative(this.#rootPath, f));
|
|
|
|
if (file.isSymbolicLink()) {
|
|
this.#symbolicFiles.add(f);
|
|
}
|
|
|
|
this.#watchFile(f);
|
|
if (file.isDirectory() && !file.isSymbolicLink()) {
|
|
this.#watchFolder(f);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.emit('error', error);
|
|
}
|
|
}
|
|
|
|
#watchFile(file) {
|
|
if (this.#closed) {
|
|
return;
|
|
}
|
|
|
|
const { watch, statSync } = lazyLoadFsSync();
|
|
|
|
if (this.#files.has(file)) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
const existingStat = statSync(file);
|
|
this.#files.set(file, existingStat);
|
|
}
|
|
|
|
const watcher = watch(file, {
|
|
persistent: this.#options.persistent,
|
|
}, (eventType, filename) => {
|
|
const existingStat = this.#files.get(file);
|
|
const currentStats = statSync(file);
|
|
|
|
this.#files.set(file, currentStats);
|
|
|
|
if (currentStats.birthtimeMs === 0 && existingStat.birthtimeMs !== 0) {
|
|
// The file is now deleted
|
|
this.#files.delete(file);
|
|
this.#watchers.delete(file);
|
|
watcher.close();
|
|
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
|
|
this.#unwatchFiles(file);
|
|
} else if (file === this.#rootPath && this.#watchingFile) {
|
|
// This case will only be triggered when watching a file with fs.watch
|
|
this.emit('change', 'change', pathBasename(file));
|
|
} else if (this.#symbolicFiles.has(file)) {
|
|
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
|
|
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
|
|
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
|
|
} else if (currentStats.isDirectory()) {
|
|
this.#watchFolder(file);
|
|
} else {
|
|
// Watching a directory will trigger a change event for child files)
|
|
this.emit('change', 'change', pathRelative(this.#rootPath, file));
|
|
}
|
|
});
|
|
this.#watchers.set(file, watcher);
|
|
}
|
|
|
|
[kFSWatchStart](filename) {
|
|
filename = pathResolve(getValidatedPath(filename));
|
|
|
|
try {
|
|
const file = lazyLoadFsSync().statSync(filename);
|
|
|
|
this.#rootPath = filename;
|
|
this.#closed = false;
|
|
this.#watchingFile = file.isFile();
|
|
|
|
this.#watchFile(filename);
|
|
if (file.isDirectory()) {
|
|
this.#watchFolder(filename);
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
error.filename = filename;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
ref() {
|
|
this.#files.forEach((file) => {
|
|
if (file instanceof StatWatcher) {
|
|
file.ref();
|
|
}
|
|
});
|
|
}
|
|
|
|
unref() {
|
|
this.#files.forEach((file) => {
|
|
if (file instanceof StatWatcher) {
|
|
file.unref();
|
|
}
|
|
});
|
|
}
|
|
|
|
[SymbolAsyncIterator]() {
|
|
const { signal } = this.#options;
|
|
const promiseExecutor = signal == null ?
|
|
(resolve) => {
|
|
this.once('change', (eventType, filename) => {
|
|
resolve({ __proto__: null, value: { eventType, filename } });
|
|
});
|
|
} : (resolve, reject) => {
|
|
const onAbort = () => {
|
|
this.close();
|
|
reject(new AbortError(undefined, { cause: signal.reason }));
|
|
};
|
|
if (signal.aborted) return onAbort();
|
|
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
|
|
signal.addEventListener('abort', onAbort, { __proto__: null, once: true, [kResistStopPropagation]: true });
|
|
this.once('change', (eventType, filename) => {
|
|
signal.removeEventListener('abort', onAbort);
|
|
resolve({ __proto__: null, value: { eventType, filename } });
|
|
});
|
|
};
|
|
return {
|
|
next: () => (this.#closed ?
|
|
{ __proto__: null, done: true } :
|
|
new Promise(promiseExecutor)),
|
|
return: () => {
|
|
this.close();
|
|
return { __proto__: null, done: true };
|
|
},
|
|
[SymbolAsyncIterator]() { return this; },
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
FSWatcher,
|
|
kFSWatchStart,
|
|
};
|