| '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, |
| }; |