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 fsPromises.watch() #37179

Closed
wants to merge 2 commits 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 51 additions & 2 deletions doc/api/fs.md
Expand Up @@ -1189,6 +1189,55 @@ The `atime` and `mtime` arguments follow these rules:
* If the value can not be converted to a number, or is `NaN`, `Infinity` or
`-Infinity`, an `Error` will be thrown.

### `fsPromises.watch(filename[, options])`
<!-- YAML
added: REPLACEME
-->

* `filename` {string|Buffer|URL}
* `options` {string|Object}
* `persistent` {boolean} Indicates whether the process should continue to run
as long as files are being watched. **Default:** `true`.
* `recursive` {boolean} Indicates whether all subdirectories should be
watched, or only the current directory. This applies when a directory is
specified, and only on supported platforms (See [caveats][]). **Default:**
`false`.
* `encoding` {string} Specifies the character encoding to be used for the
filename passed to the listener. **Default:** `'utf8'`.
* `signal` {AbortSignal} An {AbortSignal} used to signal when the watcher
should stop.
* Returns: {AsyncIterator} of objects with the properties:
* `eventType` {string} The type of change
* `filename` {string|Buffer} The name of the file changed.

Returns an async iterator that watches for changes on `filename`, where `filename`
is either a file or a directory.

```js
const { watch } = require('fs/promises');

const ac = new AbortController();
const { signal } = ac;
setTimeout(() => ac.abort(), 10000);

(async () => {
try {
const watcher = watch(__filename, { signal });
for await (const event of watcher)
console.log(event);
} catch (err) {
if (err.name === 'AbortError')
return;
throw err;
}
})();
```

On most platforms, `'rename'` is emitted whenever a filename appears or
disappears in the directory.

All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.

### `fsPromises.writeFile(file, data[, options])`
<!-- YAML
added: v10.0.0
Expand Down Expand Up @@ -3477,7 +3526,7 @@ changes:
as long as files are being watched. **Default:** `true`.
* `recursive` {boolean} Indicates whether all subdirectories should be
watched, or only the current directory. This applies when a directory is
specified, and only on supported platforms (See [Caveats][]). **Default:**
specified, and only on supported platforms (See [caveats][]). **Default:**
`false`.
* `encoding` {string} Specifies the character encoding to be used for the
filename passed to the listener. **Default:** `'utf8'`.
Expand Down Expand Up @@ -6550,7 +6599,6 @@ A call to `fs.ftruncate()` or `filehandle.truncate()` can be used to reset
the file contents.

[#25741]: https://github.com/nodejs/node/issues/25741
[Caveats]: #fs_caveats
[Common System Errors]: errors.md#errors_common_system_errors
[File access constants]: #fs_file_access_constants
[MDN-Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
Expand All @@ -6560,6 +6608,7 @@ the file contents.
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
[Readable Stream]: stream.md#stream_class_stream_readable
[Writable Stream]: stream.md#stream_class_stream_writable
[caveats]: #fs_caveats
[`AHAFS`]: https://www.ibm.com/developerworks/aix/library/au-aix_event_infrastructure/
[`Buffer.byteLength`]: buffer.md#buffer_static_method_buffer_bytelength_string_encoding
[`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/fs/promises.js
Expand Up @@ -74,6 +74,7 @@ const {
const pathModule = require('path');
const { promisify } = require('internal/util');
const { EventEmitterMixin } = require('internal/event_target');
const { watch } = require('internal/fs/watchers');

const kHandle = Symbol('kHandle');
const kFd = Symbol('kFd');
Expand Down Expand Up @@ -724,6 +725,7 @@ module.exports = {
writeFile,
appendFile,
readFile,
watch,
},

FileHandle,
Expand Down
119 changes: 114 additions & 5 deletions lib/internal/fs/watchers.js
Expand Up @@ -4,27 +4,52 @@ const {
FunctionPrototypeCall,
ObjectDefineProperty,
ObjectSetPrototypeOf,
Promise,
Symbol,
} = primordials;

const errors = require('internal/errors');
const {
AbortError,
uvException,
codes: {
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');

const {
kFsStatsFieldsNumber,
StatWatcher: _StatWatcher
} = internalBinding('fs');

const { FSEvent } = internalBinding('fs_event_wrap');
const { UV_ENOSPC } = internalBinding('uv');
const { EventEmitter } = require('events');

const {
getStatsFromBinding,
getValidatedPath
} = require('internal/fs/utils');

const {
defaultTriggerAsyncIdScope,
symbols: { owner_symbol }
} = require('internal/async_hooks');

const { toNamespacedPath } = require('path');
const { validateUint32 } = require('internal/validators');

const {
validateAbortSignal,
validateBoolean,
validateObject,
validateUint32,
} = require('internal/validators');

const {
Buffer: {
isEncoding,
},
} = require('buffer');

const assert = require('internal/assert');

const kOldStatus = Symbol('kOldStatus');
Expand Down Expand Up @@ -91,7 +116,7 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
validateUint32(interval, 'interval');
const err = this._handle.start(toNamespacedPath(filename), interval);
if (err) {
const error = errors.uvException({
const error = uvException({
errno: err,
syscall: 'watch',
path: filename
Expand Down Expand Up @@ -176,7 +201,7 @@ function FSWatcher() {
this._handle.close();
this._handle = null; // Make the handle garbage collectable.
}
const error = errors.uvException({
const error = uvException({
errno: status,
syscall: 'watch',
path: filename
Expand Down Expand Up @@ -216,7 +241,7 @@ FSWatcher.prototype[kFSWatchStart] = function(filename,
recursive,
encoding);
if (err) {
const error = errors.uvException({
const error = uvException({
errno: err,
syscall: 'watch',
path: filename,
Expand Down Expand Up @@ -270,10 +295,94 @@ ObjectDefineProperty(FSEvent.prototype, 'owner', {
set(v) { return this[owner_symbol] = v; }
});

async function* watch(filename, options = {}) {
const path = toNamespacedPath(getValidatedPath(filename));
validateObject(options, 'options');

const {
persistent = true,
recursive = false,
encoding = 'utf8',
signal,
} = options;

validateBoolean(persistent, 'options.persistent');
jasnell marked this conversation as resolved.
Show resolved Hide resolved
validateBoolean(recursive, 'options.recursive');
validateAbortSignal(signal, 'options.signal');

if (encoding && !isEncoding(encoding)) {
const reason = 'is invalid encoding';
throw new ERR_INVALID_ARG_VALUE(encoding, 'encoding', reason);
}

if (signal?.aborted)
throw new AbortError();

const handle = new FSEvent();
let res;
let rej;
const oncancel = () => {
handle.close();
rej(new AbortError());
};

try {
signal?.addEventListener('abort', oncancel, { once: true });

let promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});

handle.onchange = (status, eventType, filename) => {
if (status < 0) {
const error = uvException({
errno: status,
syscall: 'watch',
path: filename
});
error.filename = filename;
handle.close();
rej(error);
return;
}

res({ eventType, filename });
};

const err = handle.start(path, persistent, recursive, encoding);
if (err) {
const error = uvException({
errno: err,
syscall: 'watch',
path: filename,
message: err === UV_ENOSPC ?
'System limit for number of file watchers reached' : ''
});
error.filename = filename;
handle.close();
throw error;
}

while (!signal?.aborted) {
yield await promise;
benjamingr marked this conversation as resolved.
Show resolved Hide resolved
promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
}
throw new AbortError();
} finally {
handle.close();
signal?.removeEventListener('abort', oncancel);
}
}

module.exports = {
FSWatcher,
StatWatcher,
kFSWatchStart,
kFSStatWatcherStart,
kFSStatWatcherAddOrCleanRef,
watch,
};
3 changes: 3 additions & 0 deletions test/parallel/test-bootstrap-modules.js
Expand Up @@ -17,6 +17,7 @@ const expectedModules = new Set([
'Internal Binding credentials',
'Internal Binding fs',
'Internal Binding fs_dir',
'Internal Binding fs_event_wrap',
'Internal Binding messaging',
'Internal Binding module_wrap',
'Internal Binding native_module',
Expand All @@ -31,6 +32,7 @@ const expectedModules = new Set([
'Internal Binding types',
'Internal Binding url',
'Internal Binding util',
'Internal Binding uv',
'Internal Binding worker',
'NativeModule buffer',
'NativeModule events',
Expand All @@ -51,6 +53,7 @@ const expectedModules = new Set([
'NativeModule internal/fs/utils',
'NativeModule internal/fs/promises',
'NativeModule internal/fs/rimraf',
'NativeModule internal/fs/watchers',
'NativeModule internal/idna',
'NativeModule internal/linkedlist',
'NativeModule internal/modules/run_main',
Expand Down