Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: add .ref() and .unref() methods to the StatWatcher and FSWatcher #33134

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 59 additions & 0 deletions doc/api/fs.md
Expand Up @@ -579,6 +579,64 @@ added: v0.5.8
Stop watching for changes on the given `fs.FSWatcher`. Once stopped, the
`fs.FSWatcher` object is no longer usable.

### `watcher.ref()`
<!-- YAML
added: REPLACEME
-->

When called, requests that the Node.js event loop *not* exit so long as the
`FSWatcher` is active. Calling `watcher.ref()` multiple times will have
no effect.

By default, all `FSWatcher` objects are "ref'ed", making it normally
unnecessary to call `watcher.ref()` unless `watcher.unref()` had been
called previously.

### `watcher.unref()`
<!-- YAML
added: REPLACEME
-->

When called, the active `FSWatcher` object will not require the Node.js
rickyes marked this conversation as resolved.
Show resolved Hide resolved
event loop to remain active. If there is no other activity keeping the
event loop running, the process may exit before the `FSWatcher` object's
callback is invoked. Calling `watcher.unref()` multiple times will have
no effect.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods should probably also be documented for the return value of .watchFile().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, there are other methods like .stop(), I'm not sure if it should be documented in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed


## Class: `fs.StatWatcher`
<!-- YAML
added: REPLACEME
-->

* Extends {EventEmitter}

A successful call to `fs.watchFile()` method will return a new `fs.StatWatcher`
object.

### `watcher.ref()`
<!-- YAML
added: REPLACEME
-->

When called, requests that the Node.js event loop *not* exit so long as the
`StatWatcher` is active. Calling `watcher.ref()` multiple times will have
no effect.

By default, all `StatWatcher` objects are "ref'ed", making it normally
unnecessary to call `watcher.ref()` unless `watcher.unref()` had been
called previously.

rickyes marked this conversation as resolved.
Show resolved Hide resolved
### `watcher.unref()`
<!-- YAML
added: REPLACEME
-->

When called, the active `StatWatcher` object will not require the Node.js
event loop to remain active. If there is no other activity keeping the
event loop running, the process may exit before the `StatWatcher` object's
callback is invoked. Calling `watcher.unref()` multiple times will have
no effect.

## Class: `fs.ReadStream`
<!-- YAML
added: v0.1.93
Expand Down Expand Up @@ -3958,6 +4016,7 @@ changes:
* `listener` {Function}
* `current` {fs.Stats}
* `previous` {fs.Stats}
* Returns: {fs.StatWatcher}

Watch for changes on `filename`. The callback `listener` will be called each
time the file is accessed.
Expand Down
6 changes: 6 additions & 0 deletions lib/fs.js
Expand Up @@ -1489,6 +1489,8 @@ function watchFile(filename, options, listener) {
stat[watchers.kFSStatWatcherStart](filename,
options.persistent, options.interval);
statWatchers.set(filename, stat);
} else {
stat[watchers.kFSStatWatcherAddOrCleanRef]('add');
}

stat.addListener('change', listener);
Expand All @@ -1503,9 +1505,13 @@ function unwatchFile(filename, listener) {
if (stat === undefined) return;

if (typeof listener === 'function') {
const beforeListenerCount = stat.listenerCount('change');
stat.removeListener('change', listener);
if (stat.listenerCount('change') < beforeListenerCount)
stat[watchers.kFSStatWatcherAddOrCleanRef]('clean');
} else {
stat.removeAllListeners('change');
stat[watchers.kFSStatWatcherAddOrCleanRef]('cleanAll');
}

if (stat.listenerCount('change') === 0) {
Expand Down
55 changes: 53 additions & 2 deletions lib/internal/fs/watchers.js
Expand Up @@ -31,6 +31,9 @@ const kUseBigint = Symbol('kUseBigint');

const kFSWatchStart = Symbol('kFSWatchStart');
const kFSStatWatcherStart = Symbol('kFSStatWatcherStart');
const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount');
const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount');
const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef');

function emitStop(self) {
self.emit('stop');
Expand All @@ -42,6 +45,8 @@ function StatWatcher(bigint) {
this._handle = null;
this[kOldStatus] = -1;
this[kUseBigint] = bigint;
this[KFSStatWatcherRefCount] = 1;
this[KFSStatWatcherMaxRefCount] = 1;
}
ObjectSetPrototypeOf(StatWatcher.prototype, EventEmitter.prototype);
ObjectSetPrototypeOf(StatWatcher, EventEmitter);
Expand Down Expand Up @@ -75,7 +80,7 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
this._handle[owner_symbol] = this;
this._handle.onchange = onchange;
if (!persistent)
this._handle.unref();
this.unref();

// uv_fs_poll is a little more powerful than ev_stat but we curb it for
// the sake of backwards compatibility.
Expand Down Expand Up @@ -117,6 +122,41 @@ StatWatcher.prototype.stop = function() {
this._handle = null;
};

// Clean up or add ref counters.
StatWatcher.prototype[kFSStatWatcherAddOrCleanRef] = function(operate) {
if (operate === 'add') {
// Add a Ref
this[KFSStatWatcherRefCount]++;
this[KFSStatWatcherMaxRefCount]++;
} else if (operate === 'clean') {
// Clean up a single
this[KFSStatWatcherMaxRefCount]--;
this.unref();
} else if (operate === 'cleanAll') {
// Clean up all
this[KFSStatWatcherMaxRefCount] = 0;
this[KFSStatWatcherRefCount] = 0;
this._handle && this._handle.unref();
}
};

StatWatcher.prototype.ref = function() {
// Avoid refCount calling ref multiple times causing unref to have no effect.
if (this[KFSStatWatcherRefCount] === this[KFSStatWatcherMaxRefCount])
return this;
if (this._handle && this[KFSStatWatcherRefCount]++ === 0)
this._handle.ref();
return this;
};

StatWatcher.prototype.unref = function() {
// Avoid refCount calling unref multiple times causing ref to have no effect.
if (this[KFSStatWatcherRefCount] === 0) return this;
if (this._handle && --this[KFSStatWatcherRefCount] === 0)
this._handle.unref();
return this;
};
rickyes marked this conversation as resolved.
Show resolved Hide resolved


function FSWatcher() {
EventEmitter.call(this);
Expand Down Expand Up @@ -208,6 +248,16 @@ FSWatcher.prototype.close = function() {
process.nextTick(emitCloseNT, this);
};

FSWatcher.prototype.ref = function() {
if (this._handle) this._handle.ref();
return this;
};

FSWatcher.prototype.unref = function() {
if (this._handle) this._handle.unref();
return this;
};
rickyes marked this conversation as resolved.
Show resolved Hide resolved

function emitCloseNT(self) {
self.emit('close');
}
Expand All @@ -223,5 +273,6 @@ module.exports = {
FSWatcher,
StatWatcher,
kFSWatchStart,
kFSStatWatcherStart
kFSStatWatcherStart,
kFSStatWatcherAddOrCleanRef,
};
3 changes: 1 addition & 2 deletions src/fs_event_wrap.cc
Expand Up @@ -101,9 +101,8 @@ void FSEventWrap::Initialize(Local<Object> target,
FSEventWrap::kInternalFieldCount);
t->SetClassName(fsevent_string);

t->Inherit(AsyncWrap::GetConstructorTemplate(env));
t->Inherit(HandleWrap::GetConstructorTemplate(env));
env->SetProtoMethod(t, "start", Start);
env->SetProtoMethod(t, "close", Close);

Local<FunctionTemplate> get_initialized_templ =
FunctionTemplate::New(env->isolate(),
Expand Down
20 changes: 20 additions & 0 deletions test/parallel/test-fs-watch-ref-unref.js
@@ -0,0 +1,20 @@
'use strict';

const common = require('../common');

if (common.isIBMi)
common.skip('IBMi does not support `fs.watch()`');

const fs = require('fs');

const watcher = fs.watch(__filename, common.mustNotCall());

watcher.unref();

setTimeout(
common.mustCall(() => {
watcher.ref();
watcher.unref();
}),
common.platformTimeout(100)
);
35 changes: 35 additions & 0 deletions test/parallel/test-fs-watchfile-ref-unref.js
@@ -0,0 +1,35 @@
'use strict';

const common = require('../common');

const fs = require('fs');
const assert = require('assert');

const uncalledListener = common.mustNotCall();
const uncalledListener2 = common.mustNotCall();
const watcher = fs.watchFile(__filename, uncalledListener);

watcher.unref();
watcher.unref();
watcher.ref();
watcher.unref();
watcher.ref();
watcher.ref();
watcher.unref();

fs.unwatchFile(__filename, uncalledListener);

// Watch the file with two different listeners.
fs.watchFile(__filename, uncalledListener);
const watcher2 = fs.watchFile(__filename, uncalledListener2);

setTimeout(
common.mustCall(() => {
fs.unwatchFile(__filename, common.mustNotCall());
assert.strictEqual(watcher2.listenerCount('change'), 2);
fs.unwatchFile(__filename, uncalledListener);
assert.strictEqual(watcher2.listenerCount('change'), 1);
watcher2.unref();
}),
common.platformTimeout(100)
);
1 change: 1 addition & 0 deletions tools/doc/type-parser.js
Expand Up @@ -84,6 +84,7 @@ const customTypesMap = {
'fs.FSWatcher': 'fs.html#fs_class_fs_fswatcher',
'fs.ReadStream': 'fs.html#fs_class_fs_readstream',
'fs.Stats': 'fs.html#fs_class_fs_stats',
'fs.StatWatcher': 'fs.html#fs_class_fs_statwatcher',
'fs.WriteStream': 'fs.html#fs_class_fs_writestream',

'http.Agent': 'http.html#http_class_http_agent',
Expand Down