123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787 |
- /*
- 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 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"
- ) {
- 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;
- }
|