From 329fc78e4919231bf76771797878f7b0db0f73ac Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 21 Jan 2018 10:21:25 -0800 Subject: [PATCH] fs: add initial set of fs.promises APIs Initial set of fs.promises APIs with documentation and one benchmark. PR-URL: https://github.com/nodejs/node/pull/18297 Reviewed-By: Anna Henningsen Reviewed-By: Matteo Collina --- benchmark/fs/bench-stat-promise.js | 28 + doc/api/fs.md | 992 ++++++++++++++++++ lib/fs.js | 492 ++++++++- src/env.h | 3 +- src/node_file.cc | 392 ++++--- src/node_file.h | 12 +- test/parallel/test-fs-filehandle.js | 25 + test/parallel/test-fs-promises-writefile.js | 40 + test/parallel/test-fs-promises.js | 150 +++ test/sequential/test-async-wrap-getasyncid.js | 10 + 10 files changed, 1957 insertions(+), 187 deletions(-) create mode 100644 benchmark/fs/bench-stat-promise.js create mode 100644 test/parallel/test-fs-filehandle.js create mode 100644 test/parallel/test-fs-promises-writefile.js create mode 100644 test/parallel/test-fs-promises.js diff --git a/benchmark/fs/bench-stat-promise.js b/benchmark/fs/bench-stat-promise.js new file mode 100644 index 00000000000000..adc0ed4965fdfd --- /dev/null +++ b/benchmark/fs/bench-stat-promise.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); + +const bench = common.createBenchmark(main, { + n: [20e4], + statType: ['fstat', 'lstat', 'stat'] +}); + +async function run(n, statType) { + const arg = statType === 'fstat' ? + await fs.promises.open(__filename, 'r') : __filename; + let remaining = n; + bench.start(); + while (remaining-- > 0) + await fs.promises[statType](arg); + bench.end(n); + + if (typeof arg.close === 'function') + await arg.close(); +} + +function main(conf) { + const n = conf.n >>> 0; + const statType = conf.statType; + run(n, statType).catch(console.log); +} diff --git a/doc/api/fs.md b/doc/api/fs.md index 00c6d946a0b355..6fe323081348b3 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -3212,6 +3212,997 @@ changes: Synchronous versions of [`fs.write()`][]. Returns the number of bytes written. +## fs Promises API + +> Stability: 1 - Experimental + +The `fs.promises` API provides an alternative set of asynchronous file system +methods that return `Promise` objects rather than using callbacks. The +API is accessible via `fs.promises`. + +### class: FileHandle + + +A `FileHandle` object is a wrapper for a numeric file descriptor. +Instances of `FileHandle` are distinct from numeric file descriptors +in that, if the `FileHandle` is not explicitly closed using the +`filehandle.close()` method, they will automatically close the file descriptor +and will emit a process warning, thereby helping to prevent memory leaks. + +Instances of the `FileHandle` object are created internally by the +`fs.promises.open()` method. + +Unlike callback-based such as `fs.fstat()`, `fs.fchown()`, `fs.fchmod()`, +`fs.ftruncate()`, `fs.read()`, and `fs.write()`, operations -- all of which +use a simple numeric file descriptor, all `fs.promises.*` variations use the +`FileHandle` class in order to help protect against accidental leaking of +unclosed file descriptors after a `Promise` is resolved or rejected. + +#### filehandle.fd + + +Value: {number} The numeric file descriptor managed by the `FileHandle` object. + +#### filehandle.appendFile(data, options) + +* `data` {string|Buffer} +* `options` {Object|string} + * `encoding` {string|null} **Default:** `'utf8'` + * `mode` {integer} **Default:** `0o666` + * `flag` {string} **Default:** `'a'` +* Returns: {Promise} + +Asynchronously append data to this file, creating the file if it does not yet +exist. `data` can be a string or a [`Buffer`][]. The `Promise` will be +resolved with no arguments upon success. + +If `options` is a string, then it specifies the encoding. + +The `FileHandle` must have been opened for appending. + +#### filehandle.chmod(mode) + +* `mode` {integer} +* Returns: {Promise} + +Modifies the permissions on the file. The `Promise` is resolved with no +arguments upon success. + +#### filehandle.chown(uid, gid) + +* `uid` {integer} +* `gid` {integer} +* Returns: {Promise} + +Changes the ownership of the file then resolves the `Promise` with no arguments +upon success. + +#### filehandle.close() + + +* Returns: {Promise} A `Promise` that will be resolved once the underlying + file descriptor is closed, or will be rejected if an error occurs while + closing. + +Closes the file descriptor. + +```js +async function openAndClose() { + let filehandle; + try { + filehandle = await fs.promises.open('thefile.txt', 'r'); + } finally { + if (filehandle !== undefined) + await filehandle.close(); + } +} +``` + +#### filehandle.datasync() + +* Returns: {Promise} + +Asynchronous fdatasync(2). The `Promise` is resolved with no arguments upon +success. + +#### filehandle.read(buffer, offset, length, position) + +* `buffer` {Buffer|Uint8Array} +* `offset` {integer} +* `length` {integer} +* `position` {integer} +* Returns: {Promise} + +Read data from the file. + +`buffer` is the buffer that the data will be written to. + +`offset` is the offset in the buffer to start writing at. + +`length` is an integer specifying the number of bytes to read. + +`position` is an argument specifying where to begin reading from in the file. +If `position` is `null`, data will be read from the current file position, +and the file position will be updated. +If `position` is an integer, the file position will remain unchanged. + +Following successful read, the `Promise` is resolved with an object with a +`bytesRead` property specifying the number of bytes read, and a `buffer` property +that is a reference to the passed in `buffer` argument. + +#### filehandle.readFile(options) + +* `options` {Object|string} + * `encoding` {string|null} **Default:** `null` + * `flag` {string} **Default:** `'r'` +* Returns: {Promise} + +Asynchronously reads the entire contents of a file. + +The `Promise` is resolved with the contents of the file. If no encoding is +specified (using `options.encoding`), the data is returned as a `Buffer` +object. Otherwise, the data will be a string. + +If `options` is a string, then it specifies the encoding. + +When the `path` is a directory, the behavior of `fs.promises.readFile()` is +platform-specific. On macOS, Linux, and Windows, the promise will be rejected +with an error. On FreeBSD, a representation of the directory's contents will be +returned. + +The `FileHandle` has to support reading. + +#### filehandle.stat() + +* Returns: {Promise} + +Retrieves the [`fs.Stats`][] for the file. + +#### filehandle.sync() + +* Returns: {Promise} + +Asynchronous fsync(2). The `Promise` is resolved with no arguments upon +success. + +#### filehandle.truncate(len = 0) + +* `len` {integer} **Default:** `0` +* Returns: {Promise} + +Truncates the file then resolves the `Promise` with no arguments upon success. + +If the file was larger than `len` bytes, only the first `len` bytes will be +retained in the file. + +For example, the following program retains only the first four bytes of the +file: + +```js +console.log(fs.readFileSync('temp.txt', 'utf8')); +// Prints: Node.js + +async function doTruncate() { + const fd = await fs.promises.open('temp.txt', 'r+'); + await fs.promises.ftruncate(fd, 4); + console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints: Node +} + +doTruncate().catch(console.error); +``` + +If the file previously was shorter than `len` bytes, it is extended, and the +extended part is filled with null bytes ('\0'). For example, + +```js +console.log(fs.readFileSync('temp.txt', 'utf8')); +// Prints: Node.js + +async function doTruncate() { + const fd = await fs.promises.open('temp.txt', 'r+'); + await fs.promises.ftruncate(fd, 10); + console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints Node.js\0\0\0 +} + +doTruncate().catch(console.error); +``` + +The last three bytes are null bytes ('\0'), to compensate the over-truncation. + +#### filehandle.utimes(atime, mtime) + +* `atime` {number|string|Date} +* `mtime` {number|string|Date}` +* Returns: {Promise} + +Change the file system timestamps of the object referenced by the `FileHandle` +then resolves the `Promise` with no arguments upon success. + +This function does not work on AIX versions before 7.1, it will resolve the +`Promise` with an error using code `UV_ENOSYS`. + +#### filehandle.write(buffer, offset, length, position) + +* `buffer` {Buffer|Uint8Array} +* `offset` {integer} +* `length` {integer} +* `position` {integer} +* Returns: {Promise} + +Write `buffer` to the file. + +The `Promise` is resolved with an object containing a `bytesWritten` property +identifying the number of bytes written, and a `buffer` property containing +a reference to the `buffer` written. + +`offset` determines the part of the buffer to be written, and `length` is +an integer specifying the number of bytes to write. + +`position` refers to the offset from the beginning of the file where this data +should be written. If `typeof position !== 'number'`, the data will be written +at the current position. See pwrite(2). + +It is unsafe to use `filehandle.write()` multiple times on the same file +without waiting for the `Promise` to be resolved (or rejected). For this +scenario, `fs.createWriteStream` is strongly recommended. + +On Linux, positional writes do not work when the file is opened in append mode. +The kernel ignores the position argument and always appends the data to +the end of the file. + +#### filehandle.writeFile(data, options) + +* `data` {string|Buffer|Uint8Array} +* `options` {Object|string} + * `encoding` {string|null} **Default:** `'utf8'` + * `mode` {integer} **Default:** `0o666` + * `flag` {string} **Default:** `'w'` +* Returns: {Promise} + +Asynchronously writes data to a file, replacing the file if it already exists. +`data` can be a string or a buffer. The `Promise` will be resolved with no +arguments upon success. + +The `encoding` option is ignored if `data` is a buffer. It defaults +to `'utf8'`. + +If `options` is a string, then it specifies the encoding. + +The `FileHandle` has to support writing. + +It is unsafe to use `filehandle.writeFile()` multiple times on the same file +without waiting for the `Promise` to be resolved (or rejected). + +### fs.promises.access(path[, mode]) + + +* `path` {string|Buffer|URL} +* `mode` {integer} **Default:** `fs.constants.F_OK` +* Returns: {Promise} + +Tests a user's permissions for the file or directory specified by `path`. +The `mode` argument is an optional integer that specifies the accessibility +checks to be performed. The following constants define the possible values of +`mode`. It is possible to create a mask consisting of the bitwise OR of two or +more values (e.g. `fs.constants.W_OK | fs.constants.R_OK`). + +* `fs.constants.F_OK` - `path` is visible to the calling process. This is useful +for determining if a file exists, but says nothing about `rwx` permissions. +Default if no `mode` is specified. +* `fs.constants.R_OK` - `path` can be read by the calling process. +* `fs.constants.W_OK` - `path` can be written by the calling process. +* `fs.constants.X_OK` - `path` can be executed by the calling process. This has +no effect on Windows (will behave like `fs.constants.F_OK`). + +If the accessibility check is successful, the `Promise` is resolved with no +value. If any of the accessibility checks fail, the `Promise` is rejected +with an `Error` object. The following example checks if the file +`/etc/passwd` can be read and written by the current process. + +```js +fs.promises.access('/etc/passwd', fs.constants.R_OK | fs.constants.W_OK) + .then(() => console.log('can access')) + .catch(() => console.error('cannot access')); +``` + +Using `fs.promises.access()` to check for the accessibility of a file before +calling `fs.promises.open()` is not recommended. Doing so introduces a race +condition, since other processes may change the file's state between the two +calls. Instead, user code should open/read/write the file directly and handle +the error raised if the file is not accessible. + +### fs.promises.appendFile(file, data[, options]) + + +* `file` {string|Buffer|[FileHandle][]} filename or `FileHandle` +* `data` {string|Buffer} +* `options` {Object|string} + * `encoding` {string|null} **Default:** `'utf8'` + * `mode` {integer} **Default:** `0o666` + * `flag` {string} **Default:** `'a'` +* Returns: {Promise} + +Asynchronously append data to a file, creating the file if it does not yet +exist. `data` can be a string or a [`Buffer`][]. The `Promise` will be +resolved with no arguments upon success. + +If `options` is a string, then it specifies the encoding. + +The `file` may be specified as a `FileHandle` that has been opened +for appending (using `fs.promises.open()`). + +### fs.promises.chmod(path, mode) + + +* `path` {string|Buffer|URL} +* `mode` {integer} +* Returns: {Promise} + +Changes the permissions of a file then resolves the `Promise` with no +arguments upon succces. + +### fs.promises.chown(path, uid, gid) + + +* `path` {string|Buffer|URL} +* `uid` {integer} +* `gid` {integer} +* Returns: {Promise} + +Changes the ownership of a file then resolves the `Promise` with no arguments +upon success. + +### fs.promises.copyFile(src, dest[, flags]) + + +* `src` {string|Buffer|URL} source filename to copy +* `dest` {string|Buffer|URL} destination filename of the copy operation +* `flags` {number} modifiers for copy operation. **Default:** `0` +* Returns: {Promise} + +Asynchronously copies `src` to `dest`. By default, `dest` is overwritten if it +already exists. The `Promise` will be resolved with no arguments upon success. + +Node.js makes no guarantees about the atomicity of the copy operation. If an +error occurs after the destination file has been opened for writing, Node.js +will attempt to remove the destination. + +`flags` is an optional integer that specifies the behavior +of the copy operation. The only supported flag is `fs.constants.COPYFILE_EXCL`, +which causes the copy operation to fail if `dest` already exists. + +Example: + +```js +const fs = require('fs'); + +// destination.txt will be created or overwritten by default. +fs.promises.copyFile('source.txt', 'destination.txt') + .then(() => console.log('source.txt was copied to destination.txt')) + .catch(() => console.log('The file could not be copied')); +``` + +If the third argument is a number, then it specifies `flags`, as shown in the +following example. + +```js +const fs = require('fs'); +const { COPYFILE_EXCL } = fs.constants; + +// By using COPYFILE_EXCL, the operation will fail if destination.txt exists. +fs.promises.copyFile('source.txt', 'destination.txt', COPYFILE_EXCL) + .then(() => console.log('source.txt was copied to destination.txt')) + .catch(() => console.log('The file could not be copied')); +``` + +### fs.promises.fchmod(filehandle, mode) + + +* `filehandle` {[FileHandle][]} +* `mode` {integer} +* Returns: {Promise} + +Asynchronous fchmod(2). The `Promise` is resolved with no arguments upon +success. + +### fs.promises.fchown(filehandle, uid, gid) + + +* `filehandle` {[FileHandle][]} +* `uid` {integer} +* `gid` {integer} +* Returns: {Promise} + +Changes the ownership of the file represented by `filehandle` then resolves +the `Promise` with no arguments upon success. + +### fs.promises.fdatasync(filehandle) + + +* `filehandle` {[FileHandle][]} +* Returns: {Promise} + +Asynchronous fdatasync(2). The `Promise` is resolved with no arguments upon +success. + +### fs.promises.fstat(filehandle) + + +* `filehandle` {[FileHandle][]} +* Returns: {Promise} + +Retrieves the [`fs.Stats`][] for the given `filehandle`. + +### fs.promises.fsync(filehandle) + + +* `filehandle` {[FileHandle][]} +* Returns: {Promise} + +Asynchronous fsync(2). The `Promise` is resolved with no arguments upon +success. + +### fs.promises.ftruncate(filehandle[, len]) + + +* `filehandle` {[FileHandle][]} +* `len` {integer} **Default:** `0` +* Returns: {Promise} + +Truncates the file represented by `filehandle` then resolves the `Promise` +with no arguments upon success. + +If the file referred to by the `FileHandle` was larger than `len` bytes, only +the first `len` bytes will be retained in the file. + +For example, the following program retains only the first four bytes of the +file: + +```js +console.log(fs.readFileSync('temp.txt', 'utf8')); +// Prints: Node.js + +async function doTruncate() { + const fd = await fs.promises.open('temp.txt', 'r+'); + await fs.promises.ftruncate(fd, 4); + console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints: Node +} + +doTruncate().catch(console.error); +``` + +If the file previously was shorter than `len` bytes, it is extended, and the +extended part is filled with null bytes ('\0'). For example, + +```js +console.log(fs.readFileSync('temp.txt', 'utf8')); +// Prints: Node.js + +async function doTruncate() { + const fd = await fs.promises.open('temp.txt', 'r+'); + await fs.promises.ftruncate(fd, 10); + console.log(fs.readFileSync('temp.txt', 'utf8')); // Prints Node.js\0\0\0 +} + +doTruncate().catch(console.error); +``` + +The last three bytes are null bytes ('\0'), to compensate the over-truncation. + +### fs.promises.futimes(filehandle, atime, mtime) + + +* `filehandle` {[FileHandle][]} +* `atime` {number|string|Date} +* `mtime` {number|string|Date}` +* Returns: {Promise} + +Change the file system timestamps of the object referenced by the supplied +`FileHandle` then resolves the `Promise` with no arguments upon success. + +This function does not work on AIX versions before 7.1, it will resolve the +`Promise` with an error using code `UV_ENOSYS`. + +### fs.promises.lchmod(path, mode) + + +* `path` {string|Buffer} +* `mode` {integer} +* Returns: {Promise} + +Changes the permissions on a symbolic link then resolves the `Promise` with +no arguments upon success. This method is only implemented on macOS. + +### fs.promises.lchown(path, uid, gid) + + +* `path` {string|Buffer} +* `uid` {integer} +* `gid` {integer} +* Returns: {Promise} + +Changes the ownership on a symbolic link then resolves the `Promise` with +no arguments upon success. This method is only implemented on macOS. + +### fs.promises.link(existingPath, newPath) + + +* `existingPath` {string|Buffer|URL} +* `newPath` {string|Buffer|URL} +* Returns: {Promise} + +Asynchronous link(2). The `Promise` is resolved with no arguments upon success. + +### fs.promises.lstat(path) + + +* `path` {string|Buffer|URL} +* Returns: {Promise} + +Asynchronous lstat(2). The `Promise` is resolved with the [`fs.Stats`][] object +for the given symbolic link `path`. + +### fs.promises.mkdir(path[, mode]) + + +* `path` {string|Buffer|URL} +* `mode` {integer} **Default:** `0o777` +* Returns: {Promise} + +Asynchronously creates a directory then resolves the `Promise` with no +arguments upon success. + +### fs.promises.mkdtemp(prefix[, options]) + + +* `prefix` {string} +* `options` {string|Object} + * `encoding` {string} **Default:** `'utf8'` +* Returns: {Promise} + +Creates a unique temporary directory then resolves the `Promise` with the +created folder path. A unique directory name is generated by appending six +random characters to the end of the provided `prefix`. + +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use. + +Example: + +```js +fs.promises.mkdtemp(path.join(os.tmpdir(), 'foo-')) + .catch(console.error); +``` + +The `fs.mkdtemp()` method will append the six randomly selected characters +directly to the `prefix` string. For instance, given a directory `/tmp`, if the +intention is to create a temporary directory *within* `/tmp`, the `prefix` +*must* end with a trailing platform-specific path separator +(`require('path').sep`). + +### fs.promises.open(path, flags[, mode]) + + +* `path` {string|Buffer|URL} +* `flags` {string|number} +* `mode` {integer} **Default:** `0o666` +* Return: {Promise} + +Asynchronous file open that returns a `Promise` that, when resolved, yields a +`FileHandle` object. See open(2). + +The `flags` argument can be: + +* `'r'` - Open file for reading. +An exception occurs if the file does not exist. + +* `'r+'` - Open file for reading and writing. +An exception occurs if the file does not exist. + +* `'rs+'` - Open file for reading and writing in synchronous mode. Instructs + the operating system to bypass the local file system cache. + + This is primarily useful for opening files on NFS mounts as it allows skipping + the potentially stale local cache. It has a very real impact on I/O + performance so using this flag is not recommended unless it is needed. + + Note that this does not turn `fs.promises.open()` into a synchronous blocking + call. + +* `'w'` - Open file for writing. +The file is created (if it does not exist) or truncated (if it exists). + +* `'wx'` - Like `'w'` but fails if `path` exists. + +* `'w+'` - Open file for reading and writing. +The file is created (if it does not exist) or truncated (if it exists). + +* `'wx+'` - Like `'w+'` but fails if `path` exists. + +* `'a'` - Open file for appending. +The file is created if it does not exist. + +* `'ax'` - Like `'a'` but fails if `path` exists. + +* `'a+'` - Open file for reading and appending. +The file is created if it does not exist. + +* `'ax+'` - Like `'a+'` but fails if `path` exists. + +`mode` sets the file mode (permission and sticky bits), but only if the file was +created. It defaults to `0o666` (readable and writable). + +The exclusive flag `'x'` (`O_EXCL` flag in open(2)) ensures that `path` is newly +created. On POSIX systems, `path` is considered to exist even if it is a symlink +to a non-existent file. The exclusive flag may or may not work with network file +systems. + +`flags` can also be a number as documented by open(2); commonly used constants +are available from `fs.constants`. On Windows, flags are translated to +their equivalent ones where applicable, e.g. `O_WRONLY` to `FILE_GENERIC_WRITE`, +or `O_EXCL|O_CREAT` to `CREATE_NEW`, as accepted by CreateFileW. + +On Linux, positional writes don't work when the file is opened in append mode. +The kernel ignores the position argument and always appends the data to +the end of the file. + +The behavior of `fs.promises.open()` is platform-specific for some +flags. As such, opening a directory on macOS and Linux with the `'a+'` flag will +return an error. In contrast, on Windows and FreeBSD, a `FileHandle` will be +returned. + +Some characters (`< > : " / \ | ? *`) are reserved under Windows as documented +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]. + +*Note:* On Windows, opening an existing hidden file using the `w` flag (e.g. +using `fs.promises.open()`) will fail with `EPERM`. Existing hidden +files can be opened for writing with the `r+` flag. A call to +`fs.promises.ftruncate()` can be used to reset the file contents. + +### fs.promises.read(filehandle, buffer, offset, length, position) + + +* `filehandle` {[FileHandle][]} +* `buffer` {Buffer|Uint8Array} +* `offset` {integer} +* `length` {integer} +* `position` {integer} +* Returns: {Promise} + +Read data from the file specified by `filehandle`. + +`buffer` is the buffer that the data will be written to. + +`offset` is the offset in the buffer to start writing at. + +`length` is an integer specifying the number of bytes to read. + +`position` is an argument specifying where to begin reading from in the file. +If `position` is `null`, data will be read from the current file position, +and the file position will be updated. +If `position` is an integer, the file position will remain unchanged. + +Following successful read, the `Promise` is resolved with an object with a +`bytesRead` property specifying the number of bytes read, and a `buffer` property +that is a reference to the passed in `buffer` argument. + +### fs.promises.readdir(path[, options]) + + +* `path` {string|Buffer|URL} +* `options` {string|Object} + * `encoding` {string} **Default:** `'utf8'` +* Returns: {Promise} + +Reads the contents of a directory then resolves the `Promise` with an array +of the names of the files in the directory excludiing `'.'` and `'..'`. + +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the filenames. If the `encoding` is set to `'buffer'`, the filenames returned +will be passed as `Buffer` objects. + +### fs.promises.readFile(path[, options]) + + +* `path` {string|Buffer|URL|[FileHandle][]} filename or `FileHandle` +* `options` {Object|string} + * `encoding` {string|null} **Default:** `null` + * `flag` {string} **Default:** `'r'` +* Returns: {Promise} + +Asynchronously reads the entire contents of a file. + +The `Promise` is resolved with the contents of the file. If no encoding is +specified (using `options.encoding`), the data is returned as a `Buffer` +object. Otherwise, the data will be a string. + +If `options` is a string, then it specifies the encoding. + +When the `path` is a directory, the behavior of `fs.promises.readFile()` is +platform-specific. On macOS, Linux, and Windows, the promise will be rejected +with an error. On FreeBSD, a representation of the directory's contents will be +returned. + +Any specified `FileHandle` has to support reading. + +### fs.promises.readlink(path[, options]) + + +* `path` {string|Buffer|URL} +* `options` {string|Object} + * `encoding` {string} **Default:** `'utf8'` +* Returns: {Promise} + +Asynchronous readlink(2). The `Promise` is resolved with the `linkString` upon +success. + +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the link path returned. If the `encoding` is set to `'buffer'`, the link path +returned will be passed as a `Buffer` object. + +### fs.promises.realpath(path[, options]) + + +* `path` {string|Buffer|URL} +* `options` {string|Object} + * `encoding` {string} **Default:** `'utf8'` +* Returns: {Promise} + +Determines the actual location of `path` using the same semantics as the +`fs.realpath.native()` function then resolves the `Promise` with the resolved +path. + +Only paths that can be converted to UTF8 strings are supported. + +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the path. If the `encoding` is set to `'buffer'`, the path returned will be +passed as a `Buffer` object. + +On Linux, when Node.js is linked against musl libc, the procfs file system must +be mounted on `/proc` in order for this function to work. Glibc does not have +this restriction. + +### fs.promises.rename(oldPath, newPath) + + +* `oldPath` {string|Buffer|URL} +* `newPath` {string|Buffer|URL} +* Returns: {Promise} + +Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments +upon success. + +### fs.promises.rmdir(path) + + +* `path` {string|Buffer|URL} +* Returns: {Promise} + +Removes the directory identified by `path` then resolves the `Promise` with +no arguments upon success. + +Using `fs.promises.rmdir()` on a file (not a directory) results in the +`Promise` being rejected with an `ENOENT` error on Windows and an `ENOTDIR` +error on POSIX. + +### fs.promises.stat(path) + + +* `path` {string|Buffer|URL} +* Returns: {Promise} + +The `Promise` is resolved with the [`fs.Stats`][] object for the given `path`. + +### fs.promises.symlink(target, path[, type]) + + +* `target` {string|Buffer|URL} +* `path` {string|Buffer|URL} +* `type` {string} **Default:** `'file'` +* Returns: {Promise} + +Creates a symbolic link then resolves the `Promise` with no arguments upon +success. + +The `type` argument is only used on Windows platforms and can be one of `'dir'`, +`'file'`, or `'junction'` (default is `'file'`). Note that Windows junction +points require the destination path to be absolute. When using `'junction'`, +the `target` argument will automatically be normalized to absolute path. + +### fs.promises.truncate(path[, len]) + + +* `path` {string|Buffer} +* `len` {integer} **Default:** `0` +* Returns: {Promise} + +Truncates the `path` then resolves the `Promise` with no arguments upon +success. The `path` *must* be a string or `Buffer`. + +### fs.promises.unlink(path) + + +* `path` {string|Buffer|URL} +* Returns: {Promise} + +Asynchronous unlink(2). The `Promise` is resolved with no arguments upon +success. + +### fs.promises.utimes(path, atime, mtime) + + +* `path` {string|Buffer|URL} +* `atime` {number|string|Date} +* `mtime` {number|string|Date} +* Returns: {Promise} + +Change the file system timestamps of the object referenced by `path` then +resolves the `Promise` with no arguments upon success. + +The `atime` and `mtime` arguments follow these rules: +- Values can be either numbers representing Unix epoch time, `Date`s, or a + numeric string like `'123456789.0'`. +- If the value can not be converted to a number, or is `NaN`, `Infinity` or + `-Infinity`, an `Error` will be thrown. + +### fs.promises.write(filehandle, buffer[, offset[, length[, position]]]) + + +* `filehandle` {[FileHandle][]} +* `buffer` {Buffer|Uint8Array} +* `offset` {integer} +* `length` {integer} +* `position` {integer} +* Returns: {Promise} + +Write `buffer` to the file specified by `filehandle`. + +The `Promise` is resolved with an object containing a `bytesWritten` property +identifying the number of bytes written, and a `buffer` property containing +a reference to the `buffer` written. + +`offset` determines the part of the buffer to be written, and `length` is +an integer specifying the number of bytes to write. + +`position` refers to the offset from the beginning of the file where this data +should be written. If `typeof position !== 'number'`, the data will be written +at the current position. See pwrite(2). + +It is unsafe to use `fs.promises.write()` multiple times on the same file +without waiting for the `Promise` to be resolved (or rejected). For this +scenario, `fs.createWriteStream` is strongly recommended. + +On Linux, positional writes do not work when the file is opened in append mode. +The kernel ignores the position argument and always appends the data to +the end of the file. + +### fs.promises.writeFile(file, data[, options]) + + +* `file` {string|Buffer|[FileHandle][]} filename or `FileHandle` +* `data` {string|Buffer|Uint8Array} +* `options` {Object|string} + * `encoding` {string|null} **Default:** `'utf8'` + * `mode` {integer} **Default:** `0o666` + * `flag` {string} **Default:** `'w'` +* Returns: {Promise} + +Asynchronously writes data to a file, replacing the file if it already exists. +`data` can be a string or a buffer. The `Promise` will be resolved with no +arguments upon success. + +The `encoding` option is ignored if `data` is a buffer. It defaults +to `'utf8'`. + +If `options` is a string, then it specifies the encoding. + +Any specified `FileHandle` has to support writing. + +It is unsafe to use `fs.promises.writeFile()` multiple times on the same file +without waiting for the `Promise` to be resolved (or rejected). + + ## FS Constants The following constants are exported by `fs.constants`. @@ -3478,6 +4469,7 @@ The following constants are meant for use with the [`fs.Stats`][] object's [`util.promisify()`]: util.html#util_util_promisify_original [Caveats]: #fs_caveats [Common System Errors]: errors.html#errors_common_system_errors +[FileHandle]: #fs_class_filehandle [FS Constants]: #fs_fs_constants_1 [MDN-Date]: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date [MDN-Number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type diff --git a/lib/fs.js b/lib/fs.js index 0b9cf0cc9b8190..2bac855d0f3b48 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -53,6 +53,9 @@ Object.defineProperty(exports, 'constants', { value: constants }); +const kHandle = Symbol('handle'); +const { kUsePromises } = binding; + const kMinPoolSpace = 128; const { kMaxLength } = require('buffer'); @@ -343,13 +346,11 @@ Stats.prototype.isSocket = function() { const statValues = binding.statValues; -function statsFromValues() { - return new Stats(statValues[0], statValues[1], statValues[2], statValues[3], - statValues[4], statValues[5], - statValues[6] < 0 ? undefined : statValues[6], statValues[7], - statValues[8], statValues[9] < 0 ? undefined : statValues[9], - statValues[10], statValues[11], statValues[12], - statValues[13]); +function statsFromValues(stats = statValues) { + return new Stats(stats[0], stats[1], stats[2], stats[3], stats[4], stats[5], + stats[6] < 0 ? undefined : stats[6], stats[7], stats[8], + stats[9] < 0 ? undefined : stats[9], stats[10], stats[11], + stats[12], stats[13]); } // Don't allow mode to accidentally be overwritten. @@ -2654,3 +2655,480 @@ Object.defineProperty(fs, 'SyncWriteStream', { set: internalUtil.deprecate((val) => { SyncWriteStream = val; }, 'fs.SyncWriteStream is deprecated.', 'DEP0061') }); + +// Promises API + +class FileHandle { + constructor(filehandle) { + this[kHandle] = filehandle; + } + + getAsyncId() { + return this[kHandle].getAsyncId(); + } + + get fd() { + return this[kHandle].fd; + } + + appendFile(data, options) { + return promises.appendFile(this, data, options); + } + + chmod(mode) { + return promises.fchmod(this, mode); + } + + chown(uid, gid) { + return promises.fchown(this, uid, gid); + } + + datasync() { + return promises.fdatasync(this); + } + + sync() { + return promises.fsync(this); + } + + + read(buffer, offset, length, position) { + return promises.read(this, buffer, offset, length, position); + } + + readFile(options) { + return promises.readFile(this, options); + } + + stat() { + return promises.fstat(this); + } + + truncate(len = 0) { + return promises.ftruncate(this, len); + } + + utimes(atime, mtime) { + return promises.futimes(this, atime, mtime); + } + + write(buffer, offset, length, position) { + return promises.write(this, buffer, offset, length, position); + } + + writeFile(data, options) { + return promises.writeFile(this, data, options); + } + + close() { + return this[kHandle].close(); + } +} + + +function validateFileHandle(handle) { + if (!(handle instanceof FileHandle)) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'filehandle', 'FileHandle'); +} + +async function writeFileHandle(filehandle, data, options) { + let buffer = isUint8Array(data) ? + data : Buffer.from('' + data, options.encoding || 'utf8'); + let remaining = buffer.length; + if (remaining === 0) return; + do { + const { bytesWritten } = + await promises.write(filehandle, buffer, 0, + Math.min(16384, buffer.length)); + remaining -= bytesWritten; + buffer = buffer.slice(bytesWritten); + } while (remaining > 0); +} + +async function readFileHandle(filehandle, options) { + const statFields = await binding.fstat(filehandle.fd, kUsePromises); + + let size; + if ((statFields[1/*mode*/] & S_IFMT) === S_IFREG) { + size = statFields[8/*size*/]; + } else { + size = 0; + } + + if (size === 0) + return Buffer.alloc(0); + + if (size > kMaxLength) + throw new errors.RangeError('ERR_BUFFER_TOO_LARGE'); + + const chunks = []; + const chunkSize = Math.min(size, 16384); + const buf = Buffer.alloc(chunkSize); + let read = 0; + do { + const { bytesRead, buffer } = + await promises.read(filehandle, buf, 0, buf.length); + read = bytesRead; + if (read > 0) + chunks.push(buffer.slice(0, read)); + } while (read === chunkSize); + + return Buffer.concat(chunks); +} + +// All of the functions in fs.promises are defined as async in order to +// ensure that errors thrown cause promise rejections rather than being +// thrown synchronously +const promises = { + async access(path, mode = fs.F_OK) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + + mode = mode | 0; + return binding.access(pathModule.toNamespacedPath(path), mode, + kUsePromises); + }, + + async copyFile(src, dest, flags) { + handleError((src = getPathFromURL(src))); + handleError((dest = getPathFromURL(dest))); + nullCheck(src); + nullCheck(dest); + validatePath(src, 'src'); + validatePath(dest, 'dest'); + flags = flags | 0; + return binding.copyFile(pathModule.toNamespacedPath(src), + pathModule.toNamespacedPath(dest), + flags, kUsePromises); + }, + + // Note that unlike fs.open() which uses numeric file descriptors, + // promises.open() uses the fs.FileHandle class. + async open(path, flags, mode) { + mode = modeNum(mode, 0o666); + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + validateUint32(mode, 'mode'); + return new FileHandle( + await binding.openFileHandle(pathModule.toNamespacedPath(path), + stringToFlags(flags), + mode, kUsePromises)); + }, + + async read(handle, buffer, offset, length, position) { + validateFileHandle(handle); + validateBuffer(buffer); + + offset |= 0; + length |= 0; + + if (length === 0) + return { bytesRead: length, buffer }; + + validateOffsetLengthRead(offset, length, buffer.length); + + if (!isUint32(position)) + position = -1; + + const bytesRead = (await binding.read(handle.fd, buffer, offset, length, + position, kUsePromises)) || 0; + + return { bytesRead, buffer }; + }, + + async write(handle, buffer, offset, length, position) { + validateFileHandle(handle); + + if (buffer.length === 0) + return { bytesWritten: 0, buffer }; + + if (isUint8Array(buffer)) { + if (typeof offset !== 'number') + offset = 0; + if (typeof length !== 'number') + length = buffer.length - offset; + if (typeof position !== 'number') + position = null; + validateOffsetLengthWrite(offset, length, buffer.byteLength); + const bytesWritten = + (await binding.writeBuffer(handle.fd, buffer, offset, + length, position, kUsePromises)) || 0; + return { bytesWritten, buffer }; + } + + if (typeof buffer !== 'string') + buffer += ''; + if (typeof position !== 'function') { + if (typeof offset === 'function') { + position = offset; + offset = null; + } else { + position = length; + } + length = 'utf8'; + } + const bytesWritten = (await binding.writeString(handle.fd, buffer, offset, + length, kUsePromises)) || 0; + return { bytesWritten, buffer }; + }, + + async rename(oldPath, newPath) { + handleError((oldPath = getPathFromURL(oldPath))); + handleError((newPath = getPathFromURL(newPath))); + nullCheck(oldPath); + nullCheck(newPath); + validatePath(oldPath, 'oldPath'); + validatePath(newPath, 'newPath'); + return binding.rename(pathModule.toNamespacedPath(oldPath), + pathModule.toNamespacedPath(newPath), + kUsePromises); + }, + + async truncate(path, len = 0) { + return promises.ftruncate(await promises.open(path, 'r+'), len); + }, + + async ftruncate(handle, len = 0) { + validateFileHandle(handle); + validateLen(len); + len = Math.max(0, len); + return binding.ftruncate(handle.fd, len, kUsePromises); + }, + + async rmdir(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises); + }, + + async fdatasync(handle) { + validateFileHandle(handle); + return binding.fdatasync(handle.fd, kUsePromises); + }, + + async fsync(handle) { + validateFileHandle(handle); + return binding.fsync(handle.fd, kUsePromises); + }, + + async mkdir(path, mode) { + mode = modeNum(mode, 0o777); + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + validateUint32(mode, 'mode'); + return binding.mkdir(pathModule.toNamespacedPath(path), mode, kUsePromises); + }, + + async readdir(path, options) { + options = getOptions(options, {}); + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return binding.readdir(pathModule.toNamespacedPath(path), + options.encoding, kUsePromises); + }, + + async readlink(path, options) { + options = getOptions(options, {}); + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path, 'oldPath'); + return binding.readlink(pathModule.toNamespacedPath(path), + options.encoding, kUsePromises); + }, + + async symlink(target, path, type_) { + const type = (typeof type_ === 'string' ? type_ : null); + handleError((target = getPathFromURL(target))); + handleError((path = getPathFromURL(path))); + nullCheck(target); + nullCheck(path); + validatePath(target, 'target'); + validatePath(path); + return binding.symlink(preprocessSymlinkDestination(target, type, path), + pathModule.toNamespacedPath(path), + stringToSymlinkType(type), + kUsePromises); + }, + + async fstat(handle) { + validateFileHandle(handle); + return statsFromValues(await binding.fstat(handle.fd, kUsePromises)); + }, + + async lstat(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return statsFromValues( + await binding.lstat(pathModule.toNamespacedPath(path), kUsePromises)); + }, + + async stat(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return statsFromValues( + await binding.stat(pathModule.toNamespacedPath(path), kUsePromises)); + }, + + async link(existingPath, newPath) { + handleError((existingPath = getPathFromURL(existingPath))); + handleError((newPath = getPathFromURL(newPath))); + nullCheck(existingPath); + nullCheck(newPath); + validatePath(existingPath, 'existingPath'); + validatePath(newPath, 'newPath'); + return binding.link(pathModule.toNamespacedPath(existingPath), + pathModule.toNamespacedPath(newPath), + kUsePromises); + }, + + async unlink(path) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return binding.unlink(pathModule.toNamespacedPath(path), kUsePromises); + }, + + async fchmod(handle, mode) { + mode = modeNum(mode); + validateFileHandle(handle); + validateUint32(mode, 'mode'); + if (mode < 0 || mode > 0o777) + throw new errors.RangeError('ERR_OUT_OF_RANGE', 'mode'); + return binding.fchmod(handle.fd, mode, kUsePromises); + }, + + async chmod(path, mode) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + mode = modeNum(mode); + validateUint32(mode, 'mode'); + return binding.chmod(pathModule.toNamespacedPath(path), mode, kUsePromises); + }, + + async lchmod(path, mode) { + if (constants.O_SYMLINK !== undefined) { + const fd = await promises.open(path, + constants.O_WRONLY | constants.O_SYMLINK); + return promises.fschmod(fd, mode).finally(fd.close.bind(fd)); + } + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); + }, + + async lchown(path, uid, gid) { + if (constants.O_SYMLINK !== undefined) { + const fd = await promises.open(path, + constants.O_WRONLY | constants.O_SYMLINK); + return promises.fschmod(fd, uid, gid).finally(fd.close.bind(fd)); + } + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED'); + }, + + async fchown(handle, uid, gid) { + validateFileHandle(handle); + validateUint32(uid, 'uid'); + validateUint32(gid, 'gid'); + return binding.fchown(handle.fd, uid, gid, kUsePromises); + }, + + async chown(path, uid, gid) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + validateUint32(uid, 'uid'); + validateUint32(gid, 'gid'); + return binding.chown(pathModule.toNamespacedPath(path), + uid, gid, kUsePromises); + }, + + async utimes(path, atime, mtime) { + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return binding.utimes(pathModule.toNamespacedPath(path), + toUnixTimestamp(atime), + toUnixTimestamp(mtime), + kUsePromises); + }, + + async futimes(handle, atime, mtime) { + validateFileHandle(handle); + atime = toUnixTimestamp(atime, 'atime'); + mtime = toUnixTimestamp(mtime, 'mtime'); + return binding.futimes(handle.fd, atime, mtime, kUsePromises); + }, + + async realpath(path, options) { + options = getOptions(options, {}); + handleError((path = getPathFromURL(path))); + nullCheck(path); + validatePath(path); + return binding.realpath(path, options.encoding, kUsePromises); + }, + + async mkdtemp(prefix, options) { + options = getOptions(options, {}); + if (!prefix || typeof prefix !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'prefix', + 'string', + prefix); + } + nullCheck(prefix); + return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises); + }, + + async writeFile(path, data, options) { + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); + const flag = options.flag || 'w'; + + if (path instanceof FileHandle) + return writeFileHandle(path, data, options); + + const fd = await promises.open(path, flag, options.mode); + return writeFileHandle(fd, data, options).finally(fd.close.bind(fd)); + }, + + async appendFile(path, data, options) { + options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'a' }); + options = copyObject(options); + options.flag = options.flag || 'a'; + return promises.writeFile(path, data, options); + }, + + async readFile(path, options) { + options = getOptions(options, { flag: 'r' }); + + if (path instanceof FileHandle) + return readFileHandle(path, options); + + const fd = await promises.open(path, options.flag, 0o666); + return readFileHandle(fd, options).finally(fd.close.bind(fd)); + } +}; + +let warn = true; + +// TODO(jasnell): Exposing this as a property with a getter works fine with +// commonjs but is going to be problematic for named imports support under +// ESM. A different approach will have to be followed there. +Object.defineProperty(fs, 'promises', { + configurable: true, + enumerable: true, + get() { + if (warn) { + warn = false; + process.emitWarning('The fs.promises API is experimental', + 'ExperimentalWarning'); + } + return promises; + } +}); diff --git a/src/env.h b/src/env.h index 262ccdef387471..0503c7f2477116 100644 --- a/src/env.h +++ b/src/env.h @@ -242,7 +242,6 @@ class ModuleWrap; V(sni_context_string, "sni_context") \ V(stack_string, "stack") \ V(status_string, "status") \ - V(statfields_string, "statFields") \ V(stdio_string, "stdio") \ V(subject_string, "subject") \ V(subjectaltname_string, "subjectaltname") \ @@ -284,6 +283,7 @@ class ModuleWrap; V(context, v8::Context) \ V(domain_callback, v8::Function) \ V(fd_constructor_template, v8::ObjectTemplate) \ + V(fsreqpromise_constructor_template, v8::ObjectTemplate) \ V(fdclose_constructor_template, v8::ObjectTemplate) \ V(host_import_module_dynamically_callback, v8::Function) \ V(host_initialize_import_meta_object_callback, v8::Function) \ @@ -313,6 +313,7 @@ class ModuleWrap; V(vm_parsing_context_symbol, v8::Symbol) \ V(url_constructor_function, v8::Function) \ V(write_wrap_constructor_function, v8::Function) \ + V(fs_use_promises_symbol, v8::Symbol) class Environment; diff --git a/src/node_file.cc b/src/node_file.cc index c4fc802669ea57..8f016ccf02695b 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -85,6 +85,7 @@ namespace fs { using v8::Array; using v8::Context; +using v8::EscapableHandleScope; using v8::Float64Array; using v8::Function; using v8::FunctionCallbackInfo; @@ -100,6 +101,7 @@ using v8::Object; using v8::ObjectTemplate; using v8::Promise; using v8::String; +using v8::Symbol; using v8::Undefined; using v8::Value; @@ -169,7 +171,7 @@ inline void FileHandle::Close() { // 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 garbage collector is a bug. + // this because not explicitly closing the FileHandle is a bug. env()->SetUnrefImmediate([](Environment* env, void* data) { char msg[70]; err_detail* detail = static_cast(data); @@ -182,22 +184,22 @@ inline void FileHandle::Close() { } void FileHandle::CloseReq::Resolve() { - InternalCallbackScope callback_scope(this); HandleScope scope(env()->isolate()); + InternalCallbackScope callback_scope(this); Local promise = promise_.Get(env()->isolate()); Local resolver = promise.As(); - resolver->Resolve(env()->context(), Undefined(env()->isolate())); + resolver->Resolve(env()->context(), Undefined(env()->isolate())).FromJust(); } void FileHandle::CloseReq::Reject(Local reason) { - InternalCallbackScope callback_scope(this); HandleScope scope(env()->isolate()); + InternalCallbackScope callback_scope(this); Local promise = promise_.Get(env()->isolate()); Local resolver = promise.As(); - resolver->Reject(env()->context(), reason); + resolver->Reject(env()->context(), reason).FromJust(); } -FileHandle* FileHandle::CloseReq::fd() { +FileHandle* FileHandle::CloseReq::file_handle() { HandleScope scope(env()->isolate()); Local val = ref_.Get(env()->isolate()); Local obj = val.As(); @@ -209,9 +211,9 @@ FileHandle* FileHandle::CloseReq::fd() { // there was a problem closing the fd. This is the preferred mechanism for // closing the FD object even tho the object will attempt to close // automatically on gc. -inline Local FileHandle::ClosePromise() { +inline MaybeLocal FileHandle::ClosePromise() { Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); + EscapableHandleScope scope(isolate); Local context = env()->context(); auto maybe_resolver = Promise::Resolver::New(context); CHECK(!maybe_resolver.IsEmpty()); @@ -223,12 +225,12 @@ inline Local FileHandle::ClosePromise() { auto AfterClose = [](uv_fs_t* req) { CloseReq* close = static_cast(req->data); CHECK_NE(close, nullptr); - close->fd()->closing_ = false; + close->file_handle()->closing_ = false; Isolate* isolate = close->env()->isolate(); if (req->result < 0) { close->Reject(UVException(isolate, req->result, "close")); } else { - close->fd()->closed_ = true; + close->file_handle()->closed_ = true; close->Resolve(); } delete close; @@ -241,15 +243,16 @@ inline Local FileHandle::ClosePromise() { } } else { // Already closed. Just reject the promise immediately - resolver->Reject(context, UVException(isolate, UV_EBADF, "close")); + resolver->Reject(context, UVException(isolate, UV_EBADF, "close")) + .FromJust(); } - return promise; + return scope.Escape(promise); } void FileHandle::Close(const FunctionCallbackInfo& args) { FileHandle* fd; ASSIGN_OR_RETURN_UNWRAP(&fd, args.Holder()); - args.GetReturnValue().Set(fd->ClosePromise()); + args.GetReturnValue().Set(fd->ClosePromise().ToLocalChecked()); } @@ -273,24 +276,31 @@ void FSReqWrap::Resolve(Local value) { MakeCallback(env()->oncomplete_string(), arraysize(argv), argv); } +void FSReqWrap::SetReturnValue(const FunctionCallbackInfo& args) { + args.GetReturnValue().SetUndefined(); +} + +void FSReqPromise::SetReturnValue(const FunctionCallbackInfo& args) { + Local context = env()->context(); + args.GetReturnValue().Set( + object()->Get(context, env()->promise_string()).ToLocalChecked()); +} + void NewFSReqWrap(const FunctionCallbackInfo& args) { CHECK(args.IsConstructCall()); Environment* env = Environment::GetCurrent(args.GetIsolate()); new FSReqWrap(env, args.This()); } -FSReqPromise::FSReqPromise(Environment* env, Local req) - : FSReqBase(env, req, AsyncWrap::PROVIDER_FSREQPROMISE) { +FSReqPromise::FSReqPromise(Environment* env) + : FSReqBase(env, + env->fsreqpromise_constructor_template() + ->NewInstance(env->context()).ToLocalChecked(), + AsyncWrap::PROVIDER_FSREQPROMISE), + stats_field_array_(env->isolate(), 14) { auto resolver = Promise::Resolver::New(env->context()).ToLocalChecked(); - req->Set(env->context(), env->promise_string(), - resolver.As()).FromJust(); - - Local ab = - ArrayBuffer::New(env->isolate(), statFields_, - sizeof(double) * 14); - object()->Set(env->context(), - env->statfields_string(), - Float64Array::New(ab, 0, 14)).FromJust(); + object()->Set(env->context(), env->promise_string(), + resolver.As()).FromJust(); } FSReqPromise::~FSReqPromise() { @@ -300,44 +310,35 @@ FSReqPromise::~FSReqPromise() { void FSReqPromise::Reject(Local reject) { finished_ = true; - InternalCallbackScope callback_scope(this); HandleScope scope(env()->isolate()); + InternalCallbackScope callback_scope(this); Local value = object()->Get(env()->context(), env()->promise_string()).ToLocalChecked(); CHECK(value->IsPromise()); Local promise = value.As(); Local resolver = promise.As(); - resolver->Reject(env()->context(), reject); + resolver->Reject(env()->context(), reject).FromJust(); } void FSReqPromise::FillStatsArray(const uv_stat_t* stat) { - node::FillStatsArray(statFields_, stat); + node::FillStatsArray(&stats_field_array_, stat); } void FSReqPromise::ResolveStat() { - Resolve( - object()->Get(env()->context(), - env()->statfields_string()).ToLocalChecked()); + Resolve(stats_field_array_.GetJSArray()); } void FSReqPromise::Resolve(Local value) { finished_ = true; - InternalCallbackScope callback_scope(this); HandleScope scope(env()->isolate()); + InternalCallbackScope callback_scope(this); Local val = object()->Get(env()->context(), env()->promise_string()).ToLocalChecked(); CHECK(val->IsPromise()); - Local promise = val.As(); - Local resolver = promise.As(); - resolver->Resolve(env()->context(), value); -} - -void NewFSReqPromise(const FunctionCallbackInfo& args) { - CHECK(args.IsConstructCall()); - Environment* env = Environment::GetCurrent(args.GetIsolate()); - new FSReqPromise(env, args.This()); + Local resolver = val.As(); + resolver->Resolve(env()->context(), value).FromJust(); } FSReqAfterScope::FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req) @@ -519,11 +520,10 @@ class fs_req_wrap { template inline FSReqBase* AsyncDestCall(Environment* env, + FSReqBase* req_wrap, const FunctionCallbackInfo& args, const char* syscall, const char* dest, size_t len, enum encoding enc, uv_fs_cb after, Func fn, Args... fn_args) { - Local req = args[args.Length() - 1].As(); - FSReqBase* req_wrap = Unwrap(req); CHECK_NE(req_wrap, nullptr); req_wrap->Init(syscall, dest, len, enc); int err = fn(env->event_loop(), req_wrap->req(), fn_args..., after); @@ -544,10 +544,11 @@ inline FSReqBase* AsyncDestCall(Environment* env, template inline FSReqBase* AsyncCall(Environment* env, + FSReqBase* req_wrap, const FunctionCallbackInfo& args, const char* syscall, enum encoding enc, uv_fs_cb after, Func fn, Args... fn_args) { - return AsyncDestCall(env, args, + return AsyncDestCall(env, req_wrap, args, syscall, nullptr, 0, enc, after, fn, fn_args...); } @@ -576,10 +577,10 @@ inline int SyncCall(Environment* env, Local ctx, fs_req_wrap* req_wrap, } #define SYNC_DEST_CALL(func, path, dest, ...) \ - fs_req_wrap req_wrap; \ + fs_req_wrap sync_wrap; \ env->PrintSyncTrace(); \ int err = uv_fs_ ## func(env->event_loop(), \ - &req_wrap.req, \ + &sync_wrap.req, \ __VA_ARGS__, \ nullptr); \ if (err < 0) { \ @@ -589,10 +590,19 @@ inline int SyncCall(Environment* env, Local ctx, fs_req_wrap* req_wrap, #define SYNC_CALL(func, path, ...) \ SYNC_DEST_CALL(func, path, nullptr, __VA_ARGS__) \ -#define SYNC_REQ req_wrap.req +#define SYNC_REQ sync_wrap.req #define SYNC_RESULT err +inline FSReqBase* GetReqWrap(Environment* env, Local value) { + if (value->IsObject()) { + return Unwrap(value.As()); + } else if (value->StrictEquals(env->fs_use_promises_symbol())) { + return new FSReqPromise(env); + } + return nullptr; +} + void Access(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args.GetIsolate()); HandleScope scope(env->isolate()); @@ -606,10 +616,11 @@ void Access(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); - if (args[2]->IsObject()) { // access(path, mode, req) - CHECK_EQ(argc, 3); - AsyncCall(env, args, "access", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { // access(path, mode, req) + AsyncCall(env, req_wrap, args, "access", UTF8, AfterNoArgs, uv_fs_access, *path, mode); + req_wrap->SetReturnValue(args); } else { // access(path, mode, undefined, ctx) CHECK_EQ(argc, 4); fs_req_wrap req_wrap; @@ -627,10 +638,11 @@ void Close(const FunctionCallbackInfo& args) { CHECK(args[0]->IsInt32()); int fd = args[0].As()->Value(); - if (args[1]->IsObject()) { // close(fd, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "close", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { // close(fd, req) + AsyncCall(env, req_wrap, args, "close", UTF8, AfterNoArgs, uv_fs_close, fd); + req_wrap->SetReturnValue(args); } else { // close(fd, undefined, ctx) CHECK_EQ(argc, 3); fs_req_wrap req_wrap; @@ -732,10 +744,11 @@ static void Stat(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); - if (args[1]->IsObject()) { // stat(path, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "stat", UTF8, AfterStat, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { // stat(path, req) + AsyncCall(env, req_wrap, args, "stat", UTF8, AfterStat, uv_fs_stat, *path); + req_wrap->SetReturnValue(args); } else { // stat(path, undefined, ctx) CHECK_EQ(argc, 3); fs_req_wrap req_wrap; @@ -756,10 +769,11 @@ static void LStat(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); - if (args[1]->IsObject()) { // lstat(path, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "lstat", UTF8, AfterStat, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { // lstat(path, req) + AsyncCall(env, req_wrap, args, "lstat", UTF8, AfterStat, uv_fs_lstat, *path); + req_wrap->SetReturnValue(args); } else { // lstat(path, undefined, ctx) CHECK_EQ(argc, 3); fs_req_wrap req_wrap; @@ -780,10 +794,11 @@ static void FStat(const FunctionCallbackInfo& args) { CHECK(args[0]->IsInt32()); int fd = args[0].As()->Value(); - if (args[1]->IsObject()) { // fstat(fd, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "fstat", UTF8, AfterStat, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { // fstat(fd, req) + AsyncCall(env, req_wrap, args, "fstat", UTF8, AfterStat, uv_fs_fstat, fd); + req_wrap->SetReturnValue(args); } else { // fstat(fd, undefined, ctx) CHECK_EQ(argc, 3); fs_req_wrap req_wrap; @@ -809,14 +824,14 @@ static void Symlink(const FunctionCallbackInfo& args) { CHECK(args[2]->IsInt32()); int flags = args[2].As()->Value(); - if (args[3]->IsObject()) { // symlink(target, path, flags, req) - CHECK_EQ(args.Length(), 4); - AsyncDestCall(env, args, "symlink", *path, path.length(), UTF8, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { // symlink(target, path, flags, req) + AsyncDestCall(env, req_wrap, args, "symlink", *path, path.length(), UTF8, AfterNoArgs, uv_fs_symlink, *target, *path, flags); } else { // symlink(target, path, flags, undefinec, ctx) CHECK_EQ(argc, 5); - fs_req_wrap req_wrap; - SyncCall(env, args[4], &req_wrap, "symlink", + fs_req_wrap req; + SyncCall(env, args[4], &req, "symlink", uv_fs_symlink, *target, *path, flags); } } @@ -833,14 +848,15 @@ static void Link(const FunctionCallbackInfo& args) { BufferValue dest(env->isolate(), args[1]); CHECK_NE(*dest, nullptr); - if (args[2]->IsObject()) { // link(src, dest, req) - CHECK_EQ(argc, 3); - AsyncDestCall(env, args, "link", *dest, dest.length(), UTF8, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { // link(src, dest, req) + AsyncDestCall(env, req_wrap, args, "link", *dest, dest.length(), UTF8, AfterNoArgs, uv_fs_link, *src, *dest); - } else { // link(src, dest, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { // link(src, dest) CHECK_EQ(argc, 4); - fs_req_wrap req_wrap; - SyncCall(env, args[3], &req_wrap, "link", + fs_req_wrap req; + SyncCall(env, args[3], &req, "link", uv_fs_link, *src, *dest); } } @@ -856,19 +872,20 @@ static void ReadLink(const FunctionCallbackInfo& args) { const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); - if (args[2]->IsObject()) { // readlink(path, encoding, req) - CHECK_EQ(argc, 3); - AsyncCall(env, args, "readlink", encoding, AfterStringPtr, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { // readlink(path, encoding, req) + AsyncCall(env, req_wrap, args, "readlink", encoding, AfterStringPtr, uv_fs_readlink, *path); - } else { // readlink(path, encoding, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 4); - fs_req_wrap req_wrap; - int err = SyncCall(env, args[3], &req_wrap, "readlink", + fs_req_wrap req; + int err = SyncCall(env, args[3], &req, "readlink", uv_fs_readlink, *path); if (err) { return; // syscall failed, no need to continue, error info is in ctx } - const char* link_path = static_cast(req_wrap.req.ptr); + const char* link_path = static_cast(req.req.ptr); Local error; MaybeLocal rc = StringBytes::Encode(env->isolate(), @@ -896,15 +913,15 @@ static void Rename(const FunctionCallbackInfo& args) { BufferValue new_path(env->isolate(), args[1]); CHECK_NE(*new_path, nullptr); - if (args[2]->IsObject()) { // rename(old_path, new_path, req) - CHECK_EQ(argc, 3); - AsyncDestCall(env, args, "rename", *new_path, new_path.length(), + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncDestCall(env, req_wrap, args, "rename", *new_path, new_path.length(), UTF8, AfterNoArgs, uv_fs_rename, *old_path, *new_path); - } else { // rename(old_path, new_path, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 4); - fs_req_wrap req_wrap; - SyncCall(env, args[3], &req_wrap, "rename", - uv_fs_rename, *old_path, *new_path); + fs_req_wrap req; + SyncCall(env, args[3], &req, "rename", uv_fs_rename, *old_path, *new_path); } } @@ -920,15 +937,15 @@ static void FTruncate(const FunctionCallbackInfo& args) { CHECK(args[1]->IsNumber()); const int64_t len = args[1].As()->Value(); - if (args[2]->IsObject()) { // ftruncate(fd, len, req) - CHECK_EQ(argc, 3); - AsyncCall(env, args, "ftruncate", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "ftruncate", UTF8, AfterNoArgs, uv_fs_ftruncate, fd, len); - } else { // ftruncate(fd, len, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 4); - fs_req_wrap req_wrap; - SyncCall(env, args[3], &req_wrap, "ftruncate", - uv_fs_ftruncate, fd, len); + fs_req_wrap req; + SyncCall(env, args[3], &req, "ftruncate", uv_fs_ftruncate, fd, len); } } @@ -941,15 +958,15 @@ static void Fdatasync(const FunctionCallbackInfo& args) { CHECK(args[0]->IsInt32()); const int fd = args[0].As()->Value(); - if (args[1]->IsObject()) { // fdatasync(fd, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "fdatasync", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "fdatasync", UTF8, AfterNoArgs, uv_fs_fdatasync, fd); - } else { // fdatasync(fd, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 3); - fs_req_wrap req_wrap; - SyncCall(env, args[2], &req_wrap, "fdatasync", - uv_fs_fdatasync, fd); + fs_req_wrap req; + SyncCall(env, args[2], &req, "fdatasync", uv_fs_fdatasync, fd); } } @@ -962,15 +979,15 @@ static void Fsync(const FunctionCallbackInfo& args) { CHECK(args[0]->IsInt32()); const int fd = args[0].As()->Value(); - if (args[1]->IsObject()) { // fsync(fd, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "fsync", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "fsync", UTF8, AfterNoArgs, uv_fs_fsync, fd); - } else { // fsync(fd, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 3); - fs_req_wrap req_wrap; - SyncCall(env, args[2], &req_wrap, "fsync", - uv_fs_fsync, fd); + fs_req_wrap req; + SyncCall(env, args[2], &req, "fsync", uv_fs_fsync, fd); } } @@ -983,15 +1000,15 @@ static void Unlink(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); - if (args[1]->IsObject()) { // unlink(fd, req) - CHECK_EQ(argc, 2); - AsyncCall(env, args, "unlink", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "unlink", UTF8, AfterNoArgs, uv_fs_unlink, *path); - } else { // unlink(fd, undefined, ctx) + req_wrap->SetReturnValue(args); + } else { CHECK_EQ(argc, 3); - fs_req_wrap req_wrap; - SyncCall(env, args[2], &req_wrap, "unlink", - uv_fs_unlink, *path); + fs_req_wrap req; + SyncCall(env, args[2], &req, "unlink", uv_fs_unlink, *path); } } @@ -1003,10 +1020,11 @@ static void RMDir(const FunctionCallbackInfo& args) { BufferValue path(env->isolate(), args[0]); CHECK_NE(*path, nullptr); - if (args[1]->IsObject()) { - CHECK_EQ(args.Length(), 2); - AsyncCall(env, args, "rmdir", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[1]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "rmdir", UTF8, AfterNoArgs, uv_fs_rmdir, *path); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(rmdir, *path, *path) } @@ -1023,10 +1041,11 @@ static void MKDir(const FunctionCallbackInfo& args) { int mode = static_cast(args[1]->Int32Value()); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "mkdir", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "mkdir", UTF8, AfterNoArgs, uv_fs_mkdir, *path, mode); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(mkdir, *path, *path, mode) } @@ -1040,10 +1059,11 @@ static void RealPath(const FunctionCallbackInfo& args) { const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "realpath", encoding, AfterStringPtr, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "realpath", encoding, AfterStringPtr, uv_fs_realpath, *path); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(realpath, *path, *path); const char* link_path = static_cast(SYNC_REQ.ptr); @@ -1071,10 +1091,11 @@ static void ReadDir(const FunctionCallbackInfo& args) { const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "scandir", encoding, AfterScanDir, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "scandir", encoding, AfterScanDir, uv_fs_scandir, *path, 0 /*flags*/); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(scandir, *path, *path, 0 /*flags*/) @@ -1135,10 +1156,11 @@ static void Open(const FunctionCallbackInfo& args) { int flags = args[1]->Int32Value(context).ToChecked(); int mode = args[2]->Int32Value(context).ToChecked(); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "open", UTF8, AfterInteger, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "open", UTF8, AfterInteger, uv_fs_open, *path, flags, mode); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(open, *path, *path, flags, mode) args.GetReturnValue().Set(SYNC_RESULT); @@ -1159,10 +1181,11 @@ static void OpenFileHandle(const FunctionCallbackInfo& args) { int flags = args[1]->Int32Value(context).ToChecked(); int mode = args[2]->Int32Value(context).ToChecked(); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "open", UTF8, AfterOpenFileHandle, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "open", UTF8, AfterOpenFileHandle, uv_fs_open, *path, flags, mode); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(open, *path, *path, flags, mode) if (SYNC_RESULT < 0) { @@ -1187,10 +1210,11 @@ static void CopyFile(const FunctionCallbackInfo& args) { CHECK_NE(*dest, nullptr); int flags = args[2]->Int32Value(); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "copyfile", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "copyfile", UTF8, AfterNoArgs, uv_fs_copyfile, *src, *dest, flags); + req_wrap->SetReturnValue(args); } else { SYNC_DEST_CALL(copyfile, *src, *dest, *src, *dest, flags) } @@ -1229,11 +1253,11 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { uv_buf_t uvbuf = uv_buf_init(const_cast(buf), len); - if (args[5]->IsObject()) { - CHECK_EQ(args.Length(), 6); - AsyncCall(env, args, "write", UTF8, AfterInteger, + FSReqBase* req_wrap = GetReqWrap(env, args[5]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "write", UTF8, AfterInteger, uv_fs_write, fd, &uvbuf, 1, pos); - return; + return req_wrap->SetReturnValue(args); } SYNC_CALL(write, nullptr, fd, &uvbuf, 1, pos) @@ -1266,11 +1290,11 @@ static void WriteBuffers(const FunctionCallbackInfo& args) { iovs[i] = uv_buf_init(Buffer::Data(chunk), Buffer::Length(chunk)); } - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "write", UTF8, AfterInteger, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "write", UTF8, AfterInteger, uv_fs_write, fd, *iovs, iovs.length(), pos); - return; + return req_wrap->SetReturnValue(args); } SYNC_CALL(write, nullptr, fd, *iovs, iovs.length(), pos) @@ -1299,7 +1323,9 @@ static void WriteString(const FunctionCallbackInfo& args) { size_t len; const int64_t pos = GET_OFFSET(args[2]); const auto enc = ParseEncoding(env->isolate(), args[3], UTF8); - const auto is_async = args[4]->IsObject(); + + FSReqBase* req_wrap = GetReqWrap(env, args[4]); + const auto is_async = req_wrap != nullptr; // Avoid copying the string when it is externalized but only when: // 1. The target encoding is compatible with the string's encoding, and @@ -1333,10 +1359,10 @@ static void WriteString(const FunctionCallbackInfo& args) { uv_buf_t uvbuf = uv_buf_init(buf, len); - if (is_async) { - CHECK_EQ(args.Length(), 5); - AsyncCall(env, args, "write", UTF8, AfterInteger, + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "write", UTF8, AfterInteger, uv_fs_write, fd, &uvbuf, 1, pos); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(write, nullptr, fd, &uvbuf, 1, pos) return args.GetReturnValue().Set(SYNC_RESULT); @@ -1387,10 +1413,11 @@ static void Read(const FunctionCallbackInfo& args) { uv_buf_t uvbuf = uv_buf_init(const_cast(buf), len); - if (args[5]->IsObject()) { - CHECK_EQ(args.Length(), 6); - AsyncCall(env, args, "read", UTF8, AfterInteger, + FSReqBase* req_wrap = GetReqWrap(env, args[5]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "read", UTF8, AfterInteger, uv_fs_read, fd, &uvbuf, 1, pos); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(read, 0, fd, &uvbuf, 1, pos) args.GetReturnValue().Set(SYNC_RESULT); @@ -1412,10 +1439,11 @@ static void Chmod(const FunctionCallbackInfo& args) { int mode = static_cast(args[1]->Int32Value()); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "chmod", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "chmod", UTF8, AfterNoArgs, uv_fs_chmod, *path, mode); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(chmod, *path, *path, mode); } @@ -1434,10 +1462,11 @@ static void FChmod(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); int mode = static_cast(args[1]->Int32Value()); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "fchmod", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "fchmod", UTF8, AfterNoArgs, uv_fs_fchmod, fd, mode); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(fchmod, 0, fd, mode); } @@ -1461,10 +1490,11 @@ static void Chown(const FunctionCallbackInfo& args) { uv_uid_t uid = static_cast(args[1]->Uint32Value()); uv_gid_t gid = static_cast(args[2]->Uint32Value()); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "chown", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "chown", UTF8, AfterNoArgs, uv_fs_chown, *path, uid, gid); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(chown, *path, *path, uid, gid); } @@ -1485,10 +1515,11 @@ static void FChown(const FunctionCallbackInfo& args) { uv_uid_t uid = static_cast(args[1]->Uint32Value()); uv_gid_t gid = static_cast(args[2]->Uint32Value()); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "fchown", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "fchown", UTF8, AfterNoArgs, uv_fs_fchown, fd, uid, gid); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(fchown, 0, fd, uid, gid); } @@ -1508,10 +1539,11 @@ static void UTimes(const FunctionCallbackInfo& args) { const double atime = static_cast(args[1]->NumberValue()); const double mtime = static_cast(args[2]->NumberValue()); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "utime", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "utime", UTF8, AfterNoArgs, uv_fs_utime, *path, atime, mtime); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(utime, *path, *path, atime, mtime); } @@ -1528,10 +1560,11 @@ static void FUTimes(const FunctionCallbackInfo& args) { const double atime = static_cast(args[1]->NumberValue()); const double mtime = static_cast(args[2]->NumberValue()); - if (args[3]->IsObject()) { - CHECK_EQ(args.Length(), 4); - AsyncCall(env, args, "futime", UTF8, AfterNoArgs, + FSReqBase* req_wrap = GetReqWrap(env, args[3]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "futime", UTF8, AfterNoArgs, uv_fs_futime, fd, atime, mtime); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(futime, 0, fd, atime, mtime); } @@ -1547,10 +1580,11 @@ static void Mkdtemp(const FunctionCallbackInfo& args) { const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); - if (args[2]->IsObject()) { - CHECK_EQ(args.Length(), 3); - AsyncCall(env, args, "mkdtemp", encoding, AfterStringPath, + FSReqBase* req_wrap = GetReqWrap(env, args[2]); + if (req_wrap != nullptr) { + AsyncCall(env, req_wrap, args, "mkdtemp", encoding, AfterStringPath, uv_fs_mkdtemp, *tmpl); + req_wrap->SetReturnValue(args); } else { SYNC_CALL(mkdtemp, *tmpl, *tmpl); const char* path = static_cast(SYNC_REQ.path); @@ -1629,14 +1663,14 @@ void InitFs(Local target, target->Set(context, wrapString, fst->GetFunction()).FromJust(); // Create Function Template for FSReqPromise - Local fpt = - FunctionTemplate::New(env->isolate(), NewFSReqPromise); - fpt->InstanceTemplate()->SetInternalFieldCount(1); + Local fpt = FunctionTemplate::New(env->isolate()); AsyncWrap::AddWrapMethods(env, fpt); Local promiseString = FIXED_ONE_BYTE_STRING(env->isolate(), "FSReqPromise"); fpt->SetClassName(promiseString); - target->Set(context, promiseString, fpt->GetFunction()).FromJust(); + Local fpo = fpt->InstanceTemplate(); + fpo->SetInternalFieldCount(1); + env->set_fsreqpromise_constructor_template(fpo); // Create FunctionTemplate for FileHandle Local fd = FunctionTemplate::New(env->isolate()); @@ -1658,6 +1692,14 @@ void InitFs(Local target, Local fdcloset = fdclose->InstanceTemplate(); fdcloset->SetInternalFieldCount(1); env->set_fdclose_constructor_template(fdcloset); + + Local use_promises_symbol = + Symbol::New(env->isolate(), + FIXED_ONE_BYTE_STRING(env->isolate(), "use promises")); + env->set_fs_use_promises_symbol(use_promises_symbol); + target->Set(env->context(), + FIXED_ONE_BYTE_STRING(env->isolate(), "kUsePromises"), + use_promises_symbol).FromJust(); } } // namespace fs diff --git a/src/node_file.h b/src/node_file.h index 4d276aaa3f0499..d49807f5294e01 100644 --- a/src/node_file.h +++ b/src/node_file.h @@ -12,6 +12,7 @@ using v8::Context; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Local; +using v8::MaybeLocal; using v8::Object; using v8::Persistent; using v8::Promise; @@ -51,6 +52,7 @@ class FSReqBase : public ReqWrap { virtual void Reject(Local reject) = 0; virtual void Resolve(Local value) = 0; virtual void ResolveStat() = 0; + virtual void SetReturnValue(const FunctionCallbackInfo& args) = 0; const char* syscall() const { return syscall_; } const char* data() const { return data_; } @@ -77,6 +79,7 @@ class FSReqWrap : public FSReqBase { void Reject(Local reject) override; void Resolve(Local value) override; void ResolveStat() override; + void SetReturnValue(const FunctionCallbackInfo& args) override; private: DISALLOW_COPY_AND_ASSIGN(FSReqWrap); @@ -84,7 +87,7 @@ class FSReqWrap : public FSReqBase { class FSReqPromise : public FSReqBase { public: - FSReqPromise(Environment* env, Local req); + explicit FSReqPromise(Environment* env); ~FSReqPromise() override; @@ -92,10 +95,11 @@ class FSReqPromise : public FSReqBase { void Reject(Local reject) override; void Resolve(Local value) override; void ResolveStat() override; + void SetReturnValue(const FunctionCallbackInfo& args) override; private: bool finished_ = false; - double statFields_[14] {}; + AliasedBuffer stats_field_array_; DISALLOW_COPY_AND_ASSIGN(FSReqPromise); }; @@ -152,7 +156,7 @@ class FileHandle : public AsyncWrap { ref_.Empty(); } - FileHandle* fd(); + FileHandle* file_handle(); size_t self_size() const override { return sizeof(*this); } @@ -166,7 +170,7 @@ class FileHandle : public AsyncWrap { }; // Asynchronous close - inline Local ClosePromise(); + inline MaybeLocal ClosePromise(); int fd_; bool closing_ = false; diff --git a/test/parallel/test-fs-filehandle.js b/test/parallel/test-fs-filehandle.js new file mode 100644 index 00000000000000..8b7de1a35b3ab2 --- /dev/null +++ b/test/parallel/test-fs-filehandle.js @@ -0,0 +1,25 @@ +// Flags: --expose-gc --no-warnings --expose-internals +'use strict'; + +const common = require('../common'); +const path = require('path'); +const fs = process.binding('fs'); +const { stringToFlags } = require('internal/fs'); + +// Verifies that the FileHandle object is garbage collected and that a +// warning is emitted if it is not closed. + +let fdnum; +{ + fdnum = fs.openFileHandle(path.toNamespacedPath(__filename), + stringToFlags('r'), 0o666).fd; +} + +common.expectWarning( + 'Warning', + `Closing file descriptor ${fdnum} on garbage collection` +); + +gc(); // eslint-disable-line no-undef + +setTimeout(() => {}, 10); diff --git a/test/parallel/test-fs-promises-writefile.js b/test/parallel/test-fs-promises-writefile.js new file mode 100644 index 00000000000000..655dc73a1dfdb5 --- /dev/null +++ b/test/parallel/test-fs-promises-writefile.js @@ -0,0 +1,40 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const tmpDir = tmpdir.path; + +tmpdir.refresh(); + +common.crashOnUnhandledRejection(); + +const dest = path.resolve(tmpDir, 'tmp.txt'); +const buffer = Buffer.from('abc'.repeat(1000)); +const buffer2 = Buffer.from('xyz'.repeat(1000)); + +async function doWrite() { + await fs.promises.writeFile(dest, buffer); + const data = fs.readFileSync(dest); + assert.deepStrictEqual(data, buffer); +} + +async function doAppend() { + await fs.promises.appendFile(dest, buffer2); + const data = fs.readFileSync(dest); + const buf = Buffer.concat([buffer, buffer2]); + assert.deepStrictEqual(buf, data); +} + +async function doRead() { + const data = await fs.promises.readFile(dest); + const buf = fs.readFileSync(dest); + assert.deepStrictEqual(buf, data); +} + +doWrite() + .then(doAppend) + .then(doRead) + .then(common.mustCall()); diff --git a/test/parallel/test-fs-promises.js b/test/parallel/test-fs-promises.js new file mode 100644 index 00000000000000..5d493208ff85cb --- /dev/null +++ b/test/parallel/test-fs-promises.js @@ -0,0 +1,150 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const path = require('path'); +const fs = require('fs'); +const { + access, + chmod, + copyFile, + fchmod, + fdatasync, + fstat, + fsync, + ftruncate, + futimes, + link, + lstat, + mkdir, + mkdtemp, + open, + read, + readdir, + readlink, + realpath, + rename, + rmdir, + stat, + symlink, + write, + unlink, + utimes +} = fs.promises; + +const tmpDir = tmpdir.path; + +common.crashOnUnhandledRejection(); + +{ + access(__filename, 'r') + .then(common.mustCall()) + .catch(common.mustNotCall()); + + access('this file does not exist', 'r') + .then(common.mustNotCall()) + .catch(common.expectsError({ + code: 'ENOENT', + type: Error, + message: + /^ENOENT: no such file or directory, access/ + })); +} + +function verifyStatObject(stat) { + assert.strictEqual(typeof stat, 'object'); + assert.strictEqual(typeof stat.dev, 'number'); + assert.strictEqual(typeof stat.mode, 'number'); +} + +{ + async function doTest() { + tmpdir.refresh(); + const dest = path.resolve(tmpDir, 'baz.js'); + await copyFile(fixtures.path('baz.js'), dest); + await access(dest, 'r'); + + const handle = await open(dest, 'r+'); + assert.strictEqual(typeof handle, 'object'); + + let stats = await fstat(handle); + verifyStatObject(stats); + assert.strictEqual(stats.size, 35); + + await ftruncate(handle, 1); + + stats = await fstat(handle); + verifyStatObject(stats); + assert.strictEqual(stats.size, 1); + + stats = await stat(dest); + verifyStatObject(stats); + + await fdatasync(handle); + await fsync(handle); + + const buf = Buffer.from('hello world'); + + await write(handle, buf); + + const ret = await read(handle, Buffer.alloc(11), 0, 11, 0); + assert.strictEqual(ret.bytesRead, 11); + assert.deepStrictEqual(ret.buffer, buf); + + await chmod(dest, 0o666); + await fchmod(handle, 0o666); + + await utimes(dest, new Date(), new Date()); + + try { + await futimes(handle, new Date(), new Date()); + } catch (err) { + // Some systems do not have futimes. If there is an error, + // expect it to be ENOSYS + common.expectsError({ + code: 'ENOSYS', + type: Error + })(err); + } + + await handle.close(); + + const newPath = path.resolve(tmpDir, 'baz2.js'); + await rename(dest, newPath); + stats = await stat(newPath); + verifyStatObject(stats); + + const newLink = path.resolve(tmpDir, 'baz3.js'); + await symlink(newPath, newLink); + + const newLink2 = path.resolve(tmpDir, 'baz4.js'); + await link(newPath, newLink2); + + stats = await lstat(newLink); + verifyStatObject(stats); + + assert.strictEqual(newPath.toLowerCase(), + (await realpath(newLink)).toLowerCase()); + assert.strictEqual(newPath.toLowerCase(), + (await readlink(newLink)).toLowerCase()); + + await unlink(newLink); + await unlink(newLink2); + + const newdir = path.resolve(tmpDir, 'dir'); + await mkdir(newdir); + stats = await stat(newdir); + assert(stats.isDirectory()); + + const list = await readdir(tmpDir); + assert.deepStrictEqual(list, ['baz2.js', 'dir']); + + await rmdir(newdir); + + await mkdtemp(path.resolve(tmpDir, 'FOO')); + } + + doTest().then(common.mustCall()); +} diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 86a4065fb2b486..fb11f2d9e46305 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -9,6 +9,8 @@ const fixtures = require('../common/fixtures'); const tmpdir = require('../common/tmpdir'); const { getSystemErrorName } = require('util'); +common.crashOnUnhandledRejection(); + // Make sure that all Providers are tested. { const hooks = require('async_hooks').createHook({ @@ -167,6 +169,14 @@ if (common.hasCrypto) { // eslint-disable-line crypto-check testInitialized(new Signal(), 'Signal'); } +{ + async function openTest() { + const fd = await fs.promises.open(__filename, 'r'); + testInitialized(fd, 'FileHandle'); + await fd.close(); + } + openTest().then(common.mustCall()).catch(common.mustNotCall()); +} { const binding = process.binding('stream_wrap');