123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- // vendored from https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/node_watcher.js
- 'use strict';
-
- const EventEmitter = require('events').EventEmitter;
-
- const fs = require('fs');
-
- const platform = require('os').platform();
-
- const path = require('path');
-
- const common = require('./common');
- /**
- * Constants
- */
-
- const DEFAULT_DELAY = common.DEFAULT_DELAY;
- const CHANGE_EVENT = common.CHANGE_EVENT;
- const DELETE_EVENT = common.DELETE_EVENT;
- const ADD_EVENT = common.ADD_EVENT;
- const ALL_EVENT = common.ALL_EVENT;
- /**
- * Export `NodeWatcher` class.
- * Watches `dir`.
- *
- * @class NodeWatcher
- * @param {String} dir
- * @param {Object} opts
- * @public
- */
-
- module.exports = class NodeWatcher extends EventEmitter {
- constructor(dir, opts) {
- super();
- common.assignOptions(this, opts);
- this.watched = Object.create(null);
- this.changeTimers = Object.create(null);
- this.dirRegistery = Object.create(null);
- this.root = path.resolve(dir);
- this.watchdir = this.watchdir.bind(this);
- this.register = this.register.bind(this);
- this.checkedEmitError = this.checkedEmitError.bind(this);
- this.watchdir(this.root);
- common.recReaddir(
- this.root,
- this.watchdir,
- this.register,
- this.emit.bind(this, 'ready'),
- this.checkedEmitError,
- this.ignored
- );
- }
- /**
- * Register files that matches our globs to know what to type of event to
- * emit in the future.
- *
- * Registery looks like the following:
- *
- * dirRegister => Map {
- * dirpath => Map {
- * filename => true
- * }
- * }
- *
- * @param {string} filepath
- * @return {boolean} whether or not we have registered the file.
- * @private
- */
-
- register(filepath) {
- const relativePath = path.relative(this.root, filepath);
-
- if (
- !common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
- ) {
- return false;
- }
-
- const dir = path.dirname(filepath);
-
- if (!this.dirRegistery[dir]) {
- this.dirRegistery[dir] = Object.create(null);
- }
-
- const filename = path.basename(filepath);
- this.dirRegistery[dir][filename] = true;
- return true;
- }
- /**
- * Removes a file from the registery.
- *
- * @param {string} filepath
- * @private
- */
-
- unregister(filepath) {
- const dir = path.dirname(filepath);
-
- if (this.dirRegistery[dir]) {
- const filename = path.basename(filepath);
- delete this.dirRegistery[dir][filename];
- }
- }
- /**
- * Removes a dir from the registery.
- *
- * @param {string} dirpath
- * @private
- */
-
- unregisterDir(dirpath) {
- if (this.dirRegistery[dirpath]) {
- delete this.dirRegistery[dirpath];
- }
- }
- /**
- * Checks if a file or directory exists in the registery.
- *
- * @param {string} fullpath
- * @return {boolean}
- * @private
- */
-
- registered(fullpath) {
- const dir = path.dirname(fullpath);
- return (
- this.dirRegistery[fullpath] ||
- (this.dirRegistery[dir] &&
- this.dirRegistery[dir][path.basename(fullpath)])
- );
- }
- /**
- * Emit "error" event if it's not an ignorable event
- *
- * @param error
- * @private
- */
-
- checkedEmitError(error) {
- if (!isIgnorableFileError(error)) {
- this.emit('error', error);
- }
- }
- /**
- * Watch a directory.
- *
- * @param {string} dir
- * @private
- */
-
- watchdir(dir) {
- if (this.watched[dir]) {
- return;
- }
-
- const watcher = fs.watch(
- dir,
- {
- persistent: true
- },
- this.normalizeChange.bind(this, dir)
- );
- this.watched[dir] = watcher;
- watcher.on('error', this.checkedEmitError);
-
- if (this.root !== dir) {
- this.register(dir);
- }
- }
- /**
- * Stop watching a directory.
- *
- * @param {string} dir
- * @private
- */
-
- stopWatching(dir) {
- if (this.watched[dir]) {
- this.watched[dir].close();
- delete this.watched[dir];
- }
- }
- /**
- * End watching.
- *
- * @public
- */
-
- close() {
- Object.keys(this.watched).forEach(this.stopWatching, this);
- this.removeAllListeners();
- return Promise.resolve();
- }
- /**
- * On some platforms, as pointed out on the fs docs (most likely just win32)
- * the file argument might be missing from the fs event. Try to detect what
- * change by detecting if something was deleted or the most recent file change.
- *
- * @param {string} dir
- * @param {string} event
- * @param {string} file
- * @public
- */
-
- detectChangedFile(dir, event, callback) {
- if (!this.dirRegistery[dir]) {
- return;
- }
-
- let found = false;
- let closest = {
- mtime: 0
- };
- let c = 0;
- Object.keys(this.dirRegistery[dir]).forEach(function (file, i, arr) {
- fs.lstat(path.join(dir, file), (error, stat) => {
- if (found) {
- return;
- }
-
- if (error) {
- if (isIgnorableFileError(error)) {
- found = true;
- callback(file);
- } else {
- this.emit('error', error);
- }
- } else {
- if (stat.mtime > closest.mtime) {
- stat.file = file;
- closest = stat;
- }
-
- if (arr.length === ++c) {
- callback(closest.file);
- }
- }
- });
- }, this);
- }
- /**
- * Normalize fs events and pass it on to be processed.
- *
- * @param {string} dir
- * @param {string} event
- * @param {string} file
- * @public
- */
-
- normalizeChange(dir, event, file) {
- if (!file) {
- this.detectChangedFile(dir, event, actualFile => {
- if (actualFile) {
- this.processChange(dir, event, actualFile);
- }
- });
- } else {
- this.processChange(dir, event, path.normalize(file));
- }
- }
- /**
- * Process changes.
- *
- * @param {string} dir
- * @param {string} event
- * @param {string} file
- * @public
- */
-
- processChange(dir, event, file) {
- const fullPath = path.join(dir, file);
- const relativePath = path.join(path.relative(this.root, dir), file);
- fs.lstat(fullPath, (error, stat) => {
- if (error && error.code !== 'ENOENT') {
- this.emit('error', error);
- } else if (!error && stat.isDirectory()) {
- // win32 emits usless change events on dirs.
- if (event !== 'change') {
- this.watchdir(fullPath);
-
- if (
- common.isFileIncluded(
- this.globs,
- this.dot,
- this.doIgnore,
- relativePath
- )
- ) {
- this.emitEvent(ADD_EVENT, relativePath, stat);
- }
- }
- } else {
- const registered = this.registered(fullPath);
-
- if (error && error.code === 'ENOENT') {
- this.unregister(fullPath);
- this.stopWatching(fullPath);
- this.unregisterDir(fullPath);
-
- if (registered) {
- this.emitEvent(DELETE_EVENT, relativePath);
- }
- } else if (registered) {
- this.emitEvent(CHANGE_EVENT, relativePath, stat);
- } else {
- if (this.register(fullPath)) {
- this.emitEvent(ADD_EVENT, relativePath, stat);
- }
- }
- }
- });
- }
- /**
- * Triggers a 'change' event after debounding it to take care of duplicate
- * events on os x.
- *
- * @private
- */
-
- emitEvent(type, file, stat) {
- const key = type + '-' + file;
- const addKey = ADD_EVENT + '-' + file;
-
- if (type === CHANGE_EVENT && this.changeTimers[addKey]) {
- // Ignore the change event that is immediately fired after an add event.
- // (This happens on Linux).
- return;
- }
-
- clearTimeout(this.changeTimers[key]);
- this.changeTimers[key] = setTimeout(() => {
- delete this.changeTimers[key];
-
- if (type === ADD_EVENT && stat.isDirectory()) {
- // Recursively emit add events and watch for sub-files/folders
- common.recReaddir(
- path.resolve(this.root, file),
- function emitAddDir(dir, stats) {
- this.watchdir(dir);
- this.rawEmitEvent(ADD_EVENT, path.relative(this.root, dir), stats);
- }.bind(this),
- function emitAddFile(file, stats) {
- this.register(file);
- this.rawEmitEvent(ADD_EVENT, path.relative(this.root, file), stats);
- }.bind(this),
- function endCallback() {},
- this.checkedEmitError,
- this.ignored
- );
- } else {
- this.rawEmitEvent(type, file, stat);
- }
- }, DEFAULT_DELAY);
- }
- /**
- * Actually emit the events
- */
-
- rawEmitEvent(type, file, stat) {
- this.emit(type, file, this.root, stat);
- this.emit(ALL_EVENT, type, file, this.root, stat);
- }
- };
- /**
- * Determine if a given FS error can be ignored
- *
- * @private
- */
-
- function isIgnorableFileError(error) {
- return (
- error.code === 'ENOENT' || (error.code === 'EPERM' && platform === 'win32')
- );
- }
|