| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";const EventEmitter = require("events").EventEmitter;const fs = require("graceful-fs");const path = require("path");const watchEventSource = require("./watchEventSource");const EXISTANCE_ONLY_TIME_ENTRY = Object.freeze({});let FS_ACCURACY = 2000;const IS_OSX = require("os").platform() === "darwin";const IS_WIN = require("os").platform() === "win32";const WATCHPACK_POLLING = process.env.WATCHPACK_POLLING;const FORCE_POLLING =	`${+WATCHPACK_POLLING}` === WATCHPACK_POLLING		? +WATCHPACK_POLLING		: !!WATCHPACK_POLLING && WATCHPACK_POLLING !== "false";function withoutCase(str) {	return str.toLowerCase();}function needCalls(times, callback) {	return function() {		if (--times === 0) {			return callback();		}	};}class Watcher extends EventEmitter {	constructor(directoryWatcher, filePath, startTime) {		super();		this.directoryWatcher = directoryWatcher;		this.path = filePath;		this.startTime = startTime && +startTime;	}	checkStartTime(mtime, initial) {		const startTime = this.startTime;		if (typeof startTime !== "number") return !initial;		return startTime <= mtime;	}	close() {		this.emit("closed");	}}class DirectoryWatcher extends EventEmitter {	constructor(watcherManager, directoryPath, options) {		super();		if (FORCE_POLLING) {			options.poll = FORCE_POLLING;		}		this.watcherManager = watcherManager;		this.options = options;		this.path = directoryPath;		// safeTime is the point in time after which reading is safe to be unchanged		// timestamp is a value that should be compared with another timestamp (mtime)		/** @type {Map<string, { safeTime: number, timestamp: number }} */		this.files = new Map();		/** @type {Map<string, number>} */		this.filesWithoutCase = new Map();		this.directories = new Map();		this.lastWatchEvent = 0;		this.initialScan = true;		this.ignored = options.ignored || (() => false);		this.nestedWatching = false;		this.polledWatching =			typeof options.poll === "number"				? options.poll				: options.poll				? 5007				: false;		this.timeout = undefined;		this.initialScanRemoved = new Set();		this.initialScanFinished = undefined;		/** @type {Map<string, Set<Watcher>>} */		this.watchers = new Map();		this.parentWatcher = null;		this.refs = 0;		this._activeEvents = new Map();		this.closed = false;		this.scanning = false;		this.scanAgain = false;		this.scanAgainInitial = false;		this.createWatcher();		this.doScan(true);	}	createWatcher() {		try {			if (this.polledWatching) {				this.watcher = {					close: () => {						if (this.timeout) {							clearTimeout(this.timeout);							this.timeout = undefined;						}					}				};			} else {				if (IS_OSX) {					this.watchInParentDirectory();				}				this.watcher = watchEventSource.watch(this.path);				this.watcher.on("change", this.onWatchEvent.bind(this));				this.watcher.on("error", this.onWatcherError.bind(this));			}		} catch (err) {			this.onWatcherError(err);		}	}	forEachWatcher(path, fn) {		const watchers = this.watchers.get(withoutCase(path));		if (watchers !== undefined) {			for (const w of watchers) {				fn(w);			}		}	}	setMissing(itemPath, initial, type) {		if (this.initialScan) {			this.initialScanRemoved.add(itemPath);		}		const oldDirectory = this.directories.get(itemPath);		if (oldDirectory) {			if (this.nestedWatching) oldDirectory.close();			this.directories.delete(itemPath);			this.forEachWatcher(itemPath, w => w.emit("remove", type));			if (!initial) {				this.forEachWatcher(this.path, w =>					w.emit("change", itemPath, null, type, initial)				);			}		}		const oldFile = this.files.get(itemPath);		if (oldFile) {			this.files.delete(itemPath);			const key = withoutCase(itemPath);			const count = this.filesWithoutCase.get(key) - 1;			if (count <= 0) {				this.filesWithoutCase.delete(key);				this.forEachWatcher(itemPath, w => w.emit("remove", type));			} else {				this.filesWithoutCase.set(key, count);			}			if (!initial) {				this.forEachWatcher(this.path, w =>					w.emit("change", itemPath, null, type, initial)				);			}		}	}	setFileTime(filePath, mtime, initial, ignoreWhenEqual, type) {		const now = Date.now();		if (this.ignored(filePath)) return;		const old = this.files.get(filePath);		let safeTime, accuracy;		if (initial) {			safeTime = Math.min(now, mtime) + FS_ACCURACY;			accuracy = FS_ACCURACY;		} else {			safeTime = now;			accuracy = 0;			if (old && old.timestamp === mtime && mtime + FS_ACCURACY < now) {				// We are sure that mtime is untouched				// This can be caused by some file attribute change				// e. g. when access time has been changed				// but the file content is untouched				return;			}		}		if (ignoreWhenEqual && old && old.timestamp === mtime) return;		this.files.set(filePath, {			safeTime,			accuracy,			timestamp: mtime		});		if (!old) {			const key = withoutCase(filePath);			const count = this.filesWithoutCase.get(key);			this.filesWithoutCase.set(key, (count || 0) + 1);			if (count !== undefined) {				// There is already a file with case-insensitive-equal name				// On a case-insensitive filesystem we may miss the renaming				// when only casing is changed.				// To be sure that our information is correct				// we trigger a rescan here				this.doScan(false);			}			this.forEachWatcher(filePath, w => {				if (!initial || w.checkStartTime(safeTime, initial)) {					w.emit("change", mtime, type);				}			});		} else if (!initial) {			this.forEachWatcher(filePath, w => w.emit("change", mtime, type));		}		this.forEachWatcher(this.path, w => {			if (!initial || w.checkStartTime(safeTime, initial)) {				w.emit("change", filePath, safeTime, type, initial);			}		});	}	setDirectory(directoryPath, birthtime, initial, type) {		if (this.ignored(directoryPath)) return;		if (directoryPath === this.path) {			if (!initial) {				this.forEachWatcher(this.path, w =>					w.emit("change", directoryPath, birthtime, type, initial)				);			}		} else {			const old = this.directories.get(directoryPath);			if (!old) {				const now = Date.now();				if (this.nestedWatching) {					this.createNestedWatcher(directoryPath);				} else {					this.directories.set(directoryPath, true);				}				let safeTime;				if (initial) {					safeTime = Math.min(now, birthtime) + FS_ACCURACY;				} else {					safeTime = now;				}				this.forEachWatcher(directoryPath, w => {					if (!initial || w.checkStartTime(safeTime, false)) {						w.emit("change", birthtime, type);					}				});				this.forEachWatcher(this.path, w => {					if (!initial || w.checkStartTime(safeTime, initial)) {						w.emit("change", directoryPath, safeTime, type, initial);					}				});			}		}	}	createNestedWatcher(directoryPath) {		const watcher = this.watcherManager.watchDirectory(directoryPath, 1);		watcher.on("change", (filePath, mtime, type, initial) => {			this.forEachWatcher(this.path, w => {				if (!initial || w.checkStartTime(mtime, initial)) {					w.emit("change", filePath, mtime, type, initial);				}			});		});		this.directories.set(directoryPath, watcher);	}	setNestedWatching(flag) {		if (this.nestedWatching !== !!flag) {			this.nestedWatching = !!flag;			if (this.nestedWatching) {				for (const directory of this.directories.keys()) {					this.createNestedWatcher(directory);				}			} else {				for (const [directory, watcher] of this.directories) {					watcher.close();					this.directories.set(directory, true);				}			}		}	}	watch(filePath, startTime) {		const key = withoutCase(filePath);		let watchers = this.watchers.get(key);		if (watchers === undefined) {			watchers = new Set();			this.watchers.set(key, watchers);		}		this.refs++;		const watcher = new Watcher(this, filePath, startTime);		watcher.on("closed", () => {			if (--this.refs <= 0) {				this.close();				return;			}			watchers.delete(watcher);			if (watchers.size === 0) {				this.watchers.delete(key);				if (this.path === filePath) this.setNestedWatching(false);			}		});		watchers.add(watcher);		let safeTime;		if (filePath === this.path) {			this.setNestedWatching(true);			safeTime = this.lastWatchEvent;			for (const entry of this.files.values()) {				fixupEntryAccuracy(entry);				safeTime = Math.max(safeTime, entry.safeTime);			}		} else {			const entry = this.files.get(filePath);			if (entry) {				fixupEntryAccuracy(entry);				safeTime = entry.safeTime;			} else {				safeTime = 0;			}		}		if (safeTime) {			if (safeTime >= startTime) {				process.nextTick(() => {					if (this.closed) return;					if (filePath === this.path) {						watcher.emit(							"change",							filePath,							safeTime,							"watch (outdated on attach)",							true						);					} else {						watcher.emit(							"change",							safeTime,							"watch (outdated on attach)",							true						);					}				});			}		} else if (this.initialScan) {			if (this.initialScanRemoved.has(filePath)) {				process.nextTick(() => {					if (this.closed) return;					watcher.emit("remove");				});			}		} else if (			filePath !== this.path &&			!this.directories.has(filePath) &&			watcher.checkStartTime(this.initialScanFinished, false)		) {			process.nextTick(() => {				if (this.closed) return;				watcher.emit("initial-missing", "watch (missing on attach)");			});		}		return watcher;	}	onWatchEvent(eventType, filename) {		if (this.closed) return;		if (!filename) {			// In some cases no filename is provided			// This seem to happen on windows			// So some event happened but we don't know which file is affected			// We have to do a full scan of the directory			this.doScan(false);			return;		}		const filePath = path.join(this.path, filename);		if (this.ignored(filePath)) return;		if (this._activeEvents.get(filename) === undefined) {			this._activeEvents.set(filename, false);			const checkStats = () => {				if (this.closed) return;				this._activeEvents.set(filename, false);				fs.lstat(filePath, (err, stats) => {					if (this.closed) return;					if (this._activeEvents.get(filename) === true) {						process.nextTick(checkStats);						return;					}					this._activeEvents.delete(filename);					// ENOENT happens when the file/directory doesn't exist					// EPERM happens when the containing directory doesn't exist					if (err) {						if (							err.code !== "ENOENT" &&							err.code !== "EPERM" &&							err.code !== "EBUSY"						) {							this.onStatsError(err);						} else {							if (filename === path.basename(this.path)) {								// This may indicate that the directory itself was removed								if (!fs.existsSync(this.path)) {									this.onDirectoryRemoved("stat failed");								}							}						}					}					this.lastWatchEvent = Date.now();					if (!stats) {						this.setMissing(filePath, false, eventType);					} else if (stats.isDirectory()) {						this.setDirectory(							filePath,							+stats.birthtime || 1,							false,							eventType						);					} else if (stats.isFile() || stats.isSymbolicLink()) {						if (stats.mtime) {							ensureFsAccuracy(stats.mtime);						}						this.setFileTime(							filePath,							+stats.mtime || +stats.ctime || 1,							false,							false,							eventType						);					}				});			};			process.nextTick(checkStats);		} else {			this._activeEvents.set(filename, true);		}	}	onWatcherError(err) {		if (this.closed) return;		if (err) {			if (err.code !== "EPERM" && err.code !== "ENOENT") {				console.error("Watchpack Error (watcher): " + err);			}			this.onDirectoryRemoved("watch error");		}	}	onStatsError(err) {		if (err) {			console.error("Watchpack Error (stats): " + err);		}	}	onScanError(err) {		if (err) {			console.error("Watchpack Error (initial scan): " + err);		}		this.onScanFinished();	}	onScanFinished() {		if (this.polledWatching) {			this.timeout = setTimeout(() => {				if (this.closed) return;				this.doScan(false);			}, this.polledWatching);		}	}	onDirectoryRemoved(reason) {		if (this.watcher) {			this.watcher.close();			this.watcher = null;		}		this.watchInParentDirectory();		const type = `directory-removed (${reason})`;		for (const directory of this.directories.keys()) {			this.setMissing(directory, null, type);		}		for (const file of this.files.keys()) {			this.setMissing(file, null, type);		}	}	watchInParentDirectory() {		if (!this.parentWatcher) {			const parentDir = path.dirname(this.path);			// avoid watching in the root directory			// removing directories in the root directory is not supported			if (path.dirname(parentDir) === parentDir) return;			this.parentWatcher = this.watcherManager.watchFile(this.path, 1);			this.parentWatcher.on("change", (mtime, type) => {				if (this.closed) return;				// On non-osx platforms we don't need this watcher to detect				// directory removal, as an EPERM error indicates that				if ((!IS_OSX || this.polledWatching) && this.parentWatcher) {					this.parentWatcher.close();					this.parentWatcher = null;				}				// Try to create the watcher when parent directory is found				if (!this.watcher) {					this.createWatcher();					this.doScan(false);					// directory was created so we emit an event					this.forEachWatcher(this.path, w =>						w.emit("change", this.path, mtime, type, false)					);				}			});			this.parentWatcher.on("remove", () => {				this.onDirectoryRemoved("parent directory removed");			});		}	}	doScan(initial) {		if (this.scanning) {			if (this.scanAgain) {				if (!initial) this.scanAgainInitial = false;			} else {				this.scanAgain = true;				this.scanAgainInitial = initial;			}			return;		}		this.scanning = true;		if (this.timeout) {			clearTimeout(this.timeout);			this.timeout = undefined;		}		process.nextTick(() => {			if (this.closed) return;			fs.readdir(this.path, (err, items) => {				if (this.closed) return;				if (err) {					if (err.code === "ENOENT" || err.code === "EPERM") {						this.onDirectoryRemoved("scan readdir failed");					} else {						this.onScanError(err);					}					this.initialScan = false;					this.initialScanFinished = Date.now();					if (initial) {						for (const watchers of this.watchers.values()) {							for (const watcher of watchers) {								if (watcher.checkStartTime(this.initialScanFinished, false)) {									watcher.emit(										"initial-missing",										"scan (parent directory missing in initial scan)"									);								}							}						}					}					if (this.scanAgain) {						this.scanAgain = false;						this.doScan(this.scanAgainInitial);					} else {						this.scanning = false;					}					return;				}				const itemPaths = new Set(					items.map(item => path.join(this.path, item.normalize("NFC")))				);				for (const file of this.files.keys()) {					if (!itemPaths.has(file)) {						this.setMissing(file, initial, "scan (missing)");					}				}				for (const directory of this.directories.keys()) {					if (!itemPaths.has(directory)) {						this.setMissing(directory, initial, "scan (missing)");					}				}				if (this.scanAgain) {					// Early repeat of scan					this.scanAgain = false;					this.doScan(initial);					return;				}				const itemFinished = needCalls(itemPaths.size + 1, () => {					if (this.closed) return;					this.initialScan = false;					this.initialScanRemoved = null;					this.initialScanFinished = Date.now();					if (initial) {						const missingWatchers = new Map(this.watchers);						missingWatchers.delete(withoutCase(this.path));						for (const item of itemPaths) {							missingWatchers.delete(withoutCase(item));						}						for (const watchers of missingWatchers.values()) {							for (const watcher of watchers) {								if (watcher.checkStartTime(this.initialScanFinished, false)) {									watcher.emit(										"initial-missing",										"scan (missing in initial scan)"									);								}							}						}					}					if (this.scanAgain) {						this.scanAgain = false;						this.doScan(this.scanAgainInitial);					} else {						this.scanning = false;						this.onScanFinished();					}				});				for (const itemPath of itemPaths) {					fs.lstat(itemPath, (err2, stats) => {						if (this.closed) return;						if (err2) {							if (								err2.code === "ENOENT" ||								err2.code === "EPERM" ||								err2.code === "EACCES" ||								err2.code === "EBUSY" ||								// TODO https://github.com/libuv/libuv/pull/4566								(err2.code === "EINVAL" && IS_WIN)							) {								this.setMissing(itemPath, initial, "scan (" + err2.code + ")");							} else {								this.onScanError(err2);							}							itemFinished();							return;						}						if (stats.isFile() || stats.isSymbolicLink()) {							if (stats.mtime) {								ensureFsAccuracy(stats.mtime);							}							this.setFileTime(								itemPath,								+stats.mtime || +stats.ctime || 1,								initial,								true,								"scan (file)"							);						} else if (stats.isDirectory()) {							if (!initial || !this.directories.has(itemPath))								this.setDirectory(									itemPath,									+stats.birthtime || 1,									initial,									"scan (dir)"								);						}						itemFinished();					});				}				itemFinished();			});		});	}	getTimes() {		const obj = Object.create(null);		let safeTime = this.lastWatchEvent;		for (const [file, entry] of this.files) {			fixupEntryAccuracy(entry);			safeTime = Math.max(safeTime, entry.safeTime);			obj[file] = Math.max(entry.safeTime, entry.timestamp);		}		if (this.nestedWatching) {			for (const w of this.directories.values()) {				const times = w.directoryWatcher.getTimes();				for (const file of Object.keys(times)) {					const time = times[file];					safeTime = Math.max(safeTime, time);					obj[file] = time;				}			}			obj[this.path] = safeTime;		}		if (!this.initialScan) {			for (const watchers of this.watchers.values()) {				for (const watcher of watchers) {					const path = watcher.path;					if (!Object.prototype.hasOwnProperty.call(obj, path)) {						obj[path] = null;					}				}			}		}		return obj;	}	collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {		let safeTime = this.lastWatchEvent;		for (const [file, entry] of this.files) {			fixupEntryAccuracy(entry);			safeTime = Math.max(safeTime, entry.safeTime);			fileTimestamps.set(file, entry);		}		if (this.nestedWatching) {			for (const w of this.directories.values()) {				safeTime = Math.max(					safeTime,					w.directoryWatcher.collectTimeInfoEntries(						fileTimestamps,						directoryTimestamps					)				);			}			fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);			directoryTimestamps.set(this.path, {				safeTime			});		} else {			for (const dir of this.directories.keys()) {				// No additional info about this directory				// but maybe another DirectoryWatcher has info				fileTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);				if (!directoryTimestamps.has(dir))					directoryTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);			}			fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);			directoryTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);		}		if (!this.initialScan) {			for (const watchers of this.watchers.values()) {				for (const watcher of watchers) {					const path = watcher.path;					if (!fileTimestamps.has(path)) {						fileTimestamps.set(path, null);					}				}			}		}		return safeTime;	}	close() {		this.closed = true;		this.initialScan = false;		if (this.watcher) {			this.watcher.close();			this.watcher = null;		}		if (this.nestedWatching) {			for (const w of this.directories.values()) {				w.close();			}			this.directories.clear();		}		if (this.parentWatcher) {			this.parentWatcher.close();			this.parentWatcher = null;		}		this.emit("closed");	}}module.exports = DirectoryWatcher;module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY;function fixupEntryAccuracy(entry) {	if (entry.accuracy > FS_ACCURACY) {		entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY;		entry.accuracy = FS_ACCURACY;	}}function ensureFsAccuracy(mtime) {	if (!mtime) return;	if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1;	else if (FS_ACCURACY > 10 && mtime % 10 !== 0) FS_ACCURACY = 10;	else if (FS_ACCURACY > 100 && mtime % 100 !== 0) FS_ACCURACY = 100;	else if (FS_ACCURACY > 1000 && mtime % 1000 !== 0) FS_ACCURACY = 1000;}
 |