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 AbortSignal support to watch #37190

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4133,6 +4133,9 @@ this API: [`fs.utimes()`][].
<!-- YAML
added: v0.5.10
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37190
description: Added support for closing the watcher with an AbortSignal.
- version: v7.6.0
pr-url: https://github.com/nodejs/node/pull/10739
description: The `filename` parameter can be a WHATWG `URL` object using
Expand All @@ -4152,6 +4155,7 @@ changes:
`false`.
* `encoding` {string} Specifies the character encoding to be used for the
filename passed to the listener. **Default:** `'utf8'`.
* `signal` {AbortSignal} allows closing the watcher with an AbortSignal.
* `listener` {Function|undefined} **Default:** `undefined`
* `eventType` {string}
* `filename` {string|Buffer}
Expand All @@ -4174,6 +4178,9 @@ The listener callback is attached to the `'change'` event fired by
[`fs.FSWatcher`][], but it is not the same thing as the `'change'` value of
`eventType`.

If a `signal` is passed, aborting the corresponding AbortController will close
the returned [`fs.FSWatcher`][].

### Caveats

<!--type=misc-->
Expand Down
11 changes: 11 additions & 0 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,17 @@ function watch(filename, options, listener) {
if (listener) {
watcher.addListener('change', listener);
}
if (options.signal) {
if (options.signal.aborted) {
process.nextTick(() => watcher.close());
Copy link
Member

Choose a reason for hiding this comment

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

Why on next tick?

Copy link
Member Author

Choose a reason for hiding this comment

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

So that users have a chance to subscribe to the "close" event.

Copy link
Member Author

@benjamingr benjamingr Feb 3, 2021

Choose a reason for hiding this comment

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

(Just because watch returns an EventEmitter watcher and not a promise or a callback)

} else {
const listener = () => watcher.close();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: you could move the listener declaration above and pass it in process.nextTick to avoid code duplication.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually find this way more readable though I don't have very strong feelings about it.

options.signal.addEventListener('abort', listener);
Copy link
Contributor

@aduh95 aduh95 Feb 4, 2021

Choose a reason for hiding this comment

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

Not sure if that matters / is more efficient, but at least it makes it clearer that this is just a one timer:

Suggested change
options.signal.addEventListener('abort', listener);
options.signal.addEventListener('abort', listener, { once: true });

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a common issue that gets brought up from time to time. The reason I like doing { once: true } less is that it creates the false impression we are relying on the listener being a one-time listener for cleanup.

That assumption has lead to memory leaks in our code before - this way the cleanup is explicit (when the watcher is closed) and I think it makes bugs slightly less likely.

watcher.once('close', () => {
options.signal.removeEventListener('abort', listener);
});
}
}

return watcher;
}
Expand Down
32 changes: 32 additions & 0 deletions test/parallel/test-fs-watch-abort-signal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Flags: --expose-internals
'use strict';

// Verify that AbortSignal integration works for fs.watch

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

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

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


{
// Signal aborted after creating the watcher
const file = fixtures.path('empty.js');
const ac = new AbortController();
const { signal } = ac;
const watcher = fs.watch(file, { signal });
watcher.once('close', common.mustCall());
setImmediate(() => ac.abort());
}
{
// Signal aborted before creating the watcher
const file = fixtures.path('empty.js');
const ac = new AbortController();
const { signal } = ac;
ac.abort();
const watcher = fs.watch(file, { signal });
watcher.once('close', common.mustCall());
}