diff options
-rw-r--r-- | doc/api/errors.md | 6 | ||||
-rw-r--r-- | doc/api/fs.md | 216 | ||||
-rw-r--r-- | lib/fs.js | 27 | ||||
-rw-r--r-- | lib/internal/errors.js | 1 | ||||
-rw-r--r-- | lib/internal/fs/dir.js | 201 | ||||
-rw-r--r-- | lib/internal/fs/promises.js | 2 | ||||
-rw-r--r-- | lib/internal/fs/utils.js | 52 | ||||
-rw-r--r-- | node.gyp | 3 | ||||
-rw-r--r-- | src/async_wrap.h | 1 | ||||
-rw-r--r-- | src/env.h | 1 | ||||
-rw-r--r-- | src/node_binding.cc | 1 | ||||
-rw-r--r-- | src/node_dir.cc | 350 | ||||
-rw-r--r-- | src/node_dir.h | 60 | ||||
-rw-r--r-- | src/node_file.cc | 90 | ||||
-rw-r--r-- | src/node_file.h | 89 | ||||
-rw-r--r-- | test/parallel/test-bootstrap-modules.js | 2 | ||||
-rw-r--r-- | test/parallel/test-fs-opendir.js | 174 | ||||
-rw-r--r-- | test/sequential/test-async-wrap-getasyncid.js | 7 | ||||
-rw-r--r-- | tools/doc/type-parser.js | 1 |
19 files changed, 1165 insertions, 119 deletions
diff --git a/doc/api/errors.md b/doc/api/errors.md index e13e25827e..8c86012630 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -826,6 +826,11 @@ A signing `key` was not provided to the [`sign.sign()`][] method. [`crypto.timingSafeEqual()`][] was called with `Buffer`, `TypedArray`, or `DataView` arguments of different lengths. +<a id="ERR_DIR_CLOSED"></a> +### ERR_DIR_CLOSED + +The [`fs.Dir`][] was previously closed. + <a id="ERR_DNS_SET_SERVERS_FAILED"></a> ### ERR_DNS_SET_SERVERS_FAILED @@ -2388,6 +2393,7 @@ such as `process.stdout.on('data')`. [`dgram.disconnect()`]: dgram.html#dgram_socket_disconnect [`dgram.remoteAddress()`]: dgram.html#dgram_socket_remoteaddress [`errno`(3) man page]: http://man7.org/linux/man-pages/man3/errno.3.html +[`fs.Dir`]: fs.html#fs_class_fs_dir [`fs.readFileSync`]: fs.html#fs_fs_readfilesync_path_options [`fs.readdir`]: fs.html#fs_fs_readdir_path_options_callback [`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback diff --git a/doc/api/fs.md b/doc/api/fs.md index c22e169f1c..a6736d127e 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -284,13 +284,148 @@ synchronous use libuv's threadpool, which can have surprising and negative performance implications for some applications. See the [`UV_THREADPOOL_SIZE`][] documentation for more information. +## Class fs.Dir +<!-- YAML +added: REPLACEME +--> + +A class representing a directory stream. + +Created by [`fs.opendir()`][], [`fs.opendirSync()`][], or [`fsPromises.opendir()`][]. + +Example using async interation: + +```js +const fs = require('fs'); + +async function print(path) { + const dir = await fs.promises.opendir(path); + for await (const dirent of dir) { + console.log(dirent.name); + } +} +print('./').catch(console.error); +``` + +### dir.path +<!-- YAML +added: REPLACEME +--> + +* {string} + +The read-only path of this directory as was provided to [`fs.opendir()`][], +[`fs.opendirSync()`][], or [`fsPromises.opendir()`][]. + +### dir.close() +<!-- YAML +added: REPLACEME +--> + +* Returns: {Promise} + +Asynchronously close the directory's underlying resource handle. +Subsequent reads will result in errors. + +A `Promise` is returned that will be resolved after the resource has been +closed. + +### dir.close(callback) +<!-- YAML +added: REPLACEME +--> + +* `callback` {Function} + * `err` {Error} + +Asynchronously close the directory's underlying resource handle. +Subsequent reads will result in errors. + +The `callback` will be called after the resource handle has been closed. + +### dir.closeSync() +<!-- YAML +added: REPLACEME +--> + +Synchronously close the directory's underlying resource handle. +Subsequent reads will result in errors. + +### dir.read([options]) +<!-- YAML +added: REPLACEME +--> + +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* Returns: {Promise} containing {fs.Dirent} + +Asynchronously read the next directory entry via readdir(3) as an +[`fs.Dirent`][]. + +A `Promise` is returned that will be resolved with a [Dirent][] after the read +is completed. + +_Directory entries returned by this function are in no particular order as +provided by the operating system's underlying directory mechanisms._ + +### dir.read([options, ]callback) +<!-- YAML +added: REPLACEME +--> + +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* `callback` {Function} + * `err` {Error} + * `dirent` {fs.Dirent} + +Asynchronously read the next directory entry via readdir(3) as an +[`fs.Dirent`][]. + +The `callback` will be called with a [Dirent][] after the read is completed. + +The `encoding` option sets the encoding of the `name` in the `dirent`. + +_Directory entries returned by this function are in no particular order as +provided by the operating system's underlying directory mechanisms._ + +### dir.readSync([options]) +<!-- YAML +added: REPLACEME +--> + +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* Returns: {fs.Dirent} + +Synchronously read the next directory entry via readdir(3) as an +[`fs.Dirent`][]. + +The `encoding` option sets the encoding of the `name` in the `dirent`. + +_Directory entries returned by this function are in no particular order as +provided by the operating system's underlying directory mechanisms._ + +### dir\[Symbol.asyncIterator\]() +<!-- YAML +added: REPLACEME +--> + +* Returns: {AsyncIterator} to fully iterate over all entries in the directory. + +_Directory entries returned by this iterator are in no particular order as +provided by the operating system's underlying directory mechanisms._ + ## Class: fs.Dirent <!-- YAML added: v10.10.0 --> -When [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with the -`withFileTypes` option set to `true`, the resulting array is filled with +A representation of a directory entry, as returned by reading from an [`fs.Dir`][]. + +Additionally, when [`fs.readdir()`][] or [`fs.readdirSync()`][] is called with +the `withFileTypes` option set to `true`, the resulting array is filled with `fs.Dirent` objects, rather than strings or `Buffers`. ### dirent.isBlockDevice() @@ -2505,6 +2640,46 @@ Returns an integer representing the file descriptor. For detailed information, see the documentation of the asynchronous version of this API: [`fs.open()`][]. +## fs.opendir(path[, options], callback) +<!-- YAML +added: REPLACEME +--> + +* `path` {string|Buffer|URL} +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* `callback` {Function} + * `err` {Error} + * `dir` {fs.Dir} + +Asynchronously open a directory. See opendir(3). + +Creates an [`fs.Dir`][], which contains all further functions for reading from +and cleaning up the directory. + +The `encoding` option sets the encoding for the `path` while opening the +directory and subsequent read operations (unless otherwise overriden during +reads from the directory). + +## fs.opendirSync(path[, options]) +<!-- YAML +added: REPLACEME +--> + +* `path` {string|Buffer|URL} +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* Returns: {fs.Dir} + +Synchronously open a directory. See opendir(3). + +Creates an [`fs.Dir`][], which contains all further functions for reading from +and cleaning up the directory. + +The `encoding` option sets the encoding for the `path` while opening the +directory and subsequent read operations (unless otherwise overriden during +reads from the directory). + ## fs.read(fd, buffer, offset, length, position, callback) <!-- YAML added: v0.0.2 @@ -4644,6 +4819,39 @@ by [Naming Files, Paths, and Namespaces][]. Under NTFS, if the filename contains a colon, Node.js will open a file system stream, as described by [this MSDN page][MSDN-Using-Streams]. +## fsPromises.opendir(path[, options]) +<!-- YAML +added: REPLACEME +--> + +* `path` {string|Buffer|URL} +* `options` {Object} + * `encoding` {string|null} **Default:** `'utf8'` +* Returns: {Promise} containing {fs.Dir} + +Asynchronously open a directory. See opendir(3). + +Creates an [`fs.Dir`][], which contains all further functions for reading from +and cleaning up the directory. + +The `encoding` option sets the encoding for the `path` while opening the +directory and subsequent read operations (unless otherwise overriden during +reads from the directory). + +Example using async interation: + +```js +const fs = require('fs'); + +async function print(path) { + const dir = await fs.promises.opendir(path); + for await (const dirent of dir) { + console.log(dirent.name); + } +} +print('./').catch(console.error); +``` + ### fsPromises.readdir(path[, options]) <!-- YAML added: v10.0.0 @@ -5253,6 +5461,7 @@ the file contents. [`UV_THREADPOOL_SIZE`]: cli.html#cli_uv_threadpool_size_size [`WriteStream`]: #fs_class_fs_writestream [`event ports`]: https://illumos.org/man/port_create +[`fs.Dir`]: #fs_class_fs_dir [`fs.Dirent`]: #fs_class_fs_dirent [`fs.FSWatcher`]: #fs_class_fs_fswatcher [`fs.Stats`]: #fs_class_fs_stats @@ -5269,6 +5478,8 @@ the file contents. [`fs.mkdir()`]: #fs_fs_mkdir_path_options_callback [`fs.mkdtemp()`]: #fs_fs_mkdtemp_prefix_options_callback [`fs.open()`]: #fs_fs_open_path_flags_mode_callback +[`fs.opendir()`]: #fs_fs_opendir_path_options_callback +[`fs.opendirSync()`]: #fs_fs_opendirsync_path_options [`fs.read()`]: #fs_fs_read_fd_buffer_offset_length_position_callback [`fs.readFile()`]: #fs_fs_readfile_path_options_callback [`fs.readFileSync()`]: #fs_fs_readfilesync_path_options @@ -5284,6 +5495,7 @@ the file contents. [`fs.write(fd, string...)`]: #fs_fs_write_fd_string_position_encoding_callback [`fs.writeFile()`]: #fs_fs_writefile_file_data_options_callback [`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback +[`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options [`inotify(7)`]: http://man7.org/linux/man-pages/man7/inotify.7.html [`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 [`net.Socket`]: net.html#net_class_net_socket @@ -64,6 +64,7 @@ const { getDirents, getOptions, getValidatedPath, + handleErrorFromBinding, nullCheck, preprocessSymlinkDestination, Stats, @@ -80,6 +81,11 @@ const { warnOnNonPortableTemplate } = require('internal/fs/utils'); const { + Dir, + opendir, + opendirSync +} = require('internal/fs/dir'); +const { CHAR_FORWARD_SLASH, CHAR_BACKWARD_SLASH, } = require('internal/constants'); @@ -122,23 +128,6 @@ function showTruncateDeprecation() { } } -function handleErrorFromBinding(ctx) { - if (ctx.errno !== undefined) { // libuv error numbers - const err = uvException(ctx); - // eslint-disable-next-line no-restricted-syntax - Error.captureStackTrace(err, handleErrorFromBinding); - throw err; - } - if (ctx.error !== undefined) { // Errors created in C++ land. - // TODO(joyeecheung): currently, ctx.error are encoding errors - // usually caused by memory problems. We need to figure out proper error - // code(s) for this. - // eslint-disable-next-line no-restricted-syntax - Error.captureStackTrace(ctx.error, handleErrorFromBinding); - throw ctx.error; - } -} - function maybeCallback(cb) { if (typeof cb === 'function') return cb; @@ -1834,7 +1823,6 @@ function createWriteStream(path, options) { return new WriteStream(path, options); } - module.exports = fs = { appendFile, appendFileSync, @@ -1880,6 +1868,8 @@ module.exports = fs = { mkdtempSync, open, openSync, + opendir, + opendirSync, readdir, readdirSync, read, @@ -1913,6 +1903,7 @@ module.exports = fs = { writeSync, writev, writevSync, + Dir, Dirent, Stats, diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 39e6b6deda..cd3c162183 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -764,6 +764,7 @@ E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); E('ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', 'Input buffers must have the same byte length', RangeError); +E('ERR_DIR_CLOSED', 'Directory handle was closed', Error); E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]', Error); E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE', diff --git a/lib/internal/fs/dir.js b/lib/internal/fs/dir.js new file mode 100644 index 0000000000..c1a25f0de3 --- /dev/null +++ b/lib/internal/fs/dir.js @@ -0,0 +1,201 @@ +'use strict'; + +const { Object } = primordials; + +const pathModule = require('path'); +const binding = internalBinding('fs'); +const dirBinding = internalBinding('fs_dir'); +const { + codes: { + ERR_DIR_CLOSED, + ERR_INVALID_CALLBACK, + ERR_MISSING_ARGS + } +} = require('internal/errors'); + +const { FSReqCallback } = binding; +const internalUtil = require('internal/util'); +const { + getDirent, + getOptions, + getValidatedPath, + handleErrorFromBinding +} = require('internal/fs/utils'); + +const kDirHandle = Symbol('kDirHandle'); +const kDirPath = Symbol('kDirPath'); +const kDirClosed = Symbol('kDirClosed'); +const kDirOptions = Symbol('kDirOptions'); +const kDirReadPromisified = Symbol('kDirReadPromisified'); +const kDirClosePromisified = Symbol('kDirClosePromisified'); + +class Dir { + constructor(handle, path, options) { + if (handle == null) throw new ERR_MISSING_ARGS('handle'); + this[kDirHandle] = handle; + this[kDirPath] = path; + this[kDirClosed] = false; + + this[kDirOptions] = getOptions(options, { + encoding: 'utf8' + }); + + this[kDirReadPromisified] = internalUtil.promisify(this.read).bind(this); + this[kDirClosePromisified] = internalUtil.promisify(this.close).bind(this); + } + + get path() { + return this[kDirPath]; + } + + read(options, callback) { + if (this[kDirClosed] === true) { + throw new ERR_DIR_CLOSED(); + } + + callback = typeof options === 'function' ? options : callback; + if (callback === undefined) { + return this[kDirReadPromisified](options); + } else if (typeof callback !== 'function') { + throw new ERR_INVALID_CALLBACK(callback); + } + options = getOptions(options, this[kDirOptions]); + + const req = new FSReqCallback(); + req.oncomplete = (err, result) => { + if (err || result === null) { + return callback(err, result); + } + getDirent(this[kDirPath], result[0], result[1], callback); + }; + + this[kDirHandle].read( + options.encoding, + req + ); + } + + readSync(options) { + if (this[kDirClosed] === true) { + throw new ERR_DIR_CLOSED(); + } + + options = getOptions(options, this[kDirOptions]); + + const ctx = { path: this[kDirPath] }; + const result = this[kDirHandle].read( + options.encoding, + undefined, + ctx + ); + handleErrorFromBinding(ctx); + + if (result === null) { + return result; + } + + return getDirent(this[kDirPath], result[0], result[1]); + } + + close(callback) { + if (this[kDirClosed] === true) { + throw new ERR_DIR_CLOSED(); + } + + if (callback === undefined) { + return this[kDirClosePromisified](); + } else if (typeof callback !== 'function') { + throw new ERR_INVALID_CALLBACK(callback); + } + + this[kDirClosed] = true; + const req = new FSReqCallback(); + req.oncomplete = callback; + this[kDirHandle].close(req); + } + + closeSync() { + if (this[kDirClosed] === true) { + throw new ERR_DIR_CLOSED(); + } + + this[kDirClosed] = true; + const ctx = { path: this[kDirPath] }; + const result = this[kDirHandle].close(undefined, ctx); + handleErrorFromBinding(ctx); + return result; + } + + async* entries() { + try { + while (true) { + const result = await this[kDirReadPromisified](); + if (result === null) { + break; + } + yield result; + } + } finally { + await this[kDirClosePromisified](); + } + } +} + +Object.defineProperty(Dir.prototype, Symbol.asyncIterator, { + value: Dir.prototype.entries, + enumerable: false, + writable: true, + configurable: true, +}); + +function opendir(path, options, callback) { + callback = typeof options === 'function' ? options : callback; + if (typeof callback !== 'function') { + throw new ERR_INVALID_CALLBACK(callback); + } + path = getValidatedPath(path); + options = getOptions(options, { + encoding: 'utf8' + }); + + function opendirCallback(error, handle) { + if (error) { + callback(error); + } else { + callback(null, new Dir(handle, path, options)); + } + } + + const req = new FSReqCallback(); + req.oncomplete = opendirCallback; + + dirBinding.opendir( + pathModule.toNamespacedPath(path), + options.encoding, + req + ); +} + +function opendirSync(path, options) { + path = getValidatedPath(path); + options = getOptions(options, { + encoding: 'utf8' + }); + + const ctx = { path }; + const handle = dirBinding.opendir( + pathModule.toNamespacedPath(path), + options.encoding, + undefined, + ctx + ); + handleErrorFromBinding(ctx); + + return new Dir(handle, path, options); +} + +module.exports = { + Dir, + opendir, + opendirSync +}; diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 5af2ffa763..7660ff66be 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -36,6 +36,7 @@ const { validateRmdirOptions, warnOnNonPortableTemplate } = require('internal/fs/utils'); +const { opendir } = require('internal/fs/dir'); const { parseMode, validateBuffer, @@ -509,6 +510,7 @@ module.exports = { access, copyFile, open, + opendir: promisify(opendir), rename, truncate, rmdir, diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index f6050f53ee..3324ec5080 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -12,7 +12,8 @@ const { ERR_INVALID_OPT_VALUE_ENCODING, ERR_OUT_OF_RANGE }, - hideStackFrames + hideStackFrames, + uvException } = require('internal/errors'); const { isArrayBufferView, @@ -165,19 +166,33 @@ function getDirents(path, [names, types], callback) { } else { const len = names.length; for (i = 0; i < len; i++) { - const type = types[i]; - if (type === UV_DIRENT_UNKNOWN) { - const name = names[i]; - const stats = lazyLoadFs().lstatSync(pathModule.join(path, name)); - names[i] = new DirentFromStats(name, stats); - } else { - names[i] = new Dirent(names[i], types[i]); - } + names[i] = getDirent(path, names[i], types[i]); } return names; } } +function getDirent(path, name, type, callback) { + if (typeof callback === 'function') { + if (type === UV_DIRENT_UNKNOWN) { + lazyLoadFs().lstat(pathModule.join(path, name), (err, stats) => { + if (err) { + callback(err); + return; + } + callback(null, new DirentFromStats(name, stats)); + }); + } else { + callback(null, new Dirent(name, type)); + } + } else if (type === UV_DIRENT_UNKNOWN) { + const stats = lazyLoadFs().lstatSync(pathModule.join(path, name)); + return new DirentFromStats(name, stats); + } else { + return new Dirent(name, type); + } +} + function getOptions(options, defaultOptions) { if (options === null || options === undefined || typeof options === 'function') { @@ -197,6 +212,23 @@ function getOptions(options, defaultOptions) { return options; } +function handleErrorFromBinding(ctx) { + if (ctx.errno !== undefined) { // libuv error numbers + const err = uvException(ctx); + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(err, handleErrorFromBinding); + throw err; + } + if (ctx.error !== undefined) { // Errors created in C++ land. + // TODO(joyeecheung): currently, ctx.error are encoding errors + // usually caused by memory problems. We need to figure out proper error + // code(s) for this. + // eslint-disable-next-line no-restricted-syntax + Error.captureStackTrace(ctx.error, handleErrorFromBinding); + throw ctx.error; + } +} + // Check if the path contains null types if it is a string nor Uint8Array, // otherwise return silently. const nullCheck = hideStackFrames((path, propName, throwError = true) => { @@ -558,9 +590,11 @@ module.exports = { BigIntStats, // for testing copyObject, Dirent, + getDirent, getDirents, getOptions, getValidatedPath, + handleErrorFromBinding, nullCheck, preprocessSymlinkDestination, realpathCacheKey: Symbol('realpathCacheKey'), @@ -121,6 +121,7 @@ 'lib/internal/fixed_queue.js', 'lib/internal/freelist.js', 'lib/internal/freeze_intrinsics.js', + 'lib/internal/fs/dir.js', 'lib/internal/fs/promises.js', 'lib/internal/fs/read_file_context.js', 'lib/internal/fs/rimraf.js', @@ -526,6 +527,7 @@ 'src/node_constants.cc', 'src/node_contextify.cc', 'src/node_credentials.cc', + 'src/node_dir.cc', 'src/node_domain.cc', 'src/node_env_var.cc', 'src/node_errors.cc', @@ -606,6 +608,7 @@ 'src/node_constants.h', 'src/node_context_data.h', 'src/node_contextify.h', + 'src/node_dir.h', 'src/node_errors.h', 'src/node_file.h', 'src/node_http2.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 876bd0c341..2651b5a054 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -33,6 +33,7 @@ namespace node { #define NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V) \ V(NONE) \ + V(DIRHANDLE) \ V(DNSCHANNEL) \ V(ELDHISTOGRAM) \ V(FILEHANDLE) \ @@ -382,6 +382,7 @@ constexpr size_t kFsStatsBufferLength = V(async_wrap_ctor_template, v8::FunctionTemplate) \ V(async_wrap_object_ctor_template, v8::FunctionTemplate) \ V(compiled_fn_entry_template, v8::ObjectTemplate) \ + V(dir_instance_template, v8::ObjectTemplate) \ V(fd_constructor_template, v8::ObjectTemplate) \ V(fdclose_constructor_template, v8::ObjectTemplate) \ V(filehandlereadwrap_template, v8::ObjectTemplate) \ diff --git a/src/node_binding.cc b/src/node_binding.cc index 2deefefb65..f0a148a495 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -51,6 +51,7 @@ V(domain) \ V(errors) \ V(fs) \ + V(fs_dir) \ V(fs_event_wrap) \ V(heap_utils) \ V(http2) \ diff --git a/src/node_dir.cc b/src/node_dir.cc new file mode 100644 index 0000000000..c9df7e67e8 --- /dev/null +++ b/src/node_dir.cc @@ -0,0 +1,350 @@ +#include "node_dir.h" +#include "node_process.h" +#include "util.h" + +#include "tracing/trace_event.h" + +#include "req_wrap-inl.h" +#include "string_bytes.h" + +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <cstring> +#include <cerrno> +#include <climits> + +#include <memory> + +namespace node { + +namespace fs_dir { + +using fs::FSReqAfterScope; +using fs::FSReqBase; +using fs::FSReqWrapSync; +using fs::GetReqWrap; + +using v8::Array; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Null; +using v8::Object; +using v8::ObjectTemplate; +using v8::String; +using v8::Value; + +#define TRACE_NAME(name) "fs_dir.sync." #name +#define GET_TRACE_ENABLED \ + (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED \ + (TRACING_CATEGORY_NODE2(fs_dir, sync)) != 0) +#define FS_DIR_SYNC_TRACE_BEGIN(syscall, ...) \ + if (GET_TRACE_ENABLED) \ + TRACE_EVENT_BEGIN(TRACING_CATEGORY_NODE2(fs_dir, sync), TRACE_NAME(syscall), \ + ##__VA_ARGS__); +#define FS_DIR_SYNC_TRACE_END(syscall, ...) \ + if (GET_TRACE_ENABLED) \ + TRACE_EVENT_END(TRACING_CATEGORY_NODE2(fs_dir, sync), TRACE_NAME(syscall), \ + ##__VA_ARGS__); + +DirHandle::DirHandle(Environment* env, Local<Object> obj, uv_dir_t* dir) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_DIRHANDLE), + dir_(dir) { + MakeWeak(); + + dir_->nentries = 1; + dir_->dirents = &dirent_; +} + +DirHandle* DirHandle::New(Environment* env, uv_dir_t* dir) { + Local<Object> obj; + if (!env->dir_instance_template() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return nullptr; + } + + return new DirHandle(env, obj, dir); +} + +void DirHandle::New(const FunctionCallbackInfo<Value>& args) { + CHECK(args.IsConstructCall()); +} + +DirHandle::~DirHandle() { + CHECK(!closing_); // We should not be deleting while explicitly closing! + GCClose(); // Close synchronously and emit warning + CHECK(closed_); // We have to be closed at the point +} + +// Close the directory handle if it hasn't already been closed. A process +// warning will be emitted using a SetImmediate to avoid calling back to +// JS during GC. If closing the fd fails at this point, a fatal exception +// will crash the process immediately. +inline void DirHandle::GCClose() { + if (closed_) return; + uv_fs_t req; + int ret = uv_fs_closedir(nullptr, &req, dir_, nullptr); + uv_fs_req_cleanup(&req); + closing_ = false; + closed_ = true; + + struct err_detail { int ret; }; + + err_detail detail { ret }; + + if (ret < 0) { + // Do not unref this + env()->SetImmediate([detail](Environment* env) { + char msg[70]; + snprintf(msg, arraysize(msg), + "Closing directory handle on garbage collection failed"); + // This exception will end up being fatal for the process because + // it is being thrown from within the SetImmediate handler and + // there is no JS stack to bubble it to. In other words, tearing + // down the process is the only reasonable thing we can do here. + HandleScope handle_scope(env->isolate()); + env->ThrowUVException(detail.ret, "close", msg); + }); + return; + } + + // If the close was successful, we still want to emit a process warning + // to notify that the file descriptor was gc'd. We want to be noisy about + // this because not explicitly closing the DirHandle is a bug. + + env()->SetUnrefImmediate([](Environment* env) { + ProcessEmitWarning(env, + "Closing directory handle on garbage collection"); + }); +} + +void AfterClose(uv_fs_t* req) { + FSReqBase* req_wrap = FSReqBase::from_req(req); + FSReqAfterScope after(req_wrap, req); + + if (after.Proceed()) + req_wrap->Resolve(Undefined(req_wrap->env()->isolate())); +} + +void DirHandle::Close(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + + const int argc = args.Length(); + CHECK_GE(argc, 1); + + DirHandle* dir; + ASSIGN_OR_RETURN_UNWRAP(&dir, args.Holder()); + + dir->closing_ = false; + dir->closed_ = true; + + FSReqBase* req_wrap_async = GetReqWrap(env, args[0]); + if (req_wrap_async != nullptr) { // close(req) + AsyncCall(env, req_wrap_async, args, "closedir", UTF8, AfterClose, + uv_fs_closedir, dir->dir()); + } else { // close(undefined, ctx) + CHECK_EQ(argc, 2); + FSReqWrapSync req_wrap_sync; + FS_DIR_SYNC_TRACE_BEGIN(closedir); + SyncCall(env, args[1], &req_wrap_sync, "closedir", uv_fs_closedir, + dir->dir()); + FS_DIR_SYNC_TRACE_END(closedir); + } +} + +void AfterDirReadSingle(uv_fs_t* req) { + FSReqBase* req_wrap = FSReqBase::from_req(req); + FSReqAfterScope after(req_wrap, req); + + if (!after.Proceed()) { + return; + } + + Environment* env = req_wrap->env(); + Isolate* isolate = env->isolate(); + Local<Value> error; + + if (req->result == 0) { + // Done + Local<Value> done = Null(isolate); + req_wrap->Resolve(done); + return; + } + + uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr); + req->ptr = nullptr; + + // Single entries are returned without an array wrapper + const uv_dirent_t& ent = dir->dirents[0]; + + MaybeLocal<Value> filename = + StringBytes::Encode(isolate, + ent.name, + req_wrap->encoding(), + &error); + if (filename.IsEmpty()) + return req_wrap->Reject(error); + + + Local<Array> result = Array::New(isolate, 2); + result->Set(env->context(), + 0, + filename.ToLocalChecked()).FromJust(); + result->Set(env->context(), + 1, + Integer::New(isolate, ent.type)).FromJust(); + req_wrap->Resolve(result); +} + + +void DirHandle::Read(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + const int argc = args.Length(); + CHECK_GE(argc, 2); + + const enum encoding encoding = ParseEncoding(isolate, args[0], UTF8); + + DirHandle* dir; + ASSIGN_OR_RETURN_UNWRAP(&dir, args.Holder()); + + FSReqBase* req_wrap_async = static_cast<FSReqBase*>(GetReqWrap(env, args[1])); + if (req_wrap_async != nullptr) { // dir.read(encoding, req) + AsyncCall(env, req_wrap_async, args, "readdir", encoding, + AfterDirReadSingle, uv_fs_readdir, dir->dir()); + } else { // dir.read(encoding, undefined, ctx) + CHECK_EQ(argc, 3); + FSReqWrapSync req_wrap_sync; + FS_DIR_SYNC_TRACE_BEGIN(readdir); + int err = SyncCall(env, args[2], &req_wrap_sync, "readdir", uv_fs_readdir, + dir->dir()); + FS_DIR_SYNC_TRACE_END(readdir); + if (err < 0) { + return; // syscall failed, no need to continue, error info is in ctx + } + + if (req_wrap_sync.req.result == 0) { + // Done + Local<Value> done = Null(isolate); + args.GetReturnValue().Set(done); + return; + } + + CHECK_GE(req_wrap_sync.req.result, 0); + const uv_dirent_t& ent = dir->dir()->dirents[0]; + + Local<Value> error; + MaybeLocal<Value> filename = + StringBytes::Encode(isolate, + ent.name, + encoding, + &error); + if (filename.IsEmpty()) { + Local<Object> ctx = args[2].As<Object>(); + ctx->Set(env->context(), env->error_string(), error).FromJust(); + return; + } + + Local<Array> result = Array::New(isolate, 2); + result->Set(env->context(), + 0, + filename.ToLocalChecked()).FromJust(); + result->Set(env->context(), + 1, + Integer::New(isolate, ent.type)).FromJust(); + args.GetReturnValue().Set(result); + } +} + +void AfterOpenDir(uv_fs_t* req) { + FSReqBase* req_wrap = FSReqBase::from_req(req); + FSReqAfterScope after(req_wrap, req); + + if (!after.Proceed()) { + return; + } + + Environment* env = req_wrap->env(); + Local<Value> error; + + uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr); + DirHandle* handle = DirHandle::New(env, dir); + + req_wrap->Resolve(handle->object().As<Value>()); +} + +static void OpenDir(const FunctionCallbackInfo<Value>& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + const int argc = args.Length(); + CHECK_GE(argc, 3); + + BufferValue path(isolate, args[0]); + CHECK_NOT_NULL(*path); + + const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8); + + FSReqBase* req_wrap_async = static_cast<FSReqBase*>(GetReqWrap(env, args[2])); + if (req_wrap_async != nullptr) { // openDir(path, encoding, req) + AsyncCall(env, req_wrap_async, args, "opendir", encoding, AfterOpenDir, + uv_fs_opendir, *path); + } else { // openDir(path, encoding, undefined, ctx) + CHECK_EQ(argc, 4); + FSReqWrapSync req_wrap_sync; + FS_DIR_SYNC_TRACE_BEGIN(opendir); + int result = SyncCall(env, args[3], &req_wrap_sync, "opendir", + uv_fs_opendir, *path); + FS_DIR_SYNC_TRACE_END(opendir); + if (result < 0) { + return; // syscall failed, no need to continue, error info is in ctx + } + + uv_fs_t* req = &req_wrap_sync.req; + uv_dir_t* dir = static_cast<uv_dir_t*>(req->ptr); + DirHandle* handle = DirHandle::New(env, dir); + + args.GetReturnValue().Set(handle->object().As<Value>()); + } +} + +void Initialize(Local<Object> target, + Local<Value> unused, + Local<Context> context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + + env->SetMethod(target, "opendir", OpenDir); + + // Create FunctionTemplate for DirHandle + Local<FunctionTemplate> dir = env->NewFunctionTemplate(DirHandle::New); + dir->Inherit(AsyncWrap::GetConstructorTemplate(env)); + env->SetProtoMethod(dir, "read", DirHandle::Read); + env->SetProtoMethod(dir, "close", DirHandle::Close); + Local<ObjectTemplate> dirt = dir->InstanceTemplate(); + dirt->SetInternalFieldCount(DirHandle::kDirHandleFieldCount); + Local<String> handleString = + FIXED_ONE_BYTE_STRING(isolate, "DirHandle"); + dir->SetClassName(handleString); + target + ->Set(context, handleString, + dir->GetFunction(env->context()).ToLocalChecked()) + .FromJust(); + env->set_dir_instance_template(dirt); +} + +} // namespace fs_dir + +} // end namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs_dir, node::fs_dir::Initialize) diff --git a/src/node_dir.h b/src/node_dir.h new file mode 100644 index 0000000000..e099fe5510 --- /dev/null +++ b/src/node_dir.h @@ -0,0 +1,60 @@ +#ifndef SRC_NODE_DIR_H_ +#define SRC_NODE_DIR_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_file.h" +#include "node.h" +#include "req_wrap-inl.h" + +namespace node { + +namespace fs_dir { + +// Needed to propagate `uv_dir_t`. +class DirHandle : public AsyncWrap { + public: + static constexpr int kDirHandleFieldCount = 1; + + static DirHandle* New(Environment* env, uv_dir_t* dir); + ~DirHandle() override; + + static void New(const v8::FunctionCallbackInfo<v8::Value>& args); + static void Open(const v8::FunctionCallbackInfo<Value>& args); + static void Read(const v8::FunctionCallbackInfo<Value>& args); + static void Close(const v8::FunctionCallbackInfo<Value>& args); + + inline uv_dir_t* dir() { return dir_; } + AsyncWrap* GetAsyncWrap() { return this; } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackFieldWithSize("dir", sizeof(*dir_)); + } + + SET_MEMORY_INFO_NAME(DirHandle) + SET_SELF_SIZE(DirHandle) + + DirHandle(const DirHandle&) = delete; + DirHandle& operator=(const DirHandle&) = delete; + DirHandle(const DirHandle&&) = delete; + DirHandle& operator=(const DirHandle&&) = delete; + + private: + DirHandle(Environment* env, v8::Local<v8::Object> obj, uv_dir_t* dir); + + // Synchronous close that emits a warning + void GCClose(); + + uv_dir_t* dir_; + uv_dirent_t dirent_; + bool closing_ = false; + bool closed_ = false; +}; + +} // namespace fs_dir + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_DIR_H_ diff --git a/src/node_file.cc b/src/node_file.cc index 195757f43f..8b6a90989f 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -52,11 +52,9 @@ namespace node { namespace fs { using v8::Array; -using v8::BigUint64Array; using v8::Context; using v8::DontDelete; using v8::EscapableHandleScope; -using v8::Float64Array; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -678,94 +676,6 @@ void AfterScanDirWithTypes(uv_fs_t* req) { req_wrap->Resolve(result); } - -// This class is only used on sync fs calls. -// For async calls FSReqCallback is used. -class FSReqWrapSync { - public: - FSReqWrapSync() = default; - ~FSReqWrapSync() { uv_fs_req_cleanup(&req); } - uv_fs_t req; - - FSReqWrapSync(const FSReqWrapSync&) = delete; - FSReqWrapSync& operator=(const FSReqWrapSync&) = delete; -}; - -// Returns nullptr if the operation fails from the start. -template <typename Func, typename... Args> -inline FSReqBase* AsyncDestCall(Environment* env, - FSReqBase* req_wrap, - const FunctionCallbackInfo<Value>& args, - const char* syscall, const char* dest, size_t len, - enum encoding enc, uv_fs_cb after, Func fn, Args... fn_args) { - CHECK_NOT_NULL(req_wrap); - req_wrap->Init(syscall, dest, len, enc); - int err = req_wrap->Dispatch(fn, fn_args..., after); - if (err < 0) { - uv_fs_t* uv_req = req_wrap->req(); - uv_req->result = err; - uv_req->path = nullptr; - after(uv_req); // after may delete req_wrap if there is an error - req_wrap = nullptr; - } else { - req_wrap->SetReturnValue(args); - } - - return req_wrap; -} - -// Returns nullptr if the operation fails from the start. -template <typename Func, typename... Args> -inline FSReqBase* AsyncCall(Environment* env, - FSReqBase* req_wrap, - const FunctionCallbackInfo<Value>& args, - const char* syscall, enum encoding enc, - uv_fs_cb after, Func fn, Args... fn_args) { - return AsyncDestCall(env, req_wrap, args, - syscall, nullptr, 0, enc, - after, fn, fn_args...); -} - -// Template counterpart of SYNC_CALL, except that it only puts -// the error number and the syscall in the context instead of -// creating an error in the C++ land. -// ctx must be checked using value->IsObject() before being passed. -template <typename Func, typename... Args> -inline int SyncCall(Environment* env, Local<Value> ctx, FSReqWrapSync* req_wrap, - const char* syscall, Func fn, Args... args) { - env->PrintSyncTrace(); - int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr); - if (err < 0) { - Local<Context> context = env->context(); - Local<Object> ctx_obj = ctx.As<Object>(); - Isolate* isolate = env->isolate(); - ctx_obj->Set(context, - env->errno_string(), - Integer::New(isolate, err)).Check(); - ctx_obj->Set(context, - env->syscall_string(), - OneByteString(isolate, syscall)).Check(); - } - return err; -} - -// TODO(addaleax): Currently, callers check the return value and assume -// that nullptr indicates a synchronous call, rather than a failure. -// Failure conditions should be disambiguated and handled appropriately. -inline FSReqBase* GetReqWrap(Environment* env, Local<Value> value, - bool use_bigint = false) { - if (value->IsObject()) { - return Unwrap<FSReqBase>(value.As<Object>()); - } else if (value->StrictEquals(env->fs_use_promises_symbol())) { - if (use_bigint) { - return FSReqPromise<AliasedBigUint64Array>::New(env, use_bigint); - } else { - return FSReqPromise<AliasedFloat64Array>::New(env, use_bigint); - } - } - return nullptr; -} - void Access(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); diff --git a/src/node_file.h b/src/node_file.h index 2ea5af025d..84f4032cc2 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -4,7 +4,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include "node.h" +#include "aliased_buffer.h" #include "stream_base.h" +#include "memory_tracker-inl.h" #include "req_wrap-inl.h" #include <iostream> @@ -450,6 +452,93 @@ int MKDirpSync(uv_loop_t* loop, const std::string& path, int mode, uv_fs_cb cb = nullptr); + +class FSReqWrapSync { + public: + FSReqWrapSync() = default; + ~FSReqWrapSync() { uv_fs_req_cleanup(&req); } + uv_fs_t req; + + FSReqWrapSync(const FSReqWrapSync&) = delete; + FSReqWrapSync& operator=(const FSReqWrapSync&) = delete; +}; + +// TODO(addaleax): Currently, callers check the return value and assume +// that nullptr indicates a synchronous call, rather than a failure. +// Failure conditions should be disambiguated and handled appropriately. +inline FSReqBase* GetReqWrap(Environment* env, v8::Local<v8::Value> value, + bool use_bigint = false) { + if (value->IsObject()) { + return Unwrap<FSReqBase>(value.As<Object>()); + } else if (value->StrictEquals(env->fs_use_promises_symbol())) { + if (use_bigint) { + return FSReqPromise<AliasedBigUint64Array>::New(env, use_bigint); + } else { + return FSReqPromise<AliasedFloat64Array>::New(env, use_bigint); + } + } + return nullptr; +} + +// Returns nullptr if the operation fails from the start. +template <typename Func, typename... Args> +inline FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap, + const v8::FunctionCallbackInfo<Value>& args, + const char* syscall, const char* dest, + size_t len, enum encoding enc, uv_fs_cb after, + Func fn, Args... fn_args) { + CHECK_NOT_NULL(req_wrap); + req_wrap->Init(syscall, dest, len, enc); + int err = req_wrap->Dispatch(fn, fn_args..., after); + if (err < 0) { + uv_fs_t* uv_req = req_wrap->req(); + uv_req->result = err; + uv_req->path = nullptr; + after(uv_req); // after may delete req_wrap if there is an error + req_wrap = nullptr; + } else { + req_wrap->SetReturnValue(args); + } + + return req_wrap; +} + +// Returns nullptr if the operation fails from the start. +template <typename Func, typename... Args> +inline FSReqBase* AsyncCall(Environment* env, + FSReqBase* req_wrap, + const v8::FunctionCallbackInfo<Value>& args, + const char* syscall, enum encoding enc, + uv_fs_cb after, Func fn, Args... fn_args) { + return AsyncDestCall(env, req_wrap, args, + syscall, nullptr, 0, enc, + after, fn, fn_args...); +} + +// Template counterpart of SYNC_CALL, except that it only puts +// the error number and the syscall in the context instead of +// creating an error in the C++ land. +// ctx must be checked using value->IsObject() before being passed. +template <typename Func, typename... Args> +inline int SyncCall(Environment* env, v8::Local<v8::Value> ctx, + FSReqWrapSync* req_wrap, const char* syscall, + Func fn, Args... args) { + env->PrintSyncTrace(); + int err = fn(env->event_loop(), &(req_wrap->req), args..., nullptr); + if (err < 0) { + v8::Local<Context> context = env->context(); + v8::Local<Object> ctx_obj = ctx.As<v8::Object>(); + v8::Isolate* isolate = env->isolate(); + ctx_obj->Set(context, + env->errno_string(), + v8::Integer::New(isolate, err)).Check(); + ctx_obj->Set(context, + env->syscall_string(), + OneByteString(isolate, syscall)).Check(); + } + return err; +} + } // namespace fs } // namespace node diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index ae69f5aef0..7ca48c49cb 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -17,6 +17,7 @@ const expectedModules = new Set([ 'Internal Binding contextify', 'Internal Binding credentials', 'Internal Binding fs', + 'Internal Binding fs_dir', 'Internal Binding inspector', 'Internal Binding module_wrap', 'Internal Binding native_module', @@ -42,6 +43,7 @@ const expectedModules = new Set([ 'NativeModule internal/encoding', 'NativeModule internal/errors', 'NativeModule internal/fixed_queue', + 'NativeModule internal/fs/dir', 'NativeModule internal/fs/utils', 'NativeModule internal/idna', 'NativeModule internal/linkedlist', diff --git a/test/parallel/test-fs-opendir.js b/test/parallel/test-fs-opendir.js new file mode 100644 index 0000000000..c9a6d657ed --- /dev/null +++ b/test/parallel/test-fs-opendir.js @@ -0,0 +1,174 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const tmpdir = require('../common/tmpdir'); + +const testDir = tmpdir.path; +const files = ['empty', 'files', 'for', 'just', 'testing']; + +// Make sure tmp directory is clean +tmpdir.refresh(); + +// Create the necessary files +files.forEach(function(filename) { + fs.closeSync(fs.openSync(path.join(testDir, filename), 'w')); +}); + +function assertDirent(dirent) { + assert(dirent instanceof fs.Dirent); + assert.strictEqual(dirent.isFile(), true); + assert.strictEqual(dirent.isDirectory(), false); + assert.strictEqual(dirent.isSocket(), false); + assert.strictEqual(dirent.isBlockDevice(), false); + assert.strictEqual(dirent.isCharacterDevice(), false); + assert.strictEqual(dirent.isFIFO(), false); + assert.strictEqual(dirent.isSymbolicLink(), false); +} + +const dirclosedError = { + code: 'ERR_DIR_CLOSED' +}; + +// Check the opendir Sync version +{ + const dir = fs.opendirSync(testDir); + const entries = files.map(() => { + const dirent = dir.readSync(); + assertDirent(dirent); + return dirent.name; + }); + assert.deepStrictEqual(files, entries.sort()); + + // dir.read should return null when no more entries exist + assert.strictEqual(dir.readSync(), null); + + // check .path + assert.strictEqual(dir.path, testDir); + + dir.closeSync(); + + assert.throws(() => dir.readSync(), dirclosedError); + assert.throws(() => dir.closeSync(), dirclosedError); +} + +// Check the opendir async version +fs.opendir(testDir, common.mustCall(function(err, dir) { + assert.ifError(err); + dir.read(common.mustCall(function(err, dirent) { + assert.ifError(err); + + // Order is operating / file system dependent + assert(files.includes(dirent.name), `'files' should include ${dirent}`); + assertDirent(dirent); + + dir.close(common.mustCall(function(err) { + assert.ifError(err); + })); + })); +})); + +// opendir() on file should throw ENOTDIR +assert.throws(function() { + fs.opendirSync(__filename); +}, /Error: ENOTDIR: not a directory/); + +fs.opendir(__filename, common.mustCall(function(e) { + assert.strictEqual(e.code, 'ENOTDIR'); +})); + +[false, 1, [], {}, null, undefined].forEach((i) => { + common.expectsError( + () => fs.opendir(i, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); + common.expectsError( + () => fs.opendirSync(i), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); +}); + +// Promise-based tests +async function doPromiseTest() { + // Check the opendir Promise version + const dir = await fs.promises.opendir(testDir); + const entries = []; + + let i = files.length; + while (i--) { + const dirent = await dir.read(); + entries.push(dirent.name); + assertDirent(dirent); + } + + assert.deepStrictEqual(files, entries.sort()); + + // dir.read should return null when no more entries exist + assert.strictEqual(await dir.read(), null); + + await dir.close(); +} +doPromiseTest().then(common.mustCall()); + +// Async iterator +async function doAsyncIterTest() { + const entries = []; + for await (const dirent of await fs.promises.opendir(testDir)) { + entries.push(dirent.name); + assertDirent(dirent); + } + + assert.deepStrictEqual(files, entries.sort()); + + // Automatically closed during iterator +} +doAsyncIterTest().then(common.mustCall()); + +// Async iterators should do automatic cleanup + +async function doAsyncIterBreakTest() { + const dir = await fs.promises.opendir(testDir); + for await (const dirent of dir) { // eslint-disable-line no-unused-vars + break; + } + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterBreakTest().then(common.mustCall()); + +async function doAsyncIterReturnTest() { + const dir = await fs.promises.opendir(testDir); + await (async function() { + for await (const dirent of dir) { // eslint-disable-line no-unused-vars + return; + } + })(); + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterReturnTest().then(common.mustCall()); + +async function doAsyncIterThrowTest() { + const dir = await fs.promises.opendir(testDir); + try { + for await (const dirent of dir) { // eslint-disable-line no-unused-vars + throw new Error('oh no'); + } + } catch (err) { + if (err.message !== 'oh no') { + throw err; + } + } + + await assert.rejects(async () => dir.read(), dirclosedError); +} +doAsyncIterThrowTest().then(common.mustCall()); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index bef6b050ff..cc823cc17f 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -306,3 +306,10 @@ if (process.features.inspector && common.isMainThread) { { v8.getHeapSnapshot().destroy(); } + +// DIRHANDLE +{ + const dirBinding = internalBinding('fs_dir'); + const handle = dirBinding.opendir('./', 'utf8', undefined, {}); + testInitialized(handle, 'DirHandle'); +} diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 4c3ec0503e..6cc60b0403 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -72,6 +72,7 @@ const customTypesMap = { 'EventEmitter': 'events.html#events_class_eventemitter', 'FileHandle': 'fs.html#fs_class_filehandle', + 'fs.Dir': 'fs.html#fs_class_fs_dir', 'fs.Dirent': 'fs.html#fs_class_fs_dirent', 'fs.FSWatcher': 'fs.html#fs_class_fs_fswatcher', 'fs.ReadStream': 'fs.html#fs_class_fs_readstream', |