watchman_watcher.js
1 'use strict'; 2 3 const fs = require('fs'); 4 const path = require('path'); 5 const common = require('./common'); 6 const watchmanClient = require('./watchman_client'); 7 const EventEmitter = require('events').EventEmitter; 8 const RecrawlWarning = require('./utils/recrawl-warning-dedupe'); 9 10 /** 11 * Constants 12 */ 13 14 const CHANGE_EVENT = common.CHANGE_EVENT; 15 const DELETE_EVENT = common.DELETE_EVENT; 16 const ADD_EVENT = common.ADD_EVENT; 17 const ALL_EVENT = common.ALL_EVENT; 18 19 /** 20 * Export `WatchmanWatcher` class. 21 */ 22 23 module.exports = WatchmanWatcher; 24 25 /** 26 * Watches `dir`. 27 * 28 * @class WatchmanWatcher 29 * @param String dir 30 * @param {Object} opts 31 * @public 32 */ 33 34 function WatchmanWatcher(dir, opts) { 35 common.assignOptions(this, opts); 36 this.root = path.resolve(dir); 37 this._init(); 38 } 39 40 WatchmanWatcher.prototype.__proto__ = EventEmitter.prototype; 41 42 /** 43 * Run the watchman `watch` command on the root and subscribe to changes. 44 * 45 * @private 46 */ 47 WatchmanWatcher.prototype._init = function() { 48 if (this._client) { 49 this._client = null; 50 } 51 52 // Get the WatchmanClient instance corresponding to our watchmanPath (or nothing). 53 // Then subscribe, which will do the appropriate setup so that we will receive 54 // calls to handleChangeEvent when files change. 55 this._client = watchmanClient.getInstance(this.watchmanPath); 56 57 return this._client.subscribe(this, this.root).then( 58 resp => { 59 this._handleWarning(resp); 60 this.emit('ready'); 61 }, 62 error => { 63 this._handleError(error); 64 } 65 ); 66 }; 67 68 /** 69 * Called by WatchmanClient to create the options, either during initial 'subscribe' 70 * or to resubscribe after a disconnect+reconnect. Note that we are leaving out 71 * the watchman 'since' and 'relative_root' options, which are handled inside the 72 * WatchmanClient. 73 */ 74 WatchmanWatcher.prototype.createOptions = function() { 75 let options = { 76 fields: ['name', 'exists', 'new'], 77 }; 78 79 // If the server has the wildmatch capability available it supports 80 // the recursive **/*.foo style match and we can offload our globs 81 // to the watchman server. This saves both on data size to be 82 // communicated back to us and compute for evaluating the globs 83 // in our node process. 84 if (this._client.wildmatch) { 85 if (this.globs.length === 0) { 86 if (!this.dot) { 87 // Make sure we honor the dot option if even we're not using globs. 88 options.expression = [ 89 'match', 90 '**', 91 'wholename', 92 { 93 includedotfiles: false, 94 }, 95 ]; 96 } 97 } else { 98 options.expression = ['anyof']; 99 for (let i in this.globs) { 100 options.expression.push([ 101 'match', 102 this.globs[i], 103 'wholename', 104 { 105 includedotfiles: this.dot, 106 }, 107 ]); 108 } 109 } 110 } 111 112 return options; 113 }; 114 115 /** 116 * Called by WatchmanClient when it receives an error from the watchman daemon. 117 * 118 * @param {Object} resp 119 */ 120 WatchmanWatcher.prototype.handleErrorEvent = function(error) { 121 this.emit('error', error); 122 }; 123 124 /** 125 * Called by the WatchmanClient when it is notified about a file change in 126 * the tree for this particular watcher's root. 127 * 128 * @param {Object} resp 129 * @private 130 */ 131 132 WatchmanWatcher.prototype.handleChangeEvent = function(resp) { 133 if (Array.isArray(resp.files)) { 134 resp.files.forEach(this.handleFileChange, this); 135 } 136 }; 137 138 /** 139 * Handles a single change event record. 140 * 141 * @param {Object} changeDescriptor 142 * @private 143 */ 144 145 WatchmanWatcher.prototype.handleFileChange = function(changeDescriptor) { 146 let absPath; 147 let relativePath; 148 149 relativePath = changeDescriptor.name; 150 absPath = path.join(this.root, relativePath); 151 152 if ( 153 !(this._client.wildmatch && !this.hasIgnore) && 154 !common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath) 155 ) { 156 return; 157 } 158 159 if (!changeDescriptor.exists) { 160 this.emitEvent(DELETE_EVENT, relativePath, this.root); 161 } else { 162 fs.lstat(absPath, (error, stat) => { 163 // Files can be deleted between the event and the lstat call 164 // the most reliable thing to do here is to ignore the event. 165 if (error && error.code === 'ENOENT') { 166 return; 167 } 168 169 if (this._handleError(error)) { 170 return; 171 } 172 173 let eventType = changeDescriptor.new ? ADD_EVENT : CHANGE_EVENT; 174 175 // Change event on dirs are mostly useless. 176 if (!(eventType === CHANGE_EVENT && stat.isDirectory())) { 177 this.emitEvent(eventType, relativePath, this.root, stat); 178 } 179 }); 180 } 181 }; 182 183 /** 184 * Dispatches an event. 185 * 186 * @param {string} eventType 187 * @param {string} filepath 188 * @param {string} root 189 * @param {fs.Stat} stat 190 * @private 191 */ 192 193 WatchmanWatcher.prototype.emitEvent = function( 194 eventType, 195 filepath, 196 root, 197 stat 198 ) { 199 this.emit(eventType, filepath, root, stat); 200 this.emit(ALL_EVENT, eventType, filepath, root, stat); 201 }; 202 203 /** 204 * Closes the watcher. 205 * 206 * @param {function} callback 207 * @private 208 */ 209 210 WatchmanWatcher.prototype.close = function(callback) { 211 this._client.closeWatcher(this); 212 callback && callback(null, true); 213 }; 214 215 /** 216 * Handles an error and returns true if exists. 217 * 218 * @param {WatchmanWatcher} self 219 * @param {Error} error 220 * @private 221 */ 222 223 WatchmanWatcher.prototype._handleError = function(error) { 224 if (error != null) { 225 this.emit('error', error); 226 return true; 227 } else { 228 return false; 229 } 230 }; 231 232 /** 233 * Handles a warning in the watchman resp object. 234 * 235 * @param {object} resp 236 * @private 237 */ 238 239 WatchmanWatcher.prototype._handleWarning = function(resp) { 240 if ('warning' in resp) { 241 if (RecrawlWarning.isRecrawlWarningDupe(resp.warning)) { 242 return true; 243 } 244 console.warn(resp.warning); 245 return true; 246 } else { 247 return false; 248 } 249 };