1'use strict'; 2 3const { 4 FunctionPrototypeCall, 5 ObjectDefineProperty, 6 ObjectSetPrototypeOf, 7 Symbol, 8} = primordials; 9 10const { 11 AbortError, 12 uvException, 13 codes: { 14 ERR_INVALID_ARG_VALUE, 15 }, 16} = require('internal/errors'); 17const { 18 createDeferredPromise, 19 kEmptyObject, 20} = require('internal/util'); 21 22const { 23 kFsStatsFieldsNumber, 24 StatWatcher: _StatWatcher, 25} = internalBinding('fs'); 26 27const { FSEvent } = internalBinding('fs_event_wrap'); 28const { UV_ENOSPC } = internalBinding('uv'); 29const { EventEmitter } = require('events'); 30 31const { 32 getStatsFromBinding, 33 getValidatedPath, 34} = require('internal/fs/utils'); 35 36const { 37 defaultTriggerAsyncIdScope, 38 symbols: { owner_symbol }, 39} = require('internal/async_hooks'); 40 41const { toNamespacedPath } = require('path'); 42 43const { 44 validateAbortSignal, 45 validateBoolean, 46 validateObject, 47 validateUint32, 48} = require('internal/validators'); 49 50const { 51 Buffer: { 52 isEncoding, 53 }, 54} = require('buffer'); 55 56const assert = require('internal/assert'); 57 58const kOldStatus = Symbol('kOldStatus'); 59const kUseBigint = Symbol('kUseBigint'); 60 61const kFSWatchStart = Symbol('kFSWatchStart'); 62const kFSStatWatcherStart = Symbol('kFSStatWatcherStart'); 63const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount'); 64const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount'); 65const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef'); 66 67function emitStop(self) { 68 self.emit('stop'); 69} 70 71function StatWatcher(bigint) { 72 FunctionPrototypeCall(EventEmitter, this); 73 74 this._handle = null; 75 this[kOldStatus] = -1; 76 this[kUseBigint] = bigint; 77 this[KFSStatWatcherRefCount] = 1; 78 this[KFSStatWatcherMaxRefCount] = 1; 79} 80ObjectSetPrototypeOf(StatWatcher.prototype, EventEmitter.prototype); 81ObjectSetPrototypeOf(StatWatcher, EventEmitter); 82 83function onchange(newStatus, stats) { 84 const self = this[owner_symbol]; 85 if (self[kOldStatus] === -1 && 86 newStatus === -1 && 87 stats[2/* new nlink */] === stats[16/* old nlink */]) { 88 return; 89 } 90 91 self[kOldStatus] = newStatus; 92 self.emit('change', getStatsFromBinding(stats), 93 getStatsFromBinding(stats, kFsStatsFieldsNumber)); 94} 95 96// At the moment if filename is undefined, we 97// 1. Throw an Error if it's the first 98// time Symbol('kFSStatWatcherStart') is called 99// 2. Return silently if Symbol('kFSStatWatcherStart') has already been called 100// on a valid filename and the wrap has been initialized 101// This method is a noop if the watcher has already been started. 102StatWatcher.prototype[kFSStatWatcherStart] = function(filename, 103 persistent, 104 interval) { 105 if (this._handle !== null) 106 return; 107 108 this._handle = new _StatWatcher(this[kUseBigint]); 109 this._handle[owner_symbol] = this; 110 this._handle.onchange = onchange; 111 if (!persistent) 112 this.unref(); 113 114 // uv_fs_poll is a little more powerful than ev_stat but we curb it for 115 // the sake of backwards compatibility. 116 this[kOldStatus] = -1; 117 118 filename = getValidatedPath(filename, 'filename'); 119 validateUint32(interval, 'interval'); 120 const err = this._handle.start(toNamespacedPath(filename), interval); 121 if (err) { 122 const error = uvException({ 123 errno: err, 124 syscall: 'watch', 125 path: filename, 126 }); 127 error.filename = filename; 128 throw error; 129 } 130}; 131 132// To maximize backward-compatibility for the end user, 133// a no-op stub method has been added instead of 134// totally removing StatWatcher.prototype.start. 135// This should not be documented. 136StatWatcher.prototype.start = () => {}; 137 138// FIXME(joyeecheung): this method is not documented while there is 139// another documented fs.unwatchFile(). The counterpart in 140// FSWatcher is .close() 141// This method is a noop if the watcher has not been started. 142StatWatcher.prototype.stop = function() { 143 if (this._handle === null) 144 return; 145 146 defaultTriggerAsyncIdScope(this._handle.getAsyncId(), 147 process.nextTick, 148 emitStop, 149 this); 150 this._handle.close(); 151 this._handle = null; 152}; 153 154// Clean up or add ref counters. 155StatWatcher.prototype[kFSStatWatcherAddOrCleanRef] = function(operate) { 156 if (operate === 'add') { 157 // Add a Ref 158 this[KFSStatWatcherRefCount]++; 159 this[KFSStatWatcherMaxRefCount]++; 160 } else if (operate === 'clean') { 161 // Clean up a single 162 this[KFSStatWatcherMaxRefCount]--; 163 this.unref(); 164 } else if (operate === 'cleanAll') { 165 // Clean up all 166 this[KFSStatWatcherMaxRefCount] = 0; 167 this[KFSStatWatcherRefCount] = 0; 168 this._handle?.unref(); 169 } 170}; 171 172StatWatcher.prototype.ref = function() { 173 // Avoid refCount calling ref multiple times causing unref to have no effect. 174 if (this[KFSStatWatcherRefCount] === this[KFSStatWatcherMaxRefCount]) 175 return this; 176 if (this._handle && this[KFSStatWatcherRefCount]++ === 0) 177 this._handle.ref(); 178 return this; 179}; 180 181StatWatcher.prototype.unref = function() { 182 // Avoid refCount calling unref multiple times causing ref to have no effect. 183 if (this[KFSStatWatcherRefCount] === 0) return this; 184 if (this._handle && --this[KFSStatWatcherRefCount] === 0) 185 this._handle.unref(); 186 return this; 187}; 188 189 190function FSWatcher() { 191 FunctionPrototypeCall(EventEmitter, this); 192 193 this._handle = new FSEvent(); 194 this._handle[owner_symbol] = this; 195 196 this._handle.onchange = (status, eventType, filename) => { 197 // TODO(joyeecheung): we may check self._handle.initialized here 198 // and return if that is false. This allows us to avoid firing the event 199 // after the handle is closed, and to fire both UV_RENAME and UV_CHANGE 200 // if they are set by libuv at the same time. 201 if (status < 0) { 202 if (this._handle !== null) { 203 // We don't use this.close() here to avoid firing the close event. 204 this._handle.close(); 205 this._handle = null; // Make the handle garbage collectable. 206 } 207 const error = uvException({ 208 errno: status, 209 syscall: 'watch', 210 path: filename, 211 }); 212 error.filename = filename; 213 this.emit('error', error); 214 } else { 215 this.emit('change', eventType, filename); 216 } 217 }; 218} 219ObjectSetPrototypeOf(FSWatcher.prototype, EventEmitter.prototype); 220ObjectSetPrototypeOf(FSWatcher, EventEmitter); 221 222// At the moment if filename is undefined, we 223// 1. Throw an Error if it's the first time Symbol('kFSWatchStart') is called 224// 2. Return silently if Symbol('kFSWatchStart') has already been called 225// on a valid filename and the wrap has been initialized 226// 3. Return silently if the watcher has already been closed 227// This method is a noop if the watcher has already been started. 228FSWatcher.prototype[kFSWatchStart] = function(filename, 229 persistent, 230 recursive, 231 encoding) { 232 if (this._handle === null) { // closed 233 return; 234 } 235 assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); 236 if (this._handle.initialized) { // already started 237 return; 238 } 239 240 filename = getValidatedPath(filename, 'filename'); 241 242 const err = this._handle.start(toNamespacedPath(filename), 243 persistent, 244 recursive, 245 encoding); 246 if (err) { 247 const error = uvException({ 248 errno: err, 249 syscall: 'watch', 250 path: filename, 251 message: err === UV_ENOSPC ? 252 'System limit for number of file watchers reached' : '', 253 }); 254 error.filename = filename; 255 throw error; 256 } 257}; 258 259// To maximize backward-compatibility for the end user, 260// a no-op stub method has been added instead of 261// totally removing FSWatcher.prototype.start. 262// This should not be documented. 263FSWatcher.prototype.start = () => {}; 264 265// This method is a noop if the watcher has not been started or 266// has already been closed. 267FSWatcher.prototype.close = function() { 268 if (this._handle === null) { // closed 269 return; 270 } 271 assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); 272 if (!this._handle.initialized) { // not started 273 return; 274 } 275 this._handle.close(); 276 this._handle = null; // Make the handle garbage collectable. 277 process.nextTick(emitCloseNT, this); 278}; 279 280FSWatcher.prototype.ref = function() { 281 if (this._handle) this._handle.ref(); 282 return this; 283}; 284 285FSWatcher.prototype.unref = function() { 286 if (this._handle) this._handle.unref(); 287 return this; 288}; 289 290function emitCloseNT(self) { 291 self.emit('close'); 292} 293 294// Legacy alias on the C++ wrapper object. This is not public API, so we may 295// want to runtime-deprecate it at some point. There's no hurry, though. 296ObjectDefineProperty(FSEvent.prototype, 'owner', { 297 __proto__: null, 298 get() { return this[owner_symbol]; }, 299 set(v) { return this[owner_symbol] = v; }, 300}); 301 302let kResistStopPropagation; 303 304async function* watch(filename, options = kEmptyObject) { 305 const path = toNamespacedPath(getValidatedPath(filename)); 306 validateObject(options, 'options'); 307 308 const { 309 persistent = true, 310 recursive = false, 311 encoding = 'utf8', 312 signal, 313 } = options; 314 315 validateBoolean(persistent, 'options.persistent'); 316 validateBoolean(recursive, 'options.recursive'); 317 validateAbortSignal(signal, 'options.signal'); 318 319 if (encoding && !isEncoding(encoding)) { 320 const reason = 'is invalid encoding'; 321 throw new ERR_INVALID_ARG_VALUE(encoding, 'encoding', reason); 322 } 323 324 if (signal?.aborted) 325 throw new AbortError(undefined, { cause: signal?.reason }); 326 327 const handle = new FSEvent(); 328 let { promise, resolve, reject } = createDeferredPromise(); 329 const oncancel = () => { 330 handle.close(); 331 reject(new AbortError(undefined, { cause: signal?.reason })); 332 }; 333 334 try { 335 if (signal) { 336 kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; 337 signal.addEventListener('abort', oncancel, { __proto__: null, once: true, [kResistStopPropagation]: true }); 338 } 339 handle.onchange = (status, eventType, filename) => { 340 if (status < 0) { 341 const error = uvException({ 342 errno: status, 343 syscall: 'watch', 344 path: filename, 345 }); 346 error.filename = filename; 347 handle.close(); 348 reject(error); 349 return; 350 } 351 352 resolve({ eventType, filename }); 353 }; 354 355 const err = handle.start(path, persistent, recursive, encoding); 356 if (err) { 357 const error = uvException({ 358 errno: err, 359 syscall: 'watch', 360 path: filename, 361 message: err === UV_ENOSPC ? 362 'System limit for number of file watchers reached' : '', 363 }); 364 error.filename = filename; 365 handle.close(); 366 throw error; 367 } 368 369 while (!signal?.aborted) { 370 yield await promise; 371 ({ promise, resolve, reject } = createDeferredPromise()); 372 } 373 throw new AbortError(undefined, { cause: signal?.reason }); 374 } finally { 375 handle.close(); 376 signal?.removeEventListener('abort', oncancel); 377 } 378} 379 380module.exports = { 381 FSWatcher, 382 StatWatcher, 383 kFSWatchStart, 384 kFSStatWatcherStart, 385 kFSStatWatcherAddOrCleanRef, 386 watch, 387}; 388