1'use strict'; 2 3const { 4 ObjectDefineProperty, 5 ObjectSetPrototypeOf, 6 Symbol, 7} = primordials; 8 9const errors = require('internal/errors'); 10const { 11 kFsStatsFieldsNumber, 12 StatWatcher: _StatWatcher 13} = internalBinding('fs'); 14const { FSEvent } = internalBinding('fs_event_wrap'); 15const { UV_ENOSPC } = internalBinding('uv'); 16const { EventEmitter } = require('events'); 17const { 18 getStatsFromBinding, 19 getValidatedPath 20} = require('internal/fs/utils'); 21const { 22 defaultTriggerAsyncIdScope, 23 symbols: { owner_symbol } 24} = require('internal/async_hooks'); 25const { toNamespacedPath } = require('path'); 26const { validateUint32 } = require('internal/validators'); 27const assert = require('internal/assert'); 28 29const kOldStatus = Symbol('kOldStatus'); 30const kUseBigint = Symbol('kUseBigint'); 31 32const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount'); 33const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount'); 34const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef'); 35 36function emitStop(self) { 37 self.emit('stop'); 38} 39 40function StatWatcher(bigint) { 41 EventEmitter.call(this); 42 43 this._handle = null; 44 this[kOldStatus] = -1; 45 this[kUseBigint] = bigint; 46 this[KFSStatWatcherRefCount] = 1; 47 this[KFSStatWatcherMaxRefCount] = 1; 48} 49ObjectSetPrototypeOf(StatWatcher.prototype, EventEmitter.prototype); 50ObjectSetPrototypeOf(StatWatcher, EventEmitter); 51 52function onchange(newStatus, stats) { 53 const self = this[owner_symbol]; 54 if (self[kOldStatus] === -1 && 55 newStatus === -1 && 56 stats[2/* new nlink */] === stats[16/* old nlink */]) { 57 return; 58 } 59 60 self[kOldStatus] = newStatus; 61 self.emit('change', getStatsFromBinding(stats), 62 getStatsFromBinding(stats, kFsStatsFieldsNumber)); 63} 64 65// FIXME(joyeecheung): this method is not documented. 66// At the moment if filename is undefined, we 67// 1. Throw an Error if it's the first time .start() is called 68// 2. Return silently if .start() has already been called 69// on a valid filename and the wrap has been initialized 70// This method is a noop if the watcher has already been started. 71StatWatcher.prototype.start = function(filename, persistent, interval) { 72 if (this._handle !== null) 73 return; 74 75 this._handle = new _StatWatcher(this[kUseBigint]); 76 this._handle[owner_symbol] = this; 77 this._handle.onchange = onchange; 78 if (!persistent) 79 this.unref(); 80 81 // uv_fs_poll is a little more powerful than ev_stat but we curb it for 82 // the sake of backwards compatibility 83 this[kOldStatus] = -1; 84 85 filename = getValidatedPath(filename, 'filename'); 86 validateUint32(interval, 'interval'); 87 const err = this._handle.start(toNamespacedPath(filename), interval); 88 if (err) { 89 const error = errors.uvException({ 90 errno: err, 91 syscall: 'watch', 92 path: filename 93 }); 94 error.filename = filename; 95 throw error; 96 } 97}; 98 99// FIXME(joyeecheung): this method is not documented while there is 100// another documented fs.unwatchFile(). The counterpart in 101// FSWatcher is .close() 102// This method is a noop if the watcher has not been started. 103StatWatcher.prototype.stop = function() { 104 if (this._handle === null) 105 return; 106 107 defaultTriggerAsyncIdScope(this._handle.getAsyncId(), 108 process.nextTick, 109 emitStop, 110 this); 111 this._handle.close(); 112 this._handle = null; 113}; 114 115// Clean up or add ref counters. 116StatWatcher.prototype[kFSStatWatcherAddOrCleanRef] = function(operate) { 117 if (operate === 'add') { 118 // Add a Ref 119 this[KFSStatWatcherRefCount]++; 120 this[KFSStatWatcherMaxRefCount]++; 121 } else if (operate === 'clean') { 122 // Clean up a single 123 this[KFSStatWatcherMaxRefCount]--; 124 this.unref(); 125 } else if (operate === 'cleanAll') { 126 // Clean up all 127 this[KFSStatWatcherMaxRefCount] = 0; 128 this[KFSStatWatcherRefCount] = 0; 129 this._handle && this._handle.unref(); 130 } 131}; 132 133StatWatcher.prototype.ref = function() { 134 // Avoid refCount calling ref multiple times causing unref to have no effect. 135 if (this[KFSStatWatcherRefCount] === this[KFSStatWatcherMaxRefCount]) 136 return this; 137 if (this._handle && this[KFSStatWatcherRefCount]++ === 0) 138 this._handle.ref(); 139 return this; 140}; 141 142StatWatcher.prototype.unref = function() { 143 // Avoid refCount calling unref multiple times causing ref to have no effect. 144 if (this[KFSStatWatcherRefCount] === 0) return this; 145 if (this._handle && --this[KFSStatWatcherRefCount] === 0) 146 this._handle.unref(); 147 return this; 148}; 149 150 151function FSWatcher() { 152 EventEmitter.call(this); 153 154 this._handle = new FSEvent(); 155 this._handle[owner_symbol] = this; 156 157 this._handle.onchange = (status, eventType, filename) => { 158 // TODO(joyeecheung): we may check self._handle.initialized here 159 // and return if that is false. This allows us to avoid firing the event 160 // after the handle is closed, and to fire both UV_RENAME and UV_CHANGE 161 // if they are set by libuv at the same time. 162 if (status < 0) { 163 if (this._handle !== null) { 164 // We don't use this.close() here to avoid firing the close event. 165 this._handle.close(); 166 this._handle = null; // Make the handle garbage collectable 167 } 168 const error = errors.uvException({ 169 errno: status, 170 syscall: 'watch', 171 path: filename 172 }); 173 error.filename = filename; 174 this.emit('error', error); 175 } else { 176 this.emit('change', eventType, filename); 177 } 178 }; 179} 180ObjectSetPrototypeOf(FSWatcher.prototype, EventEmitter.prototype); 181ObjectSetPrototypeOf(FSWatcher, EventEmitter); 182 183 184// FIXME(joyeecheung): this method is not documented. 185// At the moment if filename is undefined, we 186// 1. Throw an Error if it's the first time .start() is called 187// 2. Return silently if .start() has already been called 188// on a valid filename and the wrap has been initialized 189// 3. Return silently if the watcher has already been closed 190// This method is a noop if the watcher has already been started. 191FSWatcher.prototype.start = function(filename, 192 persistent, 193 recursive, 194 encoding) { 195 if (this._handle === null) { // closed 196 return; 197 } 198 assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); 199 if (this._handle.initialized) { // already started 200 return; 201 } 202 203 filename = getValidatedPath(filename, 'filename'); 204 205 const err = this._handle.start(toNamespacedPath(filename), 206 persistent, 207 recursive, 208 encoding); 209 if (err) { 210 const error = errors.uvException({ 211 errno: err, 212 syscall: 'watch', 213 path: filename, 214 message: err === UV_ENOSPC ? 215 'System limit for number of file watchers reached' : '' 216 }); 217 error.filename = filename; 218 throw error; 219 } 220}; 221 222// This method is a noop if the watcher has not been started or 223// has already been closed. 224FSWatcher.prototype.close = function() { 225 if (this._handle === null) { // closed 226 return; 227 } 228 assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); 229 if (!this._handle.initialized) { // not started 230 return; 231 } 232 this._handle.close(); 233 this._handle = null; // Make the handle garbage collectable 234 process.nextTick(emitCloseNT, this); 235}; 236 237FSWatcher.prototype.ref = function() { 238 if (this._handle) this._handle.ref(); 239 return this; 240}; 241 242FSWatcher.prototype.unref = function() { 243 if (this._handle) this._handle.unref(); 244 return this; 245}; 246 247function emitCloseNT(self) { 248 self.emit('close'); 249} 250 251// Legacy alias on the C++ wrapper object. This is not public API, so we may 252// want to runtime-deprecate it at some point. There's no hurry, though. 253ObjectDefineProperty(FSEvent.prototype, 'owner', { 254 get() { return this[owner_symbol]; }, 255 set(v) { return this[owner_symbol] = v; } 256}); 257 258module.exports = { 259 FSWatcher, 260 StatWatcher, 261 kFSStatWatcherAddOrCleanRef, 262}; 263