• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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