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