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